runmat_runtime/
lib.rs

1use regex::Regex;
2use runmat_builtins::{builtin_functions, Value};
3use runmat_gc_api::GcPtr;
4use runmat_macros::runtime_builtin;
5
6pub mod arrays;
7pub mod comparison;
8pub mod concatenation;
9pub mod constants;
10pub mod elementwise;
11pub mod indexing;
12pub mod introspection;
13pub mod io;
14pub mod mathematics;
15pub mod matrix;
16pub mod plotting;
17
18#[cfg(feature = "blas-lapack")]
19pub mod blas;
20#[cfg(feature = "blas-lapack")]
21pub mod lapack;
22
23// Link to Apple's Accelerate framework on macOS
24#[cfg(all(feature = "blas-lapack", target_os = "macos"))]
25#[link(name = "Accelerate", kind = "framework")]
26extern "C" {}
27
28// Ensure OpenBLAS is linked on non-macOS platforms when BLAS/LAPACK is enabled
29#[cfg(all(feature = "blas-lapack", not(target_os = "macos")))]
30extern crate openblas_src;
31
32pub use arrays::*;
33pub use comparison::*;
34pub use concatenation::*;
35// Explicitly re-export for external users (ignition VM) that build matrices from values
36pub use concatenation::create_matrix_from_values;
37// Note: constants and mathematics modules only contain #[runtime_builtin] functions
38// and don't export public items, so they don't need to be re-exported
39pub use elementwise::*;
40pub use indexing::*;
41pub use matrix::*;
42
43#[cfg(feature = "blas-lapack")]
44pub use blas::*;
45#[cfg(feature = "blas-lapack")]
46pub use lapack::*;
47
48fn make_cell(values: Vec<Value>, rows: usize, cols: usize) -> Result<Value, String> {
49    let handles: Vec<GcPtr<Value>> = values
50        .into_iter()
51        .map(|v| runmat_gc::gc_allocate(v).expect("gc alloc"))
52        .collect();
53    let ca = runmat_builtins::CellArray::new_handles(handles, rows, cols)
54        .map_err(|e| format!("Cell creation error: {e}"))?;
55    Ok(Value::Cell(ca))
56}
57
58// Internal builtin to construct a cell from a vector of values (used by ignition)
59#[runmat_macros::runtime_builtin(name = "__make_cell")]
60fn make_cell_builtin(rest: Vec<Value>) -> Result<Value, String> {
61    let rows = 1usize;
62    let cols = rest.len();
63    make_cell(rest, rows, cols)
64}
65
66/// Call a registered language builtin by name.
67/// Supports function overloading by trying different argument patterns.
68/// Returns an error if no builtin with that name and compatible arguments is found.
69pub fn call_builtin(name: &str, args: &[Value]) -> Result<Value, String> {
70    let mut matching_builtins = Vec::new();
71
72    // Collect all builtins with the matching name
73    for b in builtin_functions() {
74        if b.name == name {
75            matching_builtins.push(b);
76        }
77    }
78
79    if matching_builtins.is_empty() {
80        // Fallback: treat as class constructor if class is registered
81        if let Some(cls) = runmat_builtins::get_class(name) {
82            // Prefer explicit constructor method with the same name as class (static)
83            if let Some(ctor) = cls.methods.get(name) {
84                // Dispatch to constructor builtin; pass args through
85                return call_builtin(&ctor.function_name, args);
86            }
87            // Otherwise default-construct object
88            return new_object_builtin(name.to_string());
89        }
90        return Err(format!(
91            "{}: Undefined function: {name}",
92            "MATLAB:UndefinedFunction"
93        ));
94    }
95
96    // Try each builtin until one succeeds
97    let mut last_error = String::new();
98    for builtin in matching_builtins {
99        let f = builtin.implementation;
100        match (f)(args) {
101            Ok(result) => return Ok(result),
102            Err(e) => last_error = e,
103        }
104    }
105
106    // If none succeeded, return the last error
107    Err(format!(
108        "No matching overload for `{}` with {} args: {}",
109        name,
110        args.len(),
111        last_error
112    ))
113}
114
115#[runmat_macros::runtime_builtin(name = "cellstr")]
116fn cellstr_builtin(a: Value) -> Result<Value, String> {
117    match a {
118        Value::String(s) => make_cell(vec![Value::String(s)], 1, 1),
119        Value::StringArray(sa) => {
120            let rows = sa.rows();
121            let cols = sa.cols();
122            let mut cells: Vec<Value> = Vec::with_capacity(sa.data.len());
123            for r in 0..rows {
124                for c in 0..cols {
125                    let idx = r + c * rows;
126                    cells.push(Value::String(sa.data[idx].clone()));
127                }
128            }
129            make_cell(cells, rows, cols)
130        }
131        other => Err(format!(
132            "cellstr: expected string or string array, got {other:?}"
133        )),
134    }
135}
136
137#[runmat_macros::runtime_builtin(name = "char")]
138fn char_builtin(a: Value) -> Result<Value, String> {
139    match a {
140        Value::String(s) => {
141            let data: Vec<f64> = s.chars().map(|ch| ch as u32 as f64).collect();
142            Ok(Value::Tensor(
143                runmat_builtins::Tensor::new(data, vec![1, s.chars().count()])
144                    .map_err(|e| format!("char: {e}"))?,
145            ))
146        }
147        Value::StringArray(sa) => {
148            let rows = sa.rows();
149            let cols = sa.cols();
150            let mut max_len = 0usize;
151            for c in 0..cols {
152                for r in 0..rows {
153                    let idx = r + c * rows;
154                    max_len = max_len.max(sa.data[idx].chars().count());
155                }
156            }
157            if rows == 0 || cols == 0 {
158                return Ok(Value::Tensor(
159                    runmat_builtins::Tensor::new(Vec::new(), vec![0, 0]).unwrap(),
160                ));
161            }
162            let out_rows = rows;
163            let out_cols = max_len * cols;
164            let mut out = vec![32.0; out_rows * out_cols];
165            for c in 0..cols {
166                for r in 0..rows {
167                    let idx = r + c * rows;
168                    let s = &sa.data[idx];
169                    for (j, ch) in s.chars().enumerate() {
170                        let oc = c * max_len + j;
171                        out[r + oc * out_rows] = ch as u32 as f64;
172                    }
173                }
174            }
175            Ok(Value::Tensor(
176                runmat_builtins::Tensor::new(out, vec![out_rows, out_cols])
177                    .map_err(|e| format!("char: {e}"))?,
178            ))
179        }
180        other => Err(format!(
181            "char: expected string or string array, got {other:?}"
182        )),
183    }
184}
185
186// size builtin centralized in introspection.rs
187
188// Linear index to subscripts (column-major)
189#[runmat_macros::runtime_builtin(name = "ind2sub")]
190fn ind2sub_builtin(dims_val: Value, idx_val: f64) -> Result<Value, String> {
191    let dims: Vec<usize> = match dims_val {
192        Value::Tensor(t) => {
193            if t.shape.len() == 2 && (t.shape[0] == 1 || t.shape[1] == 1) {
194                t.data.iter().map(|v| *v as usize).collect()
195            } else {
196                return Err("ind2sub: dims must be a vector".to_string());
197            }
198        }
199        Value::Cell(ca) => {
200            if ca.data.is_empty() {
201                vec![]
202            } else {
203                ca.data
204                    .iter()
205                    .map(|v| match &**v {
206                        Value::Num(n) => *n as usize,
207                        Value::Int(i) => i.to_i64() as usize,
208                        _ => 1usize,
209                    })
210                    .collect()
211            }
212        }
213        _ => return Err("ind2sub: dims must be a vector".to_string()),
214    };
215    if dims.is_empty() {
216        return Err("ind2sub: empty dims".to_string());
217    }
218    let mut subs: Vec<usize> = vec![1; dims.len()];
219    let idx = if idx_val < 1.0 {
220        1usize
221    } else {
222        idx_val as usize
223    } - 1; // 0-based
224    let mut stride = 1usize;
225    for d in 0..dims.len() {
226        let dim_len = dims[d];
227        let val = (idx / stride) % dim_len;
228        subs[d] = val + 1; // 1-based
229        stride *= dim_len.max(1);
230    }
231    // Return as a tensor column vector for expansion
232    let data: Vec<f64> = subs.iter().map(|s| *s as f64).collect();
233    Ok(Value::Tensor(
234        runmat_builtins::Tensor::new(data, vec![subs.len(), 1])
235            .map_err(|e| format!("ind2sub: {e}"))?,
236    ))
237}
238
239#[runmat_macros::runtime_builtin(name = "sub2ind")]
240fn sub2ind_builtin(dims_val: Value, rest: Vec<Value>) -> Result<Value, String> {
241    let dims: Vec<usize> = match dims_val {
242        Value::Tensor(t) => {
243            if t.shape.len() == 2 && (t.shape[0] == 1 || t.shape[1] == 1) {
244                t.data.iter().map(|v| *v as usize).collect()
245            } else {
246                return Err("sub2ind: dims must be a vector".to_string());
247            }
248        }
249        Value::Cell(ca) => {
250            if ca.data.is_empty() {
251                vec![]
252            } else {
253                ca.data
254                    .iter()
255                    .map(|v| match &**v {
256                        Value::Num(n) => *n as usize,
257                        Value::Int(i) => i.to_i64() as usize,
258                        _ => 1usize,
259                    })
260                    .collect()
261            }
262        }
263        _ => return Err("sub2ind: dims must be a vector".to_string()),
264    };
265    if dims.is_empty() {
266        return Err("sub2ind: empty dims".to_string());
267    }
268    if rest.len() != dims.len() {
269        return Err("sub2ind: expected one subscript per dimension".to_string());
270    }
271    let subs: Vec<usize> = rest
272        .iter()
273        .map(|v| match v {
274            Value::Num(n) => *n as isize,
275            Value::Int(i) => i.to_i64() as isize,
276            _ => 1isize,
277        })
278        .map(|x| if x < 1 { 1 } else { x as usize })
279        .collect();
280    // Column-major linear index: 1 + sum_{d=0}^{n-1} (sub[d]-1) * prod_{k<d} dims[k]
281    let mut stride = 1usize;
282    let mut lin0 = 0usize;
283    for d in 0..dims.len() {
284        let dim_len = dims[d];
285        let s = subs[d];
286        if s == 0 || s > dim_len {
287            return Err("sub2ind: subscript out of bounds".to_string());
288        }
289        lin0 += (s - 1) * stride;
290        stride *= dim_len.max(1);
291    }
292    Ok(Value::Num((lin0 + 1) as f64))
293}
294
295// -------- String constructors/conversions --------
296
297#[runmat_macros::runtime_builtin(name = "strings")]
298fn strings_ctor(rest: Vec<Value>) -> Result<Value, String> {
299    let mut shape: Vec<usize> = Vec::new();
300    if rest.is_empty() {
301        shape = vec![1, 1];
302    } else {
303        for v in rest {
304            let n: f64 = (&v).try_into()?;
305            if n < 0.0 {
306                return Err("strings: dimensions must be non-negative".to_string());
307            }
308            shape.push(n as usize);
309        }
310        if shape.is_empty() {
311            shape = vec![1, 1];
312        }
313    }
314    let total: usize = shape.iter().product();
315    let data = vec![String::new(); total];
316    Ok(Value::StringArray(
317        runmat_builtins::StringArray::new(data, shape).map_err(|e| format!("strings: {e}"))?,
318    ))
319}
320
321#[runmat_macros::runtime_builtin(name = "string.empty")]
322fn string_empty_ctor(rest: Vec<Value>) -> Result<Value, String> {
323    let mut shape: Vec<usize> = Vec::new();
324    for v in rest {
325        let n: f64 = (&v).try_into()?;
326        if n < 0.0 {
327            return Err("string.empty: dimensions must be non-negative".to_string());
328        }
329        shape.push(n as usize);
330    }
331    if shape.is_empty() {
332        shape = vec![0, 0];
333    }
334    let total: usize = shape.iter().product();
335    let data = vec![String::new(); total];
336    Ok(Value::StringArray(
337        runmat_builtins::StringArray::new(data, shape).map_err(|e| format!("string.empty: {e}"))?,
338    ))
339}
340
341#[runmat_macros::runtime_builtin(name = "string")]
342fn string_conv(a: Value) -> Result<Value, String> {
343    match a {
344        Value::String(s) => Ok(Value::StringArray(
345            runmat_builtins::StringArray::new(vec![s], vec![1, 1]).unwrap(),
346        )),
347        Value::StringArray(sa) => Ok(Value::StringArray(sa)),
348        Value::CharArray(ca) => {
349            let mut out: Vec<String> = Vec::with_capacity(ca.rows);
350            for r in 0..ca.rows {
351                let mut s = String::with_capacity(ca.cols);
352                for c in 0..ca.cols {
353                    s.push(ca.data[r * ca.cols + c]);
354                }
355                out.push(s);
356            }
357            Ok(Value::StringArray(
358                runmat_builtins::StringArray::new(out, vec![ca.rows, 1])
359                    .map_err(|e| e.to_string())?,
360            ))
361        }
362        Value::Tensor(t) => {
363            let mut out: Vec<String> = Vec::with_capacity(t.data.len());
364            for &x in &t.data {
365                out.push(x.to_string());
366            }
367            Ok(Value::StringArray(
368                runmat_builtins::StringArray::new(out, t.shape)
369                    .map_err(|e| format!("string: {e}"))?,
370            ))
371        }
372        Value::ComplexTensor(t) => {
373            let mut out: Vec<String> = Vec::with_capacity(t.data.len());
374            for &(re, im) in &t.data {
375                out.push(runmat_builtins::Value::Complex(re, im).to_string());
376            }
377            Ok(Value::StringArray(
378                runmat_builtins::StringArray::new(out, t.shape)
379                    .map_err(|e| format!("string: {e}"))?,
380            ))
381        }
382        Value::LogicalArray(la) => {
383            let mut out: Vec<String> = Vec::with_capacity(la.data.len());
384            for &b in &la.data {
385                out.push((if b != 0 { 1 } else { 0 }).to_string());
386            }
387            Ok(Value::StringArray(
388                runmat_builtins::StringArray::new(out, la.shape)
389                    .map_err(|e| format!("string: {e}"))?,
390            ))
391        }
392        Value::Cell(ca) => {
393            let mut out: Vec<String> = Vec::with_capacity(ca.rows * ca.cols);
394            for r in 0..ca.rows {
395                for c in 0..ca.cols {
396                    let v = &ca.data[r * ca.cols + c];
397                    let s: String = match &**v {
398                        Value::String(s) => s.clone(),
399                        Value::Num(n) => n.to_string(),
400                        Value::Int(i) => i.to_i64().to_string(),
401                        Value::Bool(b) => (if *b { 1 } else { 0 }).to_string(),
402                        Value::LogicalArray(la) => {
403                            if la.data.len() == 1 {
404                                (if la.data[0] != 0 { 1 } else { 0 }).to_string()
405                            } else {
406                                format!("LogicalArray(shape={:?})", la.shape)
407                            }
408                        }
409                        Value::CharArray(ch) => ch.data.iter().collect(),
410                        other => format!("{other:?}"),
411                    };
412                    out.push(s);
413                }
414            }
415            Ok(Value::StringArray(
416                runmat_builtins::StringArray::new(out, vec![ca.rows, ca.cols])
417                    .map_err(|e| e.to_string())?,
418            ))
419        }
420        Value::Num(n) => Ok(Value::StringArray(
421            runmat_builtins::StringArray::new(vec![n.to_string()], vec![1, 1]).unwrap(),
422        )),
423        Value::Complex(re, im) => {
424            let s = runmat_builtins::Value::Complex(re, im).to_string();
425            Ok(Value::StringArray(
426                runmat_builtins::StringArray::new(vec![s], vec![1, 1]).unwrap(),
427            ))
428        }
429        Value::Int(i) => Ok(Value::StringArray(
430            runmat_builtins::StringArray::new(vec![i.to_i64().to_string()], vec![1, 1]).unwrap(),
431        )),
432        Value::HandleObject(_) => {
433            // The MATLAB language string(handle) produces class-name-like text; keep conservative
434            Err("string: unsupported conversion from handle".to_string())
435        }
436        Value::Listener(_) => Err("string: unsupported conversion from listener".to_string()),
437        other => Err(format!("string: unsupported conversion from {other:?}")),
438    }
439}
440
441// -------- Logical constructors / conversions --------
442
443#[runmat_macros::runtime_builtin(name = "logical")]
444fn logical_ctor(a: Value) -> Result<Value, String> {
445    match a {
446        Value::Bool(b) => Ok(Value::Bool(b)),
447        Value::Num(n) => Ok(Value::Bool(n != 0.0)),
448        Value::Complex(re, im) => Ok(Value::Bool(!(re == 0.0 && im == 0.0))),
449        Value::Int(i) => Ok(Value::Bool(!i.is_zero())),
450        Value::Tensor(t) => {
451            let data: Vec<u8> = t
452                .data
453                .iter()
454                .map(|&x| if x != 0.0 { 1 } else { 0 })
455                .collect();
456            Ok(Value::LogicalArray(
457                runmat_builtins::LogicalArray::new(data, t.shape)
458                    .map_err(|e| format!("logical: {e}"))?,
459            ))
460        }
461        Value::StringArray(sa) => {
462            let data: Vec<u8> = sa
463                .data
464                .iter()
465                .map(|s| if !s.is_empty() { 1 } else { 0 })
466                .collect();
467            Ok(Value::LogicalArray(
468                runmat_builtins::LogicalArray::new(data, sa.shape)
469                    .map_err(|e| format!("logical: {e}"))?,
470            ))
471        }
472        Value::CharArray(ca) => {
473            let non_empty = !(ca.rows == 0 || ca.cols == 0);
474            Ok(Value::Bool(non_empty))
475        }
476        Value::LogicalArray(la) => Ok(Value::LogicalArray(la)),
477        Value::ComplexTensor(t) => {
478            // Element-wise logical array from complex tensor (non-zero magnitude)
479            let data: Vec<u8> = t
480                .data
481                .iter()
482                .map(|(re, im)| if *re != 0.0 || *im != 0.0 { 1 } else { 0 })
483                .collect();
484            Ok(Value::LogicalArray(
485                runmat_builtins::LogicalArray::new(data, t.shape)
486                    .map_err(|e| format!("logical: {e}"))?,
487            ))
488        }
489        Value::Cell(_)
490        | Value::Struct(_)
491        | Value::Object(_)
492        | Value::GpuTensor(_)
493        | Value::FunctionHandle(_)
494        | Value::Closure(_)
495        | Value::ClassRef(_)
496        | Value::MException(_)
497        | Value::String(_)
498        | Value::HandleObject(_)
499        | Value::Listener(_) => Err("logical: unsupported conversion".to_string()),
500    }
501}
502
503// -------- String functions --------
504
505fn to_string_scalar(v: &Value) -> Result<String, String> {
506    let s: String = v.try_into()?;
507    Ok(s)
508}
509
510fn map_string_array<F>(sa: &runmat_builtins::StringArray, mut f: F) -> runmat_builtins::Tensor
511where
512    F: FnMut(&str) -> f64,
513{
514    let mut out: Vec<f64> = Vec::with_capacity(sa.data.len());
515    for s in &sa.data {
516        out.push(f(s));
517    }
518    runmat_builtins::Tensor::new(out, sa.shape.clone()).unwrap()
519}
520
521#[runmat_macros::runtime_builtin(name = "strcmp")]
522fn strcmp_builtin(a: Value, b: Value) -> Result<Value, String> {
523    match (a, b) {
524        (Value::StringArray(sa), Value::StringArray(sb)) => {
525            if sa.shape != sb.shape {
526                return Err("strcmp: shape mismatch".to_string());
527            }
528            let data: Vec<f64> = sa
529                .data
530                .iter()
531                .zip(sb.data.iter())
532                .map(|(x, y)| if x == y { 1.0 } else { 0.0 })
533                .collect();
534            Ok(Value::Tensor(
535                runmat_builtins::Tensor::new(data, sa.shape).map_err(|e| format!("strcmp: {e}"))?,
536            ))
537        }
538        (Value::StringArray(sa), other) => {
539            let s = to_string_scalar(&other)?;
540            let t = map_string_array(&sa, |x| if x == s { 1.0 } else { 0.0 });
541            Ok(Value::Tensor(t))
542        }
543        (other, Value::StringArray(sb)) => {
544            let s = to_string_scalar(&other)?;
545            let t = map_string_array(&sb, |x| if x == s { 1.0 } else { 0.0 });
546            Ok(Value::Tensor(t))
547        }
548        (av, bv) => {
549            let as_ = to_string_scalar(&av)?;
550            let bs_ = to_string_scalar(&bv)?;
551            Ok(Value::Num(if as_ == bs_ { 1.0 } else { 0.0 }))
552        }
553    }
554}
555
556#[runmat_macros::runtime_builtin(name = "strncmp")]
557fn strncmp_builtin(a: Value, b: Value, n: f64) -> Result<Value, String> {
558    let n = if n < 0.0 { 0usize } else { n as usize };
559    let cmp = |x: &str, y: &str| -> f64 {
560        let xs = &x.chars().take(n).collect::<String>();
561        let ys = &y.chars().take(n).collect::<String>();
562        if xs == ys {
563            1.0
564        } else {
565            0.0
566        }
567    };
568    match (a, b) {
569        (Value::StringArray(sa), Value::StringArray(sb)) => {
570            if sa.shape != sb.shape {
571                return Err("strncmp: shape mismatch".to_string());
572            }
573            let data: Vec<f64> = sa
574                .data
575                .iter()
576                .zip(sb.data.iter())
577                .map(|(x, y)| cmp(x, y))
578                .collect();
579            Ok(Value::Tensor(
580                runmat_builtins::Tensor::new(data, sa.shape)
581                    .map_err(|e| format!("strncmp: {e}"))?,
582            ))
583        }
584        (Value::StringArray(sa), other) => {
585            let s = to_string_scalar(&other)?;
586            let t = map_string_array(&sa, |x| cmp(x, &s));
587            Ok(Value::Tensor(t))
588        }
589        (other, Value::StringArray(sb)) => {
590            let s = to_string_scalar(&other)?;
591            let t = map_string_array(&sb, |x| cmp(&s, x));
592            Ok(Value::Tensor(t))
593        }
594        (av, bv) => {
595            let as_ = to_string_scalar(&av)?;
596            let bs_ = to_string_scalar(&bv)?;
597            Ok(Value::Num(cmp(&as_, &bs_)))
598        }
599    }
600}
601
602#[runmat_macros::runtime_builtin(name = "contains")]
603fn contains_builtin(a: Value, pat: Value) -> Result<Value, String> {
604    let p = to_string_scalar(&pat)?;
605    match a {
606        Value::String(s) => Ok(Value::Num(if s.contains(&p) { 1.0 } else { 0.0 })),
607        Value::StringArray(sa) => {
608            let data: Vec<f64> = sa
609                .data
610                .iter()
611                .map(|x| if x.contains(&p) { 1.0 } else { 0.0 })
612                .collect();
613            Ok(Value::Tensor(
614                runmat_builtins::Tensor::new(data, sa.shape)
615                    .map_err(|e| format!("contains: {e}"))?,
616            ))
617        }
618        Value::CharArray(ca) => {
619            let s: String = ca.data.iter().collect();
620            Ok(Value::Num(if s.contains(&p) { 1.0 } else { 0.0 }))
621        }
622        other => Err(format!("contains: unsupported input {other:?}")),
623    }
624}
625
626#[runmat_macros::runtime_builtin(name = "strrep")]
627fn strrep_builtin(a: Value, old: Value, newv: Value) -> Result<Value, String> {
628    let old_s = to_string_scalar(&old)?;
629    let new_s = to_string_scalar(&newv)?;
630    match a {
631        Value::String(s) => Ok(Value::String(s.replace(&old_s, &new_s))),
632        Value::StringArray(sa) => {
633            let data: Vec<String> = sa.data.iter().map(|x| x.replace(&old_s, &new_s)).collect();
634            Ok(Value::StringArray(
635                runmat_builtins::StringArray::new(data, sa.shape)
636                    .map_err(|e| format!("strrep: {e}"))?,
637            ))
638        }
639        Value::CharArray(ca) => {
640            let s: String = ca.data.iter().collect();
641            Ok(Value::String(s.replace(&old_s, &new_s)))
642        }
643        other => Err(format!("strrep: unsupported input {other:?}")),
644    }
645}
646
647fn to_string_array(v: &Value) -> Result<runmat_builtins::StringArray, String> {
648    match v {
649        Value::String(s) => runmat_builtins::StringArray::new(vec![s.clone()], vec![1, 1])
650            .map_err(|e| e.to_string()),
651        Value::StringArray(sa) => Ok(sa.clone()),
652        Value::CharArray(ca) => {
653            // Convert each row to a string; treat as column vector
654            let mut out: Vec<String> = Vec::with_capacity(ca.rows);
655            for r in 0..ca.rows {
656                let mut s = String::with_capacity(ca.cols);
657                for c in 0..ca.cols {
658                    s.push(ca.data[r * ca.cols + c]);
659                }
660                out.push(s);
661            }
662            runmat_builtins::StringArray::new(out, vec![ca.rows, 1]).map_err(|e| e.to_string())
663        }
664        other => Err(format!("cannot convert to string array: {other:?}")),
665    }
666}
667
668#[runmat_macros::runtime_builtin(name = "strcat")]
669fn strcat_builtin(rest: Vec<Value>) -> Result<Value, String> {
670    if rest.is_empty() {
671        return Ok(Value::String(String::new()));
672    }
673    // Normalize all inputs to StringArray; allow scalars (1x1) to broadcast to the max shape if equal
674    let mut arrays: Vec<runmat_builtins::StringArray> = Vec::new();
675    for v in &rest {
676        arrays.push(to_string_array(v)?);
677    }
678    // Determine result shape by choosing the first non-1x1 shape; require all non-1x1 equal
679    let mut shape: Vec<usize> = vec![1, 1];
680    for a in &arrays {
681        if a.shape != vec![1, 1] {
682            if shape == vec![1, 1] {
683                shape = a.shape.clone();
684            } else if shape != a.shape {
685                return Err("strcat: shape mismatch".to_string());
686            }
687        }
688    }
689    let total = shape.iter().product::<usize>().max(1);
690    let mut out: Vec<String> = vec![String::new(); total];
691    for a in &arrays {
692        if a.shape == vec![1, 1] {
693            let s = &a.data[0];
694            for item in out.iter_mut().take(total) {
695                item.push_str(s);
696            }
697        } else {
698            for (i, item) in out.iter_mut().enumerate().take(total) {
699                item.push_str(&a.data[i]);
700            }
701        }
702    }
703    Ok(Value::StringArray(
704        runmat_builtins::StringArray::new(out, shape).map_err(|e| format!("strcat: {e}"))?,
705    ))
706}
707
708// removed duplicate strjoin (keep row-wise version below)
709
710#[runmat_macros::runtime_builtin(name = "join")]
711fn join_builtin(a: Value, delim: Value) -> Result<Value, String> {
712    strjoin_rowwise(a, delim)
713}
714
715#[runmat_macros::runtime_builtin(name = "split")]
716fn split_builtin(a: Value, delim: Value) -> Result<Value, String> {
717    let s = to_string_scalar(&a)?;
718    let d = to_string_scalar(&delim)?;
719    if d.is_empty() {
720        return Err("split: empty delimiter not supported".to_string());
721    }
722    let parts: Vec<String> = s.split(&d).map(|t| t.to_string()).collect();
723    let len = parts.len();
724    Ok(Value::StringArray(
725        runmat_builtins::StringArray::new(parts, vec![len, 1])
726            .map_err(|e| format!("split: {e}"))?,
727    ))
728}
729
730#[runmat_macros::runtime_builtin(name = "upper")]
731fn upper_builtin(a: Value) -> Result<Value, String> {
732    match a {
733        Value::String(s) => Ok(Value::String(s.to_uppercase())),
734        Value::StringArray(sa) => {
735            let data: Vec<String> = sa.data.iter().map(|x| x.to_uppercase()).collect();
736            Ok(Value::StringArray(
737                runmat_builtins::StringArray::new(data, sa.shape)
738                    .map_err(|e| format!("upper: {e}"))?,
739            ))
740        }
741        Value::CharArray(ca) => {
742            let s: String = ca.data.iter().collect();
743            Ok(Value::String(s.to_uppercase()))
744        }
745        other => Err(format!("upper: unsupported input {other:?}")),
746    }
747}
748
749#[runmat_macros::runtime_builtin(name = "lower")]
750fn lower_builtin(a: Value) -> Result<Value, String> {
751    match a {
752        Value::String(s) => Ok(Value::String(s.to_lowercase())),
753        Value::StringArray(sa) => {
754            let data: Vec<String> = sa.data.iter().map(|x| x.to_lowercase()).collect();
755            Ok(Value::StringArray(
756                runmat_builtins::StringArray::new(data, sa.shape)
757                    .map_err(|e| format!("lower: {e}"))?,
758            ))
759        }
760        Value::CharArray(ca) => {
761            let s: String = ca.data.iter().collect();
762            Ok(Value::String(s.to_lowercase()))
763        }
764        other => Err(format!("lower: unsupported input {other:?}")),
765    }
766}
767
768#[runmat_macros::runtime_builtin(name = "startsWith")]
769fn starts_with_builtin(a: Value, prefix: Value) -> Result<Value, String> {
770    let p = to_string_scalar(&prefix)?;
771    match a {
772        Value::String(s) => Ok(Value::Num(if s.starts_with(&p) { 1.0 } else { 0.0 })),
773        Value::StringArray(sa) => {
774            let data: Vec<f64> = sa
775                .data
776                .iter()
777                .map(|x| if x.starts_with(&p) { 1.0 } else { 0.0 })
778                .collect();
779            Ok(Value::Tensor(
780                runmat_builtins::Tensor::new(data, sa.shape)
781                    .map_err(|e| format!("startsWith: {e}"))?,
782            ))
783        }
784        Value::CharArray(ca) => {
785            let s: String = ca.data.iter().collect();
786            Ok(Value::Num(if s.starts_with(&p) { 1.0 } else { 0.0 }))
787        }
788        other => Err(format!("startsWith: unsupported input {other:?}")),
789    }
790}
791
792#[runmat_macros::runtime_builtin(name = "endsWith")]
793fn ends_with_builtin(a: Value, suffix: Value) -> Result<Value, String> {
794    let p = to_string_scalar(&suffix)?;
795    match a {
796        Value::String(s) => Ok(Value::Num(if s.ends_with(&p) { 1.0 } else { 0.0 })),
797        Value::StringArray(sa) => {
798            let data: Vec<f64> = sa
799                .data
800                .iter()
801                .map(|x| if x.ends_with(&p) { 1.0 } else { 0.0 })
802                .collect();
803            Ok(Value::Tensor(
804                runmat_builtins::Tensor::new(data, sa.shape)
805                    .map_err(|e| format!("endsWith: {e}"))?,
806            ))
807        }
808        Value::CharArray(ca) => {
809            let s: String = ca.data.iter().collect();
810            Ok(Value::Num(if s.ends_with(&p) { 1.0 } else { 0.0 }))
811        }
812        other => Err(format!("endsWith: unsupported input {other:?}")),
813    }
814}
815
816#[runmat_macros::runtime_builtin(name = "extractBetween")]
817fn extract_between_builtin(a: Value, start: Value, stop: Value) -> Result<Value, String> {
818    let s = to_string_scalar(&a)?;
819    let st = to_string_scalar(&start)?;
820    let en = to_string_scalar(&stop)?;
821    if st.is_empty() || en.is_empty() {
822        return Ok(Value::String(String::new()));
823    }
824    if let Some(i) = s.find(&st) {
825        if let Some(j) = s[i + st.len()..].find(&en) {
826            return Ok(Value::String(s[i + st.len()..i + st.len() + j].to_string()));
827        }
828    }
829    Ok(Value::String(String::new()))
830}
831
832#[runmat_macros::runtime_builtin(name = "erase")]
833fn erase_builtin(a: Value, pat: Value) -> Result<Value, String> {
834    let s = to_string_scalar(&a)?;
835    let p = to_string_scalar(&pat)?;
836    Ok(Value::String(s.replace(&p, "")))
837}
838
839#[runmat_macros::runtime_builtin(name = "eraseBetween")]
840fn erase_between_builtin(a: Value, start: Value, stop: Value) -> Result<Value, String> {
841    let s = to_string_scalar(&a)?;
842    let st = to_string_scalar(&start)?;
843    let en = to_string_scalar(&stop)?;
844    if st.is_empty() || en.is_empty() {
845        return Ok(Value::String(s));
846    }
847    if let Some(i) = s.find(&st) {
848        if let Some(j) = s[i + st.len()..].find(&en) {
849            let mut out = String::new();
850            out.push_str(&s[..i + st.len()]);
851            out.push_str(&s[i + st.len() + j..]);
852            return Ok(Value::String(out));
853        }
854    }
855    Ok(Value::String(s))
856}
857
858#[runmat_macros::runtime_builtin(name = "pad")]
859fn pad_builtin(a: Value, total_len: f64, rest: Vec<Value>) -> Result<Value, String> {
860    // pad(s, n, 'left'|'right', char)
861    let s = to_string_scalar(&a)?;
862    let n = if total_len < 0.0 {
863        0usize
864    } else {
865        total_len as usize
866    };
867    let mut direction = "left".to_string();
868    let mut ch = ' ';
869    if !rest.is_empty() {
870        direction = to_string_scalar(&rest[0])?;
871    }
872    if rest.len() > 1 {
873        let t = to_string_scalar(&rest[1])?;
874        ch = t.chars().next().unwrap_or(' ');
875    }
876    if s.chars().count() >= n {
877        return Ok(Value::String(s));
878    }
879    let pad_count = n - s.chars().count();
880    let pad_str: String = std::iter::repeat_n(ch, pad_count).collect();
881    if direction == "left" {
882        Ok(Value::String(format!("{pad_str}{s}")))
883    } else {
884        Ok(Value::String(format!("{s}{pad_str}")))
885    }
886}
887
888#[runmat_macros::runtime_builtin(name = "strtrim")]
889fn strtrim_builtin(a: Value) -> Result<Value, String> {
890    let s = to_string_scalar(&a)?;
891    Ok(Value::String(s.trim().to_string()))
892}
893
894#[runmat_macros::runtime_builtin(name = "strip")]
895fn strip_builtin(a: Value) -> Result<Value, String> {
896    strtrim_builtin(a)
897}
898
899#[runmat_macros::runtime_builtin(name = "regexp")]
900fn regexp_builtin(a: Value, pat: Value) -> Result<Value, String> {
901    let s = to_string_scalar(&a)?;
902    let p = to_string_scalar(&pat)?;
903    let re = Regex::new(&p).map_err(|e| format!("regexp: {e}"))?;
904    let mut matches: Vec<Value> = Vec::new();
905    for cap in re.captures_iter(&s) {
906        // tokens: full match and capture groups
907        let full = cap
908            .get(0)
909            .map(|m| m.as_str().to_string())
910            .unwrap_or_default();
911        let mut row: Vec<Value> = vec![Value::String(full)];
912        for i in 1..cap.len() {
913            row.push(Value::String(
914                cap.get(i)
915                    .map(|m| m.as_str().to_string())
916                    .unwrap_or_default(),
917            ));
918        }
919        matches.push(make_cell(row, 1, cap.len())?);
920    }
921    let len = matches.len();
922    make_cell(matches, 1, len)
923}
924
925#[runmat_macros::runtime_builtin(name = "regexpi")]
926fn regexpi_builtin(a: Value, pat: Value) -> Result<Value, String> {
927    let s = to_string_scalar(&a)?;
928    let p = to_string_scalar(&pat)?;
929    let re = Regex::new(&format!("(?i){p}")).map_err(|e| format!("regexpi: {e}"))?;
930    let mut matches: Vec<Value> = Vec::new();
931    for cap in re.captures_iter(&s) {
932        let full = cap
933            .get(0)
934            .map(|m| m.as_str().to_string())
935            .unwrap_or_default();
936        let mut row: Vec<Value> = vec![Value::String(full)];
937        for i in 1..cap.len() {
938            row.push(Value::String(
939                cap.get(i)
940                    .map(|m| m.as_str().to_string())
941                    .unwrap_or_default(),
942            ));
943        }
944        matches.push(make_cell(row, 1, cap.len())?);
945    }
946    let len = matches.len();
947    make_cell(matches, 1, len)
948}
949
950// Adjust strjoin semantics: join rows (row-wise)
951#[runmat_macros::runtime_builtin(name = "strjoin")]
952fn strjoin_rowwise(a: Value, delim: Value) -> Result<Value, String> {
953    let d = to_string_scalar(&delim)?;
954    let sa = to_string_array(&a)?;
955    let rows = *sa.shape.first().unwrap_or(&sa.data.len());
956    let cols = *sa.shape.get(1).unwrap_or(&1);
957    if rows == 0 || cols == 0 {
958        return Ok(Value::StringArray(
959            runmat_builtins::StringArray::new(Vec::new(), vec![0, 0]).unwrap(),
960        ));
961    }
962    let mut out: Vec<String> = Vec::with_capacity(rows);
963    for r in 0..rows {
964        let mut s = String::new();
965        for c in 0..cols {
966            if c > 0 {
967                s.push_str(&d);
968            }
969            s.push_str(&sa.data[r + c * rows]);
970        }
971        out.push(s);
972    }
973    Ok(Value::StringArray(
974        runmat_builtins::StringArray::new(out, vec![rows, 1])
975            .map_err(|e| format!("strjoin: {e}"))?,
976    ))
977}
978
979// deal: distribute inputs to multiple outputs (via cell for expansion)
980#[runmat_macros::runtime_builtin(name = "deal")]
981fn deal_builtin(rest: Vec<Value>) -> Result<Value, String> {
982    // Return cell row vector of inputs for expansion
983    let cols = rest.len();
984    make_cell(rest, 1, cols)
985}
986
987fn do_find_all_indices(t: &runmat_builtins::Tensor) -> Result<Value, String> {
988    let mut idxs: Vec<f64> = Vec::new();
989    for (i, &v) in t.data.iter().enumerate() {
990        if v != 0.0 {
991            idxs.push((i + 1) as f64);
992        }
993    }
994    let len = idxs.len();
995    Ok(Value::Tensor(
996        runmat_builtins::Tensor::new(idxs, vec![len, 1]).map_err(|e| format!("find: {e}"))?,
997    ))
998}
999
1000fn do_find_first_k_indices(t: &runmat_builtins::Tensor, k: usize) -> Result<Value, String> {
1001    let mut idxs: Vec<f64> = Vec::new();
1002    for (i, &v) in t.data.iter().enumerate() {
1003        if v != 0.0 {
1004            idxs.push((i + 1) as f64);
1005            if idxs.len() >= k {
1006                break;
1007            }
1008        }
1009    }
1010    let len = idxs.len();
1011    Ok(Value::Tensor(
1012        runmat_builtins::Tensor::new(idxs, vec![len, 1]).map_err(|e| format!("find: {e}"))?,
1013    ))
1014}
1015
1016#[runmat_macros::runtime_builtin(name = "find")]
1017fn find_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
1018    let t = match a {
1019        Value::Tensor(t) => t,
1020        _ => return Err("find: expected tensor".to_string()),
1021    };
1022    if rest.is_empty() {
1023        return do_find_all_indices(&t);
1024    }
1025    // Only support find(A, k) for now
1026    let k = match &rest[0] {
1027        Value::Num(n) => *n as usize,
1028        Value::Int(i) => i.to_i64() as usize,
1029        _ => 0,
1030    };
1031    if k == 0 {
1032        return do_find_all_indices(&t);
1033    }
1034    do_find_first_k_indices(&t, k)
1035}
1036// Object/handle utilities used by interpreter lowering for OOP/func handles
1037
1038#[runmat_macros::runtime_builtin(name = "getfield")]
1039fn getfield_builtin(base: Value, field: String) -> Result<Value, String> {
1040    match base {
1041        Value::MException(me) => match field.as_str() {
1042            "message" => Ok(Value::String(me.message)),
1043            "identifier" => Ok(Value::String(me.identifier)),
1044            _ => Err(format!("getfield: unknown field '{field}' on MException")),
1045        },
1046        Value::Object(obj) => {
1047            if let Some((p, _owner)) = runmat_builtins::lookup_property(&obj.class_name, &field) {
1048                if p.is_static {
1049                    return Err(format!(
1050                        "Property '{}' is static; use classref('{}').{}",
1051                        field, obj.class_name, field
1052                    ));
1053                }
1054                if p.get_access == runmat_builtins::Access::Private {
1055                    return Err(format!("Property '{field}' is private"));
1056                }
1057                if p.is_dependent {
1058                    // Try dynamic getter first
1059                    let getter = format!("get.{field}");
1060                    if let Ok(v) = crate::call_builtin(&getter, &[Value::Object(obj.clone())]) {
1061                        return Ok(v);
1062                    }
1063                    // Fallback to backing field '<field>_backing'
1064                    let backing = format!("{field}_backing");
1065                    if let Some(vb) = obj.properties.get(&backing) {
1066                        return Ok(vb.clone());
1067                    }
1068                }
1069            }
1070            if let Some(v) = obj.properties.get(&field) {
1071                Ok(v.clone())
1072            } else {
1073                Err(format!(
1074                    "Undefined property '{}' for class {}",
1075                    field, obj.class_name
1076                ))
1077            }
1078        }
1079        Value::Struct(st) => st
1080            .fields
1081            .get(&field)
1082            .cloned()
1083            .ok_or_else(|| format!("getfield: unknown field '{field}'")),
1084        other => Err(format!(
1085            "getfield unsupported on this value for field '{field}': {other:?}"
1086        )),
1087    }
1088}
1089
1090// Error handling builtins (basic compatibility)
1091#[runmat_macros::runtime_builtin(name = "error")]
1092fn error_builtin(rest: Vec<Value>) -> Result<Value, String> {
1093    // The MATLAB language is compatible: error(message) or error(identifier, message)
1094    // We surface a unified error string "IDENT: message" for VM to parse into MException
1095    if rest.is_empty() {
1096        return Err("MATLAB:error: missing message".to_string());
1097    }
1098    if rest.len() == 1 {
1099        let msg: String = (&rest[0]).try_into()?;
1100        return Err(format!("MATLAB:error: {msg}"));
1101    }
1102    let ident: String = (&rest[0]).try_into()?;
1103    let msg: String = (&rest[1]).try_into()?;
1104    let id = if ident.contains(":") {
1105        ident
1106    } else {
1107        ident.to_string()
1108    };
1109    Err(format!("{id}: {msg}"))
1110}
1111
1112#[runmat_macros::runtime_builtin(name = "rethrow")]
1113fn rethrow_builtin(e: Value) -> Result<Value, String> {
1114    match e {
1115        Value::MException(me) => Err(format!("{}: {}", me.identifier, me.message)),
1116        Value::String(s) => Err(s),
1117        other => Err(format!("MATLAB:error: {other:?}")),
1118    }
1119}
1120
1121// -------- Struct utilities --------
1122#[runmat_macros::runtime_builtin(name = "fieldnames")]
1123fn fieldnames_builtin(s: Value) -> Result<Value, String> {
1124    match s {
1125        Value::Struct(st) => {
1126            let mut names: Vec<String> = st.fields.keys().cloned().collect();
1127            names.sort();
1128            let len = names.len();
1129            let vals: Vec<Value> = names.into_iter().map(Value::String).collect();
1130            make_cell(vals, len, 1)
1131        }
1132        other => Err(format!("fieldnames: expected struct, got {other:?}")),
1133    }
1134}
1135
1136#[runmat_macros::runtime_builtin(name = "isfield")]
1137fn isfield_builtin(s: Value, name: Value) -> Result<Value, String> {
1138    match s {
1139        Value::Struct(st) => match name {
1140            Value::String(n) => Ok(Value::Num(if st.fields.contains_key(&n) {
1141                1.0
1142            } else {
1143                0.0
1144            })),
1145            Value::StringArray(sa) => {
1146                let rows = sa.rows();
1147                let cols = sa.cols();
1148                let mut out: Vec<f64> = vec![0.0; sa.data.len()];
1149                for c in 0..cols {
1150                    for r in 0..rows {
1151                        let idx = r + c * rows;
1152                        let n = &sa.data[idx];
1153                        out[idx] = if st.fields.contains_key(n) { 1.0 } else { 0.0 };
1154                    }
1155                }
1156                Ok(Value::Tensor(
1157                    runmat_builtins::Tensor::new(out, vec![rows, cols])
1158                        .map_err(|e| format!("isfield: {e}"))?,
1159                ))
1160            }
1161            Value::Cell(ca) => {
1162                let rows = ca.rows;
1163                let cols = ca.cols;
1164                let mut out: Vec<f64> = Vec::with_capacity(rows * cols);
1165                for c in 0..cols {
1166                    for r in 0..rows {
1167                        let idx = r * cols + c;
1168                        let n: String = (&*ca.data[idx]).try_into()?;
1169                        out.push(if st.fields.contains_key(&n) { 1.0 } else { 0.0 });
1170                    }
1171                }
1172                Ok(Value::Tensor(
1173                    runmat_builtins::Tensor::new(out, vec![rows, cols])
1174                        .map_err(|e| format!("isfield: {e}"))?,
1175                ))
1176            }
1177            other => {
1178                let n: String = (&other).try_into()?;
1179                Ok(Value::Num(if st.fields.contains_key(&n) {
1180                    1.0
1181                } else {
1182                    0.0
1183                }))
1184            }
1185        },
1186        non_struct => {
1187            // Support swapped argument order: isfield(names, struct)
1188            if let Value::Struct(st) = name {
1189                match non_struct {
1190                    Value::String(n) => Ok(Value::Num(if st.fields.contains_key(&n) {
1191                        1.0
1192                    } else {
1193                        0.0
1194                    })),
1195                    Value::StringArray(sa) => {
1196                        let rows = sa.rows();
1197                        let cols = sa.cols();
1198                        let mut out: Vec<f64> = vec![0.0; sa.data.len()];
1199                        for c in 0..cols {
1200                            for r in 0..rows {
1201                                let idx = r + c * rows;
1202                                let n = &sa.data[idx];
1203                                out[idx] = if st.fields.contains_key(n) { 1.0 } else { 0.0 };
1204                            }
1205                        }
1206                        Ok(Value::Tensor(
1207                            runmat_builtins::Tensor::new(out, vec![rows, cols])
1208                                .map_err(|e| format!("isfield: {e}"))?,
1209                        ))
1210                    }
1211                    Value::Cell(ca) => {
1212                        let rows = ca.rows;
1213                        let cols = ca.cols;
1214                        let mut out: Vec<f64> = Vec::with_capacity(rows * cols);
1215                        for c in 0..cols {
1216                            for r in 0..rows {
1217                                let idx = r * cols + c;
1218                                let n: String = (&*ca.data[idx]).try_into()?;
1219                                out.push(if st.fields.contains_key(&n) { 1.0 } else { 0.0 });
1220                            }
1221                        }
1222                        Ok(Value::Tensor(
1223                            runmat_builtins::Tensor::new(out, vec![rows, cols])
1224                                .map_err(|e| format!("isfield: {e}"))?,
1225                        ))
1226                    }
1227                    other => {
1228                        let n: String = (&other).try_into()?;
1229                        Ok(Value::Num(if st.fields.contains_key(&n) {
1230                            1.0
1231                        } else {
1232                            0.0
1233                        }))
1234                    }
1235                }
1236            } else {
1237                Err(format!("isfield: expected struct, got {non_struct:?}"))
1238            }
1239        }
1240    }
1241}
1242
1243#[runmat_macros::runtime_builtin(name = "rmfield")]
1244fn rmfield_builtin(s: Value, rest: Vec<Value>) -> Result<Value, String> {
1245    let mut names: Vec<String> = Vec::new();
1246    if rest.len() == 1 {
1247        match &rest[0] {
1248            Value::Cell(ca) => {
1249                for v in &ca.data {
1250                    names.push(String::try_from(&**v).map_err(|e| format!("rmfield: {e}"))?);
1251                }
1252            }
1253            other => {
1254                names.push(String::try_from(other).map_err(|e| format!("rmfield: {e}"))?);
1255            }
1256        }
1257    } else {
1258        for v in &rest {
1259            names.push(String::try_from(v).map_err(|e| format!("rmfield: {e}"))?);
1260        }
1261    }
1262    match s {
1263        Value::Struct(mut st) => {
1264            for n in names {
1265                st.fields.remove(&n);
1266            }
1267            Ok(Value::Struct(st))
1268        }
1269        other => Err(format!("rmfield: expected struct, got {other:?}")),
1270    }
1271}
1272
1273#[runmat_macros::runtime_builtin(name = "orderfields")]
1274fn orderfields_builtin(s: Value) -> Result<Value, String> {
1275    // With HashMap-backed structs, field order is not stored; ensure fieldnames() returns sorted order.
1276    // Return struct unchanged to preserve data.
1277    match s {
1278        Value::Struct(st) => Ok(Value::Struct(st)),
1279        other => Err(format!("orderfields: expected struct, got {other:?}")),
1280    }
1281}
1282
1283#[runmat_macros::runtime_builtin(name = "reshape")]
1284fn reshape_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
1285    // Accept 2 or 3 dims (for tests); implement MATLAB-style (column-major) reshape semantics
1286    let t = match a {
1287        Value::Tensor(t) => t,
1288        _ => return Err("reshape: expected tensor".to_string()),
1289    };
1290    let mut dims: Vec<usize> = Vec::new();
1291    for v in rest {
1292        let n: f64 = (&v).try_into()?;
1293        dims.push(n as usize);
1294    }
1295    if dims.len() < 2 || dims.len() > 3 {
1296        return Err("reshape: expected 2 or 3 dimension sizes".to_string());
1297    }
1298    let total: usize = dims.iter().product();
1299    if total != t.data.len() {
1300        return Err(format!(
1301            "reshape: element count mismatch {} vs {}",
1302            total,
1303            t.data.len()
1304        ));
1305    }
1306    // MATLAB uses column-major storage; reshape reinterprets without reordering
1307    let new_t =
1308        runmat_builtins::Tensor::new(t.data.clone(), dims).map_err(|e| format!("reshape: {e}"))?;
1309    Ok(Value::Tensor(new_t))
1310}
1311#[runmat_macros::runtime_builtin(name = "setfield")]
1312fn setfield_builtin(base: Value, field: String, rhs: Value) -> Result<Value, String> {
1313    match base {
1314        Value::Object(mut obj) => {
1315            if let Some((p, _owner)) = runmat_builtins::lookup_property(&obj.class_name, &field) {
1316                if p.is_static {
1317                    return Err(format!(
1318                        "Property '{}' is static; use classref('{}').{}",
1319                        field, obj.class_name, field
1320                    ));
1321                }
1322                if p.set_access == runmat_builtins::Access::Private {
1323                    return Err(format!("Property '{field}' is private"));
1324                }
1325                if p.is_dependent {
1326                    let setter = format!("set.{field}");
1327                    // Try class/user-defined setter first
1328                    if let Ok(v) =
1329                        crate::call_builtin(&setter, &[Value::Object(obj.clone()), rhs.clone()])
1330                    {
1331                        return Ok(v);
1332                    }
1333                    // Fallback: write to backing field '<field>_backing'
1334                    let backing = format!("{field}_backing");
1335                    obj.properties.insert(backing, rhs);
1336                    return Ok(Value::Object(obj));
1337                }
1338            }
1339            obj.properties.insert(field, rhs);
1340            Ok(Value::Object(obj))
1341        }
1342        Value::Struct(mut st) => {
1343            st.fields.insert(field, rhs);
1344            Ok(Value::Struct(st))
1345        }
1346        Value::HandleObject(_) | Value::Listener(_) => Err(format!(
1347            "setfield unsupported on this value for field '{field}': handle/listener"
1348        )),
1349        other => Err(format!(
1350            "setfield unsupported on this value for field '{field}': {other:?}"
1351        )),
1352    }
1353}
1354
1355#[runmat_macros::runtime_builtin(name = "call_method")]
1356fn call_method_builtin(base: Value, method: String, rest: Vec<Value>) -> Result<Value, String> {
1357    match base {
1358        Value::Object(obj) => {
1359            // Simple dynamic dispatch via builtin registry: method name may be qualified as Class.method
1360            let qualified = format!("{}.{}", obj.class_name, method);
1361            // Prepend receiver as first arg so methods can accept it
1362            let mut args = Vec::with_capacity(1 + rest.len());
1363            args.push(Value::Object(obj.clone()));
1364            args.extend(rest);
1365            if let Ok(v) = crate::call_builtin(&qualified, &args) {
1366                return Ok(v);
1367            }
1368            // Fallback to global method name
1369            crate::call_builtin(&method, &args)
1370        }
1371        Value::HandleObject(h) => {
1372            // Methods on handle classes dispatch to the underlying target's class namespace
1373            let target = unsafe { &*h.target.as_raw() };
1374            let class_name = match target {
1375                Value::Object(o) => o.class_name.clone(),
1376                Value::Struct(_) => h.class_name.clone(),
1377                _ => h.class_name.clone(),
1378            };
1379            let qualified = format!("{class_name}.{method}");
1380            let mut args = Vec::with_capacity(1 + rest.len());
1381            args.push(Value::HandleObject(h.clone()));
1382            args.extend(rest);
1383            if let Ok(v) = crate::call_builtin(&qualified, &args) {
1384                return Ok(v);
1385            }
1386            crate::call_builtin(&method, &args)
1387        }
1388        other => Err(format!(
1389            "call_method unsupported on {other:?} for method '{method}'"
1390        )),
1391    }
1392}
1393
1394// Global dispatch helpers for overloaded indexing (subsref/subsasgn) to support fallback resolution paths
1395#[runmat_macros::runtime_builtin(name = "subsasgn")]
1396fn subsasgn_dispatch(
1397    obj: Value,
1398    kind: String,
1399    payload: Value,
1400    rhs: Value,
1401) -> Result<Value, String> {
1402    match &obj {
1403        Value::Object(o) => {
1404            let qualified = format!("{}.subsasgn", o.class_name);
1405            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload, rhs])
1406        }
1407        Value::HandleObject(h) => {
1408            let target = unsafe { &*h.target.as_raw() };
1409            let class_name = match target {
1410                Value::Object(o) => o.class_name.clone(),
1411                _ => h.class_name.clone(),
1412            };
1413            let qualified = format!("{class_name}.subsasgn");
1414            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload, rhs])
1415        }
1416        other => Err(format!("subsasgn: receiver must be object, got {other:?}")),
1417    }
1418}
1419
1420#[runmat_macros::runtime_builtin(name = "subsref")]
1421fn subsref_dispatch(obj: Value, kind: String, payload: Value) -> Result<Value, String> {
1422    match &obj {
1423        Value::Object(o) => {
1424            let qualified = format!("{}.subsref", o.class_name);
1425            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload])
1426        }
1427        Value::HandleObject(h) => {
1428            let target = unsafe { &*h.target.as_raw() };
1429            let class_name = match target {
1430                Value::Object(o) => o.class_name.clone(),
1431                _ => h.class_name.clone(),
1432            };
1433            let qualified = format!("{class_name}.subsref");
1434            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload])
1435        }
1436        other => Err(format!("subsref: receiver must be object, got {other:?}")),
1437    }
1438}
1439
1440// -------- Handle classes & events --------
1441
1442#[runmat_macros::runtime_builtin(name = "new_handle_object")]
1443fn new_handle_object_builtin(class_name: String) -> Result<Value, String> {
1444    // Create an underlying object instance and wrap it in a handle
1445    let obj = new_object_builtin(class_name.clone())?;
1446    let gc = runmat_gc::gc_allocate(obj).map_err(|e| format!("gc: {e}"))?;
1447    Ok(Value::HandleObject(runmat_builtins::HandleRef {
1448        class_name,
1449        target: gc,
1450        valid: true,
1451    }))
1452}
1453
1454#[runmat_macros::runtime_builtin(name = "isvalid")]
1455fn isvalid_builtin(v: Value) -> Result<Value, String> {
1456    match v {
1457        Value::HandleObject(h) => Ok(Value::Bool(h.valid)),
1458        Value::Listener(l) => Ok(Value::Bool(l.valid && l.enabled)),
1459        _ => Ok(Value::Bool(false)),
1460    }
1461}
1462
1463#[runmat_macros::runtime_builtin(name = "delete")]
1464fn delete_builtin(v: Value) -> Result<Value, String> {
1465    match v {
1466        Value::HandleObject(mut h) => {
1467            h.valid = false;
1468            Ok(Value::HandleObject(h))
1469        }
1470        Value::Listener(mut l) => {
1471            l.valid = false;
1472            Ok(Value::Listener(l))
1473        }
1474        other => Err(format!("delete: unsupported value {other:?}")),
1475    }
1476}
1477
1478use std::sync::{Mutex, OnceLock};
1479
1480#[derive(Default)]
1481struct EventRegistry {
1482    next_id: u64,
1483    listeners: std::collections::HashMap<(usize, String), Vec<runmat_builtins::Listener>>,
1484}
1485
1486static EVENT_REGISTRY: OnceLock<Mutex<EventRegistry>> = OnceLock::new();
1487
1488fn events() -> &'static Mutex<EventRegistry> {
1489    EVENT_REGISTRY.get_or_init(|| Mutex::new(EventRegistry::default()))
1490}
1491
1492#[runmat_macros::runtime_builtin(name = "addlistener")]
1493fn addlistener_builtin(
1494    target: Value,
1495    event_name: String,
1496    callback: Value,
1497) -> Result<Value, String> {
1498    let key_ptr: usize = match &target {
1499        Value::HandleObject(h) => (unsafe { h.target.as_raw() }) as usize,
1500        Value::Object(o) => o as *const _ as usize,
1501        _ => return Err("addlistener: target must be handle or object".to_string()),
1502    };
1503    let mut reg = events().lock().unwrap();
1504    let id = {
1505        reg.next_id += 1;
1506        reg.next_id
1507    };
1508    let tgt_gc = match target {
1509        Value::HandleObject(h) => h.target,
1510        Value::Object(o) => {
1511            runmat_gc::gc_allocate(Value::Object(o)).map_err(|e| format!("gc: {e}"))?
1512        }
1513        _ => unreachable!(),
1514    };
1515    let cb_gc = runmat_gc::gc_allocate(callback).map_err(|e| format!("gc: {e}"))?;
1516    let listener = runmat_builtins::Listener {
1517        id,
1518        target: tgt_gc,
1519        event_name: event_name.clone(),
1520        callback: cb_gc,
1521        enabled: true,
1522        valid: true,
1523    };
1524    reg.listeners
1525        .entry((key_ptr, event_name))
1526        .or_default()
1527        .push(listener.clone());
1528    Ok(Value::Listener(listener))
1529}
1530
1531#[runmat_macros::runtime_builtin(name = "notify")]
1532fn notify_builtin(target: Value, event_name: String, rest: Vec<Value>) -> Result<Value, String> {
1533    let key_ptr: usize = match &target {
1534        Value::HandleObject(h) => (unsafe { h.target.as_raw() }) as usize,
1535        Value::Object(o) => o as *const _ as usize,
1536        _ => return Err("notify: target must be handle or object".to_string()),
1537    };
1538    let mut to_call: Vec<runmat_builtins::Listener> = Vec::new();
1539    {
1540        let reg = events().lock().unwrap();
1541        if let Some(list) = reg.listeners.get(&(key_ptr, event_name.clone())) {
1542            for l in list {
1543                if l.valid && l.enabled {
1544                    to_call.push(l.clone());
1545                }
1546            }
1547        }
1548    }
1549    for l in to_call {
1550        // Call callback via feval-like protocol
1551        let mut args = Vec::new();
1552        args.push(target.clone());
1553        args.extend(rest.iter().cloned());
1554        let cbv: Value = (*l.callback).clone();
1555        match &cbv {
1556            Value::String(s) if s.starts_with('@') => {
1557                let mut a = vec![Value::String(s.clone())];
1558                a.extend(args.into_iter());
1559                let _ = crate::call_builtin("feval", &a)?;
1560            }
1561            Value::FunctionHandle(name) => {
1562                let mut a = vec![Value::FunctionHandle(name.clone())];
1563                a.extend(args.into_iter());
1564                let _ = crate::call_builtin("feval", &a)?;
1565            }
1566            Value::Closure(_) => {
1567                let mut a = vec![cbv.clone()];
1568                a.extend(args.into_iter());
1569                let _ = crate::call_builtin("feval", &a)?;
1570            }
1571            _ => {}
1572        }
1573    }
1574    Ok(Value::Num(0.0))
1575}
1576
1577// Test-oriented dependent property handlers (global). If a class defines a Dependent
1578// property named 'p', the VM will try to call get.p / set.p. We provide generic
1579// implementations that read/write a conventional backing field 'p_backing'.
1580#[runmat_macros::runtime_builtin(name = "get.p")]
1581fn get_p_builtin(obj: Value) -> Result<Value, String> {
1582    match obj {
1583        Value::Object(o) => {
1584            if let Some(v) = o.properties.get("p_backing") {
1585                Ok(v.clone())
1586            } else {
1587                Ok(Value::Num(0.0))
1588            }
1589        }
1590        other => Err(format!("get.p requires object, got {other:?}")),
1591    }
1592}
1593
1594#[runmat_macros::runtime_builtin(name = "set.p")]
1595fn set_p_builtin(obj: Value, val: Value) -> Result<Value, String> {
1596    match obj {
1597        Value::Object(mut o) => {
1598            o.properties.insert("p_backing".to_string(), val);
1599            Ok(Value::Object(o))
1600        }
1601        other => Err(format!("set.p requires object, got {other:?}")),
1602    }
1603}
1604
1605#[runmat_macros::runtime_builtin(name = "make_handle")]
1606fn make_handle_builtin(name: String) -> Result<Value, String> {
1607    Ok(Value::String(format!("@{name}")))
1608}
1609
1610#[runmat_macros::runtime_builtin(name = "make_anon")]
1611fn make_anon_builtin(params: String, body: String) -> Result<Value, String> {
1612    Ok(Value::String(format!("@anon({params}) {body}")))
1613}
1614
1615#[runmat_macros::runtime_builtin(name = "new_object")]
1616fn new_object_builtin(class_name: String) -> Result<Value, String> {
1617    if let Some(def) = runmat_builtins::get_class(&class_name) {
1618        // Collect class hierarchy from root to leaf for default initialization
1619        let mut chain: Vec<runmat_builtins::ClassDef> = Vec::new();
1620        // Walk up to root
1621        let mut cursor: Option<String> = Some(def.name.clone());
1622        while let Some(name) = cursor {
1623            if let Some(cd) = runmat_builtins::get_class(&name) {
1624                chain.push(cd.clone());
1625                cursor = cd.parent.clone();
1626            } else {
1627                break;
1628            }
1629        }
1630        // Reverse to root-first
1631        chain.reverse();
1632        let mut obj = runmat_builtins::ObjectInstance::new(def.name.clone());
1633        // Apply defaults from root to leaf (leaf overrides effectively by later assignment)
1634        for cd in chain {
1635            for (k, p) in cd.properties.iter() {
1636                if !p.is_static {
1637                    if let Some(v) = &p.default_value {
1638                        obj.properties.insert(k.clone(), v.clone());
1639                    }
1640                }
1641            }
1642        }
1643        Ok(Value::Object(obj))
1644    } else {
1645        Ok(Value::Object(runmat_builtins::ObjectInstance::new(
1646            class_name,
1647        )))
1648    }
1649}
1650
1651// handle-object builtins removed for now
1652
1653#[runmat_macros::runtime_builtin(name = "classref")]
1654fn classref_builtin(class_name: String) -> Result<Value, String> {
1655    Ok(Value::ClassRef(class_name))
1656}
1657
1658#[runmat_macros::runtime_builtin(name = "__register_test_classes")]
1659fn register_test_classes_builtin() -> Result<Value, String> {
1660    use runmat_builtins::*;
1661    let mut props = std::collections::HashMap::new();
1662    props.insert(
1663        "x".to_string(),
1664        PropertyDef {
1665            name: "x".to_string(),
1666            is_static: false,
1667            is_dependent: false,
1668            get_access: Access::Public,
1669            set_access: Access::Public,
1670            default_value: Some(Value::Num(0.0)),
1671        },
1672    );
1673    props.insert(
1674        "y".to_string(),
1675        PropertyDef {
1676            name: "y".to_string(),
1677            is_static: false,
1678            is_dependent: false,
1679            get_access: Access::Public,
1680            set_access: Access::Public,
1681            default_value: Some(Value::Num(0.0)),
1682        },
1683    );
1684    props.insert(
1685        "staticValue".to_string(),
1686        PropertyDef {
1687            name: "staticValue".to_string(),
1688            is_static: true,
1689            is_dependent: false,
1690            get_access: Access::Public,
1691            set_access: Access::Public,
1692            default_value: Some(Value::Num(42.0)),
1693        },
1694    );
1695    props.insert(
1696        "secret".to_string(),
1697        PropertyDef {
1698            name: "secret".to_string(),
1699            is_static: false,
1700            is_dependent: false,
1701            get_access: Access::Private,
1702            set_access: Access::Private,
1703            default_value: Some(Value::Num(99.0)),
1704        },
1705    );
1706    let mut methods = std::collections::HashMap::new();
1707    methods.insert(
1708        "move".to_string(),
1709        MethodDef {
1710            name: "move".to_string(),
1711            is_static: false,
1712            access: Access::Public,
1713            function_name: "Point.move".to_string(),
1714        },
1715    );
1716    methods.insert(
1717        "origin".to_string(),
1718        MethodDef {
1719            name: "origin".to_string(),
1720            is_static: true,
1721            access: Access::Public,
1722            function_name: "Point.origin".to_string(),
1723        },
1724    );
1725    runmat_builtins::register_class(ClassDef {
1726        name: "Point".to_string(),
1727        parent: None,
1728        properties: props,
1729        methods,
1730    });
1731
1732    // Namespaced class example: pkg.PointNS with same shape as Point
1733    let mut ns_props = std::collections::HashMap::new();
1734    ns_props.insert(
1735        "x".to_string(),
1736        PropertyDef {
1737            name: "x".to_string(),
1738            is_static: false,
1739            is_dependent: false,
1740            get_access: Access::Public,
1741            set_access: Access::Public,
1742            default_value: Some(Value::Num(1.0)),
1743        },
1744    );
1745    ns_props.insert(
1746        "y".to_string(),
1747        PropertyDef {
1748            name: "y".to_string(),
1749            is_static: false,
1750            is_dependent: false,
1751            get_access: Access::Public,
1752            set_access: Access::Public,
1753            default_value: Some(Value::Num(2.0)),
1754        },
1755    );
1756    let ns_methods = std::collections::HashMap::new();
1757    runmat_builtins::register_class(ClassDef {
1758        name: "pkg.PointNS".to_string(),
1759        parent: None,
1760        properties: ns_props,
1761        methods: ns_methods,
1762    });
1763
1764    // Inheritance: Shape (base) and Circle (derived)
1765    let shape_props = std::collections::HashMap::new();
1766    let mut shape_methods = std::collections::HashMap::new();
1767    shape_methods.insert(
1768        "area".to_string(),
1769        MethodDef {
1770            name: "area".to_string(),
1771            is_static: false,
1772            access: Access::Public,
1773            function_name: "Shape.area".to_string(),
1774        },
1775    );
1776    runmat_builtins::register_class(ClassDef {
1777        name: "Shape".to_string(),
1778        parent: None,
1779        properties: shape_props,
1780        methods: shape_methods,
1781    });
1782
1783    let mut circle_props = std::collections::HashMap::new();
1784    circle_props.insert(
1785        "r".to_string(),
1786        PropertyDef {
1787            name: "r".to_string(),
1788            is_static: false,
1789            is_dependent: false,
1790            get_access: Access::Public,
1791            set_access: Access::Public,
1792            default_value: Some(Value::Num(0.0)),
1793        },
1794    );
1795    let mut circle_methods = std::collections::HashMap::new();
1796    circle_methods.insert(
1797        "area".to_string(),
1798        MethodDef {
1799            name: "area".to_string(),
1800            is_static: false,
1801            access: Access::Public,
1802            function_name: "Circle.area".to_string(),
1803        },
1804    );
1805    runmat_builtins::register_class(ClassDef {
1806        name: "Circle".to_string(),
1807        parent: Some("Shape".to_string()),
1808        properties: circle_props,
1809        methods: circle_methods,
1810    });
1811
1812    // Constructor demo class: Ctor with static constructor method Ctor
1813    let ctor_props = std::collections::HashMap::new();
1814    let mut ctor_methods = std::collections::HashMap::new();
1815    ctor_methods.insert(
1816        "Ctor".to_string(),
1817        MethodDef {
1818            name: "Ctor".to_string(),
1819            is_static: true,
1820            access: Access::Public,
1821            function_name: "Ctor.Ctor".to_string(),
1822        },
1823    );
1824    runmat_builtins::register_class(ClassDef {
1825        name: "Ctor".to_string(),
1826        parent: None,
1827        properties: ctor_props,
1828        methods: ctor_methods,
1829    });
1830
1831    // Overloaded indexing demo class: OverIdx with subsref/subsasgn
1832    let overidx_props = std::collections::HashMap::new();
1833    let mut overidx_methods = std::collections::HashMap::new();
1834    overidx_methods.insert(
1835        "subsref".to_string(),
1836        MethodDef {
1837            name: "subsref".to_string(),
1838            is_static: false,
1839            access: Access::Public,
1840            function_name: "OverIdx.subsref".to_string(),
1841        },
1842    );
1843    overidx_methods.insert(
1844        "subsasgn".to_string(),
1845        MethodDef {
1846            name: "subsasgn".to_string(),
1847            is_static: false,
1848            access: Access::Public,
1849            function_name: "OverIdx.subsasgn".to_string(),
1850        },
1851    );
1852    runmat_builtins::register_class(ClassDef {
1853        name: "OverIdx".to_string(),
1854        parent: None,
1855        properties: overidx_props,
1856        methods: overidx_methods,
1857    });
1858    Ok(Value::Num(1.0))
1859}
1860
1861#[cfg(feature = "test-classes")]
1862pub fn test_register_classes() {
1863    let _ = register_test_classes_builtin();
1864}
1865
1866// Example method implementation: Point.move(obj, dx, dy) -> updated obj
1867#[runmat_macros::runtime_builtin(name = "Point.move")]
1868fn point_move_method(obj: Value, dx: f64, dy: f64) -> Result<Value, String> {
1869    match obj {
1870        Value::Object(mut o) => {
1871            let mut x = 0.0;
1872            let mut y = 0.0;
1873            if let Some(Value::Num(v)) = o.properties.get("x") {
1874                x = *v;
1875            }
1876            if let Some(Value::Num(v)) = o.properties.get("y") {
1877                y = *v;
1878            }
1879            o.properties.insert("x".to_string(), Value::Num(x + dx));
1880            o.properties.insert("y".to_string(), Value::Num(y + dy));
1881            Ok(Value::Object(o))
1882        }
1883        other => Err(format!(
1884            "Point.move requires object receiver, got {other:?}"
1885        )),
1886    }
1887}
1888
1889#[runmat_macros::runtime_builtin(name = "Point.origin")]
1890fn point_origin_method() -> Result<Value, String> {
1891    let mut o = runmat_builtins::ObjectInstance::new("Point".to_string());
1892    o.properties.insert("x".to_string(), Value::Num(0.0));
1893    o.properties.insert("y".to_string(), Value::Num(0.0));
1894    Ok(Value::Object(o))
1895}
1896
1897#[runmat_macros::runtime_builtin(name = "Shape.area")]
1898fn shape_area_method(_obj: Value) -> Result<Value, String> {
1899    Ok(Value::Num(0.0))
1900}
1901
1902#[runmat_macros::runtime_builtin(name = "Circle.area")]
1903fn circle_area_method(obj: Value) -> Result<Value, String> {
1904    match obj {
1905        Value::Object(o) => {
1906            let r = if let Some(Value::Num(v)) = o.properties.get("r") {
1907                *v
1908            } else {
1909                0.0
1910            };
1911            Ok(Value::Num(std::f64::consts::PI * r * r))
1912        }
1913        other => Err(format!(
1914            "Circle.area requires object receiver, got {other:?}"
1915        )),
1916    }
1917}
1918
1919// --- Test-only helpers to validate constructors and subsref/subsasgn ---
1920#[runmat_macros::runtime_builtin(name = "Ctor.Ctor")]
1921fn ctor_ctor_method(x: f64) -> Result<Value, String> {
1922    // Construct object with property 'x' initialized
1923    let mut o = runmat_builtins::ObjectInstance::new("Ctor".to_string());
1924    o.properties.insert("x".to_string(), Value::Num(x));
1925    Ok(Value::Object(o))
1926}
1927
1928// --- Test-only package functions to exercise import precedence ---
1929#[runmat_macros::runtime_builtin(name = "PkgF.foo")]
1930fn pkgf_foo() -> Result<Value, String> {
1931    Ok(Value::Num(10.0))
1932}
1933
1934#[runmat_macros::runtime_builtin(name = "PkgG.foo")]
1935fn pkgg_foo() -> Result<Value, String> {
1936    Ok(Value::Num(20.0))
1937}
1938
1939#[runmat_macros::runtime_builtin(name = "OverIdx.subsref")]
1940fn overidx_subsref(obj: Value, kind: String, payload: Value) -> Result<Value, String> {
1941    // Simple sentinel implementation: return different values for '.' vs '()'
1942    match (obj, kind.as_str(), payload) {
1943        (Value::Object(_), "()", Value::Cell(_)) => Ok(Value::Num(99.0)),
1944        (Value::Object(o), "{}", Value::Cell(_)) => {
1945            if let Some(v) = o.properties.get("lastCell") {
1946                Ok(v.clone())
1947            } else {
1948                Ok(Value::Num(0.0))
1949            }
1950        }
1951        (Value::Object(o), ".", Value::String(field)) => {
1952            // If field exists, return it; otherwise sentinel 77
1953            if let Some(v) = o.properties.get(&field) {
1954                Ok(v.clone())
1955            } else {
1956                Ok(Value::Num(77.0))
1957            }
1958        }
1959        (Value::Object(o), ".", Value::CharArray(ca)) => {
1960            let field: String = ca.data.iter().collect();
1961            if let Some(v) = o.properties.get(&field) {
1962                Ok(v.clone())
1963            } else {
1964                Ok(Value::Num(77.0))
1965            }
1966        }
1967        _ => Err("subsref: unsupported payload".to_string()),
1968    }
1969}
1970
1971#[runmat_macros::runtime_builtin(name = "OverIdx.subsasgn")]
1972fn overidx_subsasgn(
1973    mut obj: Value,
1974    kind: String,
1975    payload: Value,
1976    rhs: Value,
1977) -> Result<Value, String> {
1978    match (&mut obj, kind.as_str(), payload) {
1979        (Value::Object(o), "()", Value::Cell(_)) => {
1980            // Store into 'last' property
1981            o.properties.insert("last".to_string(), rhs);
1982            Ok(Value::Object(o.clone()))
1983        }
1984        (Value::Object(o), "{}", Value::Cell(_)) => {
1985            o.properties.insert("lastCell".to_string(), rhs);
1986            Ok(Value::Object(o.clone()))
1987        }
1988        (Value::Object(o), ".", Value::String(field)) => {
1989            o.properties.insert(field, rhs);
1990            Ok(Value::Object(o.clone()))
1991        }
1992        (Value::Object(o), ".", Value::CharArray(ca)) => {
1993            let field: String = ca.data.iter().collect();
1994            o.properties.insert(field, rhs);
1995            Ok(Value::Object(o.clone()))
1996        }
1997        _ => Err("subsasgn: unsupported payload".to_string()),
1998    }
1999}
2000
2001// --- Operator overloading methods for OverIdx (test scaffolding) ---
2002#[runmat_macros::runtime_builtin(name = "OverIdx.plus")]
2003fn overidx_plus(obj: Value, rhs: Value) -> Result<Value, String> {
2004    let o = match obj {
2005        Value::Object(o) => o,
2006        _ => return Err("OverIdx.plus: receiver must be object".to_string()),
2007    };
2008    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2009        *v
2010    } else {
2011        0.0
2012    };
2013    let r: f64 = (&rhs).try_into()?;
2014    Ok(Value::Num(k + r))
2015}
2016
2017#[runmat_macros::runtime_builtin(name = "OverIdx.times")]
2018fn overidx_times(obj: Value, rhs: Value) -> Result<Value, String> {
2019    let o = match obj {
2020        Value::Object(o) => o,
2021        _ => return Err("OverIdx.times: receiver must be object".to_string()),
2022    };
2023    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2024        *v
2025    } else {
2026        0.0
2027    };
2028    let r: f64 = (&rhs).try_into()?;
2029    Ok(Value::Num(k * r))
2030}
2031
2032#[runmat_macros::runtime_builtin(name = "OverIdx.mtimes")]
2033fn overidx_mtimes(obj: Value, rhs: Value) -> Result<Value, String> {
2034    let o = match obj {
2035        Value::Object(o) => o,
2036        _ => return Err("OverIdx.mtimes: receiver must be object".to_string()),
2037    };
2038    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2039        *v
2040    } else {
2041        0.0
2042    };
2043    let r: f64 = (&rhs).try_into()?;
2044    Ok(Value::Num(k * r))
2045}
2046
2047#[runmat_macros::runtime_builtin(name = "OverIdx.lt")]
2048fn overidx_lt(obj: Value, rhs: Value) -> Result<Value, String> {
2049    let o = match obj {
2050        Value::Object(o) => o,
2051        _ => return Err("OverIdx.lt: receiver must be object".to_string()),
2052    };
2053    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2054        *v
2055    } else {
2056        0.0
2057    };
2058    let r: f64 = (&rhs).try_into()?;
2059    Ok(Value::Num(if k < r { 1.0 } else { 0.0 }))
2060}
2061
2062#[runmat_macros::runtime_builtin(name = "OverIdx.gt")]
2063fn overidx_gt(obj: Value, rhs: Value) -> Result<Value, String> {
2064    let o = match obj {
2065        Value::Object(o) => o,
2066        _ => return Err("OverIdx.gt: receiver must be object".to_string()),
2067    };
2068    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2069        *v
2070    } else {
2071        0.0
2072    };
2073    let r: f64 = (&rhs).try_into()?;
2074    Ok(Value::Num(if k > r { 1.0 } else { 0.0 }))
2075}
2076
2077#[runmat_macros::runtime_builtin(name = "OverIdx.eq")]
2078fn overidx_eq(obj: Value, rhs: Value) -> Result<Value, String> {
2079    let o = match obj {
2080        Value::Object(o) => o,
2081        _ => return Err("OverIdx.eq: receiver must be object".to_string()),
2082    };
2083    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2084        *v
2085    } else {
2086        0.0
2087    };
2088    let r: f64 = (&rhs).try_into()?;
2089    Ok(Value::Num(if (k - r).abs() < 1e-12 { 1.0 } else { 0.0 }))
2090}
2091
2092#[runmat_macros::runtime_builtin(name = "OverIdx.uplus")]
2093fn overidx_uplus(obj: Value) -> Result<Value, String> {
2094    // Identity
2095    Ok(obj)
2096}
2097
2098#[runmat_macros::runtime_builtin(name = "OverIdx.rdivide")]
2099fn overidx_rdivide(obj: Value, rhs: Value) -> Result<Value, String> {
2100    let o = match obj {
2101        Value::Object(o) => o,
2102        _ => return Err("OverIdx.rdivide: receiver must be object".to_string()),
2103    };
2104    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2105        *v
2106    } else {
2107        0.0
2108    };
2109    let r: f64 = (&rhs).try_into()?;
2110    Ok(Value::Num(k / r))
2111}
2112
2113#[runmat_macros::runtime_builtin(name = "OverIdx.ldivide")]
2114fn overidx_ldivide(obj: Value, rhs: Value) -> Result<Value, String> {
2115    let o = match obj {
2116        Value::Object(o) => o,
2117        _ => return Err("OverIdx.ldivide: receiver must be object".to_string()),
2118    };
2119    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2120        *v
2121    } else {
2122        0.0
2123    };
2124    let r: f64 = (&rhs).try_into()?;
2125    Ok(Value::Num(r / k))
2126}
2127
2128#[runmat_macros::runtime_builtin(name = "OverIdx.and")]
2129fn overidx_and(obj: Value, rhs: Value) -> Result<Value, String> {
2130    let o = match obj {
2131        Value::Object(o) => o,
2132        _ => return Err("OverIdx.and: receiver must be object".to_string()),
2133    };
2134    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2135        *v
2136    } else {
2137        0.0
2138    };
2139    let r: f64 = (&rhs).try_into()?;
2140    Ok(Value::Num(if (k != 0.0) && (r != 0.0) { 1.0 } else { 0.0 }))
2141}
2142
2143#[runmat_macros::runtime_builtin(name = "OverIdx.or")]
2144fn overidx_or(obj: Value, rhs: Value) -> Result<Value, String> {
2145    let o = match obj {
2146        Value::Object(o) => o,
2147        _ => return Err("OverIdx.or: receiver must be object".to_string()),
2148    };
2149    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2150        *v
2151    } else {
2152        0.0
2153    };
2154    let r: f64 = (&rhs).try_into()?;
2155    Ok(Value::Num(if (k != 0.0) || (r != 0.0) { 1.0 } else { 0.0 }))
2156}
2157
2158#[runmat_macros::runtime_builtin(name = "OverIdx.xor")]
2159fn overidx_xor(obj: Value, rhs: Value) -> Result<Value, String> {
2160    let o = match obj {
2161        Value::Object(o) => o,
2162        _ => return Err("OverIdx.xor: receiver must be object".to_string()),
2163    };
2164    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
2165        *v
2166    } else {
2167        0.0
2168    };
2169    let r: f64 = (&rhs).try_into()?;
2170    let a = k != 0.0;
2171    let b = r != 0.0;
2172    Ok(Value::Num(if a ^ b { 1.0 } else { 0.0 }))
2173}
2174
2175#[runmat_macros::runtime_builtin(name = "feval")]
2176fn feval_builtin(f: Value, rest: Vec<Value>) -> Result<Value, String> {
2177    match f {
2178        // Current handles are strings like "@sin"
2179        Value::String(s) => {
2180            if let Some(name) = s.strip_prefix('@') {
2181                crate::call_builtin(name, &rest)
2182            } else {
2183                Err(format!(
2184                    "feval: expected function handle string starting with '@', got {s}"
2185                ))
2186            }
2187        }
2188        Value::Closure(c) => {
2189            let mut args = c.captures.clone();
2190            args.extend(rest);
2191            crate::call_builtin(&c.function_name, &args)
2192        }
2193        // Future: support Value::Function variants
2194        other => Err(format!("feval: unsupported function value {other:?}")),
2195    }
2196}
2197
2198// Common mathematical functions that tests expect
2199
2200/// Transpose operation for Values
2201pub fn transpose(value: Value) -> Result<Value, String> {
2202    match value {
2203        Value::Tensor(ref m) => Ok(Value::Tensor(matrix_transpose(m))),
2204        // For complex scalars, transpose is conjugate transpose => scalar transpose is conjugate
2205        Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
2206        Value::ComplexTensor(ref ct) => {
2207            let mut data: Vec<(f64, f64)> = vec![(0.0, 0.0); ct.rows * ct.cols];
2208            // Conjugate transpose for complex matrices
2209            for i in 0..ct.rows {
2210                for j in 0..ct.cols {
2211                    let (re, im) = ct.data[i + j * ct.rows];
2212                    data[j + i * ct.cols] = (re, -im);
2213                }
2214            }
2215            Ok(Value::ComplexTensor(
2216                runmat_builtins::ComplexTensor::new_2d(data, ct.cols, ct.rows).unwrap(),
2217            ))
2218        }
2219        Value::Num(n) => Ok(Value::Num(n)), // Scalar transpose is identity
2220        _ => Err("transpose not supported for this type".to_string()),
2221    }
2222}
2223
2224// Explicit GPU usage builtins (scaffolding)
2225#[runmat_macros::runtime_builtin(name = "gpuArray")]
2226fn gpu_array_builtin(x: Value) -> Result<Value, String> {
2227    match x {
2228        Value::Tensor(t) => {
2229            // Placeholder: mark as GPU handle; device/buffer ids are dummies for now
2230            if let Some(p) = runmat_accelerate_api::provider() {
2231                let view = runmat_accelerate_api::HostTensorView {
2232                    data: &t.data,
2233                    shape: &t.shape,
2234                };
2235                let h = p
2236                    .upload(&view)
2237                    .map_err(|e| format!("gpuArray upload: {e}"))?;
2238                Ok(Value::GpuTensor(h))
2239            } else {
2240                Ok(Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
2241                    shape: t.shape.clone(),
2242                    device_id: 0,
2243                    buffer_id: 0,
2244                }))
2245            }
2246        }
2247        Value::Num(_n) => Ok(Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
2248            shape: vec![1, 1],
2249            device_id: 0,
2250            buffer_id: 0,
2251        })),
2252        other => Err(format!("gpuArray unsupported for {other:?}")),
2253    }
2254}
2255
2256#[runmat_macros::runtime_builtin(name = "gather")]
2257fn gather_builtin(x: Value) -> Result<Value, String> {
2258    match x {
2259        Value::GpuTensor(h) => {
2260            if let Some(p) = runmat_accelerate_api::provider() {
2261                let ht = p
2262                    .download(&h)
2263                    .map_err(|e| format!("gather download: {e}"))?;
2264                Ok(Value::Tensor(
2265                    runmat_builtins::Tensor::new(ht.data, ht.shape)
2266                        .map_err(|e| format!("gather build: {e}"))?,
2267                ))
2268            } else {
2269                let total: usize = h.shape.iter().product();
2270                Ok(Value::Tensor(
2271                    runmat_builtins::Tensor::new(vec![0.0; total], h.shape)
2272                        .map_err(|e| format!("gather: {e}"))?,
2273                ))
2274            }
2275        }
2276        v => Ok(v),
2277    }
2278}
2279
2280// consolidate scalar max into helper; keep one registered varargs form below
2281fn max_scalar(a: f64, b: f64) -> f64 {
2282    a.max(b)
2283}
2284
2285fn max_vector_builtin(a: Value) -> Result<Value, String> {
2286    match a {
2287        Value::Tensor(t) => {
2288            if t.shape.len() == 2 && t.shape[1] == 1 {
2289                let mut max_val = f64::NEG_INFINITY;
2290                let mut idx = 1usize;
2291                for (i, &v) in t.data.iter().enumerate() {
2292                    if v > max_val {
2293                        max_val = v;
2294                        idx = i + 1;
2295                    }
2296                }
2297                // Return a 2x1 column [max; idx] for expansion (value then index)
2298                let out = runmat_builtins::Tensor::new(vec![max_val, idx as f64], vec![2, 1])
2299                    .map_err(|e| format!("max: {e}"))?;
2300                Ok(Value::Tensor(out))
2301            } else {
2302                // Reduce across all elements -> scalar
2303                let max_val = t.data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
2304                Ok(Value::Num(max_val))
2305            }
2306        }
2307        Value::Num(n) => Ok(Value::Num(n)),
2308        _ => Err("max: unsupported input".to_string()),
2309    }
2310}
2311
2312fn min_vector_builtin(a: Value) -> Result<Value, String> {
2313    match a {
2314        Value::Tensor(t) => {
2315            if t.shape.len() == 2 && t.shape[1] == 1 {
2316                let mut min_val = f64::INFINITY;
2317                let mut idx = 1usize;
2318                for (i, &v) in t.data.iter().enumerate() {
2319                    if v < min_val {
2320                        min_val = v;
2321                        idx = i + 1;
2322                    }
2323                }
2324                let out = runmat_builtins::Tensor::new(vec![min_val, idx as f64], vec![2, 1])
2325                    .map_err(|e| format!("min: {e}"))?;
2326                Ok(Value::Tensor(out))
2327            } else {
2328                let min_val = t.data.iter().cloned().fold(f64::INFINITY, f64::min);
2329                Ok(Value::Num(min_val))
2330            }
2331        }
2332        Value::Num(n) => Ok(Value::Num(n)),
2333        _ => Err("min: unsupported input".to_string()),
2334    }
2335}
2336
2337fn max_dim_builtin(a: Value, dim: f64) -> Result<Value, String> {
2338    let t = match a {
2339        Value::Tensor(t) => t,
2340        _ => return Err("max: expected tensor for dim variant".to_string()),
2341    };
2342    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2343    if t.shape.len() < 2 {
2344        return Err("max: dim variant expects 2D tensor".to_string());
2345    }
2346    let rows = t.shape[0];
2347    let cols = t.shape[1];
2348    if dim == 1 {
2349        // column-wise maxima: return {M_row, I_row}
2350        let mut m: Vec<f64> = vec![f64::NEG_INFINITY; cols];
2351        let mut idx: Vec<f64> = vec![1.0; cols];
2352        for c in 0..cols {
2353            for r in 0..rows {
2354                let v = t.data[r + c * rows];
2355                if v > m[c] {
2356                    m[c] = v;
2357                    idx[c] = (r + 1) as f64;
2358                }
2359            }
2360        }
2361        let m_t =
2362            runmat_builtins::Tensor::new(m, vec![1, cols]).map_err(|e| format!("max: {e}"))?;
2363        let i_t =
2364            runmat_builtins::Tensor::new(idx, vec![1, cols]).map_err(|e| format!("max: {e}"))?;
2365        make_cell(vec![Value::Tensor(m_t), Value::Tensor(i_t)], 1, 2)
2366    } else if dim == 2 {
2367        // row-wise maxima: return {M_col, I_col}
2368        let mut m: Vec<f64> = vec![f64::NEG_INFINITY; rows];
2369        let mut idx: Vec<f64> = vec![1.0; rows];
2370        for r in 0..rows {
2371            for c in 0..cols {
2372                let v = t.data[r + c * rows];
2373                if v > m[r] {
2374                    m[r] = v;
2375                    idx[r] = (c + 1) as f64;
2376                }
2377            }
2378        }
2379        let m_t =
2380            runmat_builtins::Tensor::new(m, vec![rows, 1]).map_err(|e| format!("max: {e}"))?;
2381        let i_t =
2382            runmat_builtins::Tensor::new(idx, vec![rows, 1]).map_err(|e| format!("max: {e}"))?;
2383        make_cell(vec![Value::Tensor(m_t), Value::Tensor(i_t)], 1, 2)
2384    } else {
2385        Err("max: dim out of range".to_string())
2386    }
2387}
2388
2389fn min_dim_builtin(a: Value, dim: f64) -> Result<Value, String> {
2390    let t = match a {
2391        Value::Tensor(t) => t,
2392        _ => return Err("min: expected tensor for dim variant".to_string()),
2393    };
2394    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2395    if t.shape.len() < 2 {
2396        return Err("min: dim variant expects 2D tensor".to_string());
2397    }
2398    let rows = t.shape[0];
2399    let cols = t.shape[1];
2400    if dim == 1 {
2401        let mut m: Vec<f64> = vec![f64::INFINITY; cols];
2402        let mut idx: Vec<f64> = vec![1.0; cols];
2403        for c in 0..cols {
2404            for r in 0..rows {
2405                let v = t.data[r + c * rows];
2406                if v < m[c] {
2407                    m[c] = v;
2408                    idx[c] = (r + 1) as f64;
2409                }
2410            }
2411        }
2412        let m_t =
2413            runmat_builtins::Tensor::new(m, vec![1, cols]).map_err(|e| format!("min: {e}"))?;
2414        let i_t =
2415            runmat_builtins::Tensor::new(idx, vec![1, cols]).map_err(|e| format!("min: {e}"))?;
2416        make_cell(vec![Value::Tensor(m_t), Value::Tensor(i_t)], 1, 2)
2417    } else if dim == 2 {
2418        let mut m: Vec<f64> = vec![f64::INFINITY; rows];
2419        let mut idx: Vec<f64> = vec![1.0; rows];
2420        for r in 0..rows {
2421            for c in 0..cols {
2422                let v = t.data[r + c * rows];
2423                if v < m[r] {
2424                    m[r] = v;
2425                    idx[r] = (c + 1) as f64;
2426                }
2427            }
2428        }
2429        let m_t =
2430            runmat_builtins::Tensor::new(m, vec![rows, 1]).map_err(|e| format!("min: {e}"))?;
2431        let i_t =
2432            runmat_builtins::Tensor::new(idx, vec![rows, 1]).map_err(|e| format!("min: {e}"))?;
2433        make_cell(vec![Value::Tensor(m_t), Value::Tensor(i_t)], 1, 2)
2434    } else {
2435        Err("min: dim out of range".to_string())
2436    }
2437}
2438
2439fn min_scalar(a: f64, b: f64) -> f64 {
2440    a.min(b)
2441}
2442
2443#[runmat_macros::runtime_builtin(name = "max")]
2444fn max_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2445    if rest.is_empty() {
2446        return max_vector_builtin(a);
2447    }
2448    if rest.len() == 1 {
2449        let r0 = &rest[0];
2450        // Scalar pair max(a,b)
2451        if let (Value::Num(a0), Value::Num(b0)) = (a.clone(), r0.clone()) {
2452            return Ok(Value::Num(max_scalar(a0, b0)));
2453        }
2454        // Optional dim variant max(A, dim)
2455        if matches!(r0, Value::Num(_) | Value::Int(_)) {
2456            return max_dim_builtin(
2457                a,
2458                match r0 {
2459                    Value::Num(d) => *d,
2460                    Value::Int(i) => i.to_i64() as f64,
2461                    _ => unreachable!(),
2462                },
2463            );
2464        }
2465    }
2466    Err("max: unsupported arguments".to_string())
2467}
2468
2469#[runmat_macros::runtime_builtin(name = "min")]
2470fn min_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2471    if rest.is_empty() {
2472        return min_vector_builtin(a);
2473    }
2474    if rest.len() == 1 {
2475        let r0 = &rest[0];
2476        // Scalar pair min(a,b)
2477        if let (Value::Num(a0), Value::Num(b0)) = (a.clone(), r0.clone()) {
2478            return Ok(Value::Num(min_scalar(a0, b0)));
2479        }
2480        match r0 {
2481            Value::Num(d) => return min_dim_builtin(a, *d),
2482            Value::Int(i) => return min_dim_builtin(a, i.to_i64() as f64),
2483            _ => {}
2484        }
2485    }
2486    Err("min: unsupported arguments".to_string())
2487}
2488
2489#[runtime_builtin(name = "sqrt")]
2490fn sqrt_builtin(x: f64) -> Result<f64, String> {
2491    if x < 0.0 {
2492        Err("MATLAB:domainError: Cannot take square root of negative number".to_string())
2493    } else {
2494        Ok(x.sqrt())
2495    }
2496}
2497
2498/// Simple timing functions for benchmarks
2499/// tic() starts a timer and returns current time
2500#[runtime_builtin(name = "tic")]
2501fn tic_builtin() -> Result<f64, String> {
2502    use std::time::{SystemTime, UNIX_EPOCH};
2503    let now = SystemTime::now()
2504        .duration_since(UNIX_EPOCH)
2505        .map_err(|e| format!("Time error: {e}"))?;
2506    Ok(now.as_secs_f64())
2507}
2508
2509/// toc() returns elapsed time since the last tic() call
2510/// Note: In a real implementation, this would use a saved start time,
2511/// but for simplicity we'll just return a small time value
2512#[runtime_builtin(name = "toc")]
2513fn toc_builtin() -> Result<f64, String> {
2514    // For benchmark purposes, return a realistic small time
2515    Ok(0.001) // 1 millisecond
2516}
2517
2518#[runtime_builtin(name = "exp")]
2519fn exp_builtin(x: f64) -> Result<f64, String> {
2520    Ok(x.exp())
2521}
2522
2523#[runtime_builtin(name = "log")]
2524fn log_builtin(x: f64) -> Result<f64, String> {
2525    if x <= 0.0 {
2526        Err("MATLAB:domainError: Cannot take logarithm of non-positive number".to_string())
2527    } else {
2528        Ok(x.ln())
2529    }
2530}
2531
2532// -------- Reductions: sum/prod/mean/any/all --------
2533
2534fn tensor_sum_all(t: &runmat_builtins::Tensor) -> f64 {
2535    t.data.iter().sum()
2536}
2537
2538fn tensor_prod_all(t: &runmat_builtins::Tensor) -> f64 {
2539    t.data.iter().product()
2540}
2541
2542fn sum_scalar_all(a: Value) -> Result<Value, String> {
2543    match a {
2544        Value::Tensor(t) => Ok(Value::Num(tensor_sum_all(&t))),
2545        _ => Err("sum: expected tensor".to_string()),
2546    }
2547}
2548
2549fn sum_dim(a: Value, dim: f64) -> Result<Value, String> {
2550    let t = match a {
2551        Value::Tensor(t) => t,
2552        _ => return Err("sum: expected tensor".to_string()),
2553    };
2554    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2555    let rows = t.rows();
2556    let cols = t.cols();
2557    if dim == 1 {
2558        let mut out = vec![0.0f64; cols];
2559        for (c, oc) in out.iter_mut().enumerate().take(cols) {
2560            let mut s = 0.0;
2561            for r in 0..rows {
2562                s += t.data[r + c * rows];
2563            }
2564            *oc = s;
2565        }
2566        Ok(Value::Tensor(
2567            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("sum: {e}"))?,
2568        ))
2569    } else if dim == 2 {
2570        let mut out = vec![0.0f64; rows];
2571        for (r, orow) in out.iter_mut().enumerate().take(rows) {
2572            let mut s = 0.0;
2573            for c in 0..cols {
2574                s += t.data[r + c * rows];
2575            }
2576            *orow = s;
2577        }
2578        Ok(Value::Tensor(
2579            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("sum: {e}"))?,
2580        ))
2581    } else {
2582        Err("sum: dim out of range".to_string())
2583    }
2584}
2585
2586#[runmat_macros::runtime_builtin(name = "sum")]
2587fn sum_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2588    if rest.is_empty() {
2589        return sum_scalar_all(a);
2590    }
2591    if rest.len() == 1 {
2592        match &rest[0] {
2593            Value::Num(d) => return sum_dim(a, *d),
2594            Value::Int(i) => return sum_dim(a, i.to_i64() as f64),
2595            _ => {}
2596        }
2597    }
2598    Err("sum: unsupported arguments".to_string())
2599}
2600
2601fn prod_all_or_cols(a: Value) -> Result<Value, String> {
2602    match a {
2603        Value::Tensor(t) => {
2604            let rows = t.rows();
2605            let cols = t.cols();
2606            if rows > 1 && cols > 1 {
2607                let mut out = vec![1.0f64; cols];
2608                for (c, oc) in out.iter_mut().enumerate().take(cols) {
2609                    let mut p = 1.0;
2610                    for r in 0..rows {
2611                        p *= t.data[r + c * rows];
2612                    }
2613                    *oc = p;
2614                }
2615                Ok(Value::Tensor(
2616                    runmat_builtins::Tensor::new(out, vec![1, cols])
2617                        .map_err(|e| format!("prod: {e}"))?,
2618                ))
2619            } else {
2620                Ok(Value::Num(tensor_prod_all(&t)))
2621            }
2622        }
2623        _ => Err("prod: expected tensor".to_string()),
2624    }
2625}
2626
2627fn prod_dim(a: Value, dim: f64) -> Result<Value, String> {
2628    let t = match a {
2629        Value::Tensor(t) => t,
2630        _ => return Err("prod: expected tensor".to_string()),
2631    };
2632    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2633    let rows = t.rows();
2634    let cols = t.cols();
2635    if dim == 1 {
2636        let mut out = vec![1.0f64; cols];
2637        for (c, oc) in out.iter_mut().enumerate().take(cols) {
2638            let mut p = 1.0;
2639            for r in 0..rows {
2640                p *= t.data[r + c * rows];
2641            }
2642            *oc = p;
2643        }
2644        Ok(Value::Tensor(
2645            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("prod: {e}"))?,
2646        ))
2647    } else if dim == 2 {
2648        let mut out = vec![1.0f64; rows];
2649        for (r, orow) in out.iter_mut().enumerate().take(rows) {
2650            let mut p = 1.0;
2651            for c in 0..cols {
2652                p *= t.data[r + c * rows];
2653            }
2654            *orow = p;
2655        }
2656        Ok(Value::Tensor(
2657            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("prod: {e}"))?,
2658        ))
2659    } else {
2660        Err("prod: dim out of range".to_string())
2661    }
2662}
2663
2664#[runmat_macros::runtime_builtin(name = "prod")]
2665fn prod_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2666    if rest.is_empty() {
2667        return prod_all_or_cols(a);
2668    }
2669    if rest.len() == 1 {
2670        match &rest[0] {
2671            Value::Num(d) => return prod_dim(a, *d),
2672            Value::Int(i) => return prod_dim(a, i.to_i64() as f64),
2673            _ => {}
2674        }
2675    }
2676    Err("prod: unsupported arguments".to_string())
2677}
2678
2679fn mean_all_or_cols(a: Value) -> Result<Value, String> {
2680    match a {
2681        Value::Tensor(t) => {
2682            let rows = t.rows();
2683            let cols = t.cols();
2684            if rows > 1 && cols > 1 {
2685                let mut out = vec![0.0f64; cols];
2686                for (c, oc) in out.iter_mut().enumerate().take(cols) {
2687                    let mut s = 0.0;
2688                    for r in 0..rows {
2689                        s += t.data[r + c * rows];
2690                    }
2691                    *oc = s / (rows as f64);
2692                }
2693                Ok(Value::Tensor(
2694                    runmat_builtins::Tensor::new(out, vec![1, cols])
2695                        .map_err(|e| format!("mean: {e}"))?,
2696                ))
2697            } else {
2698                Ok(Value::Num(tensor_sum_all(&t) / (t.data.len() as f64)))
2699            }
2700        }
2701        _ => Err("mean: expected tensor".to_string()),
2702    }
2703}
2704
2705fn mean_dim(a: Value, dim: f64) -> Result<Value, String> {
2706    let t = match a {
2707        Value::Tensor(t) => t,
2708        _ => return Err("mean: expected tensor".to_string()),
2709    };
2710    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2711    let rows = t.rows();
2712    let cols = t.cols();
2713    if dim == 1 {
2714        let mut out = vec![0.0f64; cols];
2715        for (c, oc) in out.iter_mut().enumerate().take(cols) {
2716            let mut s = 0.0;
2717            for r in 0..rows {
2718                s += t.data[r + c * rows];
2719            }
2720            *oc = s / (rows as f64);
2721        }
2722        Ok(Value::Tensor(
2723            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("mean: {e}"))?,
2724        ))
2725    } else if dim == 2 {
2726        let mut out = vec![0.0f64; rows];
2727        for (r, orow) in out.iter_mut().enumerate().take(rows) {
2728            let mut s = 0.0;
2729            for c in 0..cols {
2730                s += t.data[r + c * rows];
2731            }
2732            *orow = s / (cols as f64);
2733        }
2734        Ok(Value::Tensor(
2735            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("mean: {e}"))?,
2736        ))
2737    } else {
2738        Err("mean: dim out of range".to_string())
2739    }
2740}
2741
2742#[runmat_macros::runtime_builtin(name = "mean")]
2743fn mean_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2744    if rest.is_empty() {
2745        return mean_all_or_cols(a);
2746    }
2747    if rest.len() == 1 {
2748        match &rest[0] {
2749            Value::Num(d) => return mean_dim(a, *d),
2750            Value::Int(i) => return mean_dim(a, i.to_i64() as f64),
2751            _ => {}
2752        }
2753    }
2754    Err("mean: unsupported arguments".to_string())
2755}
2756
2757fn any_all_or_cols(a: Value) -> Result<Value, String> {
2758    match a {
2759        Value::Tensor(t) => {
2760            let rows = t.rows();
2761            let cols = t.cols();
2762            if rows > 1 && cols > 1 {
2763                let mut out = vec![0.0f64; cols];
2764                for (c, oc) in out.iter_mut().enumerate().take(cols) {
2765                    let mut v = 0.0;
2766                    for r in 0..rows {
2767                        if t.data[r + c * rows] != 0.0 {
2768                            v = 1.0;
2769                            break;
2770                        }
2771                    }
2772                    *oc = v;
2773                }
2774                Ok(Value::Tensor(
2775                    runmat_builtins::Tensor::new(out, vec![1, cols])
2776                        .map_err(|e| format!("any: {e}"))?,
2777                ))
2778            } else {
2779                Ok(Value::Num(if t.data.iter().any(|&x| x != 0.0) {
2780                    1.0
2781                } else {
2782                    0.0
2783                }))
2784            }
2785        }
2786        _ => Err("any: expected tensor".to_string()),
2787    }
2788}
2789
2790fn any_dim(a: Value, dim: f64) -> Result<Value, String> {
2791    let t = match a {
2792        Value::Tensor(t) => t,
2793        _ => return Err("any: expected tensor".to_string()),
2794    };
2795    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2796    let rows = t.rows();
2797    let cols = t.cols();
2798    if dim == 1 {
2799        let mut out = vec![0.0f64; cols];
2800        for (c, oc) in out.iter_mut().enumerate().take(cols) {
2801            let mut v = 0.0;
2802            for r in 0..rows {
2803                if t.data[r + c * rows] != 0.0 {
2804                    v = 1.0;
2805                    break;
2806                }
2807            }
2808            *oc = v;
2809        }
2810        Ok(Value::Tensor(
2811            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("any: {e}"))?,
2812        ))
2813    } else if dim == 2 {
2814        let mut out = vec![0.0f64; rows];
2815        for (r, orow) in out.iter_mut().enumerate().take(rows) {
2816            let mut v = 0.0;
2817            for c in 0..cols {
2818                if t.data[r + c * rows] != 0.0 {
2819                    v = 1.0;
2820                    break;
2821                }
2822            }
2823            *orow = v;
2824        }
2825        Ok(Value::Tensor(
2826            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("any: {e}"))?,
2827        ))
2828    } else {
2829        Err("any: dim out of range".to_string())
2830    }
2831}
2832
2833#[runmat_macros::runtime_builtin(name = "any")]
2834fn any_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2835    if rest.is_empty() {
2836        return any_all_or_cols(a);
2837    }
2838    if rest.len() == 1 {
2839        match &rest[0] {
2840            Value::Num(d) => return any_dim(a, *d),
2841            Value::Int(i) => return any_dim(a, i.to_i64() as f64),
2842            _ => {}
2843        }
2844    }
2845    Err("any: unsupported arguments".to_string())
2846}
2847
2848fn all_all_or_cols(a: Value) -> Result<Value, String> {
2849    match a {
2850        Value::Tensor(t) => {
2851            let rows = t.rows();
2852            let cols = t.cols();
2853            if rows > 1 && cols > 1 {
2854                let mut out = vec![0.0f64; cols];
2855                for (c, oc) in out.iter_mut().enumerate().take(cols) {
2856                    let mut v = 1.0;
2857                    for r in 0..rows {
2858                        if t.data[r + c * rows] == 0.0 {
2859                            v = 0.0;
2860                            break;
2861                        }
2862                    }
2863                    *oc = v;
2864                }
2865                Ok(Value::Tensor(
2866                    runmat_builtins::Tensor::new(out, vec![1, cols])
2867                        .map_err(|e| format!("all: {e}"))?,
2868                ))
2869            } else {
2870                Ok(Value::Num(if t.data.iter().all(|&x| x != 0.0) {
2871                    1.0
2872                } else {
2873                    0.0
2874                }))
2875            }
2876        }
2877        _ => Err("all: expected tensor".to_string()),
2878    }
2879}
2880
2881fn all_dim(a: Value, dim: f64) -> Result<Value, String> {
2882    let t = match a {
2883        Value::Tensor(t) => t,
2884        _ => return Err("all: expected tensor".to_string()),
2885    };
2886    let dim = if dim < 1.0 { 1usize } else { dim as usize };
2887    let rows = t.rows();
2888    let cols = t.cols();
2889    if dim == 1 {
2890        let mut out = vec![0.0f64; cols];
2891        for (c, oc) in out.iter_mut().enumerate().take(cols) {
2892            let mut v = 1.0;
2893            for r in 0..rows {
2894                if t.data[r + c * rows] == 0.0 {
2895                    v = 0.0;
2896                    break;
2897                }
2898            }
2899            *oc = v;
2900        }
2901        Ok(Value::Tensor(
2902            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("all: {e}"))?,
2903        ))
2904    } else if dim == 2 {
2905        let mut out = vec![0.0f64; rows];
2906        for (r, orow) in out.iter_mut().enumerate().take(rows) {
2907            let mut v = 1.0;
2908            for c in 0..cols {
2909                if t.data[r + c * rows] == 0.0 {
2910                    v = 0.0;
2911                    break;
2912                }
2913            }
2914            *orow = v;
2915        }
2916        Ok(Value::Tensor(
2917            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("all: {e}"))?,
2918        ))
2919    } else {
2920        Err("all: dim out of range".to_string())
2921    }
2922}
2923
2924#[runmat_macros::runtime_builtin(name = "all")]
2925fn all_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
2926    if rest.is_empty() {
2927        return all_all_or_cols(a);
2928    }
2929    if rest.len() == 1 {
2930        match &rest[0] {
2931            Value::Num(d) => return all_dim(a, *d),
2932            Value::Int(i) => return all_dim(a, i.to_i64() as f64),
2933            _ => {}
2934        }
2935    }
2936    Err("all: unsupported arguments".to_string())
2937}
2938
2939// -------- N-D utilities: permute, squeeze, cat --------
2940
2941#[runmat_macros::runtime_builtin(name = "squeeze")]
2942fn squeeze_builtin(a: Value) -> Result<Value, String> {
2943    let t = match a {
2944        Value::Tensor(t) => t,
2945        Value::StringArray(_) => return Err("squeeze: not supported for string arrays".to_string()),
2946        Value::CharArray(_) => return Err("squeeze: not supported for char arrays".to_string()),
2947        _ => return Err("squeeze: expected tensor".to_string()),
2948    };
2949    let mut new_shape: Vec<usize> = t.shape.iter().copied().filter(|&d| d != 1).collect();
2950    if new_shape.is_empty() {
2951        new_shape.push(1);
2952    }
2953    Ok(Value::Tensor(
2954        runmat_builtins::Tensor::new(t.data.clone(), new_shape)
2955            .map_err(|e| format!("squeeze: {e}"))?,
2956    ))
2957}
2958
2959#[runmat_macros::runtime_builtin(name = "permute")]
2960fn permute_builtin(a: Value, order: Value) -> Result<Value, String> {
2961    let t = match a {
2962        Value::Tensor(t) => t,
2963        Value::StringArray(_) => return Err("permute: not supported for string arrays".to_string()),
2964        Value::CharArray(_) => return Err("permute: not supported for char arrays".to_string()),
2965        _ => return Err("permute: expected tensor".to_string()),
2966    };
2967    let ord = match order {
2968        Value::Tensor(idx) => idx.data.iter().map(|&v| v as usize).collect::<Vec<usize>>(),
2969        Value::Cell(c) => c
2970            .data
2971            .iter()
2972            .map(|v| match &**v {
2973                Value::Num(n) => *n as usize,
2974                Value::Int(i) => i.to_i64() as usize,
2975                _ => 0,
2976            })
2977            .collect(),
2978        Value::Num(n) => vec![n as usize],
2979        _ => return Err("permute: expected index vector".to_string()),
2980    };
2981    if ord.contains(&0) {
2982        return Err("permute: indices are 1-based".to_string());
2983    }
2984    let ord0: Vec<usize> = ord.into_iter().map(|k| k - 1).collect();
2985    let rank = t.shape.len();
2986    if ord0.len() != rank {
2987        return Err("permute: order length must match rank".to_string());
2988    }
2989    let mut new_shape = vec![0usize; rank];
2990    for (i, &src) in ord0.iter().enumerate() {
2991        new_shape[i] = *t.shape.get(src).unwrap_or(&1);
2992    }
2993    // Precompute strides
2994    let mut src_strides = vec![0usize; rank];
2995    let mut acc = 1usize;
2996    for (d, stride) in src_strides.iter_mut().enumerate().take(rank) {
2997        *stride = acc;
2998        acc *= t.shape[d];
2999    }
3000    let mut dst_strides = vec![0usize; rank];
3001    let mut acc2 = 1usize;
3002    for (d, stride) in dst_strides.iter_mut().enumerate().take(rank) {
3003        *stride = acc2;
3004        acc2 *= new_shape[d];
3005    }
3006    let total = t.data.len();
3007    let mut out = vec![0f64; total];
3008    // Iterate destination multi-index in column-major
3009    fn unrank(mut lin: usize, shape: &[usize]) -> Vec<usize> {
3010        let mut idx = Vec::with_capacity(shape.len());
3011        for &s in shape {
3012            idx.push(lin % s);
3013            lin /= s;
3014        }
3015        idx
3016    }
3017    for (dst_lin, item) in out.iter_mut().enumerate().take(total) {
3018        let dst_multi = unrank(dst_lin, &new_shape);
3019        // Map dst dims -> src dims by inverse order mapping
3020        let mut src_multi = vec![0usize; rank];
3021        for (dst_d, &src_d) in ord0.iter().enumerate() {
3022            src_multi[src_d] = dst_multi[dst_d];
3023        }
3024        let mut src_lin = 0usize;
3025        for d in 0..rank {
3026            src_lin += src_multi[d] * src_strides[d];
3027        }
3028        *item = t.data[src_lin];
3029    }
3030    Ok(Value::Tensor(
3031        runmat_builtins::Tensor::new(out, new_shape).map_err(|e| format!("permute: {e}"))?,
3032    ))
3033}
3034
3035// -------- Linear algebra helpers: diag, triu, tril --------
3036
3037#[runmat_macros::runtime_builtin(name = "diag")]
3038fn diag_builtin(a: Value) -> Result<Value, String> {
3039    match a {
3040        Value::Tensor(t) => {
3041            let rows = t.rows();
3042            let cols = t.cols();
3043            if rows == 1 || cols == 1 {
3044                // Vector -> diagonal matrix
3045                let n = rows.max(cols);
3046                let mut data = vec![0.0; n * n];
3047                for (i, slot) in data.iter_mut().enumerate().step_by(n + 1).take(n) {
3048                    // Map linear i on diagonal to source index
3049                    let idx = i / (n + 1);
3050                    let val = t.data[idx];
3051                    *slot = val;
3052                }
3053                Ok(Value::Tensor(
3054                    runmat_builtins::Tensor::new(data, vec![n, n])
3055                        .map_err(|e| format!("diag: {e}"))?,
3056                ))
3057            } else {
3058                // Matrix -> main diagonal as column vector
3059                let n = rows.min(cols);
3060                let mut data = vec![0.0; n];
3061                for (i, slot) in data.iter_mut().enumerate().take(n) {
3062                    *slot = t.data[i + i * rows];
3063                }
3064                Ok(Value::Tensor(
3065                    runmat_builtins::Tensor::new(data, vec![n, 1])
3066                        .map_err(|e| format!("diag: {e}"))?,
3067                ))
3068            }
3069        }
3070        _ => Err("diag: expected tensor".to_string()),
3071    }
3072}
3073
3074#[runmat_macros::runtime_builtin(name = "triu")]
3075fn triu_builtin(a: Value) -> Result<Value, String> {
3076    let t = match a {
3077        Value::Tensor(t) => t,
3078        _ => return Err("triu: expected tensor".to_string()),
3079    };
3080    let rows = t.rows();
3081    let cols = t.cols();
3082    let mut out = vec![0.0; rows * cols];
3083    for c in 0..cols {
3084        for r in 0..rows {
3085            if r <= c {
3086                out[r + c * rows] = t.data[r + c * rows];
3087            }
3088        }
3089    }
3090    Ok(Value::Tensor(
3091        runmat_builtins::Tensor::new(out, vec![rows, cols]).map_err(|e| format!("triu: {e}"))?,
3092    ))
3093}
3094
3095#[runmat_macros::runtime_builtin(name = "tril")]
3096fn tril_builtin(a: Value) -> Result<Value, String> {
3097    let t = match a {
3098        Value::Tensor(t) => t,
3099        _ => return Err("tril: expected tensor".to_string()),
3100    };
3101    let rows = t.rows();
3102    let cols = t.cols();
3103    let mut out = vec![0.0; rows * cols];
3104    for c in 0..cols {
3105        for r in 0..rows {
3106            if r >= c {
3107                out[r + c * rows] = t.data[r + c * rows];
3108            }
3109        }
3110    }
3111    Ok(Value::Tensor(
3112        runmat_builtins::Tensor::new(out, vec![rows, cols]).map_err(|e| format!("tril: {e}"))?,
3113    ))
3114}
3115
3116#[runmat_macros::runtime_builtin(name = "cat")]
3117fn cat_var_builtin(dim: f64, rest: Vec<Value>) -> Result<Value, String> {
3118    if rest.len() < 2 {
3119        return Err("cat: expects at least two arrays".to_string());
3120    }
3121    let d = if dim < 1.0 { 1usize } else { dim as usize } - 1; // zero-based
3122                                                               // If any string array/string present, do string-array cat
3123    if rest
3124        .iter()
3125        .any(|v| matches!(v, Value::StringArray(_) | Value::String(_)))
3126    {
3127        let mut arrs: Vec<runmat_builtins::StringArray> = Vec::new();
3128        for v in rest {
3129            match v {
3130                Value::StringArray(sa) => arrs.push(sa),
3131                Value::String(s) => arrs.push(
3132                    runmat_builtins::StringArray::new(vec![s], vec![1, 1])
3133                        .map_err(|e| format!("cat: {e}"))?,
3134                ),
3135                Value::Num(n) => arrs.push(
3136                    runmat_builtins::StringArray::new(vec![n.to_string()], vec![1, 1])
3137                        .map_err(|e| format!("cat: {e}"))?,
3138                ),
3139                Value::Int(i) => arrs.push(
3140                    runmat_builtins::StringArray::new(vec![i.to_i64().to_string()], vec![1, 1])
3141                        .map_err(|e| format!("cat: {e}"))?,
3142                ),
3143                other => {
3144                    return Err(format!(
3145                        "cat: expected string arrays/strings or scalars, got {other:?}"
3146                    ))
3147                }
3148            }
3149        }
3150        let rank = arrs
3151            .iter()
3152            .map(|a| a.shape.len())
3153            .max()
3154            .unwrap_or(2)
3155            .max(d + 1);
3156        let shapes: Vec<Vec<usize>> = arrs
3157            .iter()
3158            .map(|a| {
3159                let mut s = a.shape.clone();
3160                if s.len() < rank {
3161                    s.resize(rank, 1);
3162                }
3163                s
3164            })
3165            .collect();
3166        for k in 0..rank {
3167            if k == d {
3168                continue;
3169            }
3170            let first = shapes[0][k];
3171            if !shapes.iter().all(|s| s[k] == first) {
3172                return Err("cat: dimension mismatch".to_string());
3173            }
3174        }
3175        let mut out_shape = shapes[0].clone();
3176        out_shape[d] = shapes.iter().map(|s| s[d]).sum();
3177        fn strides(shape: &[usize]) -> Vec<usize> {
3178            let mut s = vec![0; shape.len()];
3179            let mut acc = 1;
3180            for i in 0..shape.len() {
3181                s[i] = acc;
3182                acc *= shape[i];
3183            }
3184            s
3185        }
3186        let out_str = strides(&out_shape);
3187        let mut out: Vec<String> = vec![String::new(); out_shape.iter().product()];
3188        let mut offset = 0usize;
3189        for (a, s) in arrs.iter().zip(shapes.iter()) {
3190            let s_str = strides(s);
3191            let total: usize = s.iter().product();
3192            let rank = out_shape.len();
3193            for idx_lin in 0..total {
3194                let mut rem = idx_lin;
3195                let mut src_multi = vec![0usize; rank];
3196                for i in 0..rank {
3197                    let si = s[i];
3198                    src_multi[i] = rem % si;
3199                    rem /= si;
3200                }
3201                let mut dst_multi = src_multi.clone();
3202                dst_multi[d] += offset;
3203                let mut s_lin = 0usize;
3204                for i in 0..rank {
3205                    s_lin += src_multi[i] * s_str[i];
3206                }
3207                let mut d_lin = 0usize;
3208                for i in 0..rank {
3209                    d_lin += dst_multi[i] * out_str[i];
3210                }
3211                out[d_lin] = a.data[s_lin].clone();
3212            }
3213            offset += s[d];
3214        }
3215        return Ok(Value::StringArray(
3216            runmat_builtins::StringArray::new(out, out_shape).map_err(|e| format!("cat: {e}"))?,
3217        ));
3218    }
3219    // Numeric cat path
3220    let tensors: Vec<runmat_builtins::Tensor> =
3221        rest.into_iter()
3222            .map(|v| match v {
3223                Value::Tensor(t) => Ok(t),
3224                Value::Num(n) => runmat_builtins::Tensor::new(vec![n], vec![1, 1])
3225                    .map_err(|e| format!("cat: {e}")),
3226                Value::Int(i) => runmat_builtins::Tensor::new(vec![i.to_f64()], vec![1, 1])
3227                    .map_err(|e| format!("cat: {e}")),
3228                Value::Bool(b) => {
3229                    runmat_builtins::Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
3230                        .map_err(|e| format!("cat: {e}"))
3231                }
3232                other => Err(format!("cat: expected tensors or scalars, got {other:?}")),
3233            })
3234            .collect::<Result<_, _>>()?;
3235    let mut rank = tensors.iter().map(|t| t.shape.len()).max().unwrap_or(2);
3236    if d + 1 > rank {
3237        rank = d + 1;
3238    }
3239    let shapes: Vec<Vec<usize>> = tensors
3240        .iter()
3241        .map(|t| {
3242            let mut s = t.shape.clone();
3243            s.resize(rank, 1);
3244            s
3245        })
3246        .collect();
3247    for k in 0..rank {
3248        if k == d {
3249            continue;
3250        }
3251        let first = shapes[0][k];
3252        if !shapes.iter().all(|s| s[k] == first) {
3253            return Err("cat: dimension mismatch".to_string());
3254        }
3255    }
3256    let mut out_shape = shapes[0].clone();
3257    out_shape[d] = shapes.iter().map(|s| s[d]).sum();
3258    let mut out = vec![0f64; out_shape.iter().product()];
3259    fn strides(shape: &[usize]) -> Vec<usize> {
3260        let mut s = vec![0; shape.len()];
3261        let mut acc = 1;
3262        for i in 0..shape.len() {
3263            s[i] = acc;
3264            acc *= shape[i];
3265        }
3266        s
3267    }
3268    let out_str = strides(&out_shape);
3269    let mut offset = 0usize;
3270    for (t, s) in tensors.iter().zip(shapes.iter()) {
3271        let s_str = strides(s);
3272        let rank = out_shape.len();
3273        let total: usize = s.iter().product();
3274        for idx_lin in 0..total {
3275            // Convert src lin to multi
3276            let mut rem = idx_lin;
3277            let mut src_multi = vec![0usize; rank];
3278            for i in 0..rank {
3279                let si = s[i];
3280                src_multi[i] = rem % si;
3281                rem /= si;
3282            }
3283            let mut dst_multi = src_multi.clone();
3284            dst_multi[d] += offset;
3285            let mut s_lin = 0usize;
3286            for i in 0..rank {
3287                s_lin += src_multi[i] * s_str[i];
3288            }
3289            let mut d_lin = 0usize;
3290            for i in 0..rank {
3291                d_lin += dst_multi[i] * out_str[i];
3292            }
3293            out[d_lin] = t.data[s_lin];
3294        }
3295        offset += s[d];
3296    }
3297    Ok(Value::Tensor(
3298        runmat_builtins::Tensor::new(out, out_shape).map_err(|e| format!("cat: {e}"))?,
3299    ))
3300}
3301
3302// repmat 2D helper (covered by varargs entry)
3303#[inline]
3304fn repmat_builtin(a: Value, m: f64, n: f64) -> Result<Value, String> {
3305    let t = match a {
3306        Value::Tensor(t) => t,
3307        _ => return Err("repmat: expected tensor".to_string()),
3308    };
3309    let m = if m < 1.0 { 1usize } else { m as usize };
3310    let n = if n < 1.0 { 1usize } else { n as usize };
3311    let mut shape = t.shape.clone();
3312    if shape.len() < 2 {
3313        shape.resize(2, 1);
3314    }
3315    let base_rows = shape[0];
3316    let base_cols = shape[1];
3317    let out_rows = base_rows * m;
3318    let out_cols = base_cols * n;
3319    let mut out = vec![0f64; out_rows * out_cols];
3320    for rep_c in 0..n {
3321        for rep_r in 0..m {
3322            for c in 0..base_cols {
3323                for r in 0..base_rows {
3324                    let src = t.data[r + c * base_rows];
3325                    let dr = r + rep_r * base_rows;
3326                    let dc = c + rep_c * base_cols;
3327                    out[dr + dc * out_rows] = src;
3328                }
3329            }
3330        }
3331    }
3332    Ok(Value::Tensor(
3333        runmat_builtins::Tensor::new(out, vec![out_rows, out_cols])
3334            .map_err(|e| format!("repmat: {e}"))?,
3335    ))
3336}
3337
3338#[runmat_macros::runtime_builtin(name = "repmat")]
3339fn repmat_nd_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
3340    let t = match a {
3341        Value::Tensor(t) => t,
3342        _ => return Err("repmat: expected tensor".to_string()),
3343    };
3344    // Build replication factors from rest
3345    let mut reps: Vec<usize> = Vec::new();
3346    if rest.len() == 1 {
3347        match &rest[0] {
3348            Value::Tensor(v) => {
3349                for &x in &v.data {
3350                    reps.push(if x < 1.0 { 1 } else { x as usize });
3351                }
3352            }
3353            _ => return Err("repmat: expected replication vector".to_string()),
3354        }
3355    } else {
3356        for v in rest {
3357            match v {
3358                Value::Num(n) => reps.push(if n < 1.0 { 1 } else { n as usize }),
3359                Value::Int(i) => {
3360                    let ii = i.to_i64();
3361                    reps.push(if ii < 1 { 1 } else { ii as usize })
3362                }
3363                _ => return Err("repmat: expected numeric reps".to_string()),
3364            }
3365        }
3366    }
3367    // If 2D case, delegate to the 2D helper to avoid unused warning.
3368    if reps.len() == 2 {
3369        return repmat_builtin(Value::Tensor(t), reps[0] as f64, reps[1] as f64);
3370    }
3371    let rank = t.shape.len().max(reps.len());
3372    let mut base = t.shape.clone();
3373    base.resize(rank, 1);
3374    reps.resize(rank, 1);
3375    let mut out_shape = base.clone();
3376    for i in 0..rank {
3377        out_shape[i] = base[i] * reps[i];
3378    }
3379    let mut out = vec![0f64; out_shape.iter().product()];
3380    fn strides(shape: &[usize]) -> Vec<usize> {
3381        let mut s = vec![0; shape.len()];
3382        let mut acc = 1;
3383        for i in 0..shape.len() {
3384            s[i] = acc;
3385            acc *= shape[i];
3386        }
3387        s
3388    }
3389    let src_str = strides(&base);
3390    // Iterate over all dest coords and map back to source via modulo
3391    let total: usize = out_shape.iter().product();
3392    for (d_lin, item) in out.iter_mut().enumerate().take(total) {
3393        // convert to multi
3394        let mut rem = d_lin;
3395        let mut multi = vec![0usize; rank];
3396        for i in 0..rank {
3397            let s = out_shape[i];
3398            multi[i] = rem % s;
3399            rem /= s;
3400        }
3401        // map to src via modulo by base size
3402        let mut src_lin = 0usize;
3403        for i in 0..rank {
3404            let coord = multi[i] % base[i];
3405            src_lin += coord * src_str[i];
3406        }
3407        *item = t.data[src_lin];
3408    }
3409    Ok(Value::Tensor(
3410        runmat_builtins::Tensor::new(out, out_shape).map_err(|e| format!("repmat: {e}"))?,
3411    ))
3412}
3413
3414// linspace and meshgrid are defined in arrays.rs; avoid duplicates here
3415
3416// -------- Vararg string/IO builtins: sprintf, fprintf, disp, warning --------
3417
3418#[runmat_macros::runtime_builtin(name = "sprintf")]
3419fn sprintf_builtin(fmt: String, rest: Vec<Value>) -> Result<Value, String> {
3420    let s = format_variadic(&fmt, &rest)?;
3421    Ok(Value::String(s))
3422}
3423
3424#[runmat_macros::runtime_builtin(name = "fprintf")]
3425fn fprintf_builtin(first: Value, rest: Vec<Value>) -> Result<Value, String> {
3426    // MATLAB: fprintf(fid, fmt, ...) or fprintf(fmt, ...)
3427    let (fmt, args) = match first {
3428        Value::String(s) => (s, rest),
3429        Value::Num(_) | Value::Int(_) => {
3430            // File IDs not supported yet; treat as stdout and expect format string next
3431            if rest.is_empty() {
3432                return Err("fprintf: missing format string".to_string());
3433            }
3434            let fmt = match &rest[0] {
3435                Value::String(s) => s.clone(),
3436                _ => return Err("fprintf: expected format string".to_string()),
3437            };
3438            (fmt, rest[1..].to_vec())
3439        }
3440        other => return Err(format!("fprintf: unsupported first argument {other:?}")),
3441    };
3442    let s = format_variadic(&fmt, &args)?;
3443    println!("{s}");
3444    Ok(Value::Num(s.len() as f64))
3445}
3446
3447#[runmat_macros::runtime_builtin(name = "warning")]
3448fn warning_builtin(fmt: String, rest: Vec<Value>) -> Result<Value, String> {
3449    let s = format_variadic(&fmt, &rest)?;
3450    eprintln!("Warning: {s}");
3451    Ok(Value::Num(0.0))
3452}
3453
3454#[runmat_macros::runtime_builtin(name = "disp")]
3455fn disp_builtin(x: Value) -> Result<Value, String> {
3456    match x {
3457        Value::String(s) => println!("{s}"),
3458        Value::Num(n) => println!("{n}"),
3459        Value::Int(i) => println!("{}", i.to_i64()),
3460        Value::Tensor(t) => println!("{:?}", t.data),
3461        other => println!("{other:?}"),
3462    }
3463    Ok(Value::Num(0.0))
3464}
3465
3466#[runmat_macros::runtime_builtin(name = "struct")]
3467fn struct_builtin(rest: Vec<Value>) -> Result<Value, String> {
3468    if !rest.len().is_multiple_of(2) {
3469        return Err("struct: expected name/value pairs".to_string());
3470    }
3471    let mut st = runmat_builtins::StructValue::new();
3472    let mut i = 0usize;
3473    while i < rest.len() {
3474        let key: String = (&rest[i]).try_into()?;
3475        let val = rest[i + 1].clone();
3476        st.fields.insert(key, val);
3477        i += 2;
3478    }
3479    Ok(Value::Struct(st))
3480}
3481
3482fn format_variadic(fmt: &str, args: &[Value]) -> Result<String, String> {
3483    // Minimal subset: supports %d/%i, %f, %s and %%
3484    let mut out = String::with_capacity(fmt.len() + args.len() * 8);
3485    let mut it = fmt.chars().peekable();
3486    let mut ai = 0usize;
3487    while let Some(c) = it.next() {
3488        if c != '%' {
3489            out.push(c);
3490            continue;
3491        }
3492        if let Some('%') = it.peek() {
3493            it.next();
3494            out.push('%');
3495            continue;
3496        }
3497        // Consume optional width/precision (very limited)
3498        let mut precision: Option<usize> = None;
3499        // skip digits for width
3500        while let Some(ch) = it.peek() {
3501            if ch.is_ascii_digit() {
3502                it.next();
3503            } else {
3504                break;
3505            }
3506        }
3507        // precision .digits
3508        if let Some('.') = it.peek() {
3509            it.next();
3510            let mut p = String::new();
3511            while let Some(ch) = it.peek() {
3512                if ch.is_ascii_digit() {
3513                    p.push(*ch);
3514                    it.next();
3515                } else {
3516                    break;
3517                }
3518            }
3519            if !p.is_empty() {
3520                precision = p.parse::<usize>().ok();
3521            }
3522        }
3523        let ty = it.next().ok_or("sprintf: incomplete format specifier")?;
3524        let val = args.get(ai).cloned().unwrap_or(Value::Num(0.0));
3525        ai += 1;
3526        match ty {
3527            'd' | 'i' => {
3528                let v: f64 = (&val).try_into()?;
3529                out.push_str(&(v as i64).to_string());
3530            }
3531            'f' => {
3532                let v: f64 = (&val).try_into()?;
3533                if let Some(p) = precision {
3534                    out.push_str(&format!("{v:.p$}"));
3535                } else {
3536                    out.push_str(&format!("{v}"));
3537                }
3538            }
3539            's' => match val {
3540                Value::String(s) => out.push_str(&s),
3541                Value::Num(n) => out.push_str(&n.to_string()),
3542                Value::Int(i) => out.push_str(&i.to_i64().to_string()),
3543                Value::Tensor(t) => out.push_str(&format!("{:?}", t.data)),
3544                other => out.push_str(&format!("{other:?}")),
3545            },
3546            other => return Err(format!("sprintf: unsupported format %{other}")),
3547        }
3548    }
3549    Ok(out)
3550}
3551
3552#[runmat_macros::runtime_builtin(name = "getmethod")]
3553fn getmethod_builtin(obj: Value, name: String) -> Result<Value, String> {
3554    match obj {
3555        Value::Object(o) => {
3556            // Return a closure capturing the receiver; feval will call runtime builtin call_method
3557            Ok(Value::Closure(runmat_builtins::Closure {
3558                function_name: "call_method".to_string(),
3559                captures: vec![Value::Object(o), Value::String(name)],
3560            }))
3561        }
3562        Value::ClassRef(cls) => Ok(Value::String(format!("@{cls}.{name}"))),
3563        other => Err(format!("getmethod unsupported on {other:?}")),
3564    }
3565}