Skip to main content

ccalc_engine/
eval.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::io::Write;
4
5use indexmap::IndexMap;
6use ndarray::Array2;
7use 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}
55
56/// Registers the autoload hook. Called by `exec::init()`.
57pub fn set_autoload_hook(f: AutoloadHook) {
58    AUTOLOAD_HOOK.with(|c| c.set(Some(f)));
59}
60
61/// Inserts a function into the autoload cache. Called by `exec::try_autoload`.
62pub fn autoload_cache_insert(name: String, val: Value) {
63    AUTOLOAD_CACHE.with(|c| c.borrow_mut().insert(name, val));
64}
65
66/// Returns an autoloaded function by name, triggering the autoload hook if needed.
67///
68/// Checks the cache first; if not present, fires the registered hook (which searches
69/// the session path for `<name>.calc` / `<name>.m`). Returns `None` when neither the
70/// cache nor the hook can resolve the name.
71pub fn resolve_autoloaded(name: &str) -> Option<Value> {
72    let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
73    if cached.is_some() {
74        return cached;
75    }
76    let hook = AUTOLOAD_HOOK.with(|c| c.get());
77    if let Some(f) = hook {
78        f(name);
79    }
80    AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned())
81}
82
83// ── Last error (thread-local) ────────────────────────────────────────────────
84
85thread_local! {
86    static LAST_ERR: RefCell<String> = const { RefCell::new(String::new()) };
87}
88
89/// Sets the last-error string (called on every caught runtime error).
90pub fn set_last_err(msg: &str) {
91    LAST_ERR.with(|e| *e.borrow_mut() = msg.to_string());
92}
93
94/// Returns the last-error string.
95pub fn get_last_err() -> String {
96    LAST_ERR.with(|e| e.borrow().clone())
97}
98
99// ── Nargout (number of expected outputs, set by exec_stmts) ─────────────────
100
101thread_local! {
102    static NARGOUT: Cell<usize> = const { Cell::new(1) };
103}
104
105/// Sets the number of output values requested by the calling assignment statement.
106///
107/// Called by `exec_stmts` before evaluating the RHS expression, so that
108/// multi-output built-ins (`eig`, `svd`, `lu`, `qr`) can determine whether to
109/// return a full `Value::Tuple` or a single value.
110pub fn set_nargout(n: usize) {
111    NARGOUT.with(|c| c.set(n));
112}
113
114fn get_nargout() -> usize {
115    NARGOUT.with(|c| c.get())
116}
117
118// ── Display context (thread-local, set by exec_stmts) ────────────────────────
119
120thread_local! {
121    static DISPLAY_FMT:     RefCell<FormatMode> = const { RefCell::new(FormatMode::Short) };
122    static DISPLAY_BASE:    Cell<Base>           = const { Cell::new(Base::Dec) };
123    static DISPLAY_COMPACT: Cell<bool>           = const { Cell::new(false) };
124}
125
126/// Sets the display context used when executing function bodies.
127///
128/// Called at the start of `exec_stmts` so that named functions called from
129/// within a block inherit the caller's display settings.
130pub fn set_display_ctx(fmt: &FormatMode, base: Base, compact: bool) {
131    DISPLAY_FMT.with(|f| *f.borrow_mut() = fmt.clone());
132    DISPLAY_BASE.with(|b| b.set(base));
133    DISPLAY_COMPACT.with(|c| c.set(compact));
134}
135
136/// Returns the current display format mode stored in the thread-local context.
137pub fn get_display_fmt() -> FormatMode {
138    DISPLAY_FMT.with(|f| f.borrow().clone())
139}
140
141/// Returns the current numeric base stored in the thread-local context.
142pub fn get_display_base() -> Base {
143    DISPLAY_BASE.with(|b| b.get())
144}
145
146/// Returns the current compact flag stored in the thread-local context.
147pub fn get_display_compact() -> bool {
148    DISPLAY_COMPACT.with(|c| c.get())
149}
150
151// ── Global variable store ────────────────────────────────────────────────────
152
153thread_local! {
154    /// Shared global workspace — variables declared `global` in any scope live here.
155    ///
156    /// Persists for the lifetime of the process. Each call to `global x` in any scope
157    /// makes `x` refer to this store rather than the local environment.
158    static GLOBAL_ENV: RefCell<Env> = RefCell::new(Env::new());
159
160    /// Stack of per-scope global name sets.
161    ///
162    /// Frame 0 = top level / script scope; each `call_user_function` call pushes a new frame
163    /// and pops it on return. `global x` in a scope adds `x` to the current (top) frame.
164    static GLOBAL_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
165        RefCell::new(vec![HashSet::new()]);
166}
167
168/// Pushes an empty global-names frame (called on function entry by `exec.rs`).
169pub fn global_frame_push() {
170    GLOBAL_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
171}
172
173/// Pops the top global-names frame (called on function exit by `exec.rs`).
174pub fn global_frame_pop() {
175    GLOBAL_NAMES_STACK.with(|s| {
176        s.borrow_mut().pop();
177    });
178}
179
180/// Declares `name` as global in the current scope.
181pub fn global_declare(name: &str) {
182    GLOBAL_NAMES_STACK.with(|s| {
183        if let Some(frame) = s.borrow_mut().last_mut() {
184            frame.insert(name.to_string());
185        }
186    });
187}
188
189/// Returns `true` if `name` is declared global in the innermost active scope.
190pub fn is_global(name: &str) -> bool {
191    GLOBAL_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|f| f.contains(name)))
192}
193
194/// Gets a value from the shared global store.
195pub fn global_get(name: &str) -> Option<Value> {
196    GLOBAL_ENV.with(|e| e.borrow().get(name).cloned())
197}
198
199/// Sets a value in the shared global store.
200pub fn global_set(name: &str, val: Value) {
201    GLOBAL_ENV.with(|e| e.borrow_mut().insert(name.to_string(), val));
202}
203
204/// Initialises `name` in the global store to `Scalar(0.0)` if not already present.
205pub fn global_init_if_absent(name: &str) {
206    GLOBAL_ENV.with(|e| {
207        e.borrow_mut()
208            .entry(name.to_string())
209            .or_insert(Value::Scalar(0.0));
210    });
211}
212
213/// Refreshes all names declared global in the current scope from `GLOBAL_ENV` into `env`.
214///
215/// Called at the end of `exec_stmts` to ensure that modifications made to global variables
216/// inside called functions are visible to the current scope's environment.
217pub fn global_refresh_into_env(env: &mut crate::env::Env) {
218    GLOBAL_NAMES_STACK.with(|s| {
219        GLOBAL_ENV.with(|ge| {
220            if let Some(frame) = s.borrow().last() {
221                let store = ge.borrow();
222                for name in frame {
223                    if let Some(val) = store.get(name) {
224                        env.insert(name.clone(), val.clone());
225                    }
226                }
227            }
228        });
229    });
230}
231
232// ── Persistent variable store ────────────────────────────────────────────────
233
234thread_local! {
235    /// Persistent variable values — keyed by `"funcname\x00varname"`.
236    ///
237    /// Values survive individual function calls and are restored on the next call
238    /// to the same function.
239    static PERSISTENT_STORE: RefCell<HashMap<String, Value>> =
240        RefCell::new(HashMap::new());
241
242    /// Stack of function names for constructing persistent-store keys.
243    ///
244    /// Empty string = top-level scope. `call_user_function` pushes the function name
245    /// before executing the body and pops it on return.
246    static FUNC_NAME_STACK: RefCell<Vec<String>> =
247        RefCell::new(vec![String::new()]);
248
249    /// Stack of per-scope persistent name sets — mirrors `GLOBAL_NAMES_STACK`.
250    static PERSISTENT_NAMES_STACK: RefCell<Vec<HashSet<String>>> =
251        RefCell::new(vec![HashSet::new()]);
252}
253
254/// Pushes a function scope for persistent tracking (called on function entry).
255pub fn persistent_frame_push(func_name: &str) {
256    FUNC_NAME_STACK.with(|s| s.borrow_mut().push(func_name.to_string()));
257    PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().push(HashSet::new()));
258}
259
260/// Pops the persistent frame and returns `(func_name, declared_persistent_names)`.
261pub fn persistent_frame_pop() -> (String, HashSet<String>) {
262    let func_name = FUNC_NAME_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
263    let names = PERSISTENT_NAMES_STACK.with(|s| s.borrow_mut().pop().unwrap_or_default());
264    (func_name, names)
265}
266
267/// Declares `name` as persistent in the current function scope.
268pub fn persistent_declare(name: &str) {
269    PERSISTENT_NAMES_STACK.with(|s| {
270        if let Some(frame) = s.borrow_mut().last_mut() {
271            frame.insert(name.to_string());
272        }
273    });
274}
275
276/// Gets a saved persistent value for `(func_name, var_name)`.
277pub fn persistent_load(func_name: &str, var_name: &str) -> Option<Value> {
278    let key = format!("{func_name}\x00{var_name}");
279    PERSISTENT_STORE.with(|s| s.borrow().get(&key).cloned())
280}
281
282/// Saves a persistent value for `(func_name, var_name)`.
283pub fn persistent_save(func_name: &str, var_name: &str, val: Value) {
284    let key = format!("{func_name}\x00{var_name}");
285    PERSISTENT_STORE.with(|s| s.borrow_mut().insert(key, val));
286}
287
288/// Returns the name of the currently executing function (top of `FUNC_NAME_STACK`).
289///
290/// Returns an empty string when executing at the top level (REPL / script scope).
291pub fn current_func_name() -> String {
292    FUNC_NAME_STACK.with(|s| s.borrow().last().cloned().unwrap_or_default())
293}
294
295/// Returns `true` if `name` is declared `persistent` in the current function frame.
296pub fn is_persistent(name: &str) -> bool {
297    PERSISTENT_NAMES_STACK.with(|s| s.borrow().last().is_some_and(|frame| frame.contains(name)))
298}
299
300// ── Random-number state ──────────────────────────────────────────────────────
301
302thread_local! {
303    /// Per-thread PRNG used by `rand`, `randn`, and `randi`.
304    ///
305    /// Seeded from OS entropy on first use. Reseed with `rng(seed)` or `rng('shuffle')`.
306    static RNG: RefCell<SmallRng> = RefCell::new(SmallRng::from_entropy());
307}
308
309/// Reseeds the thread-local RNG with the given 64-bit seed.
310pub fn rng_seed(seed: u64) {
311    RNG.with(|r| *r.borrow_mut() = SmallRng::seed_from_u64(seed));
312}
313
314/// Reseeds the thread-local RNG from OS entropy.
315pub fn rng_shuffle() {
316    RNG.with(|r| *r.borrow_mut() = SmallRng::from_entropy());
317}
318
319/// Generates one uniform [0, 1) sample.
320fn rand_uniform() -> f64 {
321    RNG.with(|r| r.borrow_mut().gen_range(0.0_f64..1.0))
322}
323
324/// Generates one standard-normal sample via the Box-Muller transform.
325fn rand_normal() -> f64 {
326    let u1 = rand_uniform().max(f64::EPSILON);
327    let u2 = rand_uniform();
328    (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
329}
330
331// ── AST types ────────────────────────────────────────────────────────────────
332
333/// An expression node in the AST.
334///
335/// Produced by the parser and consumed by [`eval`] / [`eval_with_io`].
336#[derive(Debug, Clone)]
337pub enum Expr {
338    /// A numeric literal (e.g. `3`, `2.5`, `1e-3`).
339    Number(f64),
340    /// A variable or constant reference (e.g. `x`, `pi`, `ans`).
341    Var(String),
342    /// Arithmetic negation: `-expr`.
343    UnaryMinus(Box<Expr>),
344    /// Logical NOT: `~expr`. Result is 1.0 if expr == 0.0, else 0.0.
345    UnaryNot(Box<Expr>),
346    /// Binary operation: `lhs op rhs`.
347    BinOp(Box<Expr>, Op, Box<Expr>),
348    /// Function call or variable indexing: `name(arg1, arg2, ...)`.
349    ///
350    /// Disambiguation happens at eval time: if `name` exists in the environment
351    /// it is treated as indexing, otherwise as a built-in or user function call.
352    Call(String, Vec<Expr>),
353    /// Matrix literal: `[row1; row2; ...]` where each row is a list of expressions.
354    Matrix(Vec<Vec<Expr>>),
355    /// Conjugate transpose: `A'`. For complex scalars, returns the conjugate.
356    Transpose(Box<Expr>),
357    /// Range expression: `start:stop` or `start:step:stop`.
358    /// Evaluates to a 1×N row vector.
359    Range(Box<Expr>, Option<Box<Expr>>, Box<Expr>),
360    /// Bare `:` used as an all-elements index in `A(:,j)` or `A(i,:)`.
361    /// Only valid as an argument inside an indexing expression.
362    Colon,
363    /// Single-quoted char array literal.
364    StrLiteral(String),
365    /// Double-quoted string object literal.
366    StringObjLiteral(String),
367    /// Anonymous function: `@(params) body_expr`.
368    ///
369    /// At evaluation time this is converted to `Value::Lambda`, capturing the
370    /// current environment as a lexical closure.
371    Lambda {
372        /// Parameter names in declaration order (e.g. `["x", "n"]`).
373        params: Vec<String>,
374        /// Body expression evaluated when the lambda is called.
375        body: Box<Expr>,
376        /// Source text for display (e.g. `@(x) x.^2 + 1`), stored at parse time.
377        source: String,
378    },
379    /// Non-conjugate (plain) transpose: `A.'`.
380    ///
381    /// Transposes without complex conjugation. For real matrices, identical to `A'`.
382    /// For complex: `z.'` returns `z` unchanged (no sign flip on imaginary part).
383    PlainTranspose(Box<Expr>),
384    /// Cell array literal: `{e1, e2, e3}`.
385    ///
386    /// Evaluates each element and produces `Value::Cell`.
387    CellLiteral(Vec<Expr>),
388    /// Cell array brace-indexing: `c{i}`.
389    ///
390    /// The first expression must evaluate to `Value::Cell`; the second is the
391    /// 1-based integer index.
392    CellIndex(Box<Expr>, Box<Expr>),
393    /// Function handle: `@funcname`.
394    ///
395    /// Produces a `Value::Lambda` that forwards its arguments to the named
396    /// built-in or user function.
397    FuncHandle(String),
398    /// Struct field read: `s.field` or chained `s.a.b` (parsed as `FieldGet(FieldGet(s,"a"),"b")`).
399    ///
400    /// At eval time the base expression must evaluate to `Value::Struct`.
401    FieldGet(Box<Expr>, String),
402    /// Package-qualified function call: `pkg.func(args)` or `pkg.sub.func(args)`.
403    ///
404    /// `segments` holds the dot-separated name components, e.g. `["utils", "my_function"]`.
405    /// At eval time:
406    /// - If `segments[0]` is in the environment (a struct or callable), the chain is followed
407    ///   as field accesses and the final value is called with the given arguments.
408    /// - Otherwise, the segments are treated as a package call: the autoload hook searches
409    ///   for `+utils/my_function.calc` (or `+utils/+sub/func.calc` for nested packages)
410    ///   on the session path and loads the function on demand.
411    DotCall(Vec<String>, Vec<Expr>),
412}
413
414/// A binary operator used in [`Expr::BinOp`].
415#[derive(Debug, Clone)]
416pub enum Op {
417    /// Addition: `a + b` or element-wise matrix addition.
418    Add,
419    /// Subtraction: `a - b` or element-wise matrix subtraction.
420    Sub,
421    /// Multiplication: scalar `a * b` or matrix product `A * B`.
422    Mul,
423    /// Division: scalar `a / b` or matrix right-division `A / B` (solves `X * B = A`).
424    Div,
425    /// Exponentiation: scalar `a ^ b` or matrix power `A ^ n`.
426    Pow,
427    /// Element-wise multiplication: `A .* B`.
428    ElemMul,
429    /// Element-wise division: `A ./ B`.
430    ElemDiv,
431    /// Element-wise exponentiation: `A .^ B`.
432    ElemPow,
433    // --- Comparison (element-wise, return 0.0/1.0) ---
434    /// Equality comparison: `a == b`. Returns 1.0 if equal, 0.0 otherwise.
435    Eq,
436    /// Inequality comparison: `a ~= b`. Returns 1.0 if not equal, 0.0 otherwise.
437    NotEq,
438    /// Less-than comparison: `a < b`.
439    Lt,
440    /// Greater-than comparison: `a > b`.
441    Gt,
442    /// Less-than-or-equal comparison: `a <= b`.
443    LtEq,
444    /// Greater-than-or-equal comparison: `a >= b`.
445    GtEq,
446    // --- Short-circuit logical (scalars only) ---
447    /// Short-circuit logical AND: `a && b`. Only evaluates `b` if `a` is truthy.
448    And,
449    /// Short-circuit logical OR: `a || b`. Only evaluates `b` if `a` is falsy.
450    Or,
451    // --- Element-wise logical (matrices allowed, no short-circuit) ---
452    /// Element-wise logical AND: `A & B`. Evaluates both sides; works on matrices.
453    ElemAnd,
454    /// Element-wise logical OR: `A | B`. Evaluates both sides; works on matrices.
455    ElemOr,
456    /// Left division: `A \ b` solves `A*x = b`. Scalar: `a \ b = b / a`.
457    LDiv,
458}
459
460/// The numeric base used when displaying integer-valued scalars.
461#[derive(Debug, Clone, Copy, PartialEq, Default)]
462pub enum Base {
463    /// Decimal (base 10) — the default.
464    #[default]
465    Dec,
466    /// Hexadecimal (base 16), prefix `0x` (e.g. `0xff`).
467    Hex,
468    /// Binary (base 2), prefix `0b` (e.g. `0b1010`).
469    Bin,
470    /// Octal (base 8), prefix `0o` (e.g. `0o17`).
471    Oct,
472}
473
474/// Controls how numbers are displayed (MATLAB-compatible format modes).
475#[derive(Debug, Clone, PartialEq)]
476pub enum FormatMode {
477    /// 5 significant digits, auto fixed/scientific (MATLAB `format short`).
478    Short,
479    /// 15 significant digits, auto fixed/scientific (MATLAB `format long`).
480    Long,
481    /// Always scientific notation, 4 decimal places — 5 sig digits.
482    ShortE,
483    /// Always scientific notation, 14 decimal places — 15 sig digits.
484    LongE,
485    /// Same as `Short` for scalars (MATLAB `format shortG`).
486    ShortG,
487    /// Same as `Long` for scalars (MATLAB `format longG`).
488    LongG,
489    /// Fixed 2 decimal places — currency (MATLAB `format bank`).
490    Bank,
491    /// Rational approximation `p/q` (MATLAB `format rat`).
492    Rat,
493    /// IEEE 754 hexadecimal bit pattern, 16 uppercase hex digits (MATLAB `format hex`).
494    Hex,
495    /// Sign character only: `+`, `-`, or ` ` for zero (MATLAB `format +`).
496    Plus,
497    /// N decimal places, auto fixed/scientific — legacy precision= setting.
498    Custom(usize),
499}
500
501impl Default for FormatMode {
502    fn default() -> Self {
503        FormatMode::Custom(10)
504    }
505}
506
507impl FormatMode {
508    /// Human-readable name for display in `config` / status messages.
509    pub fn name(&self) -> String {
510        match self {
511            FormatMode::Short => "short".to_string(),
512            FormatMode::Long => "long".to_string(),
513            FormatMode::ShortE => "shortE".to_string(),
514            FormatMode::LongE => "longE".to_string(),
515            FormatMode::ShortG => "shortG".to_string(),
516            FormatMode::LongG => "longG".to_string(),
517            FormatMode::Bank => "bank".to_string(),
518            FormatMode::Rat => "rat".to_string(),
519            FormatMode::Hex => "hex".to_string(),
520            FormatMode::Plus => "+".to_string(),
521            FormatMode::Custom(n) => format!("custom({n})"),
522        }
523    }
524}
525
526/// Evaluates an expression without file I/O context.
527/// This is the public API used by tests and non-I/O evaluation paths.
528pub fn eval(expr: &Expr, env: &Env) -> Result<Value, String> {
529    eval_inner(expr, env, None)
530}
531
532/// Evaluates an expression with an I/O context (file descriptor table).
533/// Used by the REPL to support `fopen`/`fclose`/`fgetl`/`fgets`/`fprintf(fd,...)`.
534pub fn eval_with_io(expr: &Expr, env: &Env, io: &mut IoContext) -> Result<Value, String> {
535    eval_inner(expr, env, Some(io))
536}
537
538fn eval_inner(expr: &Expr, env: &Env, mut io: Option<&mut IoContext>) -> Result<Value, String> {
539    match expr {
540        Expr::Number(n) => Ok(Value::Scalar(*n)),
541        Expr::Var(name) => env.get(name).cloned().ok_or(()).or_else(|_| {
542            // Check the shared global store when the name is declared global in this scope.
543            if is_global(name)
544                && let Some(val) = global_get(name)
545            {
546                return Ok(val);
547            }
548            // 'e' falls back to Euler's number if not defined in env
549            if name == "e" {
550                Ok(Value::Scalar(std::f64::consts::E))
551            } else {
552                let hint = suggest_similar(name, env);
553                match hint {
554                    Some(s) => Err(format!("Undefined variable '{name}'; did you mean '{s}'?")),
555                    None => Err(format!("Undefined variable: '{name}'")),
556                }
557            }
558        }),
559        Expr::UnaryMinus(e) => match eval_inner(e, env, io)? {
560            Value::Void => Err("Unary minus is not applicable to void".to_string()),
561            Value::Scalar(n) => Ok(Value::Scalar(-n)),
562            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
563            Value::Complex(re, im) => Ok(Value::Complex(-re, -im)),
564            Value::Str(s) => match str_to_numeric(&s) {
565                Value::Scalar(n) => Ok(Value::Scalar(-n)),
566                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| -x))),
567                _ => unreachable!(),
568            },
569            Value::StringObj(_) => {
570                Err("Unary minus is not applicable to string objects".to_string())
571            }
572            Value::Lambda(_)
573            | Value::Function { .. }
574            | Value::Tuple(_)
575            | Value::Cell(_)
576            | Value::Struct(_)
577            | Value::StructArray(_) => {
578                Err("Unary minus is not applicable to this type".to_string())
579            }
580        },
581        Expr::UnaryNot(e) => match eval_inner(e, env, io)? {
582            Value::Void => Err("Logical NOT is not applicable to void".to_string()),
583            Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
584            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
585            Value::Complex(re, im) => Ok(Value::Scalar(if re == 0.0 && im == 0.0 {
586                1.0
587            } else {
588                0.0
589            })),
590            Value::Str(s) => match str_to_numeric(&s) {
591                Value::Scalar(n) => Ok(Value::Scalar(if n == 0.0 { 1.0 } else { 0.0 })),
592                Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| if x == 0.0 { 1.0 } else { 0.0 }))),
593                _ => unreachable!(),
594            },
595            Value::StringObj(_) => {
596                Err("Logical NOT is not applicable to string objects".to_string())
597            }
598            Value::Lambda(_)
599            | Value::Function { .. }
600            | Value::Tuple(_)
601            | Value::Cell(_)
602            | Value::Struct(_)
603            | Value::StructArray(_) => {
604                Err("Logical NOT is not applicable to this type".to_string())
605            }
606        },
607        Expr::BinOp(left, op, right) => {
608            let l = eval_inner(left, env, io.as_deref_mut())?;
609            let r = eval_inner(right, env, io)?;
610            eval_binop(l, op, r)
611        }
612        Expr::Call(name, args) => {
613            // try(expr, default) — special form: evaluate expr; on error evaluate default.
614            // Arguments are NOT pre-evaluated; lazy semantics.
615            if name == "try" && args.len() == 2 {
616                return match eval_inner(&args[0], env, io.as_deref_mut()) {
617                    Ok(v) => Ok(v),
618                    Err(msg) => {
619                        set_last_err(&msg);
620                        eval_inner(&args[1], env, io.as_deref_mut())
621                    }
622                };
623            }
624
625            // If the name resolves to a variable in env, check its type.
626            // User functions (Lambda, Function) are called; other values are indexed.
627            // Variables shadow built-in function names (Octave semantics).
628            if let Some(val) = env.get(name).cloned() {
629                match &val {
630                    Value::Lambda(f) => {
631                        // Evaluate arguments and call the closure directly.
632                        // Empty call → inject ans (convenience: sq() = sq(ans)).
633                        let mut evaled = Vec::with_capacity(args.len().max(1));
634                        for a in args {
635                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
636                        }
637                        if evaled.is_empty() {
638                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
639                        }
640                        let f = f.clone();
641                        return f.0(&evaled, io);
642                    }
643                    Value::Function { .. } => {
644                        // Evaluate arguments and dispatch to the registered hook in exec.rs.
645                        // User functions receive the raw arg list — NO ans injection. Empty call
646                        // means no arguments (varargin = {}), matching MATLAB semantics.
647                        let mut evaled = Vec::with_capacity(args.len());
648                        for a in args {
649                            evaled.push(eval_inner(a, env, io.as_deref_mut())?);
650                        }
651                        return match io.as_deref_mut() {
652                            Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
653                                Some(hook) => hook(name, &val, &evaled, env, io_ref),
654                                None => Err(format!(
655                                    "'{name}': user function execution not initialized \
656                                         (call exec::init() first)"
657                                )),
658                            }),
659                            None => {
660                                // No I/O context — create a temporary one (functions that do
661                                // file I/O in this path will silently fail to open files).
662                                let mut tmp_io = IoContext::new();
663                                FN_CALL_HOOK.with(|c| match c.get() {
664                                    Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
665                                    None => Err(format!(
666                                        "'{name}': user function execution not initialized"
667                                    )),
668                                })
669                            }
670                        };
671                    }
672                    _ => return eval_index(&val, args, env),
673                }
674            }
675            // Autoload: if name is not in env and not yet tried as a builtin,
676            // ask exec.rs to search for <name>.calc / <name>.m on the path.
677            // If found, the function is inserted into env and we call it directly.
678            // Autoload: search for <name>.calc / <name>.m if not in env or cache.
679            let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned());
680            let autoloaded_val = cached.or_else(|| {
681                let loaded = AUTOLOAD_HOOK
682                    .with(|c| c.get())
683                    .is_some_and(|hook| hook(name));
684                if loaded {
685                    AUTOLOAD_CACHE.with(|c| c.borrow().get(name).cloned())
686                } else {
687                    None
688                }
689            });
690            if let Some(val) = autoloaded_val {
691                let mut evaled = Vec::with_capacity(args.len());
692                for a in args {
693                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
694                }
695                return match io.as_deref_mut() {
696                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
697                        Some(hook) => hook(name, &val, &evaled, env, io_ref),
698                        None => Err(format!("'{name}': exec::init() not called")),
699                    }),
700                    None => {
701                        let mut tmp_io = IoContext::new();
702                        FN_CALL_HOOK.with(|c| match c.get() {
703                            Some(hook) => hook(name, &val, &evaled, env, &mut tmp_io),
704                            None => Err(format!("'{name}': exec::init() not called")),
705                        })
706                    }
707                };
708            }
709
710            // Builtin path: empty call → inject ans (sqrt() = sqrt(ans)).
711            let mut evaled = Vec::with_capacity(args.len().max(1));
712            for a in args {
713                evaled.push(eval_inner(a, env, io.as_deref_mut())?);
714            }
715            // Don't inject ans for functions that take explicit struct/cell args
716            // or constructors where zero args is meaningful.
717            let no_ans_inject = matches!(
718                name.as_str(),
719                "struct"
720                    | "fieldnames"
721                    | "isfield"
722                    | "rmfield"
723                    | "isstruct"
724                    | "cell"
725                    | "iscell"
726                    | "isempty"
727                    | "cellfun"
728                    | "error"
729                    | "warning"
730                    | "lasterr"
731                    | "pcall"
732                    | "rand"
733                    | "randn"
734                    | "rng"
735            );
736            if evaled.is_empty() && !no_ans_inject {
737                evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
738            }
739            call_builtin(name, &evaled, env, io)
740        }
741
742        Expr::Lambda {
743            params,
744            body,
745            source,
746        } => {
747            // Capture the current environment and body expression at definition time.
748            // The resulting Value::Lambda is a closure that binds params on each call.
749            let captured_env = env.clone();
750            let captured_params = params.clone();
751            let captured_body = *body.clone();
752            let src = source.clone();
753            let lambda = LambdaFn(
754                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
755                    // Allow up to params.len()+1 args: the parser injects `ans` for empty f() calls.
756                    let effective = if args.len() > captured_params.len() {
757                        if args.len() > captured_params.len() + 1 {
758                            return Err(format!(
759                                "Lambda: too many arguments (expected at most {}, got {})",
760                                captured_params.len(),
761                                args.len()
762                            ));
763                        }
764                        &args[..captured_params.len()]
765                    } else {
766                        args
767                    };
768                    let mut local_env = captured_env.clone();
769                    for (p, a) in captured_params.iter().zip(effective.iter()) {
770                        local_env.insert(p.clone(), a.clone());
771                    }
772                    local_env.insert("nargin".to_string(), Value::Scalar(effective.len() as f64));
773                    eval_inner(&captured_body, &local_env, io)
774                }),
775                src,
776            );
777            Ok(Value::Lambda(lambda))
778        }
779        Expr::CellLiteral(elems) => {
780            let mut vals = Vec::with_capacity(elems.len());
781            for e in elems {
782                vals.push(eval_inner(e, env, io.as_deref_mut())?);
783            }
784            Ok(Value::Cell(vals))
785        }
786        Expr::CellIndex(cell_expr, idx_expr) => {
787            let cell = eval_inner(cell_expr, env, io.as_deref_mut())?;
788            let idx = eval_inner(idx_expr, env, io)?;
789            match (cell, idx) {
790                (Value::Cell(v), Value::Scalar(i)) => {
791                    let i = i as isize;
792                    if i < 1 || i as usize > v.len() {
793                        Err(format!("Cell index {} out of range (1..{})", i, v.len()))
794                    } else {
795                        Ok(v[(i - 1) as usize].clone())
796                    }
797                }
798                (Value::Cell(_), _) => Err("Cell index must be a scalar integer".to_string()),
799                _ => Err("Brace indexing '{}' is only valid on cell arrays".to_string()),
800            }
801        }
802        Expr::FieldGet(base_expr, field) => {
803            let base_val = eval_inner(base_expr, env, io)?;
804            match base_val {
805                Value::Struct(map) => map
806                    .get(field)
807                    .cloned()
808                    .ok_or_else(|| format!("No field '{field}' in struct")),
809                // s.field on a struct array — collect field values across all elements
810                Value::StructArray(arr) => {
811                    let mut values: Vec<Value> = Vec::with_capacity(arr.len());
812                    for (idx, elem) in arr.iter().enumerate() {
813                        let v = elem.get(field).cloned().ok_or_else(|| {
814                            format!("No field '{field}' in struct array element {}", idx + 1)
815                        })?;
816                        values.push(v);
817                    }
818                    // If all values are scalars, return a 1×N matrix; otherwise a cell.
819                    let all_scalar = values.iter().all(|v| matches!(v, Value::Scalar(_)));
820                    if all_scalar {
821                        let nums: Vec<f64> = values
822                            .into_iter()
823                            .map(|v| {
824                                if let Value::Scalar(n) = v {
825                                    n
826                                } else {
827                                    unreachable!()
828                                }
829                            })
830                            .collect();
831                        let n = nums.len();
832                        Ok(Value::Matrix(Array2::from_shape_vec((1, n), nums).unwrap()))
833                    } else {
834                        Ok(Value::Cell(values))
835                    }
836                }
837                _ => Err(format!(
838                    "Cannot access field '{field}' on a non-struct value"
839                )),
840            }
841        }
842        Expr::DotCall(segs, args) => {
843            let qualified = segs.join(".");
844            // If the head segment is a variable, follow the field chain and call the result.
845            if let Some(head_val) = env.get(&segs[0]).cloned() {
846                let mut val = head_val;
847                for field in &segs[1..] {
848                    val = match val {
849                        Value::Struct(ref map) => map
850                            .get(field)
851                            .cloned()
852                            .ok_or_else(|| format!("No field '{field}' in struct"))?,
853                        _ => {
854                            return Err(format!(
855                                "Cannot access field '{field}' on a non-struct value"
856                            ));
857                        }
858                    };
859                }
860                let mut evaled = Vec::with_capacity(args.len());
861                for a in args {
862                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
863                }
864                return match val {
865                    Value::Lambda(f) => {
866                        if evaled.is_empty() {
867                            evaled.push(env.get("ans").cloned().unwrap_or(Value::Scalar(0.0)));
868                        }
869                        f.0(&evaled, io)
870                    }
871                    Value::Function { .. } => match io.as_deref_mut() {
872                        Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
873                            Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
874                            None => Err(format!("'{qualified}': exec::init() not called")),
875                        }),
876                        None => {
877                            let mut tmp_io = IoContext::new();
878                            FN_CALL_HOOK.with(|c| match c.get() {
879                                Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
880                                None => Err(format!("'{qualified}': exec::init() not called")),
881                            })
882                        }
883                    },
884                    _ => Err(format!("'{qualified}': not a callable")),
885                };
886            }
887            // Package call: autoload from +pkg/func.calc then invoke.
888            let cached = AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned());
889            let autoloaded_val = cached.or_else(|| {
890                let loaded = AUTOLOAD_HOOK
891                    .with(|c| c.get())
892                    .is_some_and(|hook| hook(&qualified));
893                if loaded {
894                    AUTOLOAD_CACHE.with(|c| c.borrow().get(&qualified).cloned())
895                } else {
896                    None
897                }
898            });
899            if let Some(val) = autoloaded_val {
900                let mut evaled = Vec::with_capacity(args.len());
901                for a in args {
902                    evaled.push(eval_inner(a, env, io.as_deref_mut())?);
903                }
904                return match io.as_deref_mut() {
905                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
906                        Some(hook) => hook(&qualified, &val, &evaled, env, io_ref),
907                        None => Err(format!("'{qualified}': exec::init() not called")),
908                    }),
909                    None => {
910                        let mut tmp_io = IoContext::new();
911                        FN_CALL_HOOK.with(|c| match c.get() {
912                            Some(hook) => hook(&qualified, &val, &evaled, env, &mut tmp_io),
913                            None => Err(format!("'{qualified}': exec::init() not called")),
914                        })
915                    }
916                };
917            }
918            Err(format!("Unknown package function: '{qualified}'"))
919        }
920        Expr::FuncHandle(name) => {
921            let name = name.clone();
922            let captured_env = env.clone();
923            let src = format!("@{name}");
924            let lambda = LambdaFn(
925                std::rc::Rc::new(move |args: &[Value], io: Option<&mut IoContext>| {
926                    // First try the environment (user-defined function), then fall back to builtin.
927                    if let Some(f) = captured_env.get(&name) {
928                        let f = f.clone();
929                        call_function_value(&f, args, io)
930                    } else {
931                        call_builtin(&name, args, &captured_env, io)
932                    }
933                }),
934                src,
935            );
936            Ok(Value::Lambda(lambda))
937        }
938        Expr::PlainTranspose(e) => match eval_inner(e, env, io)? {
939            Value::Void => Err("Transpose is not applicable to void".to_string()),
940            Value::Scalar(n) => Ok(Value::Scalar(n)),
941            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
942            // Plain transpose: no conjugation — imaginary part unchanged
943            Value::Complex(re, im) => Ok(Value::Complex(re, im)),
944            Value::Str(s) => Ok(Value::Str(s)),
945            Value::StringObj(s) => Ok(Value::StringObj(s)),
946            Value::Lambda(_)
947            | Value::Function { .. }
948            | Value::Tuple(_)
949            | Value::Cell(_)
950            | Value::Struct(_)
951            | Value::StructArray(_) => Err("Transpose is not applicable to this type".to_string()),
952        },
953        Expr::Colon => Err("':' is only valid inside index expressions".to_string()),
954        Expr::Matrix(rows) => {
955            if rows.is_empty() {
956                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
957            }
958            // Each literal row is evaluated into a block (Array2).
959            // Elements within a row are horizontally concatenated;
960            // rows are then vertically concatenated.
961            let mut row_blocks: Vec<Array2<f64>> = Vec::with_capacity(rows.len());
962            for row in rows {
963                if row.is_empty() {
964                    continue;
965                }
966                let mut elem_mats: Vec<Array2<f64>> = Vec::with_capacity(row.len());
967                for elem_expr in row {
968                    match eval_inner(elem_expr, env, io.as_deref_mut())? {
969                        Value::Scalar(n) => elem_mats.push(Array2::from_elem((1, 1), n)),
970                        Value::Matrix(m) => elem_mats.push(m),
971                        Value::Void => {
972                            return Err("Void value cannot be used in matrix literal".to_string());
973                        }
974                        Value::Complex(_, _) => {
975                            return Err(
976                                "Complex elements in matrix literals are not supported yet"
977                                    .to_string(),
978                            );
979                        }
980                        Value::Str(_) | Value::StringObj(_) => {
981                            return Err(
982                                "String elements in matrix literals are not supported".to_string()
983                            );
984                        }
985                        Value::Lambda(_)
986                        | Value::Function { .. }
987                        | Value::Tuple(_)
988                        | Value::Cell(_)
989                        | Value::Struct(_)
990                        | Value::StructArray(_) => {
991                            return Err("Struct/function values cannot be used in matrix literals"
992                                .to_string());
993                        }
994                    }
995                }
996                // All elements in this row must have the same number of rows.
997                let nrows = elem_mats[0].nrows();
998                for (i, m) in elem_mats.iter().enumerate().skip(1) {
999                    if m.nrows() != nrows {
1000                        return Err(format!(
1001                            "Matrix row height mismatch: expected {} rows, element {} has {} rows",
1002                            nrows,
1003                            i + 1,
1004                            m.nrows()
1005                        ));
1006                    }
1007                }
1008                // Horizontal concatenation: collect each physical row across all blocks.
1009                let ncols: usize = elem_mats.iter().map(|m| m.ncols()).sum();
1010                let mut flat: Vec<f64> = Vec::with_capacity(nrows * ncols);
1011                for r in 0..nrows {
1012                    for m in &elem_mats {
1013                        flat.extend(m.row(r).iter().copied());
1014                    }
1015                }
1016                row_blocks.push(
1017                    Array2::from_shape_vec((nrows, ncols), flat)
1018                        .map_err(|e| format!("Matrix shape error: {e}"))?,
1019                );
1020            }
1021            if row_blocks.is_empty() {
1022                return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
1023            }
1024            let ncols = row_blocks[0].ncols();
1025            if ncols == 0 {
1026                let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1027                return Ok(Value::Matrix(Array2::zeros((total_rows, 0))));
1028            }
1029            // All row blocks must have the same number of columns.
1030            for (i, blk) in row_blocks.iter().enumerate().skip(1) {
1031                if blk.ncols() != ncols {
1032                    return Err(format!(
1033                        "Matrix column count mismatch: expected {} columns, row {} has {} columns",
1034                        ncols,
1035                        i + 1,
1036                        blk.ncols()
1037                    ));
1038                }
1039            }
1040            // Vertical concatenation.
1041            let total_rows: usize = row_blocks.iter().map(|b| b.nrows()).sum();
1042            let mut flat: Vec<f64> = Vec::with_capacity(total_rows * ncols);
1043            for blk in &row_blocks {
1044                flat.extend(blk.iter().copied());
1045            }
1046            let m = Array2::from_shape_vec((total_rows, ncols), flat)
1047                .map_err(|e| format!("Matrix shape error: {e}"))?;
1048            Ok(Value::Matrix(m))
1049        }
1050        Expr::Transpose(e) => match eval_inner(e, env, io)? {
1051            Value::Void => Err("Transpose is not applicable to void".to_string()),
1052            Value::Scalar(n) => Ok(Value::Scalar(n)),
1053            Value::Matrix(m) => Ok(Value::Matrix(m.t().to_owned())),
1054            Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
1055            // Transpose of a char array or string object: return as-is (1×N not fully supported)
1056            Value::Str(s) => Ok(Value::Str(s)),
1057            Value::StringObj(s) => Ok(Value::StringObj(s)),
1058            Value::Lambda(_)
1059            | Value::Function { .. }
1060            | Value::Tuple(_)
1061            | Value::Cell(_)
1062            | Value::Struct(_)
1063            | Value::StructArray(_) => Err("Transpose is not applicable to this type".to_string()),
1064        },
1065        Expr::StrLiteral(s) => Ok(Value::Str(s.clone())),
1066        Expr::StringObjLiteral(s) => Ok(Value::StringObj(s.clone())),
1067        Expr::Range(start_expr, step_expr, stop_expr) => {
1068            let start = match eval_inner(start_expr, env, io.as_deref_mut())? {
1069                Value::Scalar(n) => n,
1070                Value::Void
1071                | Value::Matrix(_)
1072                | Value::Complex(_, _)
1073                | Value::Str(_)
1074                | Value::StringObj(_)
1075                | Value::Lambda(_)
1076                | Value::Function { .. }
1077                | Value::Tuple(_)
1078                | Value::Cell(_)
1079                | Value::Struct(_)
1080                | Value::StructArray(_) => {
1081                    return Err("Range bounds must be real scalars".to_string());
1082                }
1083            };
1084            let stop = match eval_inner(stop_expr, env, io.as_deref_mut())? {
1085                Value::Scalar(n) => n,
1086                Value::Void
1087                | Value::Matrix(_)
1088                | Value::Complex(_, _)
1089                | Value::Str(_)
1090                | Value::StringObj(_)
1091                | Value::Lambda(_)
1092                | Value::Function { .. }
1093                | Value::Tuple(_)
1094                | Value::Cell(_)
1095                | Value::Struct(_)
1096                | Value::StructArray(_) => {
1097                    return Err("Range bounds must be real scalars".to_string());
1098                }
1099            };
1100            let step = match step_expr {
1101                None => 1.0,
1102                Some(s) => match eval_inner(s, env, io)? {
1103                    Value::Scalar(n) => n,
1104                    Value::Void
1105                    | Value::Matrix(_)
1106                    | Value::Complex(_, _)
1107                    | Value::Str(_)
1108                    | Value::StringObj(_)
1109                    | Value::Lambda(_)
1110                    | Value::Function { .. }
1111                    | Value::Tuple(_)
1112                    | Value::Cell(_)
1113                    | Value::Struct(_)
1114                    | Value::StructArray(_) => {
1115                        return Err("Range step must be a real scalar".to_string());
1116                    }
1117                },
1118            };
1119            if step == 0.0 {
1120                return Err("Range step cannot be zero".to_string());
1121            }
1122            let n_float = (stop - start) / step;
1123            if n_float < -1e-10 {
1124                // Empty range: step points in the wrong direction
1125                return Ok(Value::Matrix(Array2::zeros((1, 0))));
1126            }
1127            let n = (n_float + 1e-10).floor() as usize + 1;
1128            let vals: Vec<f64> = (0..n).map(|i| start + i as f64 * step).collect();
1129            let m =
1130                Array2::from_shape_vec((1, n), vals).map_err(|e| format!("Range error: {e}"))?;
1131            Ok(Value::Matrix(m))
1132        }
1133    }
1134}
1135
1136fn eval_binop(l: Value, op: &Op, r: Value) -> Result<Value, String> {
1137    match (l, r) {
1138        (Value::Void, _) | (_, Value::Void) => {
1139            Err("Cannot apply operator to void value".to_string())
1140        }
1141        // --- String object operations ---
1142        (Value::StringObj(a), Value::StringObj(b)) => match op {
1143            Op::Add => Ok(Value::StringObj(a + &b)),
1144            Op::Eq => Ok(Value::Scalar(bool_to_f64(a == b))),
1145            Op::NotEq => Ok(Value::Scalar(bool_to_f64(a != b))),
1146            _ => Err("Operator not supported on string objects".to_string()),
1147        },
1148        // Char array: convert to numeric, re-dispatch
1149        (Value::Str(s), r) => eval_binop(str_to_numeric(&s), op, r),
1150        (l, Value::Str(s)) => eval_binop(l, op, str_to_numeric(&s)),
1151        // String object mixed with other types: error
1152        (Value::StringObj(_), _) | (_, Value::StringObj(_)) => {
1153            Err("String object cannot be combined with non-string values".to_string())
1154        }
1155        // Functions, tuples, cell arrays, structs, and struct arrays are not numeric
1156        (Value::Lambda(_), _)
1157        | (_, Value::Lambda(_))
1158        | (Value::Function { .. }, _)
1159        | (_, Value::Function { .. })
1160        | (Value::Tuple(_), _)
1161        | (_, Value::Tuple(_))
1162        | (Value::Cell(_), _)
1163        | (_, Value::Cell(_))
1164        | (Value::Struct(_), _)
1165        | (_, Value::Struct(_))
1166        | (Value::StructArray(_), _)
1167        | (_, Value::StructArray(_)) => Err("Cannot apply operator to a struct value".to_string()),
1168        // --- Complex arithmetic ---
1169        (Value::Complex(re1, im1), Value::Complex(re2, im2)) => {
1170            complex_binop(re1, im1, op, re2, im2)
1171        }
1172        (Value::Complex(re, im), Value::Scalar(s)) => complex_binop(re, im, op, s, 0.0),
1173        (Value::Scalar(s), Value::Complex(re, im)) => complex_binop(s, 0.0, op, re, im),
1174        (Value::Complex(_, _), Value::Matrix(_)) | (Value::Matrix(_), Value::Complex(_, _)) => {
1175            Err("Operations between complex scalars and matrices are not supported".to_string())
1176        }
1177        (Value::Scalar(lv), Value::Scalar(rv)) => {
1178            let result = match op {
1179                Op::Add => lv + rv,
1180                Op::Sub => lv - rv,
1181                Op::Mul | Op::ElemMul => lv * rv,
1182                Op::Div | Op::ElemDiv => {
1183                    if rv == 0.0 {
1184                        return Err("Division by zero".to_string());
1185                    }
1186                    lv / rv
1187                }
1188                Op::LDiv => {
1189                    if lv == 0.0 {
1190                        return Err("Left division by zero (a \\ b requires a ≠ 0)".to_string());
1191                    }
1192                    rv / lv
1193                }
1194                Op::Pow | Op::ElemPow => lv.powf(rv),
1195                Op::Eq => bool_to_f64(lv == rv),
1196                Op::NotEq => bool_to_f64(lv != rv),
1197                Op::Lt => bool_to_f64(lv < rv),
1198                Op::Gt => bool_to_f64(lv > rv),
1199                Op::LtEq => bool_to_f64(lv <= rv),
1200                Op::GtEq => bool_to_f64(lv >= rv),
1201                Op::And | Op::ElemAnd => bool_to_f64(lv != 0.0 && rv != 0.0),
1202                Op::Or | Op::ElemOr => bool_to_f64(lv != 0.0 || rv != 0.0),
1203            };
1204            Ok(Value::Scalar(result))
1205        }
1206        (Value::Matrix(lm), Value::Matrix(rm)) => match op {
1207            Op::Add => {
1208                check_same_shape(&lm, &rm)?;
1209                Ok(Value::Matrix(&lm + &rm))
1210            }
1211            Op::Sub => {
1212                check_same_shape(&lm, &rm)?;
1213                Ok(Value::Matrix(&lm - &rm))
1214            }
1215            Op::Mul => {
1216                if lm.ncols() != rm.nrows() {
1217                    return Err(format!(
1218                        "Inner dimensions must agree: {}x{} * {}x{}",
1219                        lm.nrows(),
1220                        lm.ncols(),
1221                        rm.nrows(),
1222                        rm.ncols()
1223                    ));
1224                }
1225                Ok(Value::Matrix(lm.dot(&rm)))
1226            }
1227            Op::ElemMul => {
1228                check_same_shape(&lm, &rm)?;
1229                Ok(Value::Matrix(&lm * &rm))
1230            }
1231            Op::ElemDiv => {
1232                check_same_shape(&lm, &rm)?;
1233                Ok(Value::Matrix(&lm / &rm))
1234            }
1235            Op::ElemPow => {
1236                check_same_shape(&lm, &rm)?;
1237                Ok(Value::Matrix(
1238                    ndarray::Zip::from(&lm)
1239                        .and(&rm)
1240                        .map_collect(|a, b| a.powf(*b)),
1241                ))
1242            }
1243            Op::Eq | Op::NotEq | Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1244                check_same_shape(&lm, &rm)?;
1245                Ok(Value::Matrix(
1246                    ndarray::Zip::from(&lm)
1247                        .and(&rm)
1248                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1249                ))
1250            }
1251            Op::And | Op::Or | Op::ElemAnd | Op::ElemOr => {
1252                check_same_shape(&lm, &rm)?;
1253                Ok(Value::Matrix(
1254                    ndarray::Zip::from(&lm)
1255                        .and(&rm)
1256                        .map_collect(|a, b| bool_to_f64(cmp_op(op, *a, *b))),
1257                ))
1258            }
1259            Op::Div => Err("Matrix / Matrix: use inv(B)*A or A*inv(B)".to_string()),
1260            Op::LDiv => Ok(Value::Matrix(solve_linear(&lm, &rm)?)),
1261            Op::Pow => Err("Matrix ^ Matrix: not supported".to_string()),
1262        },
1263        (Value::Scalar(s), Value::Matrix(m)) => match op {
1264            Op::Add => Ok(Value::Matrix(s + &m)),
1265            Op::Sub => Ok(Value::Matrix(m.mapv(|x| s - x))),
1266            Op::Mul | Op::ElemMul => Ok(Value::Matrix(s * &m)),
1267            Op::Div => Err("Scalar / Matrix: not supported".to_string()),
1268            Op::ElemDiv => Err("Scalar ./ Matrix: not supported".to_string()),
1269            Op::LDiv => {
1270                if s == 0.0 {
1271                    return Err("Left division by zero (a \\ B requires a ≠ 0)".to_string());
1272                }
1273                Ok(Value::Matrix(m.mapv(|x| x / s)))
1274            }
1275            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| s.powf(x)))),
1276            Op::Eq
1277            | Op::NotEq
1278            | Op::Lt
1279            | Op::Gt
1280            | Op::LtEq
1281            | Op::GtEq
1282            | Op::And
1283            | Op::Or
1284            | Op::ElemAnd
1285            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, s, x))))),
1286        },
1287        (Value::Matrix(m), Value::Scalar(s)) => match op {
1288            Op::Add => Ok(Value::Matrix(&m + s)),
1289            Op::Sub => Ok(Value::Matrix(&m - s)),
1290            Op::Mul | Op::ElemMul => Ok(Value::Matrix(&m * s)),
1291            Op::Div | Op::ElemDiv => Ok(Value::Matrix(m.mapv(|x| x / s))),
1292            Op::LDiv => {
1293                let b = Array2::from_elem((m.nrows(), 1), s);
1294                Ok(Value::Matrix(solve_linear(&m, &b)?))
1295            }
1296            Op::Pow | Op::ElemPow => Ok(Value::Matrix(m.mapv(|x| x.powf(s)))),
1297            Op::Eq
1298            | Op::NotEq
1299            | Op::Lt
1300            | Op::Gt
1301            | Op::LtEq
1302            | Op::GtEq
1303            | Op::And
1304            | Op::Or
1305            | Op::ElemAnd
1306            | Op::ElemOr => Ok(Value::Matrix(m.mapv(|x| bool_to_f64(cmp_op(op, x, s))))),
1307        },
1308    }
1309}
1310
1311#[inline]
1312fn bool_to_f64(b: bool) -> f64 {
1313    if b { 1.0 } else { 0.0 }
1314}
1315
1316/// Applies a comparison or logical op to two scalar values.
1317fn cmp_op(op: &Op, a: f64, b: f64) -> bool {
1318    match op {
1319        Op::Eq => a == b,
1320        Op::NotEq => a != b,
1321        Op::Lt => a < b,
1322        Op::Gt => a > b,
1323        Op::LtEq => a <= b,
1324        Op::GtEq => a >= b,
1325        Op::And | Op::ElemAnd => a != 0.0 && b != 0.0,
1326        Op::Or | Op::ElemOr => a != 0.0 || b != 0.0,
1327        _ => unreachable!(),
1328    }
1329}
1330
1331/// Performs binary operations on two complex numbers `(re1+im1*i) OP (re2+im2*i)`.
1332fn complex_binop(re1: f64, im1: f64, op: &Op, re2: f64, im2: f64) -> Result<Value, String> {
1333    match op {
1334        Op::Add => Ok(make_complex(re1 + re2, im1 + im2)),
1335        Op::Sub => Ok(make_complex(re1 - re2, im1 - im2)),
1336        Op::Mul | Op::ElemMul => {
1337            // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
1338            Ok(make_complex(re1 * re2 - im1 * im2, re1 * im2 + im1 * re2))
1339        }
1340        Op::Div | Op::ElemDiv => {
1341            // (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
1342            let denom = re2 * re2 + im2 * im2;
1343            if denom == 0.0 {
1344                return Err("Division by zero (complex)".to_string());
1345            }
1346            Ok(make_complex(
1347                (re1 * re2 + im1 * im2) / denom,
1348                (im1 * re2 - re1 * im2) / denom,
1349            ))
1350        }
1351        Op::Pow | Op::ElemPow => {
1352            let r1 = (re1 * re1 + im1 * im1).sqrt();
1353            if r1 == 0.0 {
1354                if re2 > 0.0 {
1355                    return Ok(Value::Scalar(0.0));
1356                }
1357                return Ok(Value::Complex(f64::NAN, f64::NAN));
1358            }
1359            // For integer exponents with zero imaginary part, use repeated multiplication
1360            // to avoid polar-form floating-point error (e.g. i^2 = -1 exactly).
1361            if im2 == 0.0 && re2.fract() == 0.0 && re2.abs() < 1_000_000.0 {
1362                let n = re2 as i64;
1363                if n == 0 {
1364                    return Ok(Value::Scalar(1.0));
1365                }
1366                // positive power: repeated squaring
1367                let abs_n = n.unsigned_abs();
1368                let (mut rr, mut ri) = (1.0_f64, 0.0_f64);
1369                let (mut br, mut bi) = (re1, im1);
1370                let mut exp = abs_n;
1371                while exp > 0 {
1372                    if exp & 1 == 1 {
1373                        let nr = rr * br - ri * bi;
1374                        let ni = rr * bi + ri * br;
1375                        rr = nr;
1376                        ri = ni;
1377                    }
1378                    let nr = br * br - bi * bi;
1379                    let ni = 2.0 * br * bi;
1380                    br = nr;
1381                    bi = ni;
1382                    exp >>= 1;
1383                }
1384                if n < 0 {
1385                    // invert: 1/(rr+ri*i)
1386                    let denom = rr * rr + ri * ri;
1387                    return Ok(make_complex(rr / denom, -ri / denom));
1388                }
1389                return Ok(make_complex(rr, ri));
1390            }
1391            // General case: via polar form exp((c+di) * ln(a+bi))
1392            let theta1 = im1.atan2(re1);
1393            let ln_r1 = r1.ln();
1394            let exp_re = re2 * ln_r1 - im2 * theta1;
1395            let exp_im = im2 * ln_r1 + re2 * theta1;
1396            let mag = exp_re.exp();
1397            Ok(make_complex(mag * exp_im.cos(), mag * exp_im.sin()))
1398        }
1399        Op::Eq => Ok(Value::Scalar(bool_to_f64(re1 == re2 && im1 == im2))),
1400        Op::NotEq => Ok(Value::Scalar(bool_to_f64(re1 != re2 || im1 != im2))),
1401        Op::Lt | Op::Gt | Op::LtEq | Op::GtEq => {
1402            Err("Ordering is not defined for complex numbers".to_string())
1403        }
1404        Op::And | Op::ElemAnd => Ok(Value::Scalar(bool_to_f64(
1405            (re1 != 0.0 || im1 != 0.0) && (re2 != 0.0 || im2 != 0.0),
1406        ))),
1407        Op::Or | Op::ElemOr => Ok(Value::Scalar(bool_to_f64(
1408            re1 != 0.0 || im1 != 0.0 || re2 != 0.0 || im2 != 0.0,
1409        ))),
1410        Op::LDiv => Err("Left division (\\) is not supported for complex numbers".to_string()),
1411    }
1412}
1413
1414/// Constructs a `Value::Complex` or collapses to `Value::Scalar` when `im` is exactly zero.
1415#[inline]
1416fn make_complex(re: f64, im: f64) -> Value {
1417    if im == 0.0 {
1418        Value::Scalar(re)
1419    } else {
1420        Value::Complex(re, im)
1421    }
1422}
1423
1424/// Converts a char array string to its numeric representation.
1425/// Single char → Scalar(code), multi-char → 1×N Matrix, empty → 1×0 Matrix.
1426fn str_to_numeric(s: &str) -> Value {
1427    let codes: Vec<f64> = s.chars().map(|c| c as u32 as f64).collect();
1428    match codes.len() {
1429        0 => Value::Matrix(Array2::zeros((1, 0))),
1430        1 => Value::Scalar(codes[0]),
1431        n => Value::Matrix(Array2::from_shape_vec((1, n), codes).unwrap()),
1432    }
1433}
1434
1435/// Extracts a string slice from a Str or StringObj value.
1436fn string_arg<'a>(v: &'a Value, fname: &str, pos: usize) -> Result<&'a str, String> {
1437    match v {
1438        Value::Str(s) | Value::StringObj(s) => Ok(s.as_str()),
1439        _ => Err(format!(
1440            "Function '{fname}' argument {pos} must be a string"
1441        )),
1442    }
1443}
1444
1445fn check_same_shape(lm: &Array2<f64>, rm: &Array2<f64>) -> Result<(), String> {
1446    if lm.shape() != rm.shape() {
1447        return Err(format!(
1448            "Matrix size mismatch: {}x{} vs {}x{}",
1449            lm.nrows(),
1450            lm.ncols(),
1451            rm.nrows(),
1452            rm.ncols()
1453        ));
1454    }
1455    Ok(())
1456}
1457
1458fn scalar_arg(v: &Value, fname: &str, pos: usize) -> Result<f64, String> {
1459    match v {
1460        Value::Void => Err(format!(
1461            "Function '{fname}' argument {pos} must be a scalar, got void"
1462        )),
1463        Value::Scalar(n) => Ok(*n),
1464        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
1465        Value::Complex(_, _) => Err(format!(
1466            "Function '{fname}' argument {pos} must be real, got a complex number"
1467        )),
1468        Value::Matrix(_) => Err(format!(
1469            "Function '{fname}' argument {pos} must be a scalar, got a matrix"
1470        )),
1471        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
1472        Value::Str(_) | Value::StringObj(_) => Err(format!(
1473            "Function '{fname}' argument {pos} must be a scalar, got a string"
1474        )),
1475        Value::Lambda(_)
1476        | Value::Function { .. }
1477        | Value::Tuple(_)
1478        | Value::Cell(_)
1479        | Value::Struct(_)
1480        | Value::StructArray(_) => Err(format!(
1481            "Function '{fname}' argument {pos} must be a scalar, got a non-numeric value"
1482        )),
1483    }
1484}
1485
1486/// Applies a scalar function element-wise to a scalar or matrix.
1487/// Parses the first argument of `randi` into an inclusive `[lo, hi]` integer range.
1488///
1489/// Accepts either a scalar `max` (→ `[1, max]`) or a 1×2 / 2×1 vector `[min, max]`.
1490fn randi_range(v: &Value) -> Result<(i64, i64), String> {
1491    match v {
1492        Value::Scalar(n) => {
1493            let hi = *n as i64;
1494            if hi < 1 {
1495                return Err("randi: max must be a positive integer".to_string());
1496            }
1497            Ok((1, hi))
1498        }
1499        Value::Matrix(m) if m.len() == 2 => {
1500            let vals: Vec<f64> = m.iter().copied().collect();
1501            let lo = vals[0] as i64;
1502            let hi = vals[1] as i64;
1503            if lo > hi {
1504                return Err("randi: [min, max] range is empty".to_string());
1505            }
1506            Ok((lo, hi))
1507        }
1508        _ => Err("randi: first argument must be a scalar max or a [min, max] vector".to_string()),
1509    }
1510}
1511
1512// ── Descriptive statistics helpers ───────────────────────────────────────────
1513
1514/// Extracts a flat `Vec<f64>` from a `Scalar` or `Matrix` value.
1515fn numeric_vec(v: &Value, fname: &str) -> Result<Vec<f64>, String> {
1516    match v {
1517        Value::Scalar(n) => Ok(vec![*n]),
1518        Value::Matrix(m) => Ok(m.iter().copied().collect()),
1519        _ => Err(format!("{fname}: argument must be numeric")),
1520    }
1521}
1522
1523/// Computes the variance of a slice.  Returns `NaN` for empty, `0.0` for singletons.
1524/// `population = true` divides by N; `false` divides by N-1.
1525fn stat_var_vec(vals: &[f64], population: bool) -> f64 {
1526    let n = vals.len();
1527    if n == 0 {
1528        return f64::NAN;
1529    }
1530    if n == 1 {
1531        return 0.0;
1532    }
1533    let mean = vals.iter().sum::<f64>() / n as f64;
1534    let ss: f64 = vals.iter().map(|&x| (x - mean).powi(2)).sum();
1535    let denom = if population { n as f64 } else { (n - 1) as f64 };
1536    ss / denom
1537}
1538
1539/// Applies a column-wise statistical closure returning one scalar per column.
1540///
1541/// - Scalar → passes `[n]` to `f`.
1542/// - Vector (1×N or N×1) → scalar.
1543/// - M×N matrix (M>1, N>1) → 1×N row vector.
1544fn apply_stat<F>(v: &Value, mut f: F, fname: &str) -> Result<Value, String>
1545where
1546    F: FnMut(&[f64]) -> f64,
1547{
1548    match v {
1549        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
1550        Value::Matrix(m) => {
1551            if m.nrows() == 1 || m.ncols() == 1 {
1552                let vals: Vec<f64> = m.iter().copied().collect();
1553                Ok(Value::Scalar(f(&vals)))
1554            } else {
1555                let ncols = m.ncols();
1556                let result: Vec<f64> = (0..ncols)
1557                    .map(|c| {
1558                        let col: Vec<f64> = m.column(c).iter().copied().collect();
1559                        f(&col)
1560                    })
1561                    .collect();
1562                Ok(Value::Matrix(
1563                    Array2::from_shape_vec((1, ncols), result).unwrap(),
1564                ))
1565            }
1566        }
1567        _ => Err(format!("{fname}: argument must be numeric")),
1568    }
1569}
1570
1571/// Computes the p-th percentile (0–100) of a pre-sorted slice via linear interpolation.
1572fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
1573    let n = sorted.len();
1574    if n == 0 {
1575        return f64::NAN;
1576    }
1577    if n == 1 {
1578        return sorted[0];
1579    }
1580    let p = p.clamp(0.0, 100.0);
1581    let idx = p / 100.0 * (n - 1) as f64;
1582    let lo = idx.floor() as usize;
1583    let hi = idx.ceil() as usize;
1584    let frac = idx - lo as f64;
1585    sorted[lo] * (1.0 - frac) + sorted[hi] * frac
1586}
1587
1588fn apply_elem<F: Fn(f64) -> f64>(v: &Value, f: F) -> Result<Value, String> {
1589    match v {
1590        Value::Void => Err("Element-wise function not applicable to void".to_string()),
1591        Value::Scalar(n) => Ok(Value::Scalar(f(*n))),
1592        Value::Matrix(m) => Ok(Value::Matrix(m.mapv(f))),
1593        Value::Complex(re, im) if *im == 0.0 => Ok(Value::Scalar(f(*re))),
1594        Value::Complex(_, _) => {
1595            Err("Element-wise real function not applicable to complex values".to_string())
1596        }
1597        Value::Str(_) | Value::StringObj(_) => {
1598            Err("Element-wise function not applicable to strings".to_string())
1599        }
1600        Value::Lambda(_)
1601        | Value::Function { .. }
1602        | Value::Tuple(_)
1603        | Value::Cell(_)
1604        | Value::Struct(_)
1605        | Value::StructArray(_) => {
1606            Err("Element-wise function not applicable to this type".to_string())
1607        }
1608    }
1609}
1610
1611/// Reduces a scalar or matrix to a scalar (for vectors) or 1×N row vector (for M×N matrices).
1612///
1613/// - Scalar → apply `f` to `[n]`.
1614/// - Vector (1×N or N×1) → apply `f` to all elements, return scalar.
1615/// - M×N matrix (M>1, N>1) → apply `f` column-wise, return 1×N row vector.
1616fn apply_reduction<F>(v: &Value, f: F) -> Result<Value, String>
1617where
1618    F: Fn(&[f64]) -> f64,
1619{
1620    match v {
1621        Value::Void => Err("Reduction not applicable to void".to_string()),
1622        Value::Scalar(n) => Ok(Value::Scalar(f(&[*n]))),
1623        Value::Complex(_, _) => Err("Reduction not applicable to complex values".to_string()),
1624        Value::Str(_) | Value::StringObj(_) => {
1625            Err("Reduction not applicable to strings".to_string())
1626        }
1627        Value::Lambda(_)
1628        | Value::Function { .. }
1629        | Value::Tuple(_)
1630        | Value::Cell(_)
1631        | Value::Struct(_)
1632        | Value::StructArray(_) => Err("Reduction not applicable to this type".to_string()),
1633        Value::Matrix(m) => {
1634            if m.nrows() == 1 || m.ncols() == 1 {
1635                let vals: Vec<f64> = m.iter().copied().collect();
1636                Ok(Value::Scalar(f(&vals)))
1637            } else {
1638                let ncols = m.ncols();
1639                let result: Vec<f64> = (0..ncols)
1640                    .map(|c| {
1641                        let col: Vec<f64> = m.column(c).iter().copied().collect();
1642                        f(&col)
1643                    })
1644                    .collect();
1645                Ok(Value::Matrix(
1646                    Array2::from_shape_vec((1, ncols), result).unwrap(),
1647                ))
1648            }
1649        }
1650    }
1651}
1652
1653/// Computes a cumulative scan (cumsum / cumprod) along a vector or column-wise on a matrix.
1654///
1655/// `combine(accumulator, element) -> new_accumulator` — e.g. `|a, x| a + x` for cumsum.
1656fn apply_cumulative<F>(v: &Value, combine: F) -> Result<Value, String>
1657where
1658    F: Fn(f64, f64) -> f64,
1659{
1660    match v {
1661        Value::Void => Err("Cumulative reduction not applicable to void".to_string()),
1662        Value::Scalar(n) => Ok(Value::Scalar(*n)),
1663        Value::Complex(_, _) => {
1664            Err("Cumulative reduction not applicable to complex values".to_string())
1665        }
1666        Value::Str(_) | Value::StringObj(_) => {
1667            Err("Cumulative reduction not applicable to strings".to_string())
1668        }
1669        Value::Lambda(_)
1670        | Value::Function { .. }
1671        | Value::Tuple(_)
1672        | Value::Cell(_)
1673        | Value::Struct(_)
1674        | Value::StructArray(_) => {
1675            Err("Cumulative reduction not applicable to this type".to_string())
1676        }
1677        Value::Matrix(m) => {
1678            let initial = combine(0.0, 0.0); // detect identity: 0+0=0 or 0*0=0
1679            // Use 0.0 as additive identity, 1.0 as multiplicative identity.
1680            // We detect the identity from f(1.0, 1.0) vs f(0.0, 0.0).
1681            let identity = if (combine(1.0, 1.0) - 1.0).abs() < 1e-15 && initial == 0.0 {
1682                1.0 // product
1683            } else {
1684                0.0 // sum
1685            };
1686            let (nrows, ncols) = (m.nrows(), m.ncols());
1687            let mut result = m.clone();
1688            if nrows == 1 || ncols == 1 {
1689                // Vector: scan along all elements in order
1690                let mut acc = identity;
1691                for v in result.iter_mut() {
1692                    acc = combine(acc, *v);
1693                    *v = acc;
1694                }
1695            } else {
1696                // Matrix: scan each column independently
1697                for c in 0..ncols {
1698                    let mut acc = identity;
1699                    for r in 0..nrows {
1700                        acc = combine(acc, result[[r, c]]);
1701                        result[[r, c]] = acc;
1702                    }
1703                }
1704            }
1705            Ok(Value::Matrix(result))
1706        }
1707    }
1708}
1709
1710/// Returns column-major 1-based indices of non-zero elements, up to `max_k`.
1711fn find_nonzero(v: &Value, max_k: usize) -> Result<Value, String> {
1712    match v {
1713        Value::Void => Err("find: not applicable to void".to_string()),
1714        Value::Str(_) | Value::StringObj(_) => Err("find: not applicable to strings".to_string()),
1715        Value::Lambda(_)
1716        | Value::Function { .. }
1717        | Value::Tuple(_)
1718        | Value::Cell(_)
1719        | Value::Struct(_)
1720        | Value::StructArray(_) => Err("find: not applicable to this type".to_string()),
1721        Value::Complex(re, im) => {
1722            if (*re != 0.0 || *im != 0.0) && max_k >= 1 {
1723                Ok(Value::Matrix(
1724                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
1725                ))
1726            } else {
1727                Ok(Value::Matrix(Array2::zeros((1, 0))))
1728            }
1729        }
1730        Value::Scalar(n) => {
1731            if *n != 0.0 && max_k >= 1 {
1732                Ok(Value::Matrix(
1733                    Array2::from_shape_vec((1, 1), vec![1.0]).unwrap(),
1734                ))
1735            } else {
1736                Ok(Value::Matrix(Array2::zeros((1, 0))))
1737            }
1738        }
1739        Value::Matrix(m) => {
1740            let nrows = m.nrows();
1741            let total = m.len();
1742            let mut idxs: Vec<f64> = Vec::new();
1743            for i in 0..total {
1744                if idxs.len() >= max_k {
1745                    break;
1746                }
1747                let row = i % nrows;
1748                let col = i / nrows;
1749                if m[[row, col]] != 0.0 {
1750                    idxs.push((i + 1) as f64);
1751                }
1752            }
1753            let n = idxs.len();
1754            if n == 0 {
1755                Ok(Value::Matrix(Array2::zeros((1, 0))))
1756            } else {
1757                Ok(Value::Matrix(Array2::from_shape_vec((1, n), idxs).unwrap()))
1758            }
1759        }
1760    }
1761}
1762
1763// ---------------------------------------------------------------------------
1764// C-style printf format engine
1765// ---------------------------------------------------------------------------
1766
1767/// Formats `args` using a C-style `fmt` string.
1768///
1769/// Supported specifiers: `%d` `%i` `%f` `%e` `%g` `%s` `%%`.
1770/// Flags: `-` (left-align), `+` (force sign), `0` (zero-pad), ` ` (space sign).
1771/// Width and `.precision` follow standard C `printf` conventions.
1772/// Escape sequences `\n` `\t` `\\` are also processed.
1773///
1774/// Octave behaviour: if `args` is longer than the number of specifiers the
1775/// format string is repeated until all args are consumed.
1776pub fn format_printf(fmt: &str, args: &[Value]) -> Result<String, String> {
1777    let mut result = String::new();
1778    let mut arg_idx = 0;
1779
1780    loop {
1781        let consumed_before = arg_idx;
1782        let mut chars = fmt.chars().peekable();
1783
1784        while let Some(c) = chars.next() {
1785            if c == '\\' {
1786                match chars.next() {
1787                    Some('n') => result.push('\n'),
1788                    Some('t') => result.push('\t'),
1789                    Some('\\') => result.push('\\'),
1790                    Some('\'') => result.push('\''),
1791                    Some('"') => result.push('"'),
1792                    Some(other) => {
1793                        result.push('\\');
1794                        result.push(other);
1795                    }
1796                    None => result.push('\\'),
1797                }
1798                continue;
1799            }
1800
1801            if c != '%' {
1802                result.push(c);
1803                continue;
1804            }
1805
1806            // `%%` → literal `%`
1807            if chars.peek() == Some(&'%') {
1808                chars.next();
1809                result.push('%');
1810                continue;
1811            }
1812
1813            // Parse flags
1814            let mut flag_minus = false;
1815            let mut flag_plus = false;
1816            let mut flag_zero = false;
1817            let mut flag_space = false;
1818            loop {
1819                match chars.peek() {
1820                    Some('-') => {
1821                        flag_minus = true;
1822                        chars.next();
1823                    }
1824                    Some('+') => {
1825                        flag_plus = true;
1826                        chars.next();
1827                    }
1828                    Some('0') => {
1829                        flag_zero = true;
1830                        chars.next();
1831                    }
1832                    Some(' ') => {
1833                        flag_space = true;
1834                        chars.next();
1835                    }
1836                    _ => break,
1837                }
1838            }
1839
1840            // Parse width
1841            let mut width_str = String::new();
1842            while let Some(&d) = chars.peek() {
1843                if d.is_ascii_digit() {
1844                    width_str.push(d);
1845                    chars.next();
1846                } else {
1847                    break;
1848                }
1849            }
1850            let width: usize = width_str.parse().unwrap_or(0);
1851
1852            // Parse precision
1853            let mut precision: Option<usize> = None;
1854            if chars.peek() == Some(&'.') {
1855                chars.next();
1856                let mut p = String::new();
1857                while let Some(&d) = chars.peek() {
1858                    if d.is_ascii_digit() {
1859                        p.push(d);
1860                        chars.next();
1861                    } else {
1862                        break;
1863                    }
1864                }
1865                precision = Some(p.parse().unwrap_or(0));
1866            }
1867
1868            // Specifier character
1869            let spec = match chars.next() {
1870                Some(s) => s,
1871                None => {
1872                    return Err("fprintf: incomplete format specifier at end of string".to_string());
1873                }
1874            };
1875
1876            // No more args — silently skip remaining specifiers
1877            if arg_idx >= args.len() {
1878                continue;
1879            }
1880
1881            let arg = &args[arg_idx];
1882            arg_idx += 1;
1883
1884            let formatted = match spec {
1885                'd' | 'i' => {
1886                    let n = printf_scalar(arg, spec)?;
1887                    let i = n.trunc() as i64;
1888                    let s = printf_sign_str(i >= 0, flag_plus, flag_space, format!("{}", i.abs()));
1889                    printf_pad(s, width, flag_minus, flag_zero)
1890                }
1891                'f' => {
1892                    let n = printf_scalar(arg, spec)?;
1893                    let prec = precision.unwrap_or(6);
1894                    let s = printf_sign_str(
1895                        n >= 0.0,
1896                        flag_plus,
1897                        flag_space,
1898                        format!("{:.prec$}", n.abs(), prec = prec),
1899                    );
1900                    printf_pad(s, width, flag_minus, flag_zero)
1901                }
1902                'e' | 'E' => {
1903                    let n = printf_scalar(arg, spec)?;
1904                    let prec = precision.unwrap_or(6);
1905                    let s = printf_format_sci(n, prec, flag_plus, flag_space, spec == 'E');
1906                    printf_pad(s, width, flag_minus, flag_zero)
1907                }
1908                'g' | 'G' => {
1909                    let n = printf_scalar(arg, spec)?;
1910                    let prec = precision.unwrap_or(6).max(1);
1911                    let s = printf_format_g(n, prec, flag_plus, flag_space, spec == 'G');
1912                    printf_pad(s, width, flag_minus, flag_zero)
1913                }
1914                's' => {
1915                    let s = printf_string(arg)?;
1916                    let s = if let Some(max_len) = precision {
1917                        s.chars().take(max_len).collect::<String>()
1918                    } else {
1919                        s
1920                    };
1921                    printf_pad(s, width, flag_minus, false)
1922                }
1923                other => return Err(format!("fprintf: unknown format specifier '%{other}'")),
1924            };
1925
1926            result.push_str(&formatted);
1927        }
1928
1929        // Stop if all args consumed or no specifiers were found (infinite loop guard)
1930        if arg_idx >= args.len() || arg_idx == consumed_before {
1931            break;
1932        }
1933    }
1934
1935    Ok(result)
1936}
1937
1938/// Extracts a scalar f64 from a Value for use in numeric printf specifiers.
1939fn printf_scalar(v: &Value, spec: char) -> Result<f64, String> {
1940    match v {
1941        Value::Scalar(n) => Ok(*n),
1942        Value::Complex(re, im) if *im == 0.0 => Ok(*re),
1943        Value::Str(s) if s.chars().count() == 1 => Ok(s.chars().next().unwrap() as u32 as f64),
1944        _ => Err(format!(
1945            "fprintf: expected numeric argument for '%{spec}', got {:?}",
1946            std::mem::discriminant(v)
1947        )),
1948    }
1949}
1950
1951/// Extracts a string from a Value for use in `%s`.
1952fn printf_string(v: &Value) -> Result<String, String> {
1953    match v {
1954        Value::Str(s) | Value::StringObj(s) => Ok(s.clone()),
1955        Value::Scalar(n) => Ok(format_number(*n)),
1956        Value::Complex(re, im) => Ok(format_complex(*re, *im, &FormatMode::Custom(6))),
1957        Value::Void => Err("fprintf: cannot format void as string".to_string()),
1958        Value::Matrix(_) => Err("fprintf: cannot format matrix as string".to_string()),
1959        Value::Lambda(_)
1960        | Value::Function { .. }
1961        | Value::Tuple(_)
1962        | Value::Cell(_)
1963        | Value::Struct(_)
1964        | Value::StructArray(_) => Err("fprintf: cannot format this type as string".to_string()),
1965    }
1966}
1967
1968/// Builds a sign-prefixed string: `+n`, ` n`, `-n`, or bare `n`.
1969fn printf_sign_str(positive: bool, flag_plus: bool, flag_space: bool, digits: String) -> String {
1970    if positive {
1971        if flag_plus {
1972            format!("+{digits}")
1973        } else if flag_space {
1974            format!(" {digits}")
1975        } else {
1976            digits
1977        }
1978    } else {
1979        format!("-{digits}")
1980    }
1981}
1982
1983/// Right- or left-pads `s` to at least `width` chars, optionally zero-pads.
1984fn printf_pad(s: String, width: usize, left_align: bool, zero_pad: bool) -> String {
1985    if s.len() >= width {
1986        return s;
1987    }
1988    let pad_len = width - s.len();
1989    if left_align {
1990        format!("{s}{}", " ".repeat(pad_len))
1991    } else if zero_pad {
1992        // Insert zeros after optional sign
1993        let (prefix, rest) = if s.starts_with(['+', '-', ' ']) {
1994            s.split_at(1)
1995        } else {
1996            ("", s.as_str())
1997        };
1998        format!("{prefix}{}{rest}", "0".repeat(pad_len))
1999    } else {
2000        format!("{}{s}", " ".repeat(pad_len))
2001    }
2002}
2003
2004/// Formats `n` in scientific notation matching C `%e` / `%E`.
2005/// Always produces at least 2 exponent digits with an explicit sign: `1.23e+04`.
2006fn printf_format_sci(
2007    n: f64,
2008    prec: usize,
2009    flag_plus: bool,
2010    flag_space: bool,
2011    upper: bool,
2012) -> String {
2013    if n == 0.0 {
2014        let zeros = "0".repeat(prec);
2015        let sep = if prec > 0 {
2016            format!(".{zeros}")
2017        } else {
2018            String::new()
2019        };
2020        let e_char = if upper { 'E' } else { 'e' };
2021        let sign = if flag_plus {
2022            "+"
2023        } else if flag_space {
2024            " "
2025        } else {
2026            ""
2027        };
2028        return format!("{sign}0{sep}{e_char}+00");
2029    }
2030
2031    let neg = n < 0.0;
2032    let abs_n = n.abs();
2033    let exp = abs_n.log10().floor() as i32;
2034    let mantissa = abs_n / 10f64.powi(exp);
2035    let man_str = format!("{:.prec$}", mantissa, prec = prec);
2036
2037    let e_char = if upper { 'E' } else { 'e' };
2038    let exp_sign = if exp >= 0 { '+' } else { '-' };
2039    let exp_abs = exp.unsigned_abs();
2040    let exp_str = if exp_abs < 10 {
2041        format!("{e_char}{exp_sign}0{exp_abs}")
2042    } else {
2043        format!("{e_char}{exp_sign}{exp_abs}")
2044    };
2045
2046    let sign_str = if neg {
2047        "-"
2048    } else if flag_plus {
2049        "+"
2050    } else if flag_space {
2051        " "
2052    } else {
2053        ""
2054    };
2055    format!("{sign_str}{man_str}{exp_str}")
2056}
2057
2058/// Formats `n` using `%g` / `%G` rules:
2059/// uses `%e` if exponent < -4 or >= prec, otherwise `%f`; trims trailing zeros.
2060fn printf_format_g(n: f64, prec: usize, flag_plus: bool, flag_space: bool, upper: bool) -> String {
2061    if n == 0.0 {
2062        let sign = if flag_plus {
2063            "+"
2064        } else if flag_space {
2065            " "
2066        } else {
2067            ""
2068        };
2069        return format!("{sign}0");
2070    }
2071    let abs_n = n.abs();
2072    let exp = abs_n.log10().floor() as i32;
2073    if exp < -4 || exp >= prec as i32 {
2074        let s = printf_format_sci(n, prec.saturating_sub(1), flag_plus, flag_space, upper);
2075        trim_g_sci(s, upper)
2076    } else {
2077        let decimal_places = (prec as i32 - 1 - exp).max(0) as usize;
2078        let neg = n < 0.0;
2079        let s = format!("{:.prec$}", abs_n, prec = decimal_places);
2080        let s = if s.contains('.') {
2081            s.trim_end_matches('0').trim_end_matches('.').to_string()
2082        } else {
2083            s
2084        };
2085        let sign = if neg {
2086            "-"
2087        } else if flag_plus {
2088            "+"
2089        } else if flag_space {
2090            " "
2091        } else {
2092            ""
2093        };
2094        format!("{sign}{s}")
2095    }
2096}
2097
2098/// Trims trailing zeros from the mantissa of a scientific-notation string `1.230e+04` → `1.23e+04`.
2099fn trim_g_sci(s: String, upper: bool) -> String {
2100    let e_char = if upper { 'E' } else { 'e' };
2101    if let Some(e_pos) = s.find(e_char) {
2102        let mantissa = &s[..e_pos];
2103        let exp_part = &s[e_pos..];
2104        let trimmed = if mantissa.contains('.') {
2105            mantissa.trim_end_matches('0').trim_end_matches('.')
2106        } else {
2107            mantissa
2108        };
2109        format!("{trimmed}{exp_part}")
2110    } else {
2111        s
2112    }
2113}
2114
2115/// Calls a `Lambda` or `Function` value with the given arguments.
2116///
2117/// Used by `cellfun` and `arrayfun` to apply a function to each element
2118/// without going through the name-lookup path.
2119fn call_function_value(
2120    f: &Value,
2121    args: &[Value],
2122    io: Option<&mut IoContext>,
2123) -> Result<Value, String> {
2124    match f {
2125        Value::Lambda(lf) => {
2126            let lf = lf.clone();
2127            lf.0(args, io)
2128        }
2129        Value::Function { .. } => {
2130            // Named function called via cellfun/arrayfun — name is unknown at this point.
2131            // Use a minimal env that doesn't export any user variables to avoid
2132            // polluting the caller's scope. Functions see their own scope via exec.
2133            let empty_env = Env::new();
2134            match io {
2135                Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
2136                    Some(hook) => hook("<anonymous>", f, args, &empty_env, io_ref),
2137                    None => Err("User function execution not initialized".to_string()),
2138                }),
2139                None => {
2140                    let mut tmp_io = IoContext::new();
2141                    FN_CALL_HOOK.with(|c| match c.get() {
2142                        Some(hook) => hook("<anonymous>", f, args, &empty_env, &mut tmp_io),
2143                        None => Err("User function execution not initialized".to_string()),
2144                    })
2145                }
2146            }
2147        }
2148        _ => Err("cellfun/arrayfun: first argument must be a function or lambda (@fn)".to_string()),
2149    }
2150}
2151
2152/// Names of all built-in functions recognized by [`call_builtin`].
2153///
2154/// Used for REPL tab completion and "did you mean?" suggestions.
2155pub fn builtin_names() -> &'static [&'static str] {
2156    &[
2157        "abs",
2158        "acos",
2159        "all",
2160        "angle",
2161        "any",
2162        "arrayfun",
2163        "asin",
2164        "assert",
2165        "atan",
2166        "atan2",
2167        "bitand",
2168        "bitnot",
2169        "bitor",
2170        "bitshift",
2171        "bitxor",
2172        "ceil",
2173        "cell",
2174        "cellfun",
2175        "chol",
2176        "complex",
2177        "cond",
2178        "conj",
2179        "cos",
2180        "cov",
2181        "cumprod",
2182        "cumsum",
2183        "det",
2184        "diag",
2185        "disp",
2186        "dlmread",
2187        "dlmwrite",
2188        "eig",
2189        "erf",
2190        "erfc",
2191        "exist",
2192        "exp",
2193        "eye",
2194        "fclose",
2195        "fgetl",
2196        "fgets",
2197        "fieldnames",
2198        "find",
2199        "fliplr",
2200        "flipud",
2201        "floor",
2202        "fopen",
2203        "fprintf",
2204        "genpath",
2205        "histc",
2206        "hypot",
2207        "imag",
2208        "int2str",
2209        "inv",
2210        "iqr",
2211        "iscell",
2212        "ischar",
2213        "isempty",
2214        "isfield",
2215        "isfile",
2216        "isfinite",
2217        "isfolder",
2218        "isinf",
2219        "isnan",
2220        "isreal",
2221        "isstring",
2222        "isstruct",
2223        "jsonencode",
2224        "jsondecode",
2225        "kurtosis",
2226        "lasterr",
2227        "length",
2228        "linspace",
2229        "load",
2230        "log",
2231        "log10",
2232        "log2",
2233        "lower",
2234        "lu",
2235        "mat2str",
2236        "max",
2237        "mean",
2238        "median",
2239        "min",
2240        "mod",
2241        "mode",
2242        "nan",
2243        "norm",
2244        "normcdf",
2245        "normpdf",
2246        "not",
2247        "null",
2248        "num2str",
2249        "numel",
2250        "ones",
2251        "orth",
2252        "pinv",
2253        "prctile",
2254        "prod",
2255        "qr",
2256        "rand",
2257        "randi",
2258        "randn",
2259        "rank",
2260        "readmatrix",
2261        "readtable",
2262        "real",
2263        "rem",
2264        "reshape",
2265        "rmfield",
2266        "rng",
2267        "round",
2268        "sign",
2269        "sin",
2270        "size",
2271        "skewness",
2272        "sort",
2273        "sprintf",
2274        "sqrt",
2275        "std",
2276        "str2double",
2277        "str2num",
2278        "strcmp",
2279        "strcmpi",
2280        "strrep",
2281        "strsplit",
2282        "strtrim",
2283        "sum",
2284        "svd",
2285        "tan",
2286        "trace",
2287        "unique",
2288        "upper",
2289        "var",
2290        "writetable",
2291        "xor",
2292        "zeros",
2293        "zscore",
2294    ]
2295}
2296
2297/// Computes the Levenshtein edit distance between two strings.
2298fn levenshtein(a: &str, b: &str) -> usize {
2299    let a: Vec<char> = a.chars().collect();
2300    let b: Vec<char> = b.chars().collect();
2301    let (m, n) = (a.len(), b.len());
2302    let mut row: Vec<usize> = (0..=n).collect();
2303    for i in 1..=m {
2304        let mut prev = row[0];
2305        row[0] = i;
2306        for j in 1..=n {
2307            let next = if a[i - 1] == b[j - 1] {
2308                prev
2309            } else {
2310                1 + prev.min(row[j]).min(row[j - 1])
2311            };
2312            prev = row[j];
2313            row[j] = next;
2314        }
2315    }
2316    row[n]
2317}
2318
2319/// Finds the closest name in `env` keys and built-in names within Levenshtein distance 2.
2320fn suggest_similar(name: &str, env: &Env) -> Option<String> {
2321    const MAX_DIST: usize = 2;
2322    let mut best: Option<(String, usize)> = None;
2323    let mut update = |candidate: &str| {
2324        let d = levenshtein(name, candidate);
2325        if d <= MAX_DIST && best.as_ref().is_none_or(|(_, bd)| d < *bd) {
2326            best = Some((candidate.to_string(), d));
2327        }
2328    };
2329    for key in env.keys() {
2330        update(key);
2331    }
2332    for &bname in builtin_names() {
2333        update(bname);
2334    }
2335    best.map(|(s, _)| s)
2336}
2337
2338/// Checks equality of two values for `assert(a, b[, tol])`.
2339fn assert_values_equal(a: &Value, b: &Value, tol: Option<f64>) -> Result<Value, String> {
2340    match (a, b) {
2341        (Value::Scalar(x), Value::Scalar(y)) => {
2342            let ok = match tol {
2343                None => x == y,
2344                Some(t) => (x - y).abs() <= t,
2345            };
2346            if ok {
2347                Ok(Value::Void)
2348            } else if let Some(t) = tol {
2349                Err(format!(
2350                    "assert: |{x} - {y}| = {} exceeds tolerance {t}",
2351                    (x - y).abs()
2352                ))
2353            } else {
2354                Err(format!("assert: {x} ~= {y}"))
2355            }
2356        }
2357        (Value::Matrix(ma), Value::Matrix(mb)) => {
2358            if ma.shape() != mb.shape() {
2359                return Err(format!(
2360                    "assert: size mismatch [{}×{}] vs [{}×{}]",
2361                    ma.nrows(),
2362                    ma.ncols(),
2363                    mb.nrows(),
2364                    mb.ncols()
2365                ));
2366            }
2367            for (x, y) in ma.iter().zip(mb.iter()) {
2368                let ok = match tol {
2369                    None => x == y,
2370                    Some(t) => (x - y).abs() <= t,
2371                };
2372                if !ok {
2373                    if let Some(t) = tol {
2374                        return Err(format!(
2375                            "assert: difference {} exceeds tolerance {t}",
2376                            (x - y).abs()
2377                        ));
2378                    } else {
2379                        return Err(format!("assert: {x} ~= {y}"));
2380                    }
2381                }
2382            }
2383            Ok(Value::Void)
2384        }
2385        _ => {
2386            if tol.is_some() {
2387                return Err("assert: tolerance requires numeric arguments".to_string());
2388            }
2389            if a == b {
2390                Ok(Value::Void)
2391            } else {
2392                Err("assert: values not equal".to_string())
2393            }
2394        }
2395    }
2396}
2397
2398fn call_builtin(
2399    name: &str,
2400    args: &[Value],
2401    env: &Env,
2402    mut io: Option<&mut IoContext>,
2403) -> Result<Value, String> {
2404    match (name, args.len()) {
2405        // --- 1-argument scalar functions ---
2406        ("sqrt", 1) => apply_elem(&args[0], |x| x.sqrt()),
2407        ("floor", 1) => apply_elem(&args[0], |x| x.floor()),
2408        ("ceil", 1) => apply_elem(&args[0], |x| x.ceil()),
2409        ("round", 1) => apply_elem(&args[0], |x| x.round()),
2410        ("sign", 1) => apply_elem(&args[0], |x| x.signum()),
2411        ("log", 1) => apply_elem(&args[0], |x| x.ln()),
2412        ("log2", 1) => apply_elem(&args[0], |x| x.log2()),
2413        ("log10", 1) => apply_elem(&args[0], |x| x.log10()),
2414        ("exp", 1) => apply_elem(&args[0], |x| x.exp()),
2415        ("sin", 1) => apply_elem(&args[0], |x| x.sin()),
2416        ("cos", 1) => apply_elem(&args[0], |x| x.cos()),
2417        ("tan", 1) => apply_elem(&args[0], |x| x.tan()),
2418        ("asin", 1) => apply_elem(&args[0], |x| x.asin()),
2419        ("acos", 1) => apply_elem(&args[0], |x| x.acos()),
2420        ("atan", 1) => apply_elem(&args[0], |x| x.atan()),
2421        // --- Special functions (erf, normal distribution) ---
2422        ("erf", 1) => apply_elem(&args[0], libm::erf),
2423        ("erfc", 1) => apply_elem(&args[0], libm::erfc),
2424        ("normcdf", 1) => apply_elem(&args[0], |x| {
2425            0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
2426        }),
2427        ("normcdf", 3) => {
2428            let mu = scalar_arg(&args[1], name, 2)?;
2429            let s = scalar_arg(&args[2], name, 3)?;
2430            if s <= 0.0 {
2431                return Err("normcdf: sigma must be positive".to_string());
2432            }
2433            apply_elem(&args[0], move |x| {
2434                0.5 * (1.0 + libm::erf((x - mu) / (s * std::f64::consts::SQRT_2)))
2435            })
2436        }
2437        ("normpdf", 1) => apply_elem(&args[0], |x| {
2438            (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
2439        }),
2440        ("normpdf", 3) => {
2441            let mu = scalar_arg(&args[1], name, 2)?;
2442            let s = scalar_arg(&args[2], name, 3)?;
2443            if s <= 0.0 {
2444                return Err("normpdf: sigma must be positive".to_string());
2445            }
2446            apply_elem(&args[0], move |x| {
2447                let z = (x - mu) / s;
2448                (-0.5 * z * z).exp() / (s * (2.0 * std::f64::consts::PI).sqrt())
2449            })
2450        }
2451        // --- 2-argument scalar functions ---
2452        ("atan2", 2) => Ok(Value::Scalar(
2453            scalar_arg(&args[0], name, 1)?.atan2(scalar_arg(&args[1], name, 2)?),
2454        )),
2455        ("mod", 2) => {
2456            let a = scalar_arg(&args[0], name, 1)?;
2457            let b = scalar_arg(&args[1], name, 2)?;
2458            Ok(Value::Scalar(a - b * (a / b).floor()))
2459        }
2460        ("rem", 2) => {
2461            let a = scalar_arg(&args[0], name, 1)?;
2462            let b = scalar_arg(&args[1], name, 2)?;
2463            Ok(Value::Scalar(a - b * (a / b).trunc()))
2464        }
2465        ("max", 2) => Ok(Value::Scalar(
2466            scalar_arg(&args[0], name, 1)?.max(scalar_arg(&args[1], name, 2)?),
2467        )),
2468        ("min", 2) => Ok(Value::Scalar(
2469            scalar_arg(&args[0], name, 1)?.min(scalar_arg(&args[1], name, 2)?),
2470        )),
2471        ("hypot", 2) => Ok(Value::Scalar(
2472            scalar_arg(&args[0], name, 1)?.hypot(scalar_arg(&args[1], name, 2)?),
2473        )),
2474        ("log", 2) => Ok(Value::Scalar(
2475            scalar_arg(&args[0], name, 1)?.log(scalar_arg(&args[1], name, 2)?),
2476        )),
2477        // --- Matrix constructors ---
2478        ("zeros", 1) => {
2479            let n = scalar_arg(&args[0], name, 1)? as usize;
2480            Ok(Value::Matrix(Array2::zeros((n, n))))
2481        }
2482        ("zeros", 2) => {
2483            let r = scalar_arg(&args[0], name, 1)? as usize;
2484            let c = scalar_arg(&args[1], name, 2)? as usize;
2485            Ok(Value::Matrix(Array2::zeros((r, c))))
2486        }
2487        ("ones", 1) => {
2488            let n = scalar_arg(&args[0], name, 1)? as usize;
2489            Ok(Value::Matrix(Array2::ones((n, n))))
2490        }
2491        ("ones", 2) => {
2492            let r = scalar_arg(&args[0], name, 1)? as usize;
2493            let c = scalar_arg(&args[1], name, 2)? as usize;
2494            Ok(Value::Matrix(Array2::ones((r, c))))
2495        }
2496        ("eye", 1) => {
2497            let n = scalar_arg(&args[0], name, 1)? as usize;
2498            let mut m = Array2::<f64>::zeros((n, n));
2499            for i in 0..n {
2500                m[[i, i]] = 1.0;
2501            }
2502            Ok(Value::Matrix(m))
2503        }
2504        // --- Matrix properties ---
2505        ("size", 1) => match &args[0] {
2506            Value::Void => Err("size: not applicable to void".to_string()),
2507            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Matrix(
2508                Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap(),
2509            )),
2510            Value::Matrix(m) => Ok(Value::Matrix(
2511                Array2::from_shape_vec((1, 2), vec![m.nrows() as f64, m.ncols() as f64]).unwrap(),
2512            )),
2513            Value::Str(s) => Ok(Value::Matrix(
2514                Array2::from_shape_vec((1, 2), vec![1.0, s.chars().count() as f64]).unwrap(),
2515            )),
2516            Value::StringObj(_) => Ok(Value::Matrix(
2517                Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap(),
2518            )),
2519            Value::Cell(v) => Ok(Value::Matrix(
2520                Array2::from_shape_vec((1, 2), vec![1.0, v.len() as f64]).unwrap(),
2521            )),
2522            Value::StructArray(arr) => Ok(Value::Matrix(
2523                Array2::from_shape_vec((1, 2), vec![1.0, arr.len() as f64]).unwrap(),
2524            )),
2525            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
2526                Err("size: not applicable to function values".to_string())
2527            }
2528        },
2529        ("size", 2) => {
2530            let dim = scalar_arg(&args[1], name, 2)? as usize;
2531            match &args[0] {
2532                Value::Void => Err("size: not applicable to void".to_string()),
2533                Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => {
2534                    Ok(Value::Scalar(1.0))
2535                }
2536                Value::Matrix(m) => match dim {
2537                    1 => Ok(Value::Scalar(m.nrows() as f64)),
2538                    2 => Ok(Value::Scalar(m.ncols() as f64)),
2539                    _ => Err(format!("size: invalid dimension {dim}, must be 1 or 2")),
2540                },
2541                Value::Str(s) => match dim {
2542                    1 => Ok(Value::Scalar(1.0)),
2543                    2 => Ok(Value::Scalar(s.chars().count() as f64)),
2544                    _ => Err(format!("size: invalid dimension {dim}")),
2545                },
2546                Value::StringObj(_) => Ok(Value::Scalar(1.0)),
2547                Value::Cell(v) => match dim {
2548                    1 => Ok(Value::Scalar(1.0)),
2549                    2 => Ok(Value::Scalar(v.len() as f64)),
2550                    _ => Err(format!("size: invalid dimension {dim}")),
2551                },
2552                Value::StructArray(arr) => match dim {
2553                    1 => Ok(Value::Scalar(1.0)),
2554                    2 => Ok(Value::Scalar(arr.len() as f64)),
2555                    _ => Err(format!("size: invalid dimension {dim}")),
2556                },
2557                Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
2558                    Err("size: not applicable to function values".to_string())
2559                }
2560            }
2561        }
2562        ("length", 1) => match &args[0] {
2563            Value::Void => Err("length: not applicable to void".to_string()),
2564            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
2565            Value::Matrix(m) => Ok(Value::Scalar(m.nrows().max(m.ncols()) as f64)),
2566            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
2567            Value::StringObj(_) => Ok(Value::Scalar(1.0)),
2568            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
2569            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
2570            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
2571                Err("length: not applicable to function values".to_string())
2572            }
2573        },
2574        ("numel", 1) => match &args[0] {
2575            Value::Void => Err("numel: not applicable to void".to_string()),
2576            Value::Scalar(_) | Value::Complex(_, _) | Value::Struct(_) => Ok(Value::Scalar(1.0)),
2577            Value::Matrix(m) => Ok(Value::Scalar(m.len() as f64)),
2578            Value::Str(s) => Ok(Value::Scalar(s.chars().count() as f64)),
2579            Value::StringObj(_) => Ok(Value::Scalar(1.0)),
2580            Value::Cell(v) => Ok(Value::Scalar(v.len() as f64)),
2581            Value::StructArray(arr) => Ok(Value::Scalar(arr.len() as f64)),
2582            Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
2583                Err("numel: not applicable to function values".to_string())
2584            }
2585        },
2586        ("trace", 1) => match &args[0] {
2587            Value::Void => Err("trace: not applicable to void".to_string()),
2588            Value::Scalar(n) => Ok(Value::Scalar(*n)),
2589            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
2590            Value::Matrix(m) => {
2591                let n = m.nrows().min(m.ncols());
2592                Ok(Value::Scalar((0..n).map(|i| m[[i, i]]).sum()))
2593            }
2594            Value::Str(_)
2595            | Value::StringObj(_)
2596            | Value::Lambda(_)
2597            | Value::Function { .. }
2598            | Value::Tuple(_)
2599            | Value::Cell(_)
2600            | Value::Struct(_)
2601            | Value::StructArray(_) => {
2602                Err("trace: not applicable to non-numeric values".to_string())
2603            }
2604        },
2605        ("det", 1) => match &args[0] {
2606            Value::Void => Err("det: not applicable to void".to_string()),
2607            Value::Scalar(n) => Ok(Value::Scalar(*n)),
2608            Value::Complex(_, _) => Err("det: not applicable to complex scalars".to_string()),
2609            Value::Matrix(m) => Ok(Value::Scalar(det_matrix(m)?)),
2610            Value::Str(_)
2611            | Value::StringObj(_)
2612            | Value::Lambda(_)
2613            | Value::Function { .. }
2614            | Value::Tuple(_)
2615            | Value::Cell(_)
2616            | Value::Struct(_)
2617            | Value::StructArray(_) => Err("det: not applicable to non-numeric values".to_string()),
2618        },
2619        ("inv", 1) => match &args[0] {
2620            Value::Void => Err("inv: not applicable to void".to_string()),
2621            Value::Scalar(n) => {
2622                if *n == 0.0 {
2623                    Err("inv: singular (zero scalar)".to_string())
2624                } else {
2625                    Ok(Value::Scalar(1.0 / n))
2626                }
2627            }
2628            Value::Complex(re, im) => {
2629                // 1/(a+bi) = (a-bi)/(a²+b²)
2630                let denom = re * re + im * im;
2631                if denom == 0.0 {
2632                    Err("inv: singular (zero complex)".to_string())
2633                } else {
2634                    Ok(make_complex(re / denom, -im / denom))
2635                }
2636            }
2637            Value::Matrix(m) => Ok(Value::Matrix(inv_matrix(m)?)),
2638            Value::Str(_)
2639            | Value::StringObj(_)
2640            | Value::Lambda(_)
2641            | Value::Function { .. }
2642            | Value::Tuple(_)
2643            | Value::Cell(_)
2644            | Value::Struct(_)
2645            | Value::StructArray(_) => Err("inv: not applicable to non-numeric values".to_string()),
2646        },
2647        // --- Range / linspace ---
2648        ("linspace", 3) => {
2649            let a = scalar_arg(&args[0], name, 1)?;
2650            let b = scalar_arg(&args[1], name, 2)?;
2651            let n = scalar_arg(&args[2], name, 3)? as usize;
2652            if n == 0 {
2653                return Ok(Value::Matrix(Array2::zeros((1, 0))));
2654            }
2655            if n == 1 {
2656                return Ok(Value::Matrix(
2657                    Array2::from_shape_vec((1, 1), vec![b]).unwrap(),
2658                ));
2659            }
2660            let vals: Vec<f64> = (0..n)
2661                .map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
2662                .collect();
2663            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
2664        }
2665        // --- Bitwise functions ---
2666        // All operands are truncated to i64. Results are non-negative integers
2667        // returned as f64.  For bitnot the bit-width defines the mask.
2668        ("bitand", 2) => {
2669            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2670            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
2671            Ok(Value::Scalar((a & b) as f64))
2672        }
2673        ("bitor", 2) => {
2674            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2675            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
2676            Ok(Value::Scalar((a | b) as f64))
2677        }
2678        ("bitxor", 2) => {
2679            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2680            let b = to_bits(scalar_arg(&args[1], name, 2)?, name, 2)?;
2681            Ok(Value::Scalar((a ^ b) as f64))
2682        }
2683        // bitshift(a, n): n > 0 → left shift; n < 0 → logical right shift.
2684        // Shifts of 64 or more return 0.
2685        ("bitshift", 2) => {
2686            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2687            let n = scalar_arg(&args[1], name, 2)?;
2688            if n.fract() != 0.0 {
2689                return Err("bitshift: shift amount must be an integer".to_string());
2690            }
2691            let n = n as i64;
2692            let result: u64 = if n >= 64 || n <= -64 {
2693                0
2694            } else if n >= 0 {
2695                a.wrapping_shl(n as u32)
2696            } else {
2697                a.wrapping_shr((-n) as u32)
2698            };
2699            Ok(Value::Scalar(result as f64))
2700        }
2701        // bitnot(a)        — NOT within 32-bit window (Octave uint32 default)
2702        // bitnot(a, bits)  — NOT within explicit bit-width window (1–53)
2703        ("bitnot", 1) => {
2704            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2705            let mask: u64 = 0xFFFF_FFFF;
2706            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
2707        }
2708        ("bitnot", 2) => {
2709            let a = to_bits(scalar_arg(&args[0], name, 1)?, name, 1)?;
2710            let bits = scalar_arg(&args[1], name, 2)?;
2711            if bits.fract() != 0.0 || !(1.0..=53.0).contains(&bits) {
2712                return Err(format!(
2713                    "bitnot: bit-width must be an integer in [1, 53], got {bits}"
2714                ));
2715            }
2716            let mask: u64 = (1u64 << bits as u32) - 1;
2717            Ok(Value::Scalar(((a ^ mask) & mask) as f64))
2718        }
2719        // --- Special constant predicates (element-wise) ---
2720        ("isnan", 1) => apply_elem(&args[0], |x| if x.is_nan() { 1.0 } else { 0.0 }),
2721        ("isinf", 1) => apply_elem(&args[0], |x| if x.is_infinite() { 1.0 } else { 0.0 }),
2722        ("isfinite", 1) => apply_elem(&args[0], |x| if x.is_finite() { 1.0 } else { 0.0 }),
2723        // --- NaN matrix constructors ---
2724        ("nan", 1) => {
2725            let n = scalar_arg(&args[0], name, 1)? as usize;
2726            Ok(Value::Matrix(Array2::from_elem((n, n), f64::NAN)))
2727        }
2728        ("nan", 2) => {
2729            let r = scalar_arg(&args[0], name, 1)? as usize;
2730            let c = scalar_arg(&args[1], name, 2)? as usize;
2731            Ok(Value::Matrix(Array2::from_elem((r, c), f64::NAN)))
2732        }
2733        // --- Random number generation ---
2734        ("rand", 0) => Ok(Value::Scalar(rand_uniform())),
2735        ("rand", 1) => {
2736            let n = scalar_arg(&args[0], name, 1)? as usize;
2737            let data: Vec<f64> = (0..n * n).map(|_| rand_uniform()).collect();
2738            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
2739        }
2740        ("rand", 2) => {
2741            let r = scalar_arg(&args[0], name, 1)? as usize;
2742            let c = scalar_arg(&args[1], name, 2)? as usize;
2743            let data: Vec<f64> = (0..r * c).map(|_| rand_uniform()).collect();
2744            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
2745        }
2746        ("randn", 0) => Ok(Value::Scalar(rand_normal())),
2747        ("randn", 1) => {
2748            let n = scalar_arg(&args[0], name, 1)? as usize;
2749            let data: Vec<f64> = (0..n * n).map(|_| rand_normal()).collect();
2750            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
2751        }
2752        ("randn", 2) => {
2753            let r = scalar_arg(&args[0], name, 1)? as usize;
2754            let c = scalar_arg(&args[1], name, 2)? as usize;
2755            let data: Vec<f64> = (0..r * c).map(|_| rand_normal()).collect();
2756            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
2757        }
2758        ("randi", 1) => {
2759            let (lo, hi) = randi_range(&args[0])?;
2760            let v = RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64;
2761            Ok(Value::Scalar(v))
2762        }
2763        ("randi", 2) => {
2764            let (lo, hi) = randi_range(&args[0])?;
2765            let n = scalar_arg(&args[1], name, 2)? as usize;
2766            let data: Vec<f64> = (0..n * n)
2767                .map(|_| RNG.with(|r| r.borrow_mut().gen_range(lo..=hi)) as f64)
2768                .collect();
2769            Ok(Value::Matrix(Array2::from_shape_vec((n, n), data).unwrap()))
2770        }
2771        ("randi", 3) => {
2772            let (lo, hi) = randi_range(&args[0])?;
2773            let r = scalar_arg(&args[1], name, 2)? as usize;
2774            let c = scalar_arg(&args[2], name, 3)? as usize;
2775            let data: Vec<f64> = (0..r * c)
2776                .map(|_| RNG.with(|rng| rng.borrow_mut().gen_range(lo..=hi)) as f64)
2777                .collect();
2778            Ok(Value::Matrix(Array2::from_shape_vec((r, c), data).unwrap()))
2779        }
2780        ("rng", 1) => match &args[0] {
2781            Value::Scalar(n) => {
2782                rng_seed(*n as u64);
2783                Ok(Value::Void)
2784            }
2785            Value::Str(s) | Value::StringObj(s) if s == "shuffle" => {
2786                rng_shuffle();
2787                Ok(Value::Void)
2788            }
2789            _ => Err("rng: argument must be a numeric seed or 'shuffle'".to_string()),
2790        },
2791        // --- Vector reductions ---
2792        // For vectors (1×N or N×1): reduce all elements to scalar.
2793        // For M×N matrices (M>1, N>1): reduce column-wise, return 1×N row vector.
2794        ("sum", 1) => apply_reduction(&args[0], |v| v.iter().copied().sum()),
2795        ("prod", 1) => apply_reduction(&args[0], |v| v.iter().copied().product()),
2796        ("any", 1) => apply_reduction(&args[0], |v| {
2797            if v.iter().any(|&x| x != 0.0) {
2798                1.0
2799            } else {
2800                0.0
2801            }
2802        }),
2803        ("all", 1) => apply_reduction(&args[0], |v| {
2804            if v.iter().all(|&x| x != 0.0) {
2805                1.0
2806            } else {
2807                0.0
2808            }
2809        }),
2810        ("mean", 1) => apply_reduction(&args[0], |v| {
2811            if v.is_empty() {
2812                f64::NAN
2813            } else {
2814                v.iter().copied().sum::<f64>() / v.len() as f64
2815            }
2816        }),
2817        // 1-arg min/max: reduce to scalar for vectors, column-wise for matrices.
2818        // 2-arg forms (element-wise scalar min/max) are already handled above.
2819        ("min", 1) => apply_reduction(&args[0], |v| {
2820            v.iter().copied().fold(f64::INFINITY, f64::min)
2821        }),
2822        ("max", 1) => apply_reduction(&args[0], |v| {
2823            v.iter().copied().fold(f64::NEG_INFINITY, f64::max)
2824        }),
2825        // --- Norms ---
2826        ("norm", 1) => match &args[0] {
2827            Value::Void => Err("norm: not applicable to void".to_string()),
2828            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
2829            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
2830            Value::Matrix(m) => {
2831                if m.nrows() <= 1 || m.ncols() <= 1 {
2832                    // Vector: L2 norm.
2833                    Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
2834                } else {
2835                    // Matrix: 2-norm = largest singular value.
2836                    let (_, s, _) = svd_compute(m)?;
2837                    Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)))
2838                }
2839            }
2840            Value::Str(_)
2841            | Value::StringObj(_)
2842            | Value::Lambda(_)
2843            | Value::Function { .. }
2844            | Value::Tuple(_)
2845            | Value::Cell(_)
2846            | Value::Struct(_)
2847            | Value::StructArray(_) => {
2848                Err("norm: not applicable to non-numeric values".to_string())
2849            }
2850        },
2851        ("norm", 2) => match &args[1] {
2852            Value::Str(s) | Value::StringObj(s) => match s.as_str() {
2853                "fro" => match &args[0] {
2854                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
2855                    Value::Matrix(m) => {
2856                        Ok(Value::Scalar(m.iter().map(|x| x * x).sum::<f64>().sqrt()))
2857                    }
2858                    _ => Err("norm: first argument must be numeric".to_string()),
2859                },
2860                other => Err(format!("norm: unknown norm type '{other}'")),
2861            },
2862            _ => {
2863                let p = scalar_arg(&args[1], name, 2)?;
2864                match &args[0] {
2865                    Value::Void => Err("norm: not applicable to void".to_string()),
2866                    Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
2867                    Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt().powf(p))),
2868                    Value::Matrix(m) => {
2869                        if m.nrows() > 1 && m.ncols() > 1 {
2870                            // Matrix norms.
2871                            if (p - 2.0).abs() < 1e-15 {
2872                                let (_, s, _) = svd_compute(m)?;
2873                                return Ok(Value::Scalar(s.first().copied().unwrap_or(0.0)));
2874                            } else if (p - 1.0).abs() < 1e-15 {
2875                                // Maximum absolute column sum.
2876                                let v = (0..m.ncols())
2877                                    .map(|j| m.column(j).iter().map(|&x| x.abs()).sum::<f64>())
2878                                    .fold(0.0_f64, f64::max);
2879                                return Ok(Value::Scalar(v));
2880                            } else if p == f64::INFINITY {
2881                                // Maximum absolute row sum.
2882                                let v = (0..m.nrows())
2883                                    .map(|i| m.row(i).iter().map(|&x| x.abs()).sum::<f64>())
2884                                    .fold(0.0_f64, f64::max);
2885                                return Ok(Value::Scalar(v));
2886                            }
2887                        }
2888                        // Vector (or general Lp).
2889                        if p == f64::INFINITY {
2890                            Ok(Value::Scalar(
2891                                m.iter().copied().fold(0.0_f64, |acc, x| acc.max(x.abs())),
2892                            ))
2893                        } else {
2894                            Ok(Value::Scalar(
2895                                m.iter().map(|x| x.abs().powf(p)).sum::<f64>().powf(1.0 / p),
2896                            ))
2897                        }
2898                    }
2899                    Value::Str(_)
2900                    | Value::StringObj(_)
2901                    | Value::Lambda(_)
2902                    | Value::Function { .. }
2903                    | Value::Tuple(_)
2904                    | Value::Cell(_)
2905                    | Value::Struct(_)
2906                    | Value::StructArray(_) => {
2907                        Err("norm: not applicable to non-numeric values".to_string())
2908                    }
2909                }
2910            }
2911        },
2912        // --- Cumulative reductions ---
2913        ("cumsum", 1) => apply_cumulative(&args[0], |acc, x| acc + x),
2914        ("cumprod", 1) => apply_cumulative(&args[0], |acc, x| acc * x),
2915        // --- Sort ---
2916        ("sort", 1) => match &args[0] {
2917            Value::Void => Err("sort: not applicable to void".to_string()),
2918            Value::Scalar(n) => Ok(Value::Scalar(*n)),
2919            Value::Complex(_, _) => Err("sort: not applicable to complex values".to_string()),
2920            Value::Str(_)
2921            | Value::StringObj(_)
2922            | Value::Lambda(_)
2923            | Value::Function { .. }
2924            | Value::Tuple(_)
2925            | Value::Cell(_)
2926            | Value::Struct(_)
2927            | Value::StructArray(_) => {
2928                Err("sort: not applicable to non-numeric values".to_string())
2929            }
2930            Value::Matrix(m) => {
2931                if m.nrows() > 1 && m.ncols() > 1 {
2932                    return Err("sort: input must be a vector".to_string());
2933                }
2934                let mut vals: Vec<f64> = m.iter().copied().collect();
2935                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2936                Ok(Value::Matrix(
2937                    Array2::from_shape_vec(m.raw_dim(), vals).unwrap(),
2938                ))
2939            }
2940        },
2941        // --- Reshape ---
2942        ("reshape", 3) => {
2943            let r = scalar_arg(&args[1], name, 2)? as usize;
2944            let c = scalar_arg(&args[2], name, 3)? as usize;
2945            match &args[0] {
2946                Value::Void => Err("reshape: not applicable to void".to_string()),
2947                Value::Scalar(n) => {
2948                    if r * c != 1 {
2949                        return Err(format!("reshape: cannot reshape 1 element into {r}x{c}"));
2950                    }
2951                    Ok(Value::Matrix(
2952                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
2953                    ))
2954                }
2955                Value::Complex(_, _) => {
2956                    Err("reshape: not applicable to complex values".to_string())
2957                }
2958                Value::Str(_)
2959                | Value::StringObj(_)
2960                | Value::Lambda(_)
2961                | Value::Function { .. }
2962                | Value::Tuple(_)
2963                | Value::Cell(_)
2964                | Value::Struct(_)
2965                | Value::StructArray(_) => {
2966                    Err("reshape: not applicable to non-numeric values".to_string())
2967                }
2968                Value::Matrix(m) => {
2969                    let total = m.len();
2970                    if r * c != total {
2971                        return Err(format!(
2972                            "reshape: cannot reshape {total} elements into {r}x{c}"
2973                        ));
2974                    }
2975                    // Column-major order (MATLAB convention)
2976                    let flat: Vec<f64> = (0..m.ncols())
2977                        .flat_map(|col| (0..m.nrows()).map(move |row| m[[row, col]]))
2978                        .collect();
2979                    let mut result = Array2::<f64>::zeros((r, c));
2980                    for (i, &v) in flat.iter().enumerate() {
2981                        result[[i % r, i / r]] = v;
2982                    }
2983                    Ok(Value::Matrix(result))
2984                }
2985            }
2986        }
2987        // --- Flip ---
2988        ("fliplr", 1) => match &args[0] {
2989            Value::Void => Err(format!("{name}: not applicable to void")),
2990            Value::Scalar(n) => Ok(Value::Scalar(*n)),
2991            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
2992            Value::Str(_)
2993            | Value::StringObj(_)
2994            | Value::Lambda(_)
2995            | Value::Function { .. }
2996            | Value::Tuple(_)
2997            | Value::Cell(_)
2998            | Value::Struct(_)
2999            | Value::StructArray(_) => Err(format!("{name}: not applicable to non-numeric values")),
3000            Value::Matrix(m) => {
3001                let (nrows, ncols) = (m.nrows(), m.ncols());
3002                let mut result = m.clone();
3003                for r in 0..nrows {
3004                    for c in 0..ncols / 2 {
3005                        let tmp = result[[r, c]];
3006                        result[[r, c]] = result[[r, ncols - 1 - c]];
3007                        result[[r, ncols - 1 - c]] = tmp;
3008                    }
3009                }
3010                Ok(Value::Matrix(result))
3011            }
3012        },
3013        ("flipud", 1) => match &args[0] {
3014            Value::Void => Err(format!("{name}: not applicable to void")),
3015            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3016            Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
3017            Value::Str(_)
3018            | Value::StringObj(_)
3019            | Value::Lambda(_)
3020            | Value::Function { .. }
3021            | Value::Tuple(_)
3022            | Value::Cell(_)
3023            | Value::Struct(_)
3024            | Value::StructArray(_) => Err(format!("{name}: not applicable to non-numeric values")),
3025            Value::Matrix(m) => {
3026                let (nrows, ncols) = (m.nrows(), m.ncols());
3027                let mut result = m.clone();
3028                for c in 0..ncols {
3029                    for r in 0..nrows / 2 {
3030                        let tmp = result[[r, c]];
3031                        result[[r, c]] = result[[nrows - 1 - r, c]];
3032                        result[[nrows - 1 - r, c]] = tmp;
3033                    }
3034                }
3035                Ok(Value::Matrix(result))
3036            }
3037        },
3038        // --- Find ---
3039        ("find", 1) => find_nonzero(&args[0], usize::MAX),
3040        ("find", 2) => {
3041            let k = scalar_arg(&args[1], name, 2)?;
3042            if k < 0.0 {
3043                return Err("find: k must be non-negative".to_string());
3044            }
3045            find_nonzero(&args[0], k as usize)
3046        }
3047        // --- Unique ---
3048        ("unique", 1) => match &args[0] {
3049            Value::Void => Err("unique: not applicable to void".to_string()),
3050            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3051            Value::Matrix(m) => {
3052                let mut vals: Vec<f64> = m.iter().copied().collect();
3053                vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3054                let mut unique: Vec<f64> = Vec::new();
3055                for v in vals {
3056                    if unique.last().is_none_or(|&last| last != v) {
3057                        unique.push(v);
3058                    }
3059                }
3060                let n = unique.len();
3061                Ok(Value::Matrix(
3062                    Array2::from_shape_vec((1, n), unique).unwrap(),
3063                ))
3064            }
3065            Value::Complex(_, _) => Err("unique: not applicable to complex values".to_string()),
3066            Value::Str(_)
3067            | Value::StringObj(_)
3068            | Value::Lambda(_)
3069            | Value::Function { .. }
3070            | Value::Tuple(_)
3071            | Value::Cell(_)
3072            | Value::Struct(_)
3073            | Value::StructArray(_) => {
3074                Err("unique: not applicable to non-numeric values".to_string())
3075            }
3076        },
3077        // --- Descriptive statistics ---
3078        ("std", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false).sqrt(), "std"),
3079        ("std", 2) => {
3080            let w = scalar_arg(&args[1], name, 2)?;
3081            let population = w != 0.0;
3082            apply_stat(&args[0], |s| stat_var_vec(s, population).sqrt(), "std")
3083        }
3084        ("var", 1) => apply_stat(&args[0], |s| stat_var_vec(s, false), "var"),
3085        ("var", 2) => {
3086            let w = scalar_arg(&args[1], name, 2)?;
3087            let population = w != 0.0;
3088            apply_stat(&args[0], |s| stat_var_vec(s, population), "var")
3089        }
3090        ("cov", 1) => match &args[0] {
3091            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
3092            Value::Matrix(m) => {
3093                if m.nrows() == 1 || m.ncols() == 1 {
3094                    let vals: Vec<f64> = m.iter().copied().collect();
3095                    Ok(Value::Scalar(stat_var_vec(&vals, false)))
3096                } else {
3097                    let (nobs, nvars) = (m.nrows(), m.ncols());
3098                    if nobs < 2 {
3099                        return Err("cov: need at least 2 observations".to_string());
3100                    }
3101                    let mut centered = m.clone();
3102                    for c in 0..nvars {
3103                        let col_mean: f64 = m.column(c).iter().sum::<f64>() / nobs as f64;
3104                        for r in 0..nobs {
3105                            centered[[r, c]] -= col_mean;
3106                        }
3107                    }
3108                    let denom = (nobs - 1) as f64;
3109                    let mut cov_mat = Array2::<f64>::zeros((nvars, nvars));
3110                    for i in 0..nvars {
3111                        for j in 0..nvars {
3112                            let dot: f64 =
3113                                (0..nobs).map(|r| centered[[r, i]] * centered[[r, j]]).sum();
3114                            cov_mat[[i, j]] = dot / denom;
3115                        }
3116                    }
3117                    Ok(Value::Matrix(cov_mat))
3118                }
3119            }
3120            _ => Err("cov: argument must be numeric".to_string()),
3121        },
3122        ("median", 1) => apply_stat(
3123            &args[0],
3124            |s| {
3125                if s.is_empty() {
3126                    return f64::NAN;
3127                }
3128                let mut v = s.to_vec();
3129                v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3130                let n = v.len();
3131                if n % 2 == 0 {
3132                    (v[n / 2 - 1] + v[n / 2]) / 2.0
3133                } else {
3134                    v[n / 2]
3135                }
3136            },
3137            "median",
3138        ),
3139        ("mode", 1) => apply_stat(
3140            &args[0],
3141            |s| {
3142                if s.is_empty() {
3143                    return f64::NAN;
3144                }
3145                let mut counts: std::collections::HashMap<u64, usize> =
3146                    std::collections::HashMap::new();
3147                for &x in s {
3148                    *counts.entry(x.to_bits()).or_insert(0) += 1;
3149                }
3150                let max_count = counts.values().copied().max().unwrap_or(0);
3151                let mut candidates: Vec<f64> = counts
3152                    .iter()
3153                    .filter(|&(_, &c)| c == max_count)
3154                    .map(|(&bits, _)| f64::from_bits(bits))
3155                    .collect();
3156                candidates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3157                candidates[0]
3158            },
3159            "mode",
3160        ),
3161        ("skewness", 1) => apply_stat(
3162            &args[0],
3163            |s| {
3164                let n = s.len();
3165                if n == 0 {
3166                    return f64::NAN;
3167                }
3168                if n == 1 {
3169                    return 0.0;
3170                }
3171                let mean = s.iter().sum::<f64>() / n as f64;
3172                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
3173                if m2 == 0.0 {
3174                    return 0.0;
3175                }
3176                let m3 = s.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n as f64;
3177                m3 / m2.powf(1.5)
3178            },
3179            "skewness",
3180        ),
3181        ("kurtosis", 1) => apply_stat(
3182            &args[0],
3183            |s| {
3184                let n = s.len();
3185                if n < 2 {
3186                    return f64::NAN;
3187                }
3188                let mean = s.iter().sum::<f64>() / n as f64;
3189                let m2 = s.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
3190                if m2 == 0.0 {
3191                    return f64::NAN;
3192                }
3193                let m4 = s.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n as f64;
3194                m4 / m2.powi(2)
3195            },
3196            "kurtosis",
3197        ),
3198        ("hist", n) if n == 1 || n == 2 => {
3199            let n_bins = if args.len() == 2 {
3200                scalar_arg(&args[1], name, 2)? as usize
3201            } else {
3202                10
3203            };
3204            if n_bins == 0 {
3205                return Err("hist: number of bins must be positive".to_string());
3206            }
3207            let vals = numeric_vec(&args[0], name)?;
3208            if vals.is_empty() {
3209                return Ok(Value::Void);
3210            }
3211            let min_v = vals.iter().cloned().fold(f64::INFINITY, f64::min);
3212            let max_v = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3213            let range = if max_v > min_v { max_v - min_v } else { 1.0 };
3214            let mut counts = vec![0usize; n_bins];
3215            for &v in &vals {
3216                let b = ((v - min_v) / range * n_bins as f64) as usize;
3217                counts[b.min(n_bins - 1)] += 1;
3218            }
3219            let bar_cols: usize = std::env::var("COLUMNS")
3220                .ok()
3221                .and_then(|s| s.parse::<usize>().ok())
3222                .unwrap_or(80)
3223                .saturating_sub(26)
3224                .max(10);
3225            let max_count = *counts.iter().max().unwrap_or(&1).max(&1);
3226            let mut output = String::new();
3227            for (i, &c) in counts.iter().enumerate() {
3228                let lo = min_v + range * (i as f64 / n_bins as f64);
3229                let hi = min_v + range * ((i + 1) as f64 / n_bins as f64);
3230                let bar_len = c * bar_cols / max_count;
3231                output.push_str(&format!(
3232                    "{lo:8.4} {hi:8.4} |{bar:<bar_cols$}| {c}\n",
3233                    bar = "#".repeat(bar_len),
3234                ));
3235            }
3236            match io.as_deref_mut() {
3237                Some(ctx) => ctx.write_to_fd(1, &output)?,
3238                None => {
3239                    print!("{output}");
3240                    std::io::stdout().flush().ok();
3241                }
3242            }
3243            Ok(Value::Void)
3244        }
3245        ("histc", 2) => {
3246            let vals = numeric_vec(&args[0], name)?;
3247            let edges = numeric_vec(&args[1], name)?;
3248            if edges.is_empty() {
3249                return Err("histc: edges must not be empty".to_string());
3250            }
3251            let n_edges = edges.len();
3252            let mut counts = vec![0.0f64; n_edges];
3253            for &v in &vals {
3254                // Linear scan — fine for typical edge counts
3255                let last = n_edges - 1;
3256                if v == edges[last] {
3257                    counts[last] += 1.0;
3258                } else {
3259                    for i in 0..last {
3260                        if v >= edges[i] && v < edges[i + 1] {
3261                            counts[i] += 1.0;
3262                            break;
3263                        }
3264                    }
3265                }
3266            }
3267            Ok(Value::Matrix(
3268                Array2::from_shape_vec((1, n_edges), counts).unwrap(),
3269            ))
3270        }
3271        // --- Percentiles and distributions ---
3272        ("prctile", 2) => {
3273            let p_vals = numeric_vec(&args[1], name)?;
3274            let n_p = p_vals.len();
3275
3276            // Sort one column of floats and compute all requested percentiles.
3277            let compute_col = |vals: &[f64]| -> Vec<f64> {
3278                let mut s = vals.to_vec();
3279                s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3280                p_vals.iter().map(|&p| percentile_sorted(&s, p)).collect()
3281            };
3282
3283            match &args[0] {
3284                Value::Scalar(n) => {
3285                    let pr = compute_col(&[*n]);
3286                    if n_p == 1 {
3287                        Ok(Value::Scalar(pr[0]))
3288                    } else {
3289                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
3290                    }
3291                }
3292                Value::Matrix(m) if m.nrows() == 1 || m.ncols() == 1 => {
3293                    let vals: Vec<f64> = m.iter().copied().collect();
3294                    let pr = compute_col(&vals);
3295                    if n_p == 1 {
3296                        Ok(Value::Scalar(pr[0]))
3297                    } else {
3298                        Ok(Value::Matrix(Array2::from_shape_vec((1, n_p), pr).unwrap()))
3299                    }
3300                }
3301                Value::Matrix(m) => {
3302                    // M×N matrix: column-wise → n_p × ncols result
3303                    let ncols = m.ncols();
3304                    let mut result = Array2::<f64>::zeros((n_p, ncols));
3305                    for j in 0..ncols {
3306                        let col: Vec<f64> = m.column(j).iter().copied().collect();
3307                        let pr = compute_col(&col);
3308                        for (i, &v) in pr.iter().enumerate() {
3309                            result[[i, j]] = v;
3310                        }
3311                    }
3312                    if n_p == 1 {
3313                        let row: Vec<f64> = result.row(0).iter().copied().collect();
3314                        Ok(Value::Matrix(
3315                            Array2::from_shape_vec((1, ncols), row).unwrap(),
3316                        ))
3317                    } else {
3318                        Ok(Value::Matrix(result))
3319                    }
3320                }
3321                _ => Err("prctile: first argument must be numeric".to_string()),
3322            }
3323        }
3324        ("iqr", 1) => apply_stat(
3325            &args[0],
3326            |s| {
3327                let mut sorted = s.to_vec();
3328                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3329                percentile_sorted(&sorted, 75.0) - percentile_sorted(&sorted, 25.0)
3330            },
3331            "iqr",
3332        ),
3333        ("zscore", 1) => match &args[0] {
3334            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
3335            Value::Matrix(m) => {
3336                if m.nrows() == 1 || m.ncols() == 1 {
3337                    let vals: Vec<f64> = m.iter().copied().collect();
3338                    let n = vals.len() as f64;
3339                    let mean = vals.iter().sum::<f64>() / n;
3340                    let s = stat_var_vec(&vals, false).sqrt();
3341                    let result: Vec<f64> = vals
3342                        .iter()
3343                        .map(|&x| if s == 0.0 { 0.0 } else { (x - mean) / s })
3344                        .collect();
3345                    Ok(Value::Matrix(
3346                        Array2::from_shape_vec(m.raw_dim(), result).unwrap(),
3347                    ))
3348                } else {
3349                    let (nrows, ncols) = (m.nrows(), m.ncols());
3350                    let mut result = m.clone();
3351                    for j in 0..ncols {
3352                        let col: Vec<f64> = m.column(j).iter().copied().collect();
3353                        let mean = col.iter().sum::<f64>() / col.len() as f64;
3354                        let s = stat_var_vec(&col, false).sqrt();
3355                        for i in 0..nrows {
3356                            result[[i, j]] = if s == 0.0 {
3357                                0.0
3358                            } else {
3359                                (m[[i, j]] - mean) / s
3360                            };
3361                        }
3362                    }
3363                    Ok(Value::Matrix(result))
3364                }
3365            }
3366            _ => Err("zscore: argument must be numeric".to_string()),
3367        },
3368        // diag(v) — vector → diagonal matrix; diag(A) → column vector of main diagonal.
3369        ("diag", 1) => match &args[0] {
3370            Value::Scalar(n) => Ok(Value::Matrix(Array2::from_elem((1, 1), *n))),
3371            Value::Matrix(m) => {
3372                let (rows, cols) = (m.nrows(), m.ncols());
3373                if rows == 1 || cols == 1 {
3374                    // vector → N×N diagonal matrix
3375                    let v: Vec<f64> = m.iter().copied().collect();
3376                    let n = v.len();
3377                    let mut result = Array2::<f64>::zeros((n, n));
3378                    for (i, &val) in v.iter().enumerate() {
3379                        result[[i, i]] = val;
3380                    }
3381                    Ok(Value::Matrix(result))
3382                } else {
3383                    // matrix → extract main diagonal as N×1 column vector
3384                    let n = rows.min(cols);
3385                    let d: Vec<f64> = (0..n).map(|i| m[[i, i]]).collect();
3386                    Ok(Value::Matrix(Array2::from_shape_vec((n, 1), d).unwrap()))
3387                }
3388            }
3389            Value::Void => Err("diag: not applicable to void".to_string()),
3390            Value::Complex(_, _) => Err("diag: not applicable to complex values".to_string()),
3391            Value::Str(_)
3392            | Value::StringObj(_)
3393            | Value::Lambda(_)
3394            | Value::Function { .. }
3395            | Value::Tuple(_)
3396            | Value::Cell(_)
3397            | Value::Struct(_)
3398            | Value::StructArray(_) => {
3399                Err("diag: not applicable to non-numeric values".to_string())
3400            }
3401        },
3402
3403        // --- Complex built-ins ---
3404        // real(z) — real part; works on scalars too (returns the value unchanged).
3405        ("real", 1) => match &args[0] {
3406            Value::Void => Err("real: not applicable to void".to_string()),
3407            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3408            Value::Complex(re, _) => Ok(Value::Scalar(*re)),
3409            Value::Matrix(_) => Err("real: not applicable to matrices".to_string()),
3410            Value::Str(_)
3411            | Value::StringObj(_)
3412            | Value::Lambda(_)
3413            | Value::Function { .. }
3414            | Value::Tuple(_)
3415            | Value::Cell(_)
3416            | Value::Struct(_)
3417            | Value::StructArray(_) => {
3418                Err("real: not applicable to non-numeric values".to_string())
3419            }
3420        },
3421        // imag(z) — imaginary part; returns 0.0 for real scalars.
3422        ("imag", 1) => match &args[0] {
3423            Value::Void => Err("imag: not applicable to void".to_string()),
3424            Value::Scalar(_) => Ok(Value::Scalar(0.0)),
3425            Value::Complex(_, im) => Ok(Value::Scalar(*im)),
3426            Value::Matrix(_) => Err("imag: not applicable to matrices".to_string()),
3427            Value::Str(_)
3428            | Value::StringObj(_)
3429            | Value::Lambda(_)
3430            | Value::Function { .. }
3431            | Value::Tuple(_)
3432            | Value::Cell(_)
3433            | Value::Struct(_)
3434            | Value::StructArray(_) => {
3435                Err("imag: not applicable to non-numeric values".to_string())
3436            }
3437        },
3438        // abs(z) — modulus; overloads scalar abs.
3439        ("abs", 1) => match &args[0] {
3440            Value::Void => Err("abs: not applicable to void".to_string()),
3441            Value::Scalar(n) => Ok(Value::Scalar(n.abs())),
3442            Value::Complex(re, im) => Ok(Value::Scalar((re * re + im * im).sqrt())),
3443            Value::Matrix(m) => Ok(Value::Matrix(m.mapv(|x| x.abs()))),
3444            Value::Str(_)
3445            | Value::StringObj(_)
3446            | Value::Lambda(_)
3447            | Value::Function { .. }
3448            | Value::Tuple(_)
3449            | Value::Cell(_)
3450            | Value::Struct(_)
3451            | Value::StructArray(_) => Err("abs: not applicable to non-numeric values".to_string()),
3452        },
3453        // angle(z) — argument in radians; returns 0 for non-negative reals.
3454        ("angle", 1) => match &args[0] {
3455            Value::Void => Err("angle: not applicable to void".to_string()),
3456            Value::Scalar(n) => Ok(Value::Scalar(if *n >= 0.0 {
3457                0.0
3458            } else {
3459                std::f64::consts::PI
3460            })),
3461            Value::Complex(re, im) => Ok(Value::Scalar(im.atan2(*re))),
3462            Value::Matrix(_) => Err("angle: not applicable to matrices".to_string()),
3463            Value::Str(_)
3464            | Value::StringObj(_)
3465            | Value::Lambda(_)
3466            | Value::Function { .. }
3467            | Value::Tuple(_)
3468            | Value::Cell(_)
3469            | Value::Struct(_)
3470            | Value::StructArray(_) => {
3471                Err("angle: not applicable to non-numeric values".to_string())
3472            }
3473        },
3474        // conj(z) — complex conjugate; scalars are unchanged.
3475        ("conj", 1) => match &args[0] {
3476            Value::Void => Err("conj: not applicable to void".to_string()),
3477            Value::Scalar(n) => Ok(Value::Scalar(*n)),
3478            Value::Complex(re, im) => Ok(make_complex(*re, -*im)),
3479            Value::Matrix(m) => Ok(Value::Matrix(m.clone())),
3480            Value::Str(_)
3481            | Value::StringObj(_)
3482            | Value::Lambda(_)
3483            | Value::Function { .. }
3484            | Value::Tuple(_)
3485            | Value::Cell(_)
3486            | Value::Struct(_)
3487            | Value::StructArray(_) => {
3488                Err("conj: not applicable to non-numeric values".to_string())
3489            }
3490        },
3491        // complex(re, im) — construct complex from two reals.
3492        ("complex", 2) => {
3493            let re = scalar_arg(&args[0], name, 1)?;
3494            let im = scalar_arg(&args[1], name, 2)?;
3495            Ok(make_complex(re, im))
3496        }
3497        // isreal(z) — 1.0 if imaginary part is zero, 0.0 otherwise.
3498        ("isreal", 1) => match &args[0] {
3499            Value::Void => Ok(Value::Scalar(0.0)),
3500            Value::Scalar(_) => Ok(Value::Scalar(1.0)),
3501            Value::Complex(_, im) => Ok(Value::Scalar(if *im == 0.0 { 1.0 } else { 0.0 })),
3502            Value::Matrix(_) => Ok(Value::Scalar(1.0)),
3503            // Strings are not real numbers; functions are not numbers
3504            Value::Str(_) | Value::StringObj(_) => Ok(Value::Scalar(0.0)),
3505            Value::Lambda(_)
3506            | Value::Function { .. }
3507            | Value::Tuple(_)
3508            | Value::Cell(_)
3509            | Value::Struct(_)
3510            | Value::StructArray(_) => Ok(Value::Scalar(0.0)),
3511        },
3512        // --- String built-ins ---
3513        // num2str(x) — convert number to char array string
3514        ("num2str", 1) => match &args[0] {
3515            Value::Void => Err("num2str: not applicable to void".to_string()),
3516            Value::Str(s) => Ok(Value::Str(s.clone())),
3517            Value::StringObj(s) => Ok(Value::Str(s.clone())),
3518            Value::Scalar(n) => Ok(Value::Str(fmt_auto_sig(*n, 5))),
3519            Value::Complex(re, im) => Ok(Value::Str(format_complex(*re, *im, &FormatMode::Short))),
3520            Value::Matrix(m) => {
3521                let s = m
3522                    .iter()
3523                    .map(|x| fmt_auto_sig(*x, 5))
3524                    .collect::<Vec<_>>()
3525                    .join("  ");
3526                Ok(Value::Str(s))
3527            }
3528            Value::Lambda(_)
3529            | Value::Function { .. }
3530            | Value::Tuple(_)
3531            | Value::Cell(_)
3532            | Value::Struct(_)
3533            | Value::StructArray(_) => Err("num2str: not applicable to this type".to_string()),
3534        },
3535        // num2str(x, N) — N significant digits
3536        ("num2str", 2) => {
3537            let n = scalar_arg(&args[1], name, 2)? as usize;
3538            match &args[0] {
3539                Value::Void => Err("num2str: not applicable to void".to_string()),
3540                Value::Str(s) => Ok(Value::Str(s.clone())),
3541                Value::StringObj(s) => Ok(Value::Str(s.clone())),
3542                Value::Scalar(v) => Ok(Value::Str(fmt_auto_sig(*v, n))),
3543                Value::Complex(re, im) => {
3544                    Ok(Value::Str(format_complex(*re, *im, &FormatMode::Custom(n))))
3545                }
3546                Value::Matrix(m) => {
3547                    let s = m
3548                        .iter()
3549                        .map(|x| fmt_auto_sig(*x, n))
3550                        .collect::<Vec<_>>()
3551                        .join("  ");
3552                    Ok(Value::Str(s))
3553                }
3554                Value::Lambda(_)
3555                | Value::Function { .. }
3556                | Value::Tuple(_)
3557                | Value::Cell(_)
3558                | Value::Struct(_)
3559                | Value::StructArray(_) => Err("num2str: not applicable to this type".to_string()),
3560            }
3561        }
3562        // str2double(s) — parse string as f64; return NaN on failure
3563        ("str2double", 1) => {
3564            let s = string_arg(&args[0], name, 1)?;
3565            match s.trim().parse::<f64>() {
3566                Ok(n) => Ok(Value::Scalar(n)),
3567                Err(_) => Ok(Value::Scalar(f64::NAN)),
3568            }
3569        }
3570        // str2num(s) — parse string as f64; return error on failure
3571        ("str2num", 1) => {
3572            let s = string_arg(&args[0], name, 1)?;
3573            s.trim()
3574                .parse::<f64>()
3575                .map(Value::Scalar)
3576                .map_err(|_| format!("str2num: cannot convert '{}' to number", s.trim()))
3577        }
3578        // strcat(a, b, ...) — concatenate strings
3579        ("strcat", n) if n >= 2 => {
3580            let mut result = String::new();
3581            let mut any_obj = false;
3582            for (i, arg) in args.iter().enumerate() {
3583                match arg {
3584                    Value::Str(s) => result.push_str(s.trim_end()),
3585                    Value::StringObj(s) => {
3586                        result.push_str(s);
3587                        any_obj = true;
3588                    }
3589                    _ => return Err(format!("strcat: argument {} must be a string", i + 1)),
3590                }
3591            }
3592            if any_obj {
3593                Ok(Value::StringObj(result))
3594            } else {
3595                Ok(Value::Str(result))
3596            }
3597        }
3598        // ischar(s) — 1.0 if char array, 0.0 otherwise
3599        ("ischar", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Str(_)) {
3600            1.0
3601        } else {
3602            0.0
3603        })),
3604        // isstring(s) — 1.0 if string object, 0.0 otherwise
3605        ("isstring", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::StringObj(_)) {
3606            1.0
3607        } else {
3608            0.0
3609        })),
3610        // --- Struct built-ins ---
3611        // struct('k1',v1,'k2',v2,...) — construct a scalar struct from name-value pairs
3612        ("struct", _) => {
3613            if !args.len().is_multiple_of(2) {
3614                return Err(
3615                    "struct: requires an even number of arguments (name, value, ...)".to_string(),
3616                );
3617            }
3618            let mut map = IndexMap::new();
3619            for pair in args.chunks(2) {
3620                let key = match &pair[0] {
3621                    Value::Str(s) | Value::StringObj(s) => s.clone(),
3622                    _ => return Err("struct: field names must be strings".to_string()),
3623                };
3624                map.insert(key, pair[1].clone());
3625            }
3626            Ok(Value::Struct(map))
3627        }
3628        // fieldnames(s) — cell array of field names in insertion order
3629        ("fieldnames", 1) => match &args[0] {
3630            Value::Struct(map) => {
3631                let names: Vec<Value> = map.keys().map(|k| Value::Str(k.clone())).collect();
3632                Ok(Value::Cell(names))
3633            }
3634            Value::StructArray(arr) => {
3635                // Use field names from first element
3636                let names: Vec<Value> = arr
3637                    .first()
3638                    .map(|m| m.keys().map(|k| Value::Str(k.clone())).collect())
3639                    .unwrap_or_default();
3640                Ok(Value::Cell(names))
3641            }
3642            _ => Err("fieldnames: argument must be a struct".to_string()),
3643        },
3644        // isfield(s, 'name') — 1.0 if field exists, 0.0 otherwise
3645        ("isfield", 2) => {
3646            let field = match &args[1] {
3647                Value::Str(s) | Value::StringObj(s) => s.clone(),
3648                _ => return Err("isfield: second argument must be a string".to_string()),
3649            };
3650            Ok(Value::Scalar(match &args[0] {
3651                Value::Struct(map) if map.contains_key(&field) => 1.0,
3652                Value::StructArray(arr) if arr.first().is_some_and(|m| m.contains_key(&field)) => {
3653                    1.0
3654                }
3655                _ => 0.0,
3656            }))
3657        }
3658        // rmfield(s, 'name') — copy of struct with field removed
3659        ("rmfield", 2) => {
3660            let field = match &args[1] {
3661                Value::Str(s) | Value::StringObj(s) => s.clone(),
3662                _ => return Err("rmfield: second argument must be a string".to_string()),
3663            };
3664            match &args[0] {
3665                Value::Struct(map) => {
3666                    if !map.contains_key(&field) {
3667                        return Err(format!("rmfield: field '{field}' does not exist"));
3668                    }
3669                    let mut updated = map.clone();
3670                    updated.shift_remove(&field);
3671                    Ok(Value::Struct(updated))
3672                }
3673                Value::StructArray(arr) => {
3674                    let updated: Result<Vec<_>, _> = arr
3675                        .iter()
3676                        .map(|m| {
3677                            if !m.contains_key(&field) {
3678                                return Err(format!("rmfield: field '{field}' does not exist"));
3679                            }
3680                            let mut m2 = m.clone();
3681                            m2.shift_remove(&field);
3682                            Ok(m2)
3683                        })
3684                        .collect();
3685                    Ok(Value::StructArray(updated?))
3686                }
3687                _ => Err("rmfield: first argument must be a struct".to_string()),
3688            }
3689        }
3690        // isstruct(v) — 1.0 if v is a struct or struct array, 0.0 otherwise
3691        ("isstruct", 1) => Ok(Value::Scalar(
3692            if matches!(&args[0], Value::Struct(_) | Value::StructArray(_)) {
3693                1.0
3694            } else {
3695                0.0
3696            },
3697        )),
3698        // --- Cell array built-ins ---
3699        // isempty(v) — 1.0 if v has no elements, 0.0 otherwise.
3700        // Matches MATLAB: empty matrix, empty string, empty cell, or Void are empty.
3701        ("isempty", 1) => {
3702            let empty = match &args[0] {
3703                Value::Matrix(m) => m.is_empty(),
3704                Value::Str(s) | Value::StringObj(s) => s.is_empty(),
3705                Value::Cell(v) => v.is_empty(),
3706                Value::Void => true,
3707                _ => false,
3708            };
3709            Ok(Value::Scalar(if empty { 1.0 } else { 0.0 }))
3710        }
3711        // iscell(v) — 1.0 if v is a cell array, 0.0 otherwise
3712        ("iscell", 1) => Ok(Value::Scalar(if matches!(&args[0], Value::Cell(_)) {
3713            1.0
3714        } else {
3715            0.0
3716        })),
3717        // cell(n) — create 1×n cell of Scalar(0.0) slots
3718        ("cell", 1) => {
3719            let n = scalar_arg(&args[0], name, 1)? as usize;
3720            Ok(Value::Cell(vec![Value::Scalar(0.0); n]))
3721        }
3722        // cell(m, n) — create 1×(m*n) cell (2-D layout deferred; stored flat)
3723        ("cell", 2) => {
3724            let m = scalar_arg(&args[0], name, 1)? as usize;
3725            let n = scalar_arg(&args[1], name, 2)? as usize;
3726            Ok(Value::Cell(vec![Value::Scalar(0.0); m * n]))
3727        }
3728        // cellfun(f, c) — apply f to each element of cell c.
3729        // Returns Value::Matrix when all results are scalars; otherwise Value::Cell.
3730        ("cellfun", 2) => {
3731            let f = args[0].clone();
3732            match &args[1] {
3733                Value::Cell(elems) => {
3734                    let elems = elems.clone();
3735                    let mut results = Vec::with_capacity(elems.len());
3736                    for elem in &elems {
3737                        let result =
3738                            call_function_value(&f, std::slice::from_ref(elem), io.as_deref_mut())?;
3739                        results.push(result);
3740                    }
3741                    // Try uniform output (all scalars)
3742                    let all_scalar = results.iter().all(|v| matches!(v, Value::Scalar(_)));
3743                    if all_scalar {
3744                        let vals: Vec<f64> = results
3745                            .iter()
3746                            .map(|v| {
3747                                if let Value::Scalar(n) = v {
3748                                    *n
3749                                } else {
3750                                    unreachable!()
3751                                }
3752                            })
3753                            .collect();
3754                        let n = vals.len();
3755                        if n == 0 {
3756                            Ok(Value::Matrix(Array2::zeros((1, 0))))
3757                        } else {
3758                            Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
3759                        }
3760                    } else {
3761                        Ok(Value::Cell(results))
3762                    }
3763                }
3764                _ => Err("cellfun: second argument must be a cell array".to_string()),
3765            }
3766        }
3767        // arrayfun(f, v) — apply f element-wise to matrix v.
3768        // Returns same-shape Value::Matrix (scalar-returning f only).
3769        ("arrayfun", 2) => {
3770            let f = args[0].clone();
3771            match &args[1] {
3772                Value::Matrix(m) => {
3773                    let m = m.clone();
3774                    let mut flat = Vec::with_capacity(m.len());
3775                    // Iterate in column-major order
3776                    for col in 0..m.ncols() {
3777                        for row in 0..m.nrows() {
3778                            let elem = Value::Scalar(m[[row, col]]);
3779                            let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
3780                            match result {
3781                                Value::Scalar(n) => flat.push(n),
3782                                _ => {
3783                                    return Err(
3784                                        "arrayfun: function must return a scalar".to_string()
3785                                    );
3786                                }
3787                            }
3788                        }
3789                    }
3790                    Ok(Value::Matrix(
3791                        Array2::from_shape_vec((m.nrows(), m.ncols()), flat).unwrap(),
3792                    ))
3793                }
3794                Value::Scalar(n) => {
3795                    let elem = Value::Scalar(*n);
3796                    let result = call_function_value(&f, &[elem], io.as_deref_mut())?;
3797                    Ok(result)
3798                }
3799                _ => {
3800                    Err("arrayfun: second argument must be a numeric matrix or scalar".to_string())
3801                }
3802            }
3803        }
3804        // lower(s) — convert to lowercase
3805        ("lower", 1) => match &args[0] {
3806            Value::Str(s) => Ok(Value::Str(s.to_lowercase())),
3807            Value::StringObj(s) => Ok(Value::StringObj(s.to_lowercase())),
3808            _ => Err("lower: argument must be a string".to_string()),
3809        },
3810        // upper(s) — convert to uppercase
3811        ("upper", 1) => match &args[0] {
3812            Value::Str(s) => Ok(Value::Str(s.to_uppercase())),
3813            Value::StringObj(s) => Ok(Value::StringObj(s.to_uppercase())),
3814            _ => Err("upper: argument must be a string".to_string()),
3815        },
3816        // strtrim(s) — trim leading/trailing whitespace
3817        ("strtrim", 1) => match &args[0] {
3818            Value::Str(s) => Ok(Value::Str(s.trim().to_string())),
3819            Value::StringObj(s) => Ok(Value::StringObj(s.trim().to_string())),
3820            _ => Err("strtrim: argument must be a string".to_string()),
3821        },
3822        // strrep(s, old, new) — replace all occurrences
3823        ("strrep", 3) => {
3824            let s = string_arg(&args[0], name, 1)?.to_string();
3825            let old = string_arg(&args[1], name, 2)?;
3826            let new = string_arg(&args[2], name, 3)?;
3827            let result = s.replace(old, new);
3828            match &args[0] {
3829                Value::StringObj(_) => Ok(Value::StringObj(result)),
3830                _ => Ok(Value::Str(result)),
3831            }
3832        }
3833        // strcmp(a, b) — case-sensitive string comparison
3834        ("strcmp", 2) => {
3835            let a = string_arg(&args[0], name, 1)?;
3836            let b = string_arg(&args[1], name, 2)?;
3837            Ok(Value::Scalar(bool_to_f64(a == b)))
3838        }
3839        // strcmpi(a, b) — case-insensitive comparison
3840        ("strcmpi", 2) => {
3841            let a = string_arg(&args[0], name, 1)?.to_lowercase();
3842            let b = string_arg(&args[1], name, 2)?.to_lowercase();
3843            Ok(Value::Scalar(bool_to_f64(a == b)))
3844        }
3845        // disp(x) — display value without variable name, like MATLAB disp()
3846        ("disp", 1) => {
3847            use std::io::Write;
3848            let mode = get_display_fmt();
3849            let output = match &args[0] {
3850                Value::Str(s) | Value::StringObj(s) => format!("{s}\n"),
3851                v => match format_value_full(v, &mode) {
3852                    Some(block) => format!("{block}\n\n"),
3853                    None => format!("{}\n", format_value(v, get_display_base(), &mode)),
3854                },
3855            };
3856            match io {
3857                Some(ctx) => ctx.write_to_fd(1, &output)?,
3858                None => {
3859                    print!("{output}");
3860                    std::io::stdout().flush().ok();
3861                }
3862            }
3863            Ok(Value::Void)
3864        }
3865        // sprintf(fmt, ...) — format and return as char array
3866        ("sprintf", n) if n >= 1 => {
3867            let fmt = string_arg(&args[0], name, 1)?.to_string();
3868            let result = format_printf(&fmt, &args[1..])?;
3869            Ok(Value::Str(result))
3870        }
3871        // fprintf([fd,] fmt, ...) — format and print; fd defaults to 1 (stdout)
3872        ("fprintf", n) if n >= 1 => {
3873            // If first arg is a numeric scalar, treat it as a file descriptor.
3874            let (fd, fmt_idx) = match &args[0] {
3875                Value::Scalar(n) => (*n as i32, 1),
3876                _ => (1, 0),
3877            };
3878            if fmt_idx >= args.len() {
3879                return Err("fprintf: missing format string".to_string());
3880            }
3881            let fmt = string_arg(&args[fmt_idx], name, fmt_idx + 1)?.to_string();
3882            let output = format_printf(&fmt, &args[fmt_idx + 1..])?;
3883            match io {
3884                Some(ctx) => ctx.write_to_fd(fd, &output)?,
3885                None => {
3886                    // No I/O context: only stdout (fd 1) is allowed
3887                    if fd == 1 {
3888                        use std::io::Write;
3889                        print!("{output}");
3890                        std::io::stdout().flush().ok();
3891                    } else {
3892                        return Err("fprintf: file I/O not available in this context".to_string());
3893                    }
3894                }
3895            }
3896            Ok(Value::Void)
3897        }
3898        // fopen(path, mode) — open a file; returns fd or -1 on failure
3899        ("fopen", 2) => {
3900            let path = string_arg(&args[0], name, 1)?;
3901            let mode = string_arg(&args[1], name, 2)?;
3902            match io {
3903                Some(ctx) => Ok(Value::Scalar(ctx.fopen(path, mode) as f64)),
3904                None => Err("fopen: file I/O not available in this context".to_string()),
3905            }
3906        }
3907        // fclose(fd) or fclose('all')
3908        ("fclose", 1) => match &args[0] {
3909            Value::Str(s) if s == "all" => {
3910                if let Some(ctx) = io {
3911                    ctx.fclose_all();
3912                }
3913                Ok(Value::Scalar(0.0))
3914            }
3915            _ => {
3916                let fd = scalar_arg(&args[0], name, 1)? as i32;
3917                match io {
3918                    Some(ctx) => Ok(Value::Scalar(ctx.fclose(fd) as f64)),
3919                    None => Err("fclose: file I/O not available in this context".to_string()),
3920                }
3921            }
3922        },
3923        // fgetl(fd) — read line, strip newline; returns Str or Scalar(-1) at EOF
3924        ("fgetl", 1) => {
3925            let fd = scalar_arg(&args[0], name, 1)? as i32;
3926            match io {
3927                Some(ctx) => match ctx.fgetl(fd) {
3928                    Some(line) => Ok(Value::Str(line)),
3929                    None => Ok(Value::Scalar(-1.0)),
3930                },
3931                None => Err("fgetl: file I/O not available in this context".to_string()),
3932            }
3933        }
3934        // fgets(fd) — read line, keep newline; returns Str or Scalar(-1) at EOF
3935        ("fgets", 1) => {
3936            let fd = scalar_arg(&args[0], name, 1)? as i32;
3937            match io {
3938                Some(ctx) => match ctx.fgets(fd) {
3939                    Some(line) => Ok(Value::Str(line)),
3940                    None => Ok(Value::Scalar(-1.0)),
3941                },
3942                None => Err("fgets: file I/O not available in this context".to_string()),
3943            }
3944        }
3945        // isfile(path) — 1.0 if path exists and is a regular file, else 0.0
3946        ("isfile", 1) => {
3947            let path = string_arg(&args[0], name, 1)?;
3948            let is_file = std::fs::metadata(path)
3949                .map(|m| m.is_file())
3950                .unwrap_or(false);
3951            Ok(Value::Scalar(bool_to_f64(is_file)))
3952        }
3953        // isfolder(path) — 1.0 if path exists and is a directory, else 0.0
3954        ("isfolder", 1) => {
3955            let path = string_arg(&args[0], name, 1)?;
3956            let is_dir = std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false);
3957            Ok(Value::Scalar(bool_to_f64(is_dir)))
3958        }
3959        // genpath(dir) — return dir and all subdirectories as a path separator-delimited string
3960        ("genpath", 1) => {
3961            let root = string_arg(&args[0], name, 1)?;
3962            let sep = if cfg!(windows) { ';' } else { ':' };
3963            let mut dirs: Vec<String> = Vec::new();
3964            let mut stack = vec![std::path::PathBuf::from(root)];
3965            while let Some(dir) = stack.pop() {
3966                if !dir.is_dir() {
3967                    continue;
3968                }
3969                dirs.push(dir.to_string_lossy().into_owned());
3970                if let Ok(entries) = std::fs::read_dir(&dir) {
3971                    let mut children: Vec<std::path::PathBuf> = entries
3972                        .filter_map(|e| e.ok())
3973                        .map(|e| e.path())
3974                        .filter(|p| p.is_dir())
3975                        .collect();
3976                    children.sort();
3977                    children.reverse();
3978                    stack.extend(children);
3979                }
3980            }
3981            Ok(Value::Str(dirs.join(&sep.to_string())))
3982        }
3983        // pwd() — current working directory as a char array (parser sends ans as sole arg for empty calls)
3984        ("pwd", _) => {
3985            let cwd = std::env::current_dir()
3986                .map(|p| p.to_string_lossy().into_owned())
3987                .unwrap_or_default();
3988            Ok(Value::Str(cwd))
3989        }
3990        // exist(name) — check var (1), then file (2), else 0
3991        ("exist", 1) => {
3992            let name_arg = string_arg(&args[0], name, 1)?;
3993            if env.contains_key(name_arg) {
3994                Ok(Value::Scalar(1.0))
3995            } else if std::path::Path::new(name_arg).is_file() {
3996                Ok(Value::Scalar(2.0))
3997            } else {
3998                Ok(Value::Scalar(0.0))
3999            }
4000        }
4001        // exist(name, 'var') or exist(name, 'file')
4002        ("exist", 2) => {
4003            let name_arg = string_arg(&args[0], name, 1)?;
4004            let kind = string_arg(&args[1], name, 2)?;
4005            match kind {
4006                "var" => Ok(Value::Scalar(if env.contains_key(name_arg) {
4007                    1.0
4008                } else {
4009                    0.0
4010                })),
4011                "file" => Ok(Value::Scalar(if std::path::Path::new(name_arg).is_file() {
4012                    2.0
4013                } else {
4014                    0.0
4015                })),
4016                other => Err(format!(
4017                    "exist: unknown type '{other}', expected 'var' or 'file'"
4018                )),
4019            }
4020        }
4021        // dlmread(path) / dlmread(path, delim)
4022        ("dlmread", 1) => {
4023            let path = string_arg(&args[0], name, 1)?.to_string();
4024            dlmread_impl(&path, None)
4025        }
4026        ("dlmread", 2) => {
4027            let path = string_arg(&args[0], name, 1)?.to_string();
4028            let delim = interpret_delim(string_arg(&args[1], name, 2)?);
4029            dlmread_impl(&path, Some(delim))
4030        }
4031        // dlmwrite(path, A) / dlmwrite(path, A, delim)
4032        ("dlmwrite", 2) => {
4033            let path = string_arg(&args[0], name, 1)?.to_string();
4034            dlmwrite_impl(&path, &args[1], None)
4035        }
4036        ("dlmwrite", 3) => {
4037            let path = string_arg(&args[0], name, 1)?.to_string();
4038            let delim = interpret_delim(string_arg(&args[2], name, 3)?);
4039            dlmwrite_impl(&path, &args[1], Some(delim))
4040        }
4041        // readmatrix(path) / readmatrix(path, 'Delimiter', d)
4042        ("readmatrix", n) if n == 1 || n == 3 => {
4043            let path = string_arg(&args[0], name, 1)?.to_string();
4044            let delim = parse_delimiter_opt(name, args, 1)?;
4045            readmatrix_impl(&path, delim)
4046        }
4047        // readtable(path) / readtable(path, 'Delimiter', d)
4048        ("readtable", n) if n == 1 || n == 3 => {
4049            let path = string_arg(&args[0], name, 1)?.to_string();
4050            let delim = parse_delimiter_opt(name, args, 1)?;
4051            readtable_impl(&path, delim)
4052        }
4053        // writetable(T, path) / writetable(T, path, 'Delimiter', d)
4054        ("writetable", n) if n == 2 || n == 4 => {
4055            let path = string_arg(&args[1], name, 2)?.to_string();
4056            let delim = parse_delimiter_opt(name, args, 2)?;
4057            writetable_impl(&args[0], &path, delim)
4058        }
4059        // xor(a, b) — element-wise XOR: (a != 0) XOR (b != 0)
4060        ("xor", 2) => {
4061            let a = &args[0];
4062            let b = &args[1];
4063            match (a, b) {
4064                (Value::Scalar(x), Value::Scalar(y)) => {
4065                    Ok(Value::Scalar(bool_to_f64((*x != 0.0) ^ (*y != 0.0))))
4066                }
4067                (Value::Matrix(mx), Value::Matrix(my)) => {
4068                    if mx.shape() != my.shape() {
4069                        return Err("xor: matrices must have the same dimensions".to_string());
4070                    }
4071                    Ok(Value::Matrix(ndarray::Zip::from(mx).and(my).map_collect(
4072                        |a, b| bool_to_f64((*a != 0.0) ^ (*b != 0.0)),
4073                    )))
4074                }
4075                (Value::Scalar(s), Value::Matrix(m)) => {
4076                    let sv = *s != 0.0;
4077                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64(sv ^ (x != 0.0)))))
4078                }
4079                (Value::Matrix(m), Value::Scalar(s)) => {
4080                    let sv = *s != 0.0;
4081                    Ok(Value::Matrix(m.mapv(|x| bool_to_f64((x != 0.0) ^ sv))))
4082                }
4083                _ => Err("xor: arguments must be numeric".to_string()),
4084            }
4085        }
4086        // not(a) — element-wise NOT (alias for ~a)
4087        ("not", 1) => apply_elem(&args[0], |x| if x == 0.0 { 1.0 } else { 0.0 }),
4088        // int2str(x) — round to nearest integer, return as char array
4089        ("int2str", 1) => match &args[0] {
4090            Value::Scalar(n) => Ok(Value::Str(format!("{}", n.round() as i64))),
4091            Value::Matrix(m) => {
4092                let parts: Vec<String> =
4093                    m.iter().map(|x| format!("{}", x.round() as i64)).collect();
4094                Ok(Value::Str(parts.join("  ")))
4095            }
4096            _ => Err("int2str: argument must be numeric".to_string()),
4097        },
4098        // mat2str(A) — matrix to MATLAB literal syntax string
4099        ("mat2str", 1) => match &args[0] {
4100            Value::Scalar(n) => Ok(Value::Str(format!("{n}"))),
4101            Value::Matrix(m) => {
4102                if m.nrows() == 0 || m.ncols() == 0 {
4103                    return Ok(Value::Str("[]".to_string()));
4104                }
4105                let mut s = String::from("[");
4106                for (r, row) in m.rows().into_iter().enumerate() {
4107                    if r > 0 {
4108                        s.push(';');
4109                    }
4110                    for (c, val) in row.iter().enumerate() {
4111                        if c > 0 {
4112                            s.push(' ');
4113                        }
4114                        s.push_str(&format!("{val}"));
4115                    }
4116                }
4117                s.push(']');
4118                Ok(Value::Str(s))
4119            }
4120            _ => Err("mat2str: argument must be numeric".to_string()),
4121        },
4122        // strsplit(s, delim) — split string by delimiter, return cell array
4123        ("strsplit", 2) => {
4124            let s = string_arg(&args[0], name, 1)?.to_string();
4125            let delim = string_arg(&args[1], name, 2)?.to_string();
4126            let parts: Vec<Value> = s
4127                .split(delim.as_str())
4128                .map(|p| Value::Str(p.to_string()))
4129                .collect();
4130            Ok(Value::Cell(parts))
4131        }
4132        // strsplit(s) — split on whitespace
4133        ("strsplit", 1) => {
4134            let s = string_arg(&args[0], name, 1)?.to_string();
4135            let parts: Vec<Value> = s
4136                .split_whitespace()
4137                .map(|p| Value::Str(p.to_string()))
4138                .collect();
4139            Ok(Value::Cell(parts))
4140        }
4141        // error(fmt, args...) — raise a runtime error with a formatted message
4142        ("error", _) if !args.is_empty() => {
4143            let fmt_str = match &args[0] {
4144                Value::Str(s) | Value::StringObj(s) => s.clone(),
4145                _ => return Err("error: first argument must be a format string".to_string()),
4146            };
4147            let msg = format_printf(&fmt_str, &args[1..])?;
4148            Err(msg)
4149        }
4150        // warning(fmt, args...) — print a warning to stderr, continue execution
4151        ("warning", _) if !args.is_empty() => {
4152            let fmt_str = match &args[0] {
4153                Value::Str(s) | Value::StringObj(s) => s.clone(),
4154                _ => return Err("warning: first argument must be a format string".to_string()),
4155            };
4156            let msg = format_printf(&fmt_str, &args[1..])?;
4157            eprintln!("warning: {msg}");
4158            Ok(Value::Void)
4159        }
4160        // lasterr() — return last error message; lasterr(msg) — set and return previous
4161        ("lasterr", 0) => Ok(Value::Str(get_last_err())),
4162        ("lasterr", 1) => {
4163            let prev = get_last_err();
4164            let new_msg = match &args[0] {
4165                Value::Str(s) | Value::StringObj(s) => s.clone(),
4166                _ => return Err("lasterr: argument must be a string".to_string()),
4167            };
4168            set_last_err(&new_msg);
4169            Ok(Value::Str(prev))
4170        }
4171        // pcall(@func, args...) — protected call; returns [ok, result_or_msg]
4172        ("pcall", _) if !args.is_empty() => {
4173            let callable = args[0].clone();
4174            let call_args = &args[1..];
4175            let result = match &callable {
4176                Value::Lambda(f) => {
4177                    let f = f.clone();
4178                    f.0(call_args, io)
4179                }
4180                Value::Function { .. } => match io {
4181                    Some(io_ref) => FN_CALL_HOOK.with(|c| match c.get() {
4182                        Some(hook) => hook("<pcall>", &callable, call_args, env, io_ref),
4183                        None => Err("pcall: function execution not initialized".to_string()),
4184                    }),
4185                    None => {
4186                        let mut tmp_io = IoContext::new();
4187                        FN_CALL_HOOK.with(|c| match c.get() {
4188                            Some(hook) => hook("<pcall>", &callable, call_args, env, &mut tmp_io),
4189                            None => Err("pcall: function execution not initialized".to_string()),
4190                        })
4191                    }
4192                },
4193                _ => {
4194                    return Err(
4195                        "pcall: first argument must be a function handle (@func)".to_string()
4196                    );
4197                }
4198            };
4199            match result {
4200                Ok(v) => Ok(Value::Tuple(vec![Value::Scalar(1.0), v])),
4201                Err(msg) => {
4202                    set_last_err(&msg);
4203                    Ok(Value::Tuple(vec![Value::Scalar(0.0), Value::Str(msg)]))
4204                }
4205            }
4206        }
4207        // ── Phase 18 — Advanced linear algebra ──────────────────────────────
4208
4209        // eig(A): d = eig(A) → eigenvalue column vector; [V,D] = eig(A) → tuple.
4210        ("eig", 1) => match &args[0] {
4211            Value::Scalar(n) => {
4212                if get_nargout() <= 1 {
4213                    Ok(Value::Matrix(
4214                        Array2::from_shape_vec((1, 1), vec![*n]).unwrap(),
4215                    ))
4216                } else {
4217                    Ok(Value::Tuple(vec![
4218                        Value::Matrix(Array2::eye(1)),
4219                        Value::Matrix(Array2::from_elem((1, 1), *n)),
4220                    ]))
4221                }
4222            }
4223            Value::Matrix(m) => {
4224                let (evals, evecs) = eig_compute(m)?;
4225                let nn = evals.len();
4226                if get_nargout() <= 1 {
4227                    let col: Vec<f64> = evals;
4228                    Ok(Value::Matrix(Array2::from_shape_vec((nn, 1), col).unwrap()))
4229                } else {
4230                    let mut d = Array2::<f64>::zeros((nn, nn));
4231                    for (i, &e) in evals.iter().enumerate() {
4232                        d[[i, i]] = e;
4233                    }
4234                    Ok(Value::Tuple(vec![Value::Matrix(evecs), Value::Matrix(d)]))
4235                }
4236            }
4237            _ => Err("eig: argument must be a real numeric matrix".to_string()),
4238        },
4239
4240        // svd(A): s = svd(A) → singular values; [U,S,V] = svd(A) → full tuple.
4241        // svd(A, 'econ') → economy tuple.
4242        ("svd", 1) => match &args[0] {
4243            Value::Scalar(n) => {
4244                let sv = n.abs();
4245                if get_nargout() <= 1 {
4246                    Ok(Value::Matrix(
4247                        Array2::from_shape_vec((1, 1), vec![sv]).unwrap(),
4248                    ))
4249                } else {
4250                    Ok(Value::Tuple(vec![
4251                        Value::Matrix(Array2::eye(1)),
4252                        Value::Matrix(Array2::from_elem((1, 1), sv)),
4253                        Value::Matrix(Array2::eye(1)),
4254                    ]))
4255                }
4256            }
4257            Value::Matrix(m) => {
4258                let mm = m.nrows();
4259                let nn = m.ncols();
4260                let (u_c, s_v, v_c) = svd_compute(m)?;
4261                let k = s_v.len();
4262                if get_nargout() <= 1 {
4263                    let col: Vec<f64> = s_v;
4264                    Ok(Value::Matrix(Array2::from_shape_vec((k, 1), col).unwrap()))
4265                } else {
4266                    // Full SVD: extend U to m×m, S to m×n.
4267                    let u_full = complete_orthonormal_basis(&u_c);
4268                    let mut s_mat = Array2::<f64>::zeros((mm, nn));
4269                    for (i, &sv) in s_v.iter().enumerate() {
4270                        s_mat[[i, i]] = sv;
4271                    }
4272                    Ok(Value::Tuple(vec![
4273                        Value::Matrix(u_full),
4274                        Value::Matrix(s_mat),
4275                        Value::Matrix(v_c),
4276                    ]))
4277                }
4278            }
4279            _ => Err("svd: argument must be a real numeric matrix".to_string()),
4280        },
4281        ("svd", 2) => match (&args[0], &args[1]) {
4282            (Value::Matrix(m), Value::Str(opt) | Value::StringObj(opt)) if opt == "econ" => {
4283                let (u_c, s_v, v_c) = svd_compute(m)?;
4284                let k = s_v.len();
4285                let mut s_mat = Array2::<f64>::zeros((k, k));
4286                for (i, &sv) in s_v.iter().enumerate() {
4287                    s_mat[[i, i]] = sv;
4288                }
4289                Ok(Value::Tuple(vec![
4290                    Value::Matrix(u_c),
4291                    Value::Matrix(s_mat),
4292                    Value::Matrix(v_c),
4293                ]))
4294            }
4295            _ => Err("svd: expected svd(A, 'econ')".to_string()),
4296        },
4297
4298        // lu(A): R = lu(A) → U factor; [L,U,P] = lu(A) → full tuple (PA=LU).
4299        ("lu", 1) => match &args[0] {
4300            Value::Scalar(n) => {
4301                if get_nargout() <= 1 {
4302                    Ok(Value::Scalar(*n))
4303                } else {
4304                    Ok(Value::Tuple(vec![
4305                        Value::Matrix(Array2::eye(1)),
4306                        Value::Matrix(Array2::from_elem((1, 1), *n)),
4307                        Value::Matrix(Array2::eye(1)),
4308                    ]))
4309                }
4310            }
4311            Value::Matrix(m) => {
4312                let (l, u, p) = lu_decompose(m)?;
4313                if get_nargout() <= 1 {
4314                    Ok(Value::Matrix(u))
4315                } else {
4316                    Ok(Value::Tuple(vec![
4317                        Value::Matrix(l),
4318                        Value::Matrix(u),
4319                        Value::Matrix(p),
4320                    ]))
4321                }
4322            }
4323            _ => Err("lu: argument must be a real numeric matrix".to_string()),
4324        },
4325
4326        // qr(A): R = qr(A) → R factor; [Q,R] = qr(A) → full tuple.
4327        ("qr", 1) => match &args[0] {
4328            Value::Scalar(n) => {
4329                if get_nargout() <= 1 {
4330                    Ok(Value::Scalar(*n))
4331                } else {
4332                    Ok(Value::Tuple(vec![
4333                        Value::Matrix(Array2::from_elem(
4334                            (1, 1),
4335                            if *n >= 0.0 { 1.0 } else { -1.0 },
4336                        )),
4337                        Value::Matrix(Array2::from_elem((1, 1), n.abs())),
4338                    ]))
4339                }
4340            }
4341            Value::Matrix(m) => {
4342                let (q, r) = qr_decompose(m)?;
4343                if get_nargout() <= 1 {
4344                    Ok(Value::Matrix(r))
4345                } else {
4346                    Ok(Value::Tuple(vec![Value::Matrix(q), Value::Matrix(r)]))
4347                }
4348            }
4349            _ => Err("qr: argument must be a real numeric matrix".to_string()),
4350        },
4351
4352        // chol(A): always returns upper triangular R such that A = R'*R.
4353        ("chol", 1) => match &args[0] {
4354            Value::Scalar(n) => {
4355                if *n < 0.0 {
4356                    Err("chol: value is not positive definite".to_string())
4357                } else {
4358                    Ok(Value::Scalar(n.sqrt()))
4359                }
4360            }
4361            Value::Matrix(m) => Ok(Value::Matrix(chol_decompose(m)?)),
4362            _ => Err("chol: argument must be a real numeric matrix".to_string()),
4363        },
4364
4365        // rank(A): numerical rank via SVD threshold.
4366        ("rank", 1) => match &args[0] {
4367            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() > 1e-15 { 1.0 } else { 0.0 })),
4368            Value::Matrix(m) => {
4369                let (_, s_v, _) = svd_compute(m)?;
4370                let tol = (m.nrows().max(m.ncols())) as f64
4371                    * s_v.first().copied().unwrap_or(0.0)
4372                    * f64::EPSILON
4373                    * 2.0;
4374                let r = s_v.iter().filter(|&&s| s > tol).count();
4375                Ok(Value::Scalar(r as f64))
4376            }
4377            _ => Err("rank: argument must be a real numeric matrix".to_string()),
4378        },
4379
4380        // null(A): orthonormal basis for null space of A (columns of V for ~0 singular values).
4381        ("null", 1) => match &args[0] {
4382            Value::Scalar(_) => Ok(Value::Matrix(Array2::zeros((1, 0)))),
4383            Value::Matrix(m) => {
4384                let nn = m.ncols();
4385                let (_, s_v, v_c) = svd_compute(m)?;
4386                let tol = (m.nrows().max(nn)) as f64
4387                    * s_v.first().copied().unwrap_or(0.0)
4388                    * f64::EPSILON
4389                    * 2.0;
4390                let r = s_v.iter().filter(|&&s| s > tol).count();
4391                let null_k = nn.saturating_sub(r);
4392                if null_k == 0 {
4393                    return Ok(Value::Matrix(Array2::zeros((nn, 0))));
4394                }
4395                let mut result = Array2::<f64>::zeros((nn, null_k));
4396                for j in 0..null_k {
4397                    let col_idx = r + j;
4398                    if col_idx < v_c.ncols() {
4399                        for i in 0..nn {
4400                            result[[i, j]] = v_c[[i, col_idx]];
4401                        }
4402                    }
4403                }
4404                Ok(Value::Matrix(result))
4405            }
4406            _ => Err("null: argument must be a real numeric matrix".to_string()),
4407        },
4408
4409        // orth(A): orthonormal basis for column space of A (columns of U for nonzero singular values).
4410        ("orth", 1) => match &args[0] {
4411            Value::Scalar(x) => {
4412                if x.abs() > 1e-15 {
4413                    Ok(Value::Matrix(Array2::from_elem((1, 1), 1.0)))
4414                } else {
4415                    Ok(Value::Matrix(Array2::zeros((1, 0))))
4416                }
4417            }
4418            Value::Matrix(m) => {
4419                let mm = m.nrows();
4420                let (u_c, s_v, _) = svd_compute(m)?;
4421                let tol = (mm.max(m.ncols())) as f64
4422                    * s_v.first().copied().unwrap_or(0.0)
4423                    * f64::EPSILON
4424                    * 2.0;
4425                let r = s_v.iter().filter(|&&s| s > tol).count();
4426                if r == 0 {
4427                    return Ok(Value::Matrix(Array2::zeros((mm, 0))));
4428                }
4429                let mut result = Array2::<f64>::zeros((mm, r));
4430                for j in 0..r {
4431                    if j < u_c.ncols() {
4432                        for i in 0..mm {
4433                            result[[i, j]] = u_c[[i, j]];
4434                        }
4435                    }
4436                }
4437                Ok(Value::Matrix(result))
4438            }
4439            _ => Err("orth: argument must be a real numeric matrix".to_string()),
4440        },
4441
4442        // cond(A): condition number = σ_max / σ_min (2-norm by default).
4443        ("cond", 1) => match &args[0] {
4444            Value::Scalar(x) => {
4445                if x.abs() < 1e-15 {
4446                    Ok(Value::Scalar(f64::INFINITY))
4447                } else {
4448                    Ok(Value::Scalar(1.0))
4449                }
4450            }
4451            Value::Matrix(m) => {
4452                let (_, s_v, _) = svd_compute(m)?;
4453                if s_v.is_empty() {
4454                    return Ok(Value::Scalar(1.0));
4455                }
4456                let s_max = s_v[0];
4457                let s_min = *s_v.last().unwrap();
4458                Ok(Value::Scalar(if s_min < 1e-15 {
4459                    f64::INFINITY
4460                } else {
4461                    s_max / s_min
4462                }))
4463            }
4464            _ => Err("cond: argument must be a real numeric matrix".to_string()),
4465        },
4466
4467        // pinv(A): Moore-Penrose pseudoinverse via SVD.
4468        ("pinv", 1) => match &args[0] {
4469            Value::Scalar(x) => Ok(Value::Scalar(if x.abs() < 1e-15 { 0.0 } else { 1.0 / x })),
4470            Value::Matrix(m) => {
4471                let mm = m.nrows();
4472                let nn = m.ncols();
4473                let (u_c, s_v, v_c) = svd_compute(m)?;
4474                let k = s_v.len();
4475                let tol =
4476                    (mm.max(nn)) as f64 * s_v.first().copied().unwrap_or(0.0) * f64::EPSILON * 2.0;
4477                // pinv = V * diag(1/σ) * U^T
4478                let mut result = Array2::<f64>::zeros((nn, mm));
4479                for j in 0..k {
4480                    if s_v[j] > tol {
4481                        let inv_s = 1.0 / s_v[j];
4482                        for r in 0..nn {
4483                            for c in 0..mm {
4484                                result[[r, c]] += v_c[[r, j]] * inv_s * u_c[[c, j]];
4485                            }
4486                        }
4487                    }
4488                }
4489                Ok(Value::Matrix(result))
4490            }
4491            _ => Err("pinv: argument must be a real numeric matrix".to_string()),
4492        },
4493
4494        // jsondecode(str) / jsonencode(val)
4495        ("jsondecode", 1) => jsondecode_impl(&args[0]),
4496        ("jsonencode", 1) => jsonencode_impl(&args[0]),
4497
4498        // load('file.mat') — assignment form: data = load('file.mat')
4499        ("load", 1) => {
4500            let path = match &args[0] {
4501                Value::Str(s) | Value::StringObj(s) => s.clone(),
4502                _ => return Err("load: argument must be a string path".to_string()),
4503            };
4504            if !path.ends_with(".mat") {
4505                return Err("load: use bare 'load path' syntax for non-.mat files".to_string());
4506            }
4507            load_mat_file(&path)
4508        }
4509
4510        // assert(cond)
4511        ("assert", 1) => {
4512            let truthy = match &args[0] {
4513                Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
4514                Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
4515                Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
4516                Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
4517                _ => false,
4518            };
4519            if truthy {
4520                Ok(Value::Void)
4521            } else {
4522                Err("assert: condition is false".to_string())
4523            }
4524        }
4525
4526        // assert(expected, actual)
4527        ("assert", 2) => assert_values_equal(&args[0], &args[1], None),
4528
4529        // assert(expected, actual, tol)
4530        ("assert", 3) => {
4531            let tol = match &args[2] {
4532                Value::Scalar(t) => *t,
4533                _ => return Err("assert: tolerance must be a scalar".to_string()),
4534            };
4535            assert_values_equal(&args[0], &args[1], Some(tol))
4536        }
4537
4538        _ => {
4539            let hint = suggest_similar(name, env);
4540            match hint {
4541                Some(s) => Err(format!("Unknown function '{name}'; did you mean '{s}'?")),
4542                None => Err(format!("Unknown function: '{name}'")),
4543            }
4544        }
4545    }
4546}
4547
4548/// Interprets backslash escape sequences in delimiter strings.
4549/// `\t` → tab, `\n` → newline. Other strings are used as-is.
4550fn interpret_delim(s: &str) -> String {
4551    match s {
4552        r"\t" => "\t".to_string(),
4553        r"\n" => "\n".to_string(),
4554        other => other.to_string(),
4555    }
4556}
4557
4558/// Returns true if splitting every line by `delim` gives the same field count > 1.
4559fn delim_consistent(lines: &[&str], delim: char) -> bool {
4560    let counts: Vec<usize> = lines.iter().map(|l| l.split(delim).count()).collect();
4561    counts.iter().all(|&c| c > 1) && counts.windows(2).all(|w| w[0] == w[1])
4562}
4563
4564/// Reads a delimiter-separated numeric file and returns a `Value::Matrix`.
4565fn dlmread_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
4566    let content =
4567        std::fs::read_to_string(path).map_err(|e| format!("dlmread: cannot read '{path}': {e}"))?;
4568
4569    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
4570
4571    if lines.is_empty() {
4572        return Ok(Value::Matrix(Array2::zeros((0, 0))));
4573    }
4574
4575    // Determine delimiter: explicit → auto-detect (comma → tab → whitespace)
4576    let delim: Option<String> = match explicit_delim {
4577        Some(d) => Some(d),
4578        None => {
4579            if delim_consistent(&lines, ',') {
4580                Some(",".to_string())
4581            } else if delim_consistent(&lines, '\t') {
4582                Some("\t".to_string())
4583            } else {
4584                None // split by whitespace
4585            }
4586        }
4587    };
4588
4589    let mut rows: Vec<Vec<f64>> = Vec::new();
4590    for (line_num, line) in lines.iter().enumerate() {
4591        let fields: Vec<&str> = match &delim {
4592            Some(d) => line.split(d.as_str()).collect(),
4593            None => line.split_whitespace().collect(),
4594        };
4595        let mut row_vals: Vec<f64> = Vec::with_capacity(fields.len());
4596        for field in &fields {
4597            let trimmed = field.trim();
4598            if trimmed.is_empty() {
4599                row_vals.push(0.0);
4600            } else {
4601                row_vals.push(trimmed.parse::<f64>().map_err(|_| {
4602                    format!(
4603                        "dlmread: non-numeric value '{trimmed}' on line {}",
4604                        line_num + 1
4605                    )
4606                })?);
4607            }
4608        }
4609        if !row_vals.is_empty() {
4610            rows.push(row_vals);
4611        }
4612    }
4613
4614    if rows.is_empty() {
4615        return Ok(Value::Matrix(Array2::zeros((0, 0))));
4616    }
4617
4618    let ncols = rows[0].len();
4619    for (i, row) in rows.iter().enumerate() {
4620        if row.len() != ncols {
4621            return Err(format!(
4622                "dlmread: row {} has {} fields, expected {ncols}",
4623                i + 1,
4624                row.len()
4625            ));
4626        }
4627    }
4628
4629    let nrows = rows.len();
4630    let flat: Vec<f64> = rows.into_iter().flatten().collect();
4631    Array2::from_shape_vec((nrows, ncols), flat)
4632        .map_err(|e| format!("dlmread: shape error: {e}"))
4633        .map(Value::Matrix)
4634}
4635
4636/// Formats one f64 value for use in a delimited file.
4637/// Integers are written without decimal point; floats use full precision.
4638fn fmt_dlm_number(n: f64) -> String {
4639    if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
4640        format!("{}", n as i64)
4641    } else {
4642        format!("{n}")
4643    }
4644}
4645
4646/// Writes a scalar or matrix to a delimiter-separated file.
4647fn dlmwrite_impl(path: &str, val: &Value, explicit_delim: Option<String>) -> Result<Value, String> {
4648    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
4649
4650    let content = match val {
4651        Value::Scalar(n) => format!("{}\n", fmt_dlm_number(*n)),
4652        Value::Matrix(m) => {
4653            let mut out = String::new();
4654            for row in m.rows() {
4655                let parts: Vec<String> = row.iter().map(|n| fmt_dlm_number(*n)).collect();
4656                out.push_str(&parts.join(&delim));
4657                out.push('\n');
4658            }
4659            out
4660        }
4661        _ => {
4662            return Err("dlmwrite: second argument must be a numeric scalar or matrix".to_string());
4663        }
4664    };
4665
4666    std::fs::write(path, content).map_err(|e| format!("dlmwrite: cannot write '{path}': {e}"))?;
4667    Ok(Value::Void)
4668}
4669
4670// --- CSV read/write helpers (readmatrix / readtable / writetable) ---
4671
4672/// Selects the delimiter shared across all lines; falls back to `None` (whitespace splitting).
4673///
4674/// Uses CSV-aware splitting (quoting) when checking for comma consistency.
4675fn auto_detect_delim(lines: &[&str]) -> Option<String> {
4676    // Comma: use CSV-aware split so quoted fields with commas don't confuse the count.
4677    let comma_counts: Vec<usize> = lines.iter().map(|l| split_csv_row(l, ",").len()).collect();
4678    if comma_counts.iter().all(|&c| c > 1) && comma_counts.windows(2).all(|w| w[0] == w[1]) {
4679        return Some(",".to_string());
4680    }
4681    if delim_consistent(lines, '\t') {
4682        Some("\t".to_string())
4683    } else {
4684        None
4685    }
4686}
4687
4688/// Splits one CSV line by `delim`, respecting RFC 4180 double-quoted fields.
4689/// `""` inside a quoted field encodes a literal `"`.
4690/// Falls back to a plain `str::split` for multi-character delimiters.
4691fn split_csv_row(line: &str, delim: &str) -> Vec<String> {
4692    if delim.chars().count() != 1 {
4693        return line.split(delim).map(str::to_string).collect();
4694    }
4695    let delim_char = delim.chars().next().unwrap();
4696    let chars: Vec<char> = line.chars().collect();
4697    let mut fields: Vec<String> = Vec::new();
4698    let mut field = String::new();
4699    let mut i = 0;
4700    let mut in_quotes = false;
4701    while i < chars.len() {
4702        let c = chars[i];
4703        if in_quotes {
4704            if c == '"' && i + 1 < chars.len() && chars[i + 1] == '"' {
4705                field.push('"');
4706                i += 2;
4707                continue;
4708            } else if c == '"' {
4709                in_quotes = false;
4710            } else {
4711                field.push(c);
4712            }
4713        } else if c == '"' {
4714            in_quotes = true;
4715        } else if c == delim_char {
4716            fields.push(std::mem::take(&mut field));
4717        } else {
4718            field.push(c);
4719        }
4720        i += 1;
4721    }
4722    fields.push(field);
4723    fields
4724}
4725
4726/// Splits a CSV row with an optional delimiter; `None` splits by whitespace.
4727fn split_csv_row_opt(line: &str, delim: &Option<String>) -> Vec<String> {
4728    match delim {
4729        None => line.split_whitespace().map(str::to_string).collect(),
4730        Some(d) => split_csv_row(line, d),
4731    }
4732}
4733
4734/// Returns `true` if any non-empty field in `fields` cannot be parsed as `f64`.
4735fn row_is_header(fields: &[String]) -> bool {
4736    fields
4737        .iter()
4738        .any(|f| !f.trim().is_empty() && f.trim().parse::<f64>().is_err())
4739}
4740
4741/// Converts a raw header string to a valid identifier-like name.
4742/// Runs of non-alphanumeric characters collapse to `_`; a leading digit gets an `x` prefix.
4743/// Empty results fall back to `x{col}`.
4744fn sanitize_header(s: &str, col_1based: usize) -> String {
4745    let s = s.trim();
4746    if s.is_empty() {
4747        return format!("x{col_1based}");
4748    }
4749    let mut out = String::new();
4750    for c in s.chars() {
4751        if c.is_alphanumeric() || c == '_' {
4752            out.push(c);
4753        } else if !out.ends_with('_') {
4754            out.push('_');
4755        }
4756    }
4757    let out = out.trim_end_matches('_').to_string();
4758    if out.is_empty() {
4759        return format!("x{col_1based}");
4760    }
4761    if out.chars().next().unwrap().is_ascii_digit() {
4762        format!("x{out}")
4763    } else {
4764        out
4765    }
4766}
4767
4768/// Appends `_N` (1-based) suffixes to duplicate entries in a header list.
4769/// Note: collisions between deduplicated names and pre-existing `_N` names are not resolved.
4770fn deduplicate_headers(headers: Vec<String>) -> Vec<String> {
4771    let mut count: HashMap<String, usize> = HashMap::new();
4772    for h in &headers {
4773        *count.entry(h.clone()).or_insert(0) += 1;
4774    }
4775    let mut seen: HashMap<String, usize> = HashMap::new();
4776    headers
4777        .into_iter()
4778        .map(|h| {
4779            if *count.get(&h).unwrap() == 1 {
4780                h
4781            } else {
4782                let idx = seen.entry(h.clone()).or_insert(0);
4783                *idx += 1;
4784                format!("{h}_{idx}")
4785            }
4786        })
4787        .collect()
4788}
4789
4790/// Parses an optional `('Delimiter', d)` argument pair starting at `args[start]`.
4791/// Returns `Ok(None)` when no extra arguments are present.
4792fn parse_delimiter_opt(
4793    fn_name: &str,
4794    args: &[Value],
4795    start: usize,
4796) -> Result<Option<String>, String> {
4797    if args.len() <= start {
4798        return Ok(None);
4799    }
4800    let key = string_arg(&args[start], fn_name, start + 1)?;
4801    if !key.eq_ignore_ascii_case("delimiter") {
4802        return Err(format!(
4803            "{fn_name}: expected 'Delimiter' option at argument {}, got '{key}'",
4804            start + 1
4805        ));
4806    }
4807    if args.len() <= start + 1 {
4808        return Err(format!("{fn_name}: 'Delimiter' option requires a value"));
4809    }
4810    let val = interpret_delim(string_arg(&args[start + 1], fn_name, start + 2)?);
4811    Ok(Some(val))
4812}
4813
4814/// Reads a delimiter-separated file and returns a [`Value::Matrix`].
4815///
4816/// Auto-detects the delimiter (comma → tab → whitespace). When the first row contains
4817/// non-numeric text it is treated as a header and skipped. Empty cells become `NaN`
4818/// (unlike [`dlmread_impl`], which uses `0.0`).
4819fn readmatrix_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
4820    let content = std::fs::read_to_string(path)
4821        .map_err(|e| format!("readmatrix: cannot read '{path}': {e}"))?;
4822
4823    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
4824    if lines.is_empty() {
4825        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
4826    }
4827
4828    let delim = match explicit_delim {
4829        Some(d) => Some(d),
4830        None => auto_detect_delim(&lines),
4831    };
4832
4833    let first_fields = split_csv_row_opt(lines[0], &delim);
4834    let skip_header = row_is_header(&first_fields);
4835    let data_lines = if skip_header { &lines[1..] } else { &lines[..] };
4836
4837    if data_lines.is_empty() {
4838        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
4839    }
4840
4841    let mut rows: Vec<Vec<f64>> = Vec::new();
4842    for (i, line) in data_lines.iter().enumerate() {
4843        let fields = split_csv_row_opt(line, &delim);
4844        let mut row: Vec<f64> = Vec::with_capacity(fields.len());
4845        for f in &fields {
4846            let t = f.trim();
4847            if t.is_empty() {
4848                row.push(f64::NAN);
4849            } else {
4850                row.push(t.parse::<f64>().map_err(|_| {
4851                    format!(
4852                        "readmatrix: non-numeric value '{t}' on line {}",
4853                        i + 1 + usize::from(skip_header)
4854                    )
4855                })?);
4856            }
4857        }
4858        rows.push(row);
4859    }
4860
4861    if rows.is_empty() {
4862        return Ok(Value::Matrix(Array2::<f64>::zeros((0, 0))));
4863    }
4864
4865    let ncols = rows[0].len();
4866    for (i, row) in rows.iter().enumerate() {
4867        if row.len() != ncols {
4868            return Err(format!(
4869                "readmatrix: row {} has {} fields, expected {ncols}",
4870                i + 1,
4871                row.len()
4872            ));
4873        }
4874    }
4875
4876    let nrows = rows.len();
4877    let flat: Vec<f64> = rows.into_iter().flatten().collect();
4878    Array2::from_shape_vec((nrows, ncols), flat)
4879        .map_err(|e| format!("readmatrix: shape error: {e}"))
4880        .map(Value::Matrix)
4881}
4882
4883/// Reads a delimiter-separated file with a header row and returns a [`Value::Struct`] of columns.
4884///
4885/// The first row is always treated as column headers. Numeric columns become `Matrix` (N×1);
4886/// columns with any non-numeric cell become `Cell` of [`Value::Str`].
4887/// Whitespace is trimmed from all cell values after CSV unquoting.
4888fn readtable_impl(path: &str, explicit_delim: Option<String>) -> Result<Value, String> {
4889    let content = std::fs::read_to_string(path)
4890        .map_err(|e| format!("readtable: cannot read '{path}': {e}"))?;
4891
4892    let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
4893    if lines.is_empty() {
4894        return Ok(Value::Struct(IndexMap::new()));
4895    }
4896
4897    let delim = match explicit_delim {
4898        Some(d) => Some(d),
4899        None => auto_detect_delim(&lines),
4900    };
4901
4902    let raw_headers = split_csv_row_opt(lines[0], &delim);
4903    let ncols = raw_headers.len();
4904    let headers: Vec<String> = deduplicate_headers(
4905        raw_headers
4906            .iter()
4907            .enumerate()
4908            .map(|(i, h)| sanitize_header(h.trim(), i + 1))
4909            .collect(),
4910    );
4911
4912    let data_lines = &lines[1..];
4913    if data_lines.is_empty() {
4914        let mut s: IndexMap<String, Value> = IndexMap::new();
4915        for h in &headers {
4916            s.insert(h.clone(), Value::Matrix(Array2::<f64>::zeros((0, 1))));
4917        }
4918        return Ok(Value::Struct(s));
4919    }
4920
4921    let mut all_rows: Vec<Vec<String>> = Vec::new();
4922    for (i, line) in data_lines.iter().enumerate() {
4923        let fields = split_csv_row_opt(line, &delim);
4924        if fields.len() != ncols {
4925            return Err(format!(
4926                "readtable: row {} has {} fields, expected {ncols}",
4927                i + 2,
4928                fields.len()
4929            ));
4930        }
4931        all_rows.push(fields.into_iter().map(|f| f.trim().to_string()).collect());
4932    }
4933
4934    let nrows = all_rows.len();
4935    let mut s: IndexMap<String, Value> = IndexMap::new();
4936    for col in 0..ncols {
4937        let all_numeric = all_rows.iter().all(|row| {
4938            let t = row[col].as_str();
4939            t.is_empty() || t.parse::<f64>().is_ok()
4940        });
4941        if all_numeric {
4942            let vals: Vec<f64> = all_rows
4943                .iter()
4944                .map(|row| {
4945                    let t = row[col].as_str();
4946                    if t.is_empty() {
4947                        f64::NAN
4948                    } else {
4949                        t.parse::<f64>().unwrap()
4950                    }
4951                })
4952                .collect();
4953            let col_mat = Array2::from_shape_vec((nrows, 1), vals)
4954                .map_err(|e| format!("readtable: shape error: {e}"))?;
4955            s.insert(headers[col].clone(), Value::Matrix(col_mat));
4956        } else {
4957            let vals: Vec<Value> = all_rows
4958                .iter()
4959                .map(|row| Value::Str(row[col].clone()))
4960                .collect();
4961            s.insert(headers[col].clone(), Value::Cell(vals));
4962        }
4963    }
4964    Ok(Value::Struct(s))
4965}
4966
4967/// Quotes a CSV cell if it contains the delimiter, a double-quote, or a newline (RFC 4180).
4968fn csv_quote_cell(s: &str, delim: &str) -> String {
4969    if s.contains('"') || s.contains('\n') || s.contains(delim) {
4970        let escaped = s.replace('"', "\"\"");
4971        format!("\"{escaped}\"")
4972    } else {
4973        s.to_string()
4974    }
4975}
4976
4977/// Returns the number of rows in a struct column value.
4978///
4979/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` (1 row each).
4980/// Returns `None` for unsupported types or non-column matrices.
4981fn col_nrows(v: &Value) -> Option<usize> {
4982    match v {
4983        Value::Matrix(m) if m.ncols() == 1 || m.nrows() == 0 => Some(m.nrows()),
4984        Value::Cell(c) => Some(c.len()),
4985        Value::Scalar(_) => Some(1),
4986        Value::Str(_) | Value::StringObj(_) => Some(1),
4987        _ => None,
4988    }
4989}
4990
4991/// Returns the formatted CSV cell string for row `row` of a struct column value.
4992fn col_cell_str(v: &Value, row: usize, delim: &str) -> Result<String, String> {
4993    match v {
4994        Value::Matrix(m) => Ok(csv_quote_cell(&fmt_dlm_number(m[[row, 0]]), delim)),
4995        Value::Cell(c) => match &c[row] {
4996            Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
4997            Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
4998            _ => Err(format!(
4999                "writetable: cell element at row {} has unsupported type",
5000                row + 1
5001            )),
5002        },
5003        Value::Scalar(n) => Ok(csv_quote_cell(&fmt_dlm_number(*n), delim)),
5004        Value::Str(s) | Value::StringObj(s) => Ok(csv_quote_cell(s, delim)),
5005        _ => Err(format!(
5006            "writetable: unsupported column type at row {}",
5007            row + 1
5008        )),
5009    }
5010}
5011
5012/// Writes a struct table to a delimiter-separated file with a header row.
5013///
5014/// Each struct field is one column. All fields must have the same number of rows.
5015/// Accepts `Matrix` (N×1), `Cell`, `Scalar`, and `Str`/`StringObj` columns.
5016/// Cell values that contain the delimiter, `"`, or newlines are quoted per RFC 4180.
5017fn writetable_impl(
5018    tbl: &Value,
5019    path: &str,
5020    explicit_delim: Option<String>,
5021) -> Result<Value, String> {
5022    let delim = explicit_delim.unwrap_or_else(|| ",".to_string());
5023    let fields = match tbl {
5024        Value::Struct(m) => m,
5025        _ => return Err("writetable: first argument must be a struct".to_string()),
5026    };
5027    if fields.is_empty() {
5028        std::fs::write(path, "").map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
5029        return Ok(Value::Void);
5030    }
5031
5032    let nrows = {
5033        let (first_name, first_val) = fields.iter().next().unwrap();
5034        col_nrows(first_val).ok_or_else(|| {
5035            format!("writetable: column '{first_name}' must be a Matrix (N×1), Cell, or scalar")
5036        })?
5037    };
5038    for (cname, cval) in fields.iter() {
5039        let n = col_nrows(cval).ok_or_else(|| {
5040            format!("writetable: column '{cname}' must be a Matrix (N×1), Cell, or scalar")
5041        })?;
5042        if n != nrows {
5043            return Err(format!(
5044                "writetable: column '{cname}' has {n} rows, expected {nrows}"
5045            ));
5046        }
5047    }
5048
5049    let mut out = String::new();
5050    let header_parts: Vec<String> = fields.keys().map(|k| csv_quote_cell(k, &delim)).collect();
5051    out.push_str(&header_parts.join(&delim));
5052    out.push('\n');
5053
5054    for row in 0..nrows {
5055        let mut parts: Vec<String> = Vec::with_capacity(fields.len());
5056        for cval in fields.values() {
5057            parts.push(col_cell_str(cval, row, &delim)?);
5058        }
5059        out.push_str(&parts.join(&delim));
5060        out.push('\n');
5061    }
5062
5063    std::fs::write(path, out).map_err(|e| format!("writetable: cannot write '{path}': {e}"))?;
5064    Ok(Value::Void)
5065}
5066
5067/// Converts an f64 to u64 for bitwise operations.
5068/// Requires a non-negative integer value; returns an error otherwise.
5069fn to_bits(v: f64, fname: &str, pos: usize) -> Result<u64, String> {
5070    if v < 0.0 {
5071        return Err(format!(
5072            "{fname}: argument {pos} must be non-negative, got {v}"
5073        ));
5074    }
5075    if v.fract() != 0.0 {
5076        return Err(format!(
5077            "{fname}: argument {pos} must be an integer, got {v}"
5078        ));
5079    }
5080    if v > u64::MAX as f64 {
5081        return Err(format!(
5082            "{fname}: argument {pos} is too large for bitwise operations"
5083        ));
5084    }
5085    Ok(v as u64)
5086}
5087
5088/// Computes determinant of a square matrix via Gaussian elimination.
5089/// Computes the determinant of a square matrix via Gaussian elimination with
5090/// partial pivoting (pure Rust, no external dependencies).
5091fn det_matrix(m: &Array2<f64>) -> Result<f64, String> {
5092    let n = m.nrows();
5093    if m.ncols() != n {
5094        return Err("det: matrix must be square".to_string());
5095    }
5096    if n == 0 {
5097        return Ok(1.0);
5098    }
5099    let mut a = m.clone();
5100    let mut sign: f64 = 1.0;
5101    for col in 0..n {
5102        // Partial pivoting: swap in the row with the largest absolute value.
5103        let pivot = (col..n)
5104            .max_by(|&r1, &r2| a[[r1, col]].abs().partial_cmp(&a[[r2, col]].abs()).unwrap())
5105            .unwrap();
5106        if a[[pivot, col]].abs() < 1e-15 {
5107            return Ok(0.0); // singular
5108        }
5109        if pivot != col {
5110            for j in 0..n {
5111                let tmp = a[[pivot, j]];
5112                a[[pivot, j]] = a[[col, j]];
5113                a[[col, j]] = tmp;
5114            }
5115            sign = -sign;
5116        }
5117        let pv = a[[col, col]];
5118        for row in (col + 1)..n {
5119            let factor = a[[row, col]] / pv;
5120            for j in col..n {
5121                let val = a[[col, j]] * factor;
5122                a[[row, j]] -= val;
5123            }
5124        }
5125    }
5126    Ok(sign * (0..n).map(|i| a[[i, i]]).product::<f64>())
5127}
5128
5129/// Computes the inverse of a square matrix via Gauss-Jordan elimination with
5130/// partial pivoting (pure Rust, no external dependencies).
5131fn inv_matrix(m: &Array2<f64>) -> Result<Array2<f64>, String> {
5132    let n = m.nrows();
5133    if m.ncols() != n {
5134        return Err("inv: matrix must be square".to_string());
5135    }
5136    let cols = 2 * n;
5137    let mut aug = vec![0.0f64; n * cols];
5138    for i in 0..n {
5139        for j in 0..n {
5140            aug[i * cols + j] = m[[i, j]];
5141        }
5142        aug[i * cols + n + i] = 1.0;
5143    }
5144    for col in 0..n {
5145        // Partial pivoting: swap in the row with the largest absolute value.
5146        let pivot = (col..n)
5147            .max_by(|&r1, &r2| {
5148                aug[r1 * cols + col]
5149                    .abs()
5150                    .partial_cmp(&aug[r2 * cols + col].abs())
5151                    .unwrap()
5152            })
5153            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
5154            .ok_or_else(|| "inv: matrix is singular".to_string())?;
5155        if pivot != col {
5156            for j in 0..cols {
5157                aug.swap(col * cols + j, pivot * cols + j);
5158            }
5159        }
5160        let pv = aug[col * cols + col];
5161        for j in 0..cols {
5162            aug[col * cols + j] /= pv;
5163        }
5164        for row in 0..n {
5165            if row == col {
5166                continue;
5167            }
5168            let factor = aug[row * cols + col];
5169            for j in 0..cols {
5170                let val = aug[col * cols + j] * factor;
5171                aug[row * cols + j] -= val;
5172            }
5173        }
5174    }
5175    let mut result = Array2::<f64>::zeros((n, n));
5176    for i in 0..n {
5177        for j in 0..n {
5178            result[[i, j]] = aug[i * cols + n + j];
5179        }
5180    }
5181    Ok(result)
5182}
5183
5184/// Solves the linear system `A * x = B` using Gaussian elimination with partial pivoting.
5185///
5186/// `A` must be square (n×n); `B` must have n rows. Returns x (n × k where k = B.ncols()).
5187/// This is the engine for the `\` left-division operator.
5188fn solve_linear(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>, String> {
5189    let n = a.nrows();
5190    if a.ncols() != n {
5191        return Err(format!(
5192            "\\: coefficient matrix must be square, got {}×{}",
5193            n,
5194            a.ncols()
5195        ));
5196    }
5197    let k = b.ncols();
5198    if b.nrows() != n {
5199        return Err(format!(
5200            "\\: size mismatch — A is {}×{} but b has {} rows",
5201            n,
5202            n,
5203            b.nrows()
5204        ));
5205    }
5206    if n == 0 {
5207        return Ok(Array2::zeros((0, k)));
5208    }
5209    let cols = n + k;
5210    let mut aug = vec![0.0f64; n * cols];
5211    for i in 0..n {
5212        for j in 0..n {
5213            aug[i * cols + j] = a[[i, j]];
5214        }
5215        for j in 0..k {
5216            aug[i * cols + n + j] = b[[i, j]];
5217        }
5218    }
5219    for col in 0..n {
5220        let pivot = (col..n)
5221            .max_by(|&r1, &r2| {
5222                aug[r1 * cols + col]
5223                    .abs()
5224                    .partial_cmp(&aug[r2 * cols + col].abs())
5225                    .unwrap()
5226            })
5227            .filter(|&r| aug[r * cols + col].abs() > 1e-12)
5228            .ok_or_else(|| "\\: matrix is singular or nearly singular".to_string())?;
5229        if pivot != col {
5230            for j in 0..cols {
5231                aug.swap(col * cols + j, pivot * cols + j);
5232            }
5233        }
5234        let pv = aug[col * cols + col];
5235        for j in col..cols {
5236            aug[col * cols + j] /= pv;
5237        }
5238        for row in 0..n {
5239            if row == col {
5240                continue;
5241            }
5242            let factor = aug[row * cols + col];
5243            if factor == 0.0 {
5244                continue;
5245            }
5246            for j in col..cols {
5247                let val = aug[col * cols + j] * factor;
5248                aug[row * cols + j] -= val;
5249            }
5250        }
5251    }
5252    let mut result = Array2::<f64>::zeros((n, k));
5253    for i in 0..n {
5254        for j in 0..k {
5255            result[[i, j]] = aug[i * cols + n + j];
5256        }
5257    }
5258    Ok(result)
5259}
5260
5261// ---------------------------------------------------------------------------
5262// Advanced linear algebra helpers (Phase 18)
5263// ---------------------------------------------------------------------------
5264
5265/// QR decomposition via Householder reflectors.
5266///
5267/// For an m×n matrix A returns (Q, R) where Q is m×m orthogonal and R is
5268/// m×n upper triangular such that A = Q * R.
5269fn qr_decompose(a: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>), String> {
5270    let m = a.nrows();
5271    let n = a.ncols();
5272    let k = m.min(n);
5273    let mut r = a.clone();
5274    let mut q = Array2::<f64>::eye(m);
5275
5276    for j in 0..k {
5277        let col_len = m - j;
5278        let mut v: Vec<f64> = (j..m).map(|i| r[[i, j]]).collect();
5279
5280        let norm_x = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
5281        if norm_x < 1e-14 {
5282            continue;
5283        }
5284        // Householder sign convention avoids cancellation.
5285        v[0] += if v[0] >= 0.0 { norm_x } else { -norm_x };
5286        let v_sq: f64 = v.iter().map(|&x| x * x).sum();
5287        if v_sq < 1e-28 {
5288            continue;
5289        }
5290
5291        // Apply H from left to R: R[j:m, :] -= 2*v*(v^T*R[j:m,:])/v^Tv
5292        for col in j..n {
5293            let dot: f64 = (0..col_len).map(|i| v[i] * r[[j + i, col]]).sum();
5294            let fac = 2.0 * dot / v_sq;
5295            for i in 0..col_len {
5296                r[[j + i, col]] -= fac * v[i];
5297            }
5298        }
5299        // Accumulate Q from right: Q[:, j:m] -= (Q[:,j:m]*v) * 2*v^T/v^Tv
5300        for row in 0..m {
5301            let dot: f64 = (0..col_len).map(|i| q[[row, j + i]] * v[i]).sum();
5302            let fac = 2.0 * dot / v_sq;
5303            for i in 0..col_len {
5304                q[[row, j + i]] -= fac * v[i];
5305            }
5306        }
5307    }
5308
5309    Ok((q, r))
5310}
5311
5312/// LU decomposition with partial pivoting.
5313///
5314/// For an n×n square matrix A returns (L, U, P) where P*A = L*U,
5315/// L is unit lower triangular, U is upper triangular, and P is a
5316/// permutation matrix.
5317type LuResult = Result<(Array2<f64>, Array2<f64>, Array2<f64>), String>;
5318fn lu_decompose(a: &Array2<f64>) -> LuResult {
5319    let n = a.nrows();
5320    if a.ncols() != n {
5321        return Err("lu: matrix must be square".to_string());
5322    }
5323    let mut u = a.clone();
5324    let mut l = Array2::<f64>::eye(n);
5325    let mut perm: Vec<usize> = (0..n).collect();
5326
5327    for j in 0..n {
5328        let pivot = (j..n)
5329            .max_by(|&r1, &r2| {
5330                u[[r1, j]]
5331                    .abs()
5332                    .partial_cmp(&u[[r2, j]].abs())
5333                    .unwrap_or(std::cmp::Ordering::Equal)
5334            })
5335            .unwrap();
5336
5337        if pivot != j {
5338            for col in 0..n {
5339                let tmp = u[[j, col]];
5340                u[[j, col]] = u[[pivot, col]];
5341                u[[pivot, col]] = tmp;
5342            }
5343            for col in 0..j {
5344                let tmp = l[[j, col]];
5345                l[[j, col]] = l[[pivot, col]];
5346                l[[pivot, col]] = tmp;
5347            }
5348            perm.swap(j, pivot);
5349        }
5350
5351        if u[[j, j]].abs() < 1e-15 {
5352            continue;
5353        }
5354        for i in (j + 1)..n {
5355            l[[i, j]] = u[[i, j]] / u[[j, j]];
5356            for k in j..n {
5357                let val = l[[i, j]] * u[[j, k]];
5358                u[[i, k]] -= val;
5359            }
5360        }
5361    }
5362
5363    let mut p = Array2::<f64>::zeros((n, n));
5364    for (i, &j) in perm.iter().enumerate() {
5365        p[[i, j]] = 1.0;
5366    }
5367    Ok((l, u, p))
5368}
5369
5370/// Cholesky decomposition.
5371///
5372/// For a symmetric positive-definite n×n matrix A returns the upper triangular
5373/// factor R such that A = R^T * R (MATLAB convention).
5374fn chol_decompose(a: &Array2<f64>) -> Result<Array2<f64>, String> {
5375    let n = a.nrows();
5376    if a.ncols() != n {
5377        return Err("chol: matrix must be square".to_string());
5378    }
5379    let mut r = Array2::<f64>::zeros((n, n));
5380    for j in 0..n {
5381        let mut s = a[[j, j]];
5382        for k in 0..j {
5383            s -= r[[k, j]] * r[[k, j]];
5384        }
5385        if s <= 0.0 {
5386            return Err("chol: matrix is not positive definite".to_string());
5387        }
5388        r[[j, j]] = s.sqrt();
5389        for i in (j + 1)..n {
5390            let mut t = a[[j, i]];
5391            for k in 0..j {
5392                t -= r[[k, j]] * r[[k, i]];
5393            }
5394            r[[j, i]] = t / r[[j, j]];
5395        }
5396    }
5397    Ok(r)
5398}
5399
5400/// One-sided Jacobi SVD (economy form).
5401///
5402/// For an m×n matrix A returns (U, s, V) where
5403/// - U is m×k with orthonormal columns (k = min(m,n))
5404/// - s is a `Vec<f64>` of singular values in descending order (length k)
5405/// - V is n×k with orthonormal columns
5406///
5407/// For m < n the inputs are transparently transposed and outputs swapped.
5408type SvdResult = Result<(Array2<f64>, Vec<f64>, Array2<f64>), String>;
5409fn svd_compute(a: &Array2<f64>) -> SvdResult {
5410    let m = a.nrows();
5411    let n = a.ncols();
5412    if m < n {
5413        let (v, s, u) = svd_compute(&a.t().to_owned())?;
5414        return Ok((u, s, v));
5415    }
5416    // m >= n from here.
5417    let k = n;
5418    let mut b = a.clone();
5419    let mut v = Array2::<f64>::eye(k);
5420
5421    const MAX_ITER: usize = 200;
5422    const EPS: f64 = 1e-14;
5423
5424    'outer: for _ in 0..MAX_ITER {
5425        let mut changed = false;
5426        for p in 0..k {
5427            for q in (p + 1)..k {
5428                let alpha: f64 = (0..m).map(|i| b[[i, p]] * b[[i, p]]).sum();
5429                let beta: f64 = (0..m).map(|i| b[[i, q]] * b[[i, q]]).sum();
5430                let gamma: f64 = (0..m).map(|i| b[[i, p]] * b[[i, q]]).sum();
5431
5432                if gamma.abs() <= EPS * (alpha * beta).sqrt() {
5433                    continue;
5434                }
5435                changed = true;
5436
5437                let zeta = (beta - alpha) / (2.0 * gamma);
5438                let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
5439                let c = 1.0 / (1.0 + t * t).sqrt();
5440                let s = c * t;
5441
5442                for i in 0..m {
5443                    let bp = b[[i, p]];
5444                    let bq = b[[i, q]];
5445                    b[[i, p]] = c * bp - s * bq;
5446                    b[[i, q]] = s * bp + c * bq;
5447                }
5448                for i in 0..k {
5449                    let vp = v[[i, p]];
5450                    let vq = v[[i, q]];
5451                    v[[i, p]] = c * vp - s * vq;
5452                    v[[i, q]] = s * vp + c * vq;
5453                }
5454            }
5455        }
5456        if !changed {
5457            break 'outer;
5458        }
5459    }
5460
5461    let mut sigma: Vec<f64> = (0..k)
5462        .map(|j| (0..m).map(|i| b[[i, j]] * b[[i, j]]).sum::<f64>().sqrt())
5463        .collect();
5464    let mut u_mat = Array2::<f64>::zeros((m, k));
5465    for j in 0..k {
5466        if sigma[j] > EPS {
5467            for i in 0..m {
5468                u_mat[[i, j]] = b[[i, j]] / sigma[j];
5469            }
5470        }
5471    }
5472
5473    // Sort descending by singular value.
5474    let mut order: Vec<usize> = (0..k).collect();
5475    order.sort_by(|&a, &b| {
5476        sigma[b]
5477            .partial_cmp(&sigma[a])
5478            .unwrap_or(std::cmp::Ordering::Equal)
5479    });
5480    let sigma_s: Vec<f64> = order.iter().map(|&i| sigma[i]).collect();
5481    let mut u_s = Array2::<f64>::zeros((m, k));
5482    let mut v_s = Array2::<f64>::zeros((n, k));
5483    for (ni, &oi) in order.iter().enumerate() {
5484        for r in 0..m {
5485            u_s[[r, ni]] = u_mat[[r, oi]];
5486        }
5487        for r in 0..k {
5488            v_s[[r, ni]] = v[[r, oi]];
5489        }
5490    }
5491    sigma = sigma_s;
5492
5493    Ok((u_s, sigma, v_s))
5494}
5495
5496/// Extends an m×k matrix with orthonormal columns to a full m×m orthogonal matrix.
5497///
5498/// Tries each standard basis vector e_0..e_{m-1} in order; keeps those that
5499/// have non-negligible component orthogonal to the existing basis.
5500fn complete_orthonormal_basis(u: &Array2<f64>) -> Array2<f64> {
5501    let m = u.nrows();
5502    let k = u.ncols();
5503    let mut basis: Vec<Vec<f64>> = (0..k).map(|j| u.column(j).to_vec()).collect();
5504
5505    let mut ei = 0usize;
5506    while basis.len() < m && ei < m {
5507        let mut v: Vec<f64> = vec![0.0; m];
5508        v[ei] = 1.0;
5509        ei += 1;
5510        for b in &basis {
5511            let dot: f64 = v.iter().zip(b.iter()).map(|(&a, &b)| a * b).sum();
5512            for (vi, &bi) in v.iter_mut().zip(b.iter()) {
5513                *vi -= dot * bi;
5514            }
5515        }
5516        let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
5517        if norm > 1e-10 {
5518            for vi in &mut v {
5519                *vi /= norm;
5520            }
5521            basis.push(v);
5522        }
5523    }
5524
5525    let mut result = Array2::<f64>::zeros((m, m));
5526    for (j, b) in basis.iter().enumerate() {
5527        for (i, &val) in b.iter().enumerate() {
5528            result[[i, j]] = val;
5529        }
5530    }
5531    result
5532}
5533
5534/// QR-iteration eigendecomposition for a real square matrix.
5535///
5536/// Returns `(eigenvalues, eigenvectors)` where eigenvalues is a `Vec<f64>` of
5537/// length n and eigenvectors is an n×n matrix whose columns are the eigenvectors.
5538/// Uses the basic QR iteration with a simple diagonal shift (Wilkinson-style).
5539/// Convergence is reliable for symmetric matrices; general matrices converge for
5540/// most well-conditioned inputs within `MAX_ITER` steps.
5541fn eig_compute(a: &Array2<f64>) -> Result<(Vec<f64>, Array2<f64>), String> {
5542    let n = a.nrows();
5543    if a.ncols() != n {
5544        return Err("eig: matrix must be square".to_string());
5545    }
5546    if n == 0 {
5547        return Ok((vec![], Array2::zeros((0, 0))));
5548    }
5549    if n == 1 {
5550        return Ok((vec![a[[0, 0]]], Array2::eye(1)));
5551    }
5552
5553    let mut ak = a.clone();
5554    let mut evecs = Array2::<f64>::eye(n);
5555
5556    const MAX_ITER: usize = 2000;
5557    const EPS: f64 = 1e-12;
5558
5559    for _ in 0..MAX_ITER {
5560        // Wilkinson shift: uses the trailing 2×2 submatrix for cubic convergence.
5561        let mu = {
5562            let d = ak[[n - 1, n - 1]];
5563            if n >= 2 {
5564                let a = ak[[n - 2, n - 2]];
5565                let b = ak[[n - 2, n - 1]];
5566                let delta = (a - d) / 2.0;
5567                if delta.abs() < 1e-30 {
5568                    d - b.abs()
5569                } else {
5570                    d - b * b / (delta + delta.signum() * (delta * delta + b * b).sqrt())
5571                }
5572            } else {
5573                d
5574            }
5575        };
5576
5577        for i in 0..n {
5578            ak[[i, i]] -= mu;
5579        }
5580        let (q, r) = qr_decompose(&ak)?;
5581        ak = r.dot(&q);
5582        for i in 0..n {
5583            ak[[i, i]] += mu;
5584        }
5585        evecs = evecs.dot(&q);
5586
5587        let max_sub = (0..(n - 1))
5588            .map(|i| ak[[i + 1, i]].abs())
5589            .fold(0.0_f64, f64::max);
5590        if max_sub < EPS {
5591            break;
5592        }
5593    }
5594
5595    let evals: Vec<f64> = (0..n).map(|i| ak[[i, i]]).collect();
5596    Ok((evals, evecs))
5597}
5598
5599// ---------------------------------------------------------------------------
5600// Indexing
5601// ---------------------------------------------------------------------------
5602
5603/// Creates a copy of `env` with `end` set to `dim_size`.
5604/// Used by `eval_index` so that `end` in index expressions resolves to the correct dimension size.
5605fn env_with_end(env: &Env, dim_size: usize) -> Env {
5606    let mut e = env.clone();
5607    e.insert("end".to_string(), Value::Scalar(dim_size as f64));
5608    e
5609}
5610
5611/// Evaluates `val(args...)` — indexing a variable with one or two index arguments.
5612///
5613/// Disambiguation rule (Octave semantics): a name that exists in `Env` is always
5614/// treated as a variable to be indexed, never as a function call.
5615fn eval_index(val: &Value, args: &[Expr], env: &Env) -> Result<Value, String> {
5616    match args.len() {
5617        0 => Err("Indexing requires at least one index".to_string()),
5618        1 => {
5619            // v(i), v(1:3), v(:), v(end), v(end-1:end)
5620            match val {
5621                Value::Void => Err("Cannot index into void".to_string()),
5622                Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => {
5623                    Err("Cannot index into a function value".to_string())
5624                }
5625                Value::Cell(_) => Err("Use c{i} to index into a cell array, not c(i)".to_string()),
5626                Value::Struct(_) => {
5627                    Err("Use s.field to access struct fields, not s(i)".to_string())
5628                }
5629                Value::StructArray(arr) => {
5630                    let total = arr.len();
5631                    let env1 = env_with_end(env, total);
5632                    match resolve_dim(&args[0], total, &env1)? {
5633                        DimIdx::All => {
5634                            // s(:) — return all elements as a new struct array
5635                            Ok(Value::StructArray(arr.clone()))
5636                        }
5637                        DimIdx::Indices(idxs) => {
5638                            if idxs.len() == 1 {
5639                                let i = idxs[0];
5640                                if i >= total {
5641                                    return Err(format!(
5642                                        "Index {} out of range (1..{})",
5643                                        i + 1,
5644                                        total
5645                                    ));
5646                                }
5647                                Ok(Value::Struct(arr[i].clone()))
5648                            } else {
5649                                let mut selected = Vec::with_capacity(idxs.len());
5650                                for &i in &idxs {
5651                                    if i >= total {
5652                                        return Err(format!(
5653                                            "Index {} out of range (1..{})",
5654                                            i + 1,
5655                                            total
5656                                        ));
5657                                    }
5658                                    selected.push(arr[i].clone());
5659                                }
5660                                Ok(Value::StructArray(selected))
5661                            }
5662                        }
5663                    }
5664                }
5665                Value::Scalar(n) => {
5666                    let env1 = env_with_end(env, 1);
5667                    match resolve_dim(&args[0], 1, &env1)? {
5668                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Scalar(*n)),
5669                    }
5670                }
5671                Value::Complex(re, im) => {
5672                    let env1 = env_with_end(env, 1);
5673                    match resolve_dim(&args[0], 1, &env1)? {
5674                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::Complex(*re, *im)),
5675                    }
5676                }
5677                Value::Matrix(m) => {
5678                    let total = m.nrows() * m.ncols();
5679                    let env1 = env_with_end(env, total);
5680                    match resolve_dim(&args[0], total, &env1)? {
5681                        DimIdx::All => {
5682                            // A(:) → column vector, column-major order
5683                            let mut flat = Vec::with_capacity(total);
5684                            for col in 0..m.ncols() {
5685                                for row in 0..m.nrows() {
5686                                    flat.push(m[[row, col]]);
5687                                }
5688                            }
5689                            Ok(Value::Matrix(
5690                                Array2::from_shape_vec((total, 1), flat).unwrap(),
5691                            ))
5692                        }
5693                        DimIdx::Indices(idxs) => {
5694                            // Column-major linear indexing
5695                            let nrows = m.nrows();
5696                            let ncols_m = m.ncols();
5697                            let vals: Result<Vec<f64>, String> = idxs
5698                                .iter()
5699                                .map(|&i| {
5700                                    // i is 0-based, column-major
5701                                    let row = i % nrows;
5702                                    let col = i / nrows;
5703                                    if col >= ncols_m {
5704                                        Err(format!("Index {} out of range (1..{})", i + 1, total))
5705                                    } else {
5706                                        Ok(m[[row, col]])
5707                                    }
5708                                })
5709                                .collect();
5710                            let vals = vals?;
5711                            if vals.len() == 1 {
5712                                Ok(Value::Scalar(vals[0]))
5713                            } else {
5714                                let n = vals.len();
5715                                Ok(Value::Matrix(Array2::from_shape_vec((1, n), vals).unwrap()))
5716                            }
5717                        }
5718                    }
5719                }
5720                Value::Str(s) => {
5721                    // Index into a char array — returns char code(s)
5722                    let chars: Vec<char> = s.chars().collect();
5723                    let total = chars.len();
5724                    let env1 = env_with_end(env, total);
5725                    match resolve_dim(&args[0], total, &env1)? {
5726                        DimIdx::All => {
5727                            let codes: Vec<f64> = chars.iter().map(|&c| c as u32 as f64).collect();
5728                            if codes.len() == 1 {
5729                                Ok(Value::Scalar(codes[0]))
5730                            } else {
5731                                let n = codes.len();
5732                                Ok(Value::Matrix(
5733                                    Array2::from_shape_vec((1, n), codes).unwrap(),
5734                                ))
5735                            }
5736                        }
5737                        DimIdx::Indices(idxs) => {
5738                            let mut selected = String::new();
5739                            for &i in &idxs {
5740                                if i >= chars.len() {
5741                                    return Err(format!("Index {} out of range", i + 1));
5742                                }
5743                                selected.push(chars[i]);
5744                            }
5745                            if selected.chars().count() == 1 {
5746                                Ok(Value::Scalar(selected.chars().next().unwrap() as u32 as f64))
5747                            } else {
5748                                Ok(Value::Str(selected))
5749                            }
5750                        }
5751                    }
5752                }
5753                Value::StringObj(s) => {
5754                    // String object indexing — treat as single element
5755                    let env1 = env_with_end(env, 1);
5756                    match resolve_dim(&args[0], 1, &env1)? {
5757                        DimIdx::All | DimIdx::Indices(_) => Ok(Value::StringObj(s.clone())),
5758                    }
5759                }
5760            }
5761        }
5762        2 => {
5763            // A(i, j), A(:, j), A(i, :), A(:, :), A(end, :), A(1:end, 2)
5764            if matches!(
5765                val,
5766                Value::Void
5767                    | Value::Str(_)
5768                    | Value::StringObj(_)
5769                    | Value::Lambda(_)
5770                    | Value::Function { .. }
5771                    | Value::Tuple(_)
5772                    | Value::Cell(_)
5773                    | Value::Struct(_)
5774                    | Value::StructArray(_)
5775            ) {
5776                return Err("2D indexing not supported for this type".to_string());
5777            }
5778            let (nrows, ncols) = match val {
5779                Value::Scalar(_) | Value::Complex(_, _) => (1, 1),
5780                Value::Matrix(m) => (m.nrows(), m.ncols()),
5781                Value::Void
5782                | Value::Str(_)
5783                | Value::StringObj(_)
5784                | Value::Lambda(_)
5785                | Value::Function { .. }
5786                | Value::Tuple(_)
5787                | Value::Cell(_)
5788                | Value::Struct(_)
5789                | Value::StructArray(_) => unreachable!(),
5790            };
5791            let env_r = env_with_end(env, nrows);
5792            let env_c = env_with_end(env, ncols);
5793            let row_idx = resolve_dim(&args[0], nrows, &env_r)?;
5794            let col_idx = resolve_dim(&args[1], ncols, &env_c)?;
5795
5796            let rows: Vec<usize> = match row_idx {
5797                DimIdx::All => (0..nrows).collect(),
5798                DimIdx::Indices(v) => v,
5799            };
5800            let cols: Vec<usize> = match col_idx {
5801                DimIdx::All => (0..ncols).collect(),
5802                DimIdx::Indices(v) => v,
5803            };
5804
5805            if rows.len() == 1 && cols.len() == 1 {
5806                match val {
5807                    Value::Void
5808                    | Value::Str(_)
5809                    | Value::StringObj(_)
5810                    | Value::Lambda(_)
5811                    | Value::Function { .. }
5812                    | Value::Tuple(_)
5813                    | Value::Cell(_)
5814                    | Value::Struct(_)
5815                    | Value::StructArray(_) => unreachable!(),
5816                    Value::Scalar(n) => Ok(Value::Scalar(*n)),
5817                    Value::Complex(re, im) => Ok(Value::Complex(*re, *im)),
5818                    Value::Matrix(m) => Ok(Value::Scalar(m[[rows[0], cols[0]]])),
5819                }
5820            } else {
5821                let out_r = rows.len();
5822                let out_c = cols.len();
5823                let flat: Vec<f64> = rows
5824                    .iter()
5825                    .flat_map(|&r| {
5826                        cols.iter().map(move |&c| match val {
5827                            Value::Void
5828                            | Value::Str(_)
5829                            | Value::StringObj(_)
5830                            | Value::Lambda(_)
5831                            | Value::Function { .. }
5832                            | Value::Tuple(_)
5833                            | Value::Cell(_)
5834                            | Value::Struct(_)
5835                            | Value::StructArray(_) => unreachable!(),
5836                            Value::Scalar(n) => *n,
5837                            Value::Complex(re, _) => *re,
5838                            Value::Matrix(m) => m[[r, c]],
5839                        })
5840                    })
5841                    .collect();
5842                Ok(Value::Matrix(
5843                    Array2::from_shape_vec((out_r, out_c), flat).unwrap(),
5844                ))
5845            }
5846        }
5847        n => Err(format!(
5848            "Indexing with {n} indices is not supported (max 2)"
5849        )),
5850    }
5851}
5852
5853/// Resolved index along one dimension. Indices are 0-based.
5854enum DimIdx {
5855    All,
5856    Indices(Vec<usize>),
5857}
5858
5859/// Resolves one index argument for a dimension of size `dim_size`.
5860/// `Expr::Colon` → `DimIdx::All`.
5861/// Scalar → single 0-based index (validates 1-based bounds).
5862/// Row/column vector → multiple 0-based indices.
5863/// Logical mask: a 0/1 vector whose length equals `dim_size` selects positions where value is 1.
5864fn resolve_dim(expr: &Expr, dim_size: usize, env: &Env) -> Result<DimIdx, String> {
5865    if matches!(expr, Expr::Colon) {
5866        return Ok(DimIdx::All);
5867    }
5868    let val = eval(expr, env)?;
5869    let floats: Vec<f64> = match val {
5870        Value::Void => {
5871            return Err("Index must be numeric, not void".to_string());
5872        }
5873        Value::Scalar(n) => vec![n],
5874        Value::Complex(re, im) => {
5875            if im != 0.0 {
5876                return Err("Index must be real, not complex".to_string());
5877            }
5878            vec![re]
5879        }
5880        Value::Matrix(m) => {
5881            // Allow 2-D matrices only when they qualify as a logical mask (same numel as dim_size).
5882            let total = m.nrows() * m.ncols();
5883            if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
5884                return Err("Index must be a scalar or vector, not a matrix".to_string());
5885            }
5886            // Collect in column-major order so mask positions align with linear indexing.
5887            if m.nrows() > 1 && m.ncols() > 1 {
5888                let mut v = Vec::with_capacity(total);
5889                for col in 0..m.ncols() {
5890                    for row in 0..m.nrows() {
5891                        v.push(m[[row, col]]);
5892                    }
5893                }
5894                v
5895            } else {
5896                m.iter().copied().collect()
5897            }
5898        }
5899        Value::Str(_) | Value::StringObj(_) => {
5900            return Err("Index must be numeric, not a string".to_string());
5901        }
5902        Value::Lambda(_)
5903        | Value::Function { .. }
5904        | Value::Tuple(_)
5905        | Value::Cell(_)
5906        | Value::Struct(_)
5907        | Value::StructArray(_) => {
5908            return Err("Index must be numeric, not a function".to_string());
5909        }
5910    };
5911    // Logical mask: a 0/1 array whose element count matches dim_size selects by boolean mask.
5912    if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
5913        let idxs: Vec<usize> = floats
5914            .iter()
5915            .enumerate()
5916            .filter(|&(_, &f)| f == 1.0)
5917            .map(|(i, _)| i)
5918            .collect();
5919        return Ok(DimIdx::Indices(idxs));
5920    }
5921    let mut idxs = Vec::with_capacity(floats.len());
5922    for n in floats {
5923        let i = n.round() as i64;
5924        if i < 1 || i as usize > dim_size {
5925            return Err(format!("Index {i} out of range (1..{dim_size})"));
5926        }
5927        idxs.push(i as usize - 1);
5928    }
5929    Ok(DimIdx::Indices(idxs))
5930}
5931
5932/// Formats a number for display: integers without decimal point,
5933/// floats with up to 10 significant fractional digits, trailing zeros trimmed.
5934/// Always decimal — used for expression re-display, not user-facing output.
5935pub fn format_number(n: f64) -> String {
5936    if n.fract() == 0.0 && n.abs() < 1e15 {
5937        format!("{}", n as i64)
5938    } else if n != 0.0 && (n.abs() >= 1e15 || n.abs() < 1e-9) {
5939        trim_sci(&format!("{:.15e}", n))
5940    } else {
5941        let s = format!("{:.10}", n);
5942        s.trim_end_matches('0').trim_end_matches('.').to_string()
5943    }
5944}
5945
5946/// Formats a scalar `f64` for user-facing output using the given base and format mode.
5947pub fn format_scalar(n: f64, base: Base, mode: &FormatMode) -> String {
5948    // FormatMode::Hex always shows IEEE 754 bits regardless of base.
5949    if matches!(mode, FormatMode::Hex) {
5950        return format_decimal(n, mode);
5951    }
5952    match base {
5953        Base::Dec => format_decimal(n, mode),
5954        _ => format_non_dec(n, base),
5955    }
5956}
5957
5958/// Formats a complex number `re + im*i` for display.
5959///
5960/// - `a + 0i` → `a`  (pure real)
5961/// - `0 + bi` → `bi`
5962/// - `im == ±1` suppresses the coefficient: `i`, `-i`, `a + i`, `a - i`
5963pub fn format_complex(re: f64, im: f64, mode: &FormatMode) -> String {
5964    if im == 0.0 {
5965        return format_decimal(re, mode);
5966    }
5967    let im_abs = im.abs();
5968    let im_str = if im_abs == 1.0 {
5969        String::new()
5970    } else {
5971        format_decimal(im_abs, mode)
5972    };
5973    if re == 0.0 {
5974        if im < 0.0 {
5975            format!("-{}i", im_str)
5976        } else {
5977            format!("{}i", im_str)
5978        }
5979    } else {
5980        let re_str = format_decimal(re, mode);
5981        if im < 0.0 {
5982            format!("{} - {}i", re_str, im_str)
5983        } else {
5984            format!("{} + {}i", re_str, im_str)
5985        }
5986    }
5987}
5988
5989/// Reconstructs a source-like string from an `Expr`.
5990///
5991/// Used to populate the display string of lambda values so that
5992/// `f = @(x) x.^2` shows `f = @(x) x .^ 2` in the REPL.
5993pub fn expr_to_string(e: &Expr) -> String {
5994    match e {
5995        Expr::Number(n) => {
5996            if n.is_nan() {
5997                "nan".to_string()
5998            } else if n.is_infinite() {
5999                if *n > 0.0 {
6000                    "inf".to_string()
6001                } else {
6002                    "-inf".to_string()
6003                }
6004            } else {
6005                format!("{n}")
6006            }
6007        }
6008        Expr::Var(name) => name.clone(),
6009        Expr::UnaryMinus(e) => format!("-{}", expr_to_string(e)),
6010        Expr::UnaryNot(e) => format!("~{}", expr_to_string(e)),
6011        Expr::BinOp(l, op, r) => {
6012            let op_str = match op {
6013                Op::Add => "+",
6014                Op::Sub => "-",
6015                Op::Mul => "*",
6016                Op::Div => "/",
6017                Op::Pow => "^",
6018                Op::ElemMul => ".*",
6019                Op::ElemDiv => "./",
6020                Op::ElemPow => ".^",
6021                Op::Eq => "==",
6022                Op::NotEq => "~=",
6023                Op::Lt => "<",
6024                Op::Gt => ">",
6025                Op::LtEq => "<=",
6026                Op::GtEq => ">=",
6027                Op::And => "&&",
6028                Op::Or => "||",
6029                Op::ElemAnd => "&",
6030                Op::ElemOr => "|",
6031                Op::LDiv => "\\",
6032            };
6033            format!("{} {op_str} {}", expr_to_string(l), expr_to_string(r))
6034        }
6035        Expr::Call(name, args) => {
6036            let args_str = args
6037                .iter()
6038                .map(expr_to_string)
6039                .collect::<Vec<_>>()
6040                .join(", ");
6041            format!("{name}({args_str})")
6042        }
6043        Expr::Transpose(e) => format!("{}'", expr_to_string(e)),
6044        Expr::PlainTranspose(e) => format!("{}.'", expr_to_string(e)),
6045        Expr::Range(start, step, stop) => {
6046            if let Some(step) = step {
6047                format!(
6048                    "{}:{}:{}",
6049                    expr_to_string(start),
6050                    expr_to_string(step),
6051                    expr_to_string(stop)
6052                )
6053            } else {
6054                format!("{}:{}", expr_to_string(start), expr_to_string(stop))
6055            }
6056        }
6057        Expr::StrLiteral(s) => format!("'{s}'"),
6058        Expr::StringObjLiteral(s) => format!("\"{s}\""),
6059        Expr::Lambda { params, body, .. } => {
6060            format!("@({}) {}", params.join(", "), expr_to_string(body))
6061        }
6062        Expr::FuncHandle(name) => format!("@{name}"),
6063        Expr::Matrix(_) => "[...]".to_string(),
6064        Expr::CellLiteral(_) => "{...}".to_string(),
6065        Expr::CellIndex(e, i) => format!("{}{{{}}}", expr_to_string(e), expr_to_string(i)),
6066        Expr::Colon => ":".to_string(),
6067        Expr::FieldGet(base, field) => format!("{}.{field}", expr_to_string(base)),
6068        Expr::DotCall(segs, args) => {
6069            let args_str = args
6070                .iter()
6071                .map(expr_to_string)
6072                .collect::<Vec<_>>()
6073                .join(", ");
6074            format!("{}({args_str})", segs.join("."))
6075        }
6076    }
6077}
6078
6079/// Formats a `Value` compactly: scalars as a number string, matrices as `[NxM double]`.
6080pub fn format_value(v: &Value, base: Base, mode: &FormatMode) -> String {
6081    match v {
6082        Value::Void => String::new(),
6083        Value::Scalar(n) => format_scalar(*n, base, mode),
6084        Value::Matrix(m) => format!("[{}x{} double]", m.nrows(), m.ncols()),
6085        Value::Complex(re, im) => format_complex(*re, *im, mode),
6086        Value::Str(s) => s.clone(),
6087        Value::StringObj(s) => s.clone(),
6088        Value::Lambda(lf) => lf.1.clone(),
6089        Value::Function {
6090            params, outputs, ..
6091        } => {
6092            let params_str = params.join(", ");
6093            let out_str = match outputs.len() {
6094                0 => String::new(),
6095                1 => format!("{} = ", outputs[0]),
6096                _ => format!("[{}] = ", outputs.join(", ")),
6097            };
6098            format!("@function {out_str}f({params_str})")
6099        }
6100        Value::Tuple(vals) => {
6101            let parts: Vec<String> = vals.iter().map(|v| format_value(v, base, mode)).collect();
6102            format!("({})", parts.join(", "))
6103        }
6104        Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
6105        Value::Struct(_) => "[1×1 struct]".to_string(),
6106        Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
6107    }
6108}
6109
6110/// Returns `None` for scalars, complex numbers, strings, and void (displayed inline or suppressed);
6111/// `Some(full_string)` for matrices (MATLAB-style column-aligned display).
6112pub fn format_value_full(v: &Value, mode: &FormatMode) -> Option<String> {
6113    match v {
6114        Value::Void
6115        | Value::Scalar(_)
6116        | Value::Complex(_, _)
6117        | Value::Str(_)
6118        | Value::StringObj(_)
6119        | Value::Lambda(_)
6120        | Value::Function { .. }
6121        | Value::Tuple(_) => None,
6122        Value::Matrix(m) => Some(format_matrix(m, mode)),
6123        Value::Cell(elems) => Some(format_cell(elems, mode)),
6124        Value::Struct(map) => Some(format_struct(map, mode)),
6125        Value::StructArray(arr) => Some(format_struct_array(arr, mode)),
6126    }
6127}
6128
6129/// Formats a cell array in MATLAB-style multi-line display.
6130fn format_cell(elems: &[Value], mode: &FormatMode) -> String {
6131    if elems.is_empty() {
6132        return "  {}".to_string();
6133    }
6134    let mut lines = vec!["  {".to_string()];
6135    for (i, val) in elems.iter().enumerate() {
6136        let label = format!("    [1,{}]", i + 1);
6137        match val {
6138            Value::Matrix(_) => {
6139                lines.push(format!("{label}:"));
6140                if let Some(full) = format_value_full(val, mode) {
6141                    for line in full.lines() {
6142                        lines.push(format!("   {line}"));
6143                    }
6144                }
6145            }
6146            Value::Cell(_) => {
6147                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
6148            }
6149            _ => {
6150                lines.push(format!("{label}: {}", format_value(val, Base::Dec, mode)));
6151            }
6152        }
6153    }
6154    lines.push("  }".to_string());
6155    lines.join("\n")
6156}
6157
6158/// Formats a struct in MATLAB 2014b+ multi-line style.
6159fn format_struct(map: &IndexMap<String, Value>, mode: &FormatMode) -> String {
6160    let mut lines = vec![
6161        String::new(),
6162        "  struct with fields:".to_string(),
6163        String::new(),
6164    ];
6165    for (key, val) in map {
6166        let val_str = match val {
6167            Value::Struct(_) => "[1×1 struct]".to_string(),
6168            Value::StructArray(arr) => format!("[1×{} struct]", arr.len()),
6169            Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
6170            Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
6171            _ => format_value(val, Base::Dec, mode),
6172        };
6173        lines.push(format!("    {key}: {val_str}"));
6174    }
6175    lines.join("\n")
6176}
6177
6178/// Formats a 1×N struct array (shows each element's fields).
6179fn format_struct_array(arr: &[IndexMap<String, Value>], mode: &FormatMode) -> String {
6180    let n = arr.len();
6181    let mut lines = vec![
6182        String::new(),
6183        format!("  1×{n} struct array with fields:"),
6184        String::new(),
6185    ];
6186    // Collect field names from the first element
6187    if let Some(first) = arr.first() {
6188        for key in first.keys() {
6189            lines.push(format!("    {key}"));
6190        }
6191    }
6192    // Show first element's values if array has exactly 1 element
6193    if n == 1
6194        && let Some(first) = arr.first()
6195    {
6196        lines.clear();
6197        lines.push(String::new());
6198        lines.push("  struct with fields:".to_string());
6199        lines.push(String::new());
6200        for (key, val) in first {
6201            let val_str = match val {
6202                Value::Struct(_) => "[1×1 struct]".to_string(),
6203                Value::StructArray(a) => format!("[1×{} struct]", a.len()),
6204                Value::Matrix(m) => format!("[{}×{} double]", m.nrows(), m.ncols()),
6205                Value::Cell(v) => format!("{{1×{} cell}}", v.len()),
6206                _ => format_value(val, Base::Dec, mode),
6207            };
6208            lines.push(format!("    {key}: {val_str}"));
6209        }
6210    }
6211    lines.join("\n")
6212}
6213
6214/// Formats a matrix with right-aligned columns, 3-space indent, 3 spaces between columns.
6215/// `FormatMode::Plus` renders a sign grid (`+`, `-`, `0`).
6216fn format_matrix(m: &Array2<f64>, mode: &FormatMode) -> String {
6217    if m.nrows() == 0 || m.ncols() == 0 {
6218        return "   []".to_string();
6219    }
6220    // Special rendering for format +
6221    if matches!(mode, FormatMode::Plus) {
6222        let lines: Vec<String> = m
6223            .rows()
6224            .into_iter()
6225            .map(|row| {
6226                let chars: String = row
6227                    .iter()
6228                    .map(|&x| {
6229                        if x > 0.0 {
6230                            '+'
6231                        } else if x < 0.0 {
6232                            '-'
6233                        } else {
6234                            '0'
6235                        }
6236                    })
6237                    .collect();
6238                format!("   {}", chars)
6239            })
6240            .collect();
6241        return lines.join("\n");
6242    }
6243    let ncols = m.ncols();
6244    let cells: Vec<Vec<String>> = m
6245        .rows()
6246        .into_iter()
6247        .map(|row| row.iter().map(|&x| format_decimal(x, mode)).collect())
6248        .collect();
6249    let col_widths: Vec<usize> = (0..ncols)
6250        .map(|c| cells.iter().map(|row| row[c].len()).max().unwrap_or(0))
6251        .collect();
6252    let mut lines = Vec::new();
6253    for row in &cells {
6254        let mut line = String::from("   ");
6255        for (c, cell) in row.iter().enumerate() {
6256            if c > 0 {
6257                line.push_str("   ");
6258            }
6259            let pad = col_widths[c].saturating_sub(cell.len());
6260            for _ in 0..pad {
6261                line.push(' ');
6262            }
6263            line.push_str(cell);
6264        }
6265        lines.push(line);
6266    }
6267    lines.join("\n")
6268}
6269
6270/// Formats a number in a non-decimal integer base (hex/bin/oct).
6271/// Rounds to the nearest integer before formatting.
6272pub fn format_non_dec(n: f64, base: Base) -> String {
6273    let i = n.round() as i64;
6274    let u = i.unsigned_abs();
6275    let sign = if i < 0 { "-" } else { "" };
6276    match base {
6277        Base::Hex => format!("{}0x{:X}", sign, u),
6278        Base::Bin => format!("{}0b{:b}", sign, u),
6279        Base::Oct => format!("{}0o{:o}", sign, u),
6280        Base::Dec => format_decimal(n, &FormatMode::default()),
6281    }
6282}
6283
6284// ---------------------------------------------------------------------------
6285// Internal decimal formatters
6286// ---------------------------------------------------------------------------
6287
6288fn format_decimal(n: f64, mode: &FormatMode) -> String {
6289    if n.is_nan() {
6290        return "NaN".to_string();
6291    }
6292    if n.is_infinite() {
6293        return if n > 0.0 { "Inf" } else { "-Inf" }.to_string();
6294    }
6295    match mode {
6296        FormatMode::Short | FormatMode::ShortG => fmt_auto_sig(n, 5),
6297        FormatMode::Long | FormatMode::LongG => fmt_auto_sig(n, 15),
6298        FormatMode::ShortE => fmt_sci_dp(n, 4),
6299        FormatMode::LongE => fmt_sci_dp(n, 14),
6300        FormatMode::Bank => format!("{:.2}", n),
6301        FormatMode::Rat => fmt_rat(n),
6302        FormatMode::Hex => fmt_hex_ieee754(n),
6303        FormatMode::Plus => fmt_plus_sign(n),
6304        FormatMode::Custom(prec) => fmt_custom_prec(n, *prec),
6305    }
6306}
6307
6308/// Integer shortcut: fits in i64 without fractional part.
6309#[inline]
6310fn is_exact_int(n: f64) -> bool {
6311    n.fract() == 0.0 && n.abs() < 1e15
6312}
6313
6314/// Auto fixed/scientific with `sig` significant digits (MATLAB-compatible).
6315/// Uses fixed notation for exponents in [-3, sig), scientific otherwise.
6316/// Integers are shown without a decimal point.
6317fn fmt_auto_sig(n: f64, sig: usize) -> String {
6318    if is_exact_int(n) {
6319        return format!("{}", n as i64);
6320    }
6321    let abs_n = n.abs();
6322    let exp = if abs_n == 0.0 {
6323        0i32
6324    } else {
6325        abs_n.log10().floor() as i32
6326    };
6327    if exp >= -3 && exp < sig as i32 {
6328        let dp = (sig as i32 - 1 - exp) as usize;
6329        let s = format!("{:.prec$}", n, prec = dp);
6330        // Only strip trailing zeros when there is a decimal point.
6331        if s.contains('.') {
6332            s.trim_end_matches('0').trim_end_matches('.').to_string()
6333        } else {
6334            s
6335        }
6336    } else {
6337        let s = format!("{:.prec$e}", n, prec = sig - 1);
6338        trim_sci(&s)
6339    }
6340}
6341
6342/// Always scientific notation with `dp` decimal places.
6343fn fmt_sci_dp(n: f64, dp: usize) -> String {
6344    let s = format!("{:.prec$e}", n, prec = dp);
6345    trim_sci(&s)
6346}
6347
6348/// Legacy custom-precision: N decimal places, auto fixed/scientific.
6349fn fmt_custom_prec(n: f64, prec: usize) -> String {
6350    if is_exact_int(n) {
6351        return format!("{}", n as i64);
6352    }
6353    if n.abs() >= 1e15 || (n != 0.0 && n.abs() < 1e-9) {
6354        let s = format!("{:.prec$e}", n, prec = prec);
6355        trim_sci(&s)
6356    } else {
6357        let s = format!("{:.prec$}", n, prec = prec);
6358        s.trim_end_matches('0').trim_end_matches('.').to_string()
6359    }
6360}
6361
6362/// Rational approximation via continued fractions. Returns `"p/q"` or `"p"` if denominator is 1.
6363fn fmt_rat(n: f64) -> String {
6364    if is_exact_int(n) {
6365        return format!("{}", n as i64);
6366    }
6367    let sign = if n < 0.0 { -1i64 } else { 1i64 };
6368    let x = n.abs();
6369    let (mut h1, mut h2): (i64, i64) = (1, 0);
6370    let (mut k1, mut k2): (i64, i64) = (0, 1);
6371    let mut b = x;
6372    for _ in 0..64 {
6373        let a = b.floor() as i64;
6374        let (nh, nk) = (a * h1 + h2, a * k1 + k2);
6375        if nk > 10_000 {
6376            break;
6377        }
6378        h2 = h1;
6379        h1 = nh;
6380        k2 = k1;
6381        k1 = nk;
6382        let frac = b - a as f64;
6383        if frac < 1e-12 || (h1 as f64 / k1 as f64 - x).abs() < 1e-6 {
6384            break;
6385        }
6386        b = 1.0 / frac;
6387    }
6388    let p = sign * h1;
6389    if k1 == 1 {
6390        format!("{}", p)
6391    } else {
6392        format!("{}/{}", p, k1)
6393    }
6394}
6395
6396/// IEEE 754 double-precision bit pattern as 16 uppercase hex digits.
6397fn fmt_hex_ieee754(n: f64) -> String {
6398    format!("{:016X}", n.to_bits())
6399}
6400
6401/// Sign indicator: `+`, `-`, or ` ` for zero.
6402fn fmt_plus_sign(n: f64) -> String {
6403    if n > 0.0 {
6404        "+".to_string()
6405    } else if n < 0.0 {
6406        "-".to_string()
6407    } else {
6408        " ".to_string()
6409    }
6410}
6411
6412fn trim_sci(s: &str) -> String {
6413    if let Some(e_pos) = s.find('e') {
6414        let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
6415        let exp_str = &s[e_pos + 1..];
6416        let (sign, digits) = if let Some(d) = exp_str.strip_prefix('-') {
6417            ("-", d)
6418        } else if let Some(d) = exp_str.strip_prefix('+') {
6419            ("+", d)
6420        } else {
6421            ("+", exp_str)
6422        };
6423        let exp_num: i32 = digits.parse().unwrap_or(0);
6424        format!("{}e{}{:02}", mantissa, sign, exp_num)
6425    } else {
6426        s.to_string()
6427    }
6428}
6429
6430// --- MAT built-in helpers ---
6431
6432/// Loads a MATLAB Level 5/7 MAT file and returns a [`Value::Struct`].
6433///
6434/// Requires the `mat` Cargo feature; without it, always returns an error.
6435pub fn load_mat_file(path: &str) -> Result<Value, String> {
6436    load_mat_file_impl(path)
6437}
6438
6439#[cfg(feature = "mat")]
6440fn load_mat_file_impl(path: &str) -> Result<Value, String> {
6441    crate::mat::mat_load(path)
6442}
6443
6444#[cfg(not(feature = "mat"))]
6445fn load_mat_file_impl(_path: &str) -> Result<Value, String> {
6446    Err("load: .mat support not available — rebuild with --features mat".to_string())
6447}
6448
6449// --- JSON built-in helpers ---
6450
6451#[cfg(feature = "json")]
6452fn jsondecode_impl(arg: &Value) -> Result<Value, String> {
6453    let s = match arg {
6454        Value::Str(s) | Value::StringObj(s) => s.as_str(),
6455        _ => return Err("jsondecode: argument must be a string".to_string()),
6456    };
6457    let jval: serde_json::Value =
6458        serde_json::from_str(s).map_err(|e| format!("jsondecode: invalid JSON: {e}"))?;
6459    Ok(crate::json::json_to_value(&jval))
6460}
6461
6462#[cfg(not(feature = "json"))]
6463fn jsondecode_impl(_arg: &Value) -> Result<Value, String> {
6464    Err("jsondecode: not available — rebuild with --features json".to_string())
6465}
6466
6467#[cfg(feature = "json")]
6468fn jsonencode_impl(arg: &Value) -> Result<Value, String> {
6469    let jval = crate::json::value_to_json(arg)?;
6470    let s = serde_json::to_string(&jval)
6471        .map_err(|e| format!("jsonencode: serialization error: {e}"))?;
6472    Ok(Value::Str(s))
6473}
6474
6475#[cfg(not(feature = "json"))]
6476fn jsonencode_impl(_arg: &Value) -> Result<Value, String> {
6477    Err("jsonencode: not available — rebuild with --features json".to_string())
6478}
6479
6480#[cfg(test)]
6481#[path = "eval_tests.rs"]
6482mod tests;