runmat_runtime/
lib.rs

1use crate::builtins::common::format::format_variadic;
2use runmat_builtins::Value;
3use runmat_gc_api::GcPtr;
4
5pub mod dispatcher;
6
7pub mod arrays;
8pub mod builtins;
9pub mod comparison;
10pub mod concatenation;
11pub mod elementwise;
12pub mod indexing;
13pub mod matrix;
14pub mod plotting;
15pub mod workspace;
16
17#[cfg(feature = "blas-lapack")]
18pub mod blas;
19#[cfg(feature = "blas-lapack")]
20pub mod lapack;
21
22// Link to Apple's Accelerate framework on macOS
23#[cfg(all(feature = "blas-lapack", target_os = "macos"))]
24#[link(name = "Accelerate", kind = "framework")]
25extern "C" {}
26
27// Ensure OpenBLAS is linked on non-macOS platforms when BLAS/LAPACK is enabled
28#[cfg(all(feature = "blas-lapack", not(target_os = "macos")))]
29extern crate openblas_src;
30
31pub use dispatcher::{call_builtin, gather_if_needed, is_gpu_value, value_contains_gpu};
32
33// Transitional public shim for tests using matrix_transpose
34pub use crate::matrix::matrix_transpose;
35
36// Pruned legacy re-exports; prefer builtins::* and explicit shims only
37// Transitional root-level shims for widely used helpers
38pub use arrays::create_range;
39pub use concatenation::create_matrix_from_values;
40pub use elementwise::{elementwise_div, elementwise_mul, elementwise_neg, elementwise_pow, power};
41pub use indexing::perform_indexing;
42// Explicitly re-export for external users (ignition VM) that build matrices from values
43// (kept above)
44// Note: constants and mathematics modules only contain #[runtime_builtin] functions
45// and don't export public items, so they don't need to be re-exported
46
47#[cfg(feature = "blas-lapack")]
48pub use blas::*;
49#[cfg(feature = "blas-lapack")]
50pub use lapack::*;
51
52pub(crate) fn make_cell_with_shape(values: Vec<Value>, shape: Vec<usize>) -> Result<Value, String> {
53    let handles: Vec<GcPtr<Value>> = values
54        .into_iter()
55        .map(|v| runmat_gc::gc_allocate(v).expect("gc alloc"))
56        .collect();
57    let ca = runmat_builtins::CellArray::new_handles_with_shape(handles, shape)
58        .map_err(|e| format!("Cell creation error: {e}"))?;
59    Ok(Value::Cell(ca))
60}
61
62pub(crate) fn make_cell(values: Vec<Value>, rows: usize, cols: usize) -> Result<Value, String> {
63    make_cell_with_shape(values, vec![rows, cols])
64}
65
66// Internal builtin to construct a cell from a vector of values (used by ignition)
67#[runmat_macros::runtime_builtin(name = "__make_cell")]
68fn make_cell_builtin(rest: Vec<Value>) -> Result<Value, String> {
69    let rows = 1usize;
70    let cols = rest.len();
71    make_cell(rest, rows, cols)
72}
73
74fn to_string_scalar(v: &Value) -> Result<String, String> {
75    let s: String = v.try_into()?;
76    Ok(s)
77}
78
79fn to_string_array(v: &Value) -> Result<runmat_builtins::StringArray, String> {
80    match v {
81        Value::String(s) => runmat_builtins::StringArray::new(vec![s.clone()], vec![1, 1])
82            .map_err(|e| e.to_string()),
83        Value::StringArray(sa) => Ok(sa.clone()),
84        Value::CharArray(ca) => {
85            // Convert each row to a string; treat as column vector
86            let mut out: Vec<String> = Vec::with_capacity(ca.rows);
87            for r in 0..ca.rows {
88                let mut s = String::with_capacity(ca.cols);
89                for c in 0..ca.cols {
90                    s.push(ca.data[r * ca.cols + c]);
91                }
92                out.push(s);
93            }
94            runmat_builtins::StringArray::new(out, vec![ca.rows, 1]).map_err(|e| e.to_string())
95        }
96        other => Err(format!("cannot convert to string array: {other:?}")),
97    }
98}
99
100#[runmat_macros::runtime_builtin(name = "strtrim")]
101fn strtrim_builtin(a: Value) -> Result<Value, String> {
102    let s = to_string_scalar(&a)?;
103    Ok(Value::String(s.trim().to_string()))
104}
105
106// Adjust strjoin semantics: join rows (row-wise)
107#[runmat_macros::runtime_builtin(name = "strjoin")]
108fn strjoin_rowwise(a: Value, delim: Value) -> Result<Value, String> {
109    let d = to_string_scalar(&delim)?;
110    let sa = to_string_array(&a)?;
111    let rows = *sa.shape.first().unwrap_or(&sa.data.len());
112    let cols = *sa.shape.get(1).unwrap_or(&1);
113    if rows == 0 || cols == 0 {
114        return Ok(Value::StringArray(
115            runmat_builtins::StringArray::new(Vec::new(), vec![0, 0]).unwrap(),
116        ));
117    }
118    let mut out: Vec<String> = Vec::with_capacity(rows);
119    for r in 0..rows {
120        let mut s = String::new();
121        for c in 0..cols {
122            if c > 0 {
123                s.push_str(&d);
124            }
125            s.push_str(&sa.data[r + c * rows]);
126        }
127        out.push(s);
128    }
129    Ok(Value::StringArray(
130        runmat_builtins::StringArray::new(out, vec![rows, 1])
131            .map_err(|e| format!("strjoin: {e}"))?,
132    ))
133}
134
135// deal: distribute inputs to multiple outputs (via cell for expansion)
136#[runmat_macros::runtime_builtin(name = "deal")]
137fn deal_builtin(rest: Vec<Value>) -> Result<Value, String> {
138    // Return cell row vector of inputs for expansion
139    let cols = rest.len();
140    make_cell(rest, 1, cols)
141}
142
143// Object/handle utilities used by interpreter lowering for OOP/func handles
144
145#[runmat_macros::runtime_builtin(name = "rethrow")]
146fn rethrow_builtin(e: Value) -> Result<Value, String> {
147    match e {
148        Value::MException(me) => Err(format!("{}: {}", me.identifier, me.message)),
149        Value::String(s) => Err(s),
150        other => Err(format!("MATLAB:error: {other:?}")),
151    }
152}
153
154#[runmat_macros::runtime_builtin(name = "call_method")]
155fn call_method_builtin(base: Value, method: String, rest: Vec<Value>) -> Result<Value, String> {
156    match base {
157        Value::Object(obj) => {
158            // Simple dynamic dispatch via builtin registry: method name may be qualified as Class.method
159            let qualified = format!("{}.{}", obj.class_name, method);
160            // Prepend receiver as first arg so methods can accept it
161            let mut args = Vec::with_capacity(1 + rest.len());
162            args.push(Value::Object(obj.clone()));
163            args.extend(rest);
164            if let Ok(v) = crate::call_builtin(&qualified, &args) {
165                return Ok(v);
166            }
167            // Fallback to global method name
168            crate::call_builtin(&method, &args)
169        }
170        Value::HandleObject(h) => {
171            // Methods on handle classes dispatch to the underlying target's class namespace
172            let target = unsafe { &*h.target.as_raw() };
173            let class_name = match target {
174                Value::Object(o) => o.class_name.clone(),
175                Value::Struct(_) => h.class_name.clone(),
176                _ => h.class_name.clone(),
177            };
178            let qualified = format!("{class_name}.{method}");
179            let mut args = Vec::with_capacity(1 + rest.len());
180            args.push(Value::HandleObject(h.clone()));
181            args.extend(rest);
182            if let Ok(v) = crate::call_builtin(&qualified, &args) {
183                return Ok(v);
184            }
185            crate::call_builtin(&method, &args)
186        }
187        other => Err(format!(
188            "call_method unsupported on {other:?} for method '{method}'"
189        )),
190    }
191}
192
193// Global dispatch helpers for overloaded indexing (subsref/subsasgn) to support fallback resolution paths
194#[runmat_macros::runtime_builtin(name = "subsasgn")]
195fn subsasgn_dispatch(
196    obj: Value,
197    kind: String,
198    payload: Value,
199    rhs: Value,
200) -> Result<Value, String> {
201    match &obj {
202        Value::Object(o) => {
203            let qualified = format!("{}.subsasgn", o.class_name);
204            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload, rhs])
205        }
206        Value::HandleObject(h) => {
207            let target = unsafe { &*h.target.as_raw() };
208            let class_name = match target {
209                Value::Object(o) => o.class_name.clone(),
210                _ => h.class_name.clone(),
211            };
212            let qualified = format!("{class_name}.subsasgn");
213            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload, rhs])
214        }
215        other => Err(format!("subsasgn: receiver must be object, got {other:?}")),
216    }
217}
218
219#[runmat_macros::runtime_builtin(name = "subsref")]
220fn subsref_dispatch(obj: Value, kind: String, payload: Value) -> Result<Value, String> {
221    match &obj {
222        Value::Object(o) => {
223            let qualified = format!("{}.subsref", o.class_name);
224            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload])
225        }
226        Value::HandleObject(h) => {
227            let target = unsafe { &*h.target.as_raw() };
228            let class_name = match target {
229                Value::Object(o) => o.class_name.clone(),
230                _ => h.class_name.clone(),
231            };
232            let qualified = format!("{class_name}.subsref");
233            crate::call_builtin(&qualified, &[obj, Value::String(kind), payload])
234        }
235        other => Err(format!("subsref: receiver must be object, got {other:?}")),
236    }
237}
238
239// -------- Handle classes & events --------
240
241#[runmat_macros::runtime_builtin(name = "new_handle_object")]
242fn new_handle_object_builtin(class_name: String) -> Result<Value, String> {
243    // Create an underlying object instance and wrap it in a handle
244    let obj = new_object_builtin(class_name.clone())?;
245    let gc = runmat_gc::gc_allocate(obj).map_err(|e| format!("gc: {e}"))?;
246    Ok(Value::HandleObject(runmat_builtins::HandleRef {
247        class_name,
248        target: gc,
249        valid: true,
250    }))
251}
252
253#[runmat_macros::runtime_builtin(name = "isvalid")]
254fn isvalid_builtin(v: Value) -> Result<Value, String> {
255    match v {
256        Value::HandleObject(h) => Ok(Value::Bool(h.valid)),
257        Value::Listener(l) => Ok(Value::Bool(l.valid && l.enabled)),
258        _ => Ok(Value::Bool(false)),
259    }
260}
261
262use std::sync::{Mutex, OnceLock};
263
264#[derive(Default)]
265struct EventRegistry {
266    next_id: u64,
267    listeners: std::collections::HashMap<(usize, String), Vec<runmat_builtins::Listener>>,
268}
269
270static EVENT_REGISTRY: OnceLock<Mutex<EventRegistry>> = OnceLock::new();
271
272fn events() -> &'static Mutex<EventRegistry> {
273    EVENT_REGISTRY.get_or_init(|| Mutex::new(EventRegistry::default()))
274}
275
276#[runmat_macros::runtime_builtin(name = "addlistener")]
277fn addlistener_builtin(
278    target: Value,
279    event_name: String,
280    callback: Value,
281) -> Result<Value, String> {
282    let key_ptr: usize = match &target {
283        Value::HandleObject(h) => (unsafe { h.target.as_raw() }) as usize,
284        Value::Object(o) => o as *const _ as usize,
285        _ => return Err("addlistener: target must be handle or object".to_string()),
286    };
287    let mut reg = events().lock().unwrap();
288    let id = {
289        reg.next_id += 1;
290        reg.next_id
291    };
292    let tgt_gc = match target {
293        Value::HandleObject(h) => h.target,
294        Value::Object(o) => {
295            runmat_gc::gc_allocate(Value::Object(o)).map_err(|e| format!("gc: {e}"))?
296        }
297        _ => unreachable!(),
298    };
299    let cb_gc = runmat_gc::gc_allocate(callback).map_err(|e| format!("gc: {e}"))?;
300    let listener = runmat_builtins::Listener {
301        id,
302        target: tgt_gc,
303        event_name: event_name.clone(),
304        callback: cb_gc,
305        enabled: true,
306        valid: true,
307    };
308    reg.listeners
309        .entry((key_ptr, event_name))
310        .or_default()
311        .push(listener.clone());
312    Ok(Value::Listener(listener))
313}
314
315#[runmat_macros::runtime_builtin(name = "notify")]
316fn notify_builtin(target: Value, event_name: String, rest: Vec<Value>) -> Result<Value, String> {
317    let key_ptr: usize = match &target {
318        Value::HandleObject(h) => (unsafe { h.target.as_raw() }) as usize,
319        Value::Object(o) => o as *const _ as usize,
320        _ => return Err("notify: target must be handle or object".to_string()),
321    };
322    let mut to_call: Vec<runmat_builtins::Listener> = Vec::new();
323    {
324        let reg = events().lock().unwrap();
325        if let Some(list) = reg.listeners.get(&(key_ptr, event_name.clone())) {
326            for l in list {
327                if l.valid && l.enabled {
328                    to_call.push(l.clone());
329                }
330            }
331        }
332    }
333    for l in to_call {
334        // Call callback via feval-like protocol
335        let mut args = Vec::new();
336        args.push(target.clone());
337        args.extend(rest.iter().cloned());
338        let cbv: Value = (*l.callback).clone();
339        match &cbv {
340            Value::String(s) if s.starts_with('@') => {
341                let mut a = vec![Value::String(s.clone())];
342                a.extend(args.into_iter());
343                let _ = crate::call_builtin("feval", &a)?;
344            }
345            Value::FunctionHandle(name) => {
346                let mut a = vec![Value::FunctionHandle(name.clone())];
347                a.extend(args.into_iter());
348                let _ = crate::call_builtin("feval", &a)?;
349            }
350            Value::Closure(_) => {
351                let mut a = vec![cbv.clone()];
352                a.extend(args.into_iter());
353                let _ = crate::call_builtin("feval", &a)?;
354            }
355            _ => {}
356        }
357    }
358    Ok(Value::Num(0.0))
359}
360
361// Test-oriented dependent property handlers (global). If a class defines a Dependent
362// property named 'p', the VM will try to call get.p / set.p. We provide generic
363// implementations that read/write a conventional backing field 'p_backing'.
364#[runmat_macros::runtime_builtin(name = "get.p")]
365fn get_p_builtin(obj: Value) -> Result<Value, String> {
366    match obj {
367        Value::Object(o) => {
368            if let Some(v) = o.properties.get("p_backing") {
369                Ok(v.clone())
370            } else {
371                Ok(Value::Num(0.0))
372            }
373        }
374        other => Err(format!("get.p requires object, got {other:?}")),
375    }
376}
377
378#[runmat_macros::runtime_builtin(name = "set.p")]
379fn set_p_builtin(obj: Value, val: Value) -> Result<Value, String> {
380    match obj {
381        Value::Object(mut o) => {
382            o.properties.insert("p_backing".to_string(), val);
383            Ok(Value::Object(o))
384        }
385        other => Err(format!("set.p requires object, got {other:?}")),
386    }
387}
388
389#[runmat_macros::runtime_builtin(name = "make_handle")]
390fn make_handle_builtin(name: String) -> Result<Value, String> {
391    Ok(Value::String(format!("@{name}")))
392}
393
394#[runmat_macros::runtime_builtin(name = "make_anon")]
395fn make_anon_builtin(params: String, body: String) -> Result<Value, String> {
396    Ok(Value::String(format!("@anon({params}) {body}")))
397}
398
399#[runmat_macros::runtime_builtin(name = "new_object")]
400pub(crate) fn new_object_builtin(class_name: String) -> Result<Value, String> {
401    if let Some(def) = runmat_builtins::get_class(&class_name) {
402        // Collect class hierarchy from root to leaf for default initialization
403        let mut chain: Vec<runmat_builtins::ClassDef> = Vec::new();
404        // Walk up to root
405        let mut cursor: Option<String> = Some(def.name.clone());
406        while let Some(name) = cursor {
407            if let Some(cd) = runmat_builtins::get_class(&name) {
408                chain.push(cd.clone());
409                cursor = cd.parent.clone();
410            } else {
411                break;
412            }
413        }
414        // Reverse to root-first
415        chain.reverse();
416        let mut obj = runmat_builtins::ObjectInstance::new(def.name.clone());
417        // Apply defaults from root to leaf (leaf overrides effectively by later assignment)
418        for cd in chain {
419            for (k, p) in cd.properties.iter() {
420                if !p.is_static {
421                    if let Some(v) = &p.default_value {
422                        obj.properties.insert(k.clone(), v.clone());
423                    }
424                }
425            }
426        }
427        Ok(Value::Object(obj))
428    } else {
429        Ok(Value::Object(runmat_builtins::ObjectInstance::new(
430            class_name,
431        )))
432    }
433}
434
435// handle-object builtins removed for now
436
437#[runmat_macros::runtime_builtin(name = "classref")]
438fn classref_builtin(class_name: String) -> Result<Value, String> {
439    Ok(Value::ClassRef(class_name))
440}
441
442#[runmat_macros::runtime_builtin(name = "__register_test_classes")]
443fn register_test_classes_builtin() -> Result<Value, String> {
444    use runmat_builtins::*;
445    let mut props = std::collections::HashMap::new();
446    props.insert(
447        "x".to_string(),
448        PropertyDef {
449            name: "x".to_string(),
450            is_static: false,
451            is_dependent: false,
452            get_access: Access::Public,
453            set_access: Access::Public,
454            default_value: Some(Value::Num(0.0)),
455        },
456    );
457    props.insert(
458        "y".to_string(),
459        PropertyDef {
460            name: "y".to_string(),
461            is_static: false,
462            is_dependent: false,
463            get_access: Access::Public,
464            set_access: Access::Public,
465            default_value: Some(Value::Num(0.0)),
466        },
467    );
468    props.insert(
469        "staticValue".to_string(),
470        PropertyDef {
471            name: "staticValue".to_string(),
472            is_static: true,
473            is_dependent: false,
474            get_access: Access::Public,
475            set_access: Access::Public,
476            default_value: Some(Value::Num(42.0)),
477        },
478    );
479    props.insert(
480        "secret".to_string(),
481        PropertyDef {
482            name: "secret".to_string(),
483            is_static: false,
484            is_dependent: false,
485            get_access: Access::Private,
486            set_access: Access::Private,
487            default_value: Some(Value::Num(99.0)),
488        },
489    );
490    let mut methods = std::collections::HashMap::new();
491    methods.insert(
492        "move".to_string(),
493        MethodDef {
494            name: "move".to_string(),
495            is_static: false,
496            access: Access::Public,
497            function_name: "Point.move".to_string(),
498        },
499    );
500    methods.insert(
501        "origin".to_string(),
502        MethodDef {
503            name: "origin".to_string(),
504            is_static: true,
505            access: Access::Public,
506            function_name: "Point.origin".to_string(),
507        },
508    );
509    runmat_builtins::register_class(ClassDef {
510        name: "Point".to_string(),
511        parent: None,
512        properties: props,
513        methods,
514    });
515
516    // Namespaced class example: pkg.PointNS with same shape as Point
517    let mut ns_props = std::collections::HashMap::new();
518    ns_props.insert(
519        "x".to_string(),
520        PropertyDef {
521            name: "x".to_string(),
522            is_static: false,
523            is_dependent: false,
524            get_access: Access::Public,
525            set_access: Access::Public,
526            default_value: Some(Value::Num(1.0)),
527        },
528    );
529    ns_props.insert(
530        "y".to_string(),
531        PropertyDef {
532            name: "y".to_string(),
533            is_static: false,
534            is_dependent: false,
535            get_access: Access::Public,
536            set_access: Access::Public,
537            default_value: Some(Value::Num(2.0)),
538        },
539    );
540    let ns_methods = std::collections::HashMap::new();
541    runmat_builtins::register_class(ClassDef {
542        name: "pkg.PointNS".to_string(),
543        parent: None,
544        properties: ns_props,
545        methods: ns_methods,
546    });
547
548    // Inheritance: Shape (base) and Circle (derived)
549    let shape_props = std::collections::HashMap::new();
550    let mut shape_methods = std::collections::HashMap::new();
551    shape_methods.insert(
552        "area".to_string(),
553        MethodDef {
554            name: "area".to_string(),
555            is_static: false,
556            access: Access::Public,
557            function_name: "Shape.area".to_string(),
558        },
559    );
560    runmat_builtins::register_class(ClassDef {
561        name: "Shape".to_string(),
562        parent: None,
563        properties: shape_props,
564        methods: shape_methods,
565    });
566
567    let mut circle_props = std::collections::HashMap::new();
568    circle_props.insert(
569        "r".to_string(),
570        PropertyDef {
571            name: "r".to_string(),
572            is_static: false,
573            is_dependent: false,
574            get_access: Access::Public,
575            set_access: Access::Public,
576            default_value: Some(Value::Num(0.0)),
577        },
578    );
579    let mut circle_methods = std::collections::HashMap::new();
580    circle_methods.insert(
581        "area".to_string(),
582        MethodDef {
583            name: "area".to_string(),
584            is_static: false,
585            access: Access::Public,
586            function_name: "Circle.area".to_string(),
587        },
588    );
589    runmat_builtins::register_class(ClassDef {
590        name: "Circle".to_string(),
591        parent: Some("Shape".to_string()),
592        properties: circle_props,
593        methods: circle_methods,
594    });
595
596    // Constructor demo class: Ctor with static constructor method Ctor
597    let ctor_props = std::collections::HashMap::new();
598    let mut ctor_methods = std::collections::HashMap::new();
599    ctor_methods.insert(
600        "Ctor".to_string(),
601        MethodDef {
602            name: "Ctor".to_string(),
603            is_static: true,
604            access: Access::Public,
605            function_name: "Ctor.Ctor".to_string(),
606        },
607    );
608    runmat_builtins::register_class(ClassDef {
609        name: "Ctor".to_string(),
610        parent: None,
611        properties: ctor_props,
612        methods: ctor_methods,
613    });
614
615    // Overloaded indexing demo class: OverIdx with subsref/subsasgn
616    let overidx_props = std::collections::HashMap::new();
617    let mut overidx_methods = std::collections::HashMap::new();
618    overidx_methods.insert(
619        "subsref".to_string(),
620        MethodDef {
621            name: "subsref".to_string(),
622            is_static: false,
623            access: Access::Public,
624            function_name: "OverIdx.subsref".to_string(),
625        },
626    );
627    overidx_methods.insert(
628        "subsasgn".to_string(),
629        MethodDef {
630            name: "subsasgn".to_string(),
631            is_static: false,
632            access: Access::Public,
633            function_name: "OverIdx.subsasgn".to_string(),
634        },
635    );
636    runmat_builtins::register_class(ClassDef {
637        name: "OverIdx".to_string(),
638        parent: None,
639        properties: overidx_props,
640        methods: overidx_methods,
641    });
642    Ok(Value::Num(1.0))
643}
644
645#[cfg(feature = "test-classes")]
646pub fn test_register_classes() {
647    let _ = register_test_classes_builtin();
648}
649
650// Example method implementation: Point.move(obj, dx, dy) -> updated obj
651#[runmat_macros::runtime_builtin(name = "Point.move")]
652fn point_move_method(obj: Value, dx: f64, dy: f64) -> Result<Value, String> {
653    match obj {
654        Value::Object(mut o) => {
655            let mut x = 0.0;
656            let mut y = 0.0;
657            if let Some(Value::Num(v)) = o.properties.get("x") {
658                x = *v;
659            }
660            if let Some(Value::Num(v)) = o.properties.get("y") {
661                y = *v;
662            }
663            o.properties.insert("x".to_string(), Value::Num(x + dx));
664            o.properties.insert("y".to_string(), Value::Num(y + dy));
665            Ok(Value::Object(o))
666        }
667        other => Err(format!(
668            "Point.move requires object receiver, got {other:?}"
669        )),
670    }
671}
672
673#[runmat_macros::runtime_builtin(name = "Point.origin")]
674fn point_origin_method() -> Result<Value, String> {
675    let mut o = runmat_builtins::ObjectInstance::new("Point".to_string());
676    o.properties.insert("x".to_string(), Value::Num(0.0));
677    o.properties.insert("y".to_string(), Value::Num(0.0));
678    Ok(Value::Object(o))
679}
680
681#[runmat_macros::runtime_builtin(name = "Shape.area")]
682fn shape_area_method(_obj: Value) -> Result<Value, String> {
683    Ok(Value::Num(0.0))
684}
685
686#[runmat_macros::runtime_builtin(name = "Circle.area")]
687fn circle_area_method(obj: Value) -> Result<Value, String> {
688    match obj {
689        Value::Object(o) => {
690            let r = if let Some(Value::Num(v)) = o.properties.get("r") {
691                *v
692            } else {
693                0.0
694            };
695            Ok(Value::Num(std::f64::consts::PI * r * r))
696        }
697        other => Err(format!(
698            "Circle.area requires object receiver, got {other:?}"
699        )),
700    }
701}
702
703// --- Test-only helpers to validate constructors and subsref/subsasgn ---
704#[runmat_macros::runtime_builtin(name = "Ctor.Ctor")]
705fn ctor_ctor_method(x: f64) -> Result<Value, String> {
706    // Construct object with property 'x' initialized
707    let mut o = runmat_builtins::ObjectInstance::new("Ctor".to_string());
708    o.properties.insert("x".to_string(), Value::Num(x));
709    Ok(Value::Object(o))
710}
711
712// --- Test-only package functions to exercise import precedence ---
713#[runmat_macros::runtime_builtin(name = "PkgF.foo")]
714fn pkgf_foo() -> Result<Value, String> {
715    Ok(Value::Num(10.0))
716}
717
718#[runmat_macros::runtime_builtin(name = "PkgG.foo")]
719fn pkgg_foo() -> Result<Value, String> {
720    Ok(Value::Num(20.0))
721}
722
723#[runmat_macros::runtime_builtin(name = "OverIdx.subsref")]
724fn overidx_subsref(obj: Value, kind: String, payload: Value) -> Result<Value, String> {
725    // Simple sentinel implementation: return different values for '.' vs '()'
726    match (obj, kind.as_str(), payload) {
727        (Value::Object(_), "()", Value::Cell(_)) => Ok(Value::Num(99.0)),
728        (Value::Object(o), "{}", Value::Cell(_)) => {
729            if let Some(v) = o.properties.get("lastCell") {
730                Ok(v.clone())
731            } else {
732                Ok(Value::Num(0.0))
733            }
734        }
735        (Value::Object(o), ".", Value::String(field)) => {
736            // If field exists, return it; otherwise sentinel 77
737            if let Some(v) = o.properties.get(&field) {
738                Ok(v.clone())
739            } else {
740                Ok(Value::Num(77.0))
741            }
742        }
743        (Value::Object(o), ".", Value::CharArray(ca)) => {
744            let field: String = ca.data.iter().collect();
745            if let Some(v) = o.properties.get(&field) {
746                Ok(v.clone())
747            } else {
748                Ok(Value::Num(77.0))
749            }
750        }
751        _ => Err("subsref: unsupported payload".to_string()),
752    }
753}
754
755#[runmat_macros::runtime_builtin(name = "OverIdx.subsasgn")]
756fn overidx_subsasgn(
757    mut obj: Value,
758    kind: String,
759    payload: Value,
760    rhs: Value,
761) -> Result<Value, String> {
762    match (&mut obj, kind.as_str(), payload) {
763        (Value::Object(o), "()", Value::Cell(_)) => {
764            // Store into 'last' property
765            o.properties.insert("last".to_string(), rhs);
766            Ok(Value::Object(o.clone()))
767        }
768        (Value::Object(o), "{}", Value::Cell(_)) => {
769            o.properties.insert("lastCell".to_string(), rhs);
770            Ok(Value::Object(o.clone()))
771        }
772        (Value::Object(o), ".", Value::String(field)) => {
773            o.properties.insert(field, rhs);
774            Ok(Value::Object(o.clone()))
775        }
776        (Value::Object(o), ".", Value::CharArray(ca)) => {
777            let field: String = ca.data.iter().collect();
778            o.properties.insert(field, rhs);
779            Ok(Value::Object(o.clone()))
780        }
781        _ => Err("subsasgn: unsupported payload".to_string()),
782    }
783}
784
785// --- Operator overloading methods for OverIdx (test scaffolding) ---
786#[runmat_macros::runtime_builtin(name = "OverIdx.plus")]
787fn overidx_plus(obj: Value, rhs: Value) -> Result<Value, String> {
788    let o = match obj {
789        Value::Object(o) => o,
790        _ => return Err("OverIdx.plus: receiver must be object".to_string()),
791    };
792    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
793        *v
794    } else {
795        0.0
796    };
797    let r: f64 = (&rhs).try_into()?;
798    Ok(Value::Num(k + r))
799}
800
801#[runmat_macros::runtime_builtin(name = "OverIdx.times")]
802fn overidx_times(obj: Value, rhs: Value) -> Result<Value, String> {
803    let o = match obj {
804        Value::Object(o) => o,
805        _ => return Err("OverIdx.times: receiver must be object".to_string()),
806    };
807    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
808        *v
809    } else {
810        0.0
811    };
812    let r: f64 = (&rhs).try_into()?;
813    Ok(Value::Num(k * r))
814}
815
816#[runmat_macros::runtime_builtin(name = "OverIdx.mtimes")]
817fn overidx_mtimes(obj: Value, rhs: Value) -> Result<Value, String> {
818    let o = match obj {
819        Value::Object(o) => o,
820        _ => return Err("OverIdx.mtimes: receiver must be object".to_string()),
821    };
822    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
823        *v
824    } else {
825        0.0
826    };
827    let r: f64 = (&rhs).try_into()?;
828    Ok(Value::Num(k * r))
829}
830
831#[runmat_macros::runtime_builtin(name = "OverIdx.lt")]
832fn overidx_lt(obj: Value, rhs: Value) -> Result<Value, String> {
833    let o = match obj {
834        Value::Object(o) => o,
835        _ => return Err("OverIdx.lt: receiver must be object".to_string()),
836    };
837    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
838        *v
839    } else {
840        0.0
841    };
842    let r: f64 = (&rhs).try_into()?;
843    Ok(Value::Num(if k < r { 1.0 } else { 0.0 }))
844}
845
846#[runmat_macros::runtime_builtin(name = "OverIdx.gt")]
847fn overidx_gt(obj: Value, rhs: Value) -> Result<Value, String> {
848    let o = match obj {
849        Value::Object(o) => o,
850        _ => return Err("OverIdx.gt: receiver must be object".to_string()),
851    };
852    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
853        *v
854    } else {
855        0.0
856    };
857    let r: f64 = (&rhs).try_into()?;
858    Ok(Value::Num(if k > r { 1.0 } else { 0.0 }))
859}
860
861#[runmat_macros::runtime_builtin(name = "OverIdx.eq")]
862fn overidx_eq(obj: Value, rhs: Value) -> Result<Value, String> {
863    let o = match obj {
864        Value::Object(o) => o,
865        _ => return Err("OverIdx.eq: receiver must be object".to_string()),
866    };
867    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
868        *v
869    } else {
870        0.0
871    };
872    let r: f64 = (&rhs).try_into()?;
873    Ok(Value::Num(if (k - r).abs() < 1e-12 { 1.0 } else { 0.0 }))
874}
875
876#[runmat_macros::runtime_builtin(name = "OverIdx.uplus")]
877fn overidx_uplus(obj: Value) -> Result<Value, String> {
878    // Identity
879    Ok(obj)
880}
881
882#[runmat_macros::runtime_builtin(name = "OverIdx.rdivide")]
883fn overidx_rdivide(obj: Value, rhs: Value) -> Result<Value, String> {
884    let o = match obj {
885        Value::Object(o) => o,
886        _ => return Err("OverIdx.rdivide: receiver must be object".to_string()),
887    };
888    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
889        *v
890    } else {
891        0.0
892    };
893    let r: f64 = (&rhs).try_into()?;
894    Ok(Value::Num(k / r))
895}
896
897#[runmat_macros::runtime_builtin(name = "OverIdx.ldivide")]
898fn overidx_ldivide(obj: Value, rhs: Value) -> Result<Value, String> {
899    let o = match obj {
900        Value::Object(o) => o,
901        _ => return Err("OverIdx.ldivide: receiver must be object".to_string()),
902    };
903    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
904        *v
905    } else {
906        0.0
907    };
908    let r: f64 = (&rhs).try_into()?;
909    Ok(Value::Num(r / k))
910}
911
912#[runmat_macros::runtime_builtin(name = "OverIdx.and")]
913fn overidx_and(obj: Value, rhs: Value) -> Result<Value, String> {
914    let o = match obj {
915        Value::Object(o) => o,
916        _ => return Err("OverIdx.and: receiver must be object".to_string()),
917    };
918    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
919        *v
920    } else {
921        0.0
922    };
923    let r: f64 = (&rhs).try_into()?;
924    Ok(Value::Num(if (k != 0.0) && (r != 0.0) { 1.0 } else { 0.0 }))
925}
926
927#[runmat_macros::runtime_builtin(name = "OverIdx.or")]
928fn overidx_or(obj: Value, rhs: Value) -> Result<Value, String> {
929    let o = match obj {
930        Value::Object(o) => o,
931        _ => return Err("OverIdx.or: receiver must be object".to_string()),
932    };
933    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
934        *v
935    } else {
936        0.0
937    };
938    let r: f64 = (&rhs).try_into()?;
939    Ok(Value::Num(if (k != 0.0) || (r != 0.0) { 1.0 } else { 0.0 }))
940}
941
942#[runmat_macros::runtime_builtin(name = "OverIdx.xor")]
943fn overidx_xor(obj: Value, rhs: Value) -> Result<Value, String> {
944    let o = match obj {
945        Value::Object(o) => o,
946        _ => return Err("OverIdx.xor: receiver must be object".to_string()),
947    };
948    let k = if let Some(Value::Num(v)) = o.properties.get("k") {
949        *v
950    } else {
951        0.0
952    };
953    let r: f64 = (&rhs).try_into()?;
954    let a = k != 0.0;
955    let b = r != 0.0;
956    Ok(Value::Num(if a ^ b { 1.0 } else { 0.0 }))
957}
958
959#[runmat_macros::runtime_builtin(name = "feval")]
960fn feval_builtin(f: Value, rest: Vec<Value>) -> Result<Value, String> {
961    match f {
962        // Current handles are strings like "@sin"
963        Value::String(s) => {
964            if let Some(name) = s.strip_prefix('@') {
965                crate::call_builtin(name, &rest)
966            } else {
967                Err(format!(
968                    "feval: expected function handle string starting with '@', got {s}"
969                ))
970            }
971        }
972        // Also accept character row vector handles like '@max'
973        Value::CharArray(ca) => {
974            if ca.rows == 1 {
975                let s: String = ca.data.iter().collect();
976                if let Some(name) = s.strip_prefix('@') {
977                    crate::call_builtin(name, &rest)
978                } else {
979                    Err(format!(
980                        "feval: expected function handle string starting with '@', got {s}"
981                    ))
982                }
983            } else {
984                Err("feval: function handle char array must be a row vector".to_string())
985            }
986        }
987        Value::Closure(c) => {
988            let mut args = c.captures.clone();
989            args.extend(rest);
990            crate::call_builtin(&c.function_name, &args)
991        }
992        // Future: support Value::Function variants
993        other => Err(format!("feval: unsupported function value {other:?}")),
994    }
995}
996
997// Common mathematical functions that tests expect
998
999/// Transpose operation for Values
1000pub fn transpose(value: Value) -> Result<Value, String> {
1001    match value {
1002        Value::GpuTensor(h) => {
1003            if let Some(p) = runmat_accelerate_api::provider() {
1004                if let Ok(hc) = p.transpose(&h) {
1005                    return Ok(Value::GpuTensor(hc));
1006                }
1007            }
1008            Err("transpose: unsupported for gpuArray".to_string())
1009        }
1010        Value::Tensor(ref m) => Ok(Value::Tensor(crate::matrix::matrix_transpose(m))),
1011        // For complex scalars, transpose is conjugate transpose => scalar transpose is conjugate
1012        Value::Complex(re, im) => Ok(Value::Complex(re, -im)),
1013        Value::ComplexTensor(ref ct) => {
1014            let mut data: Vec<(f64, f64)> = vec![(0.0, 0.0); ct.rows * ct.cols];
1015            // Conjugate transpose for complex matrices
1016            for i in 0..ct.rows {
1017                for j in 0..ct.cols {
1018                    let (re, im) = ct.data[i + j * ct.rows];
1019                    data[j + i * ct.cols] = (re, -im);
1020                }
1021            }
1022            Ok(Value::ComplexTensor(
1023                runmat_builtins::ComplexTensor::new_2d(data, ct.cols, ct.rows).unwrap(),
1024            ))
1025        }
1026        Value::Num(n) => Ok(Value::Num(n)), // Scalar transpose is identity
1027        _ => Err("transpose not supported for this type".to_string()),
1028    }
1029}
1030
1031// -------- Reductions: sum/prod/mean/any/all --------
1032
1033#[allow(dead_code)]
1034fn tensor_sum_all(t: &runmat_builtins::Tensor) -> f64 {
1035    t.data.iter().sum()
1036}
1037
1038fn tensor_prod_all(t: &runmat_builtins::Tensor) -> f64 {
1039    t.data.iter().product()
1040}
1041
1042fn prod_all_or_cols(a: Value) -> Result<Value, String> {
1043    match a {
1044        Value::Tensor(t) => {
1045            let rows = t.rows();
1046            let cols = t.cols();
1047            if rows > 1 && cols > 1 {
1048                let mut out = vec![1.0f64; cols];
1049                for (c, oc) in out.iter_mut().enumerate().take(cols) {
1050                    let mut p = 1.0;
1051                    for r in 0..rows {
1052                        p *= t.data[r + c * rows];
1053                    }
1054                    *oc = p;
1055                }
1056                Ok(Value::Tensor(
1057                    runmat_builtins::Tensor::new(out, vec![1, cols])
1058                        .map_err(|e| format!("prod: {e}"))?,
1059                ))
1060            } else {
1061                Ok(Value::Num(tensor_prod_all(&t)))
1062            }
1063        }
1064        _ => Err("prod: expected tensor".to_string()),
1065    }
1066}
1067
1068fn prod_dim(a: Value, dim: f64) -> Result<Value, String> {
1069    let t = match a {
1070        Value::Tensor(t) => t,
1071        _ => return Err("prod: expected tensor".to_string()),
1072    };
1073    let dim = if dim < 1.0 { 1usize } else { dim as usize };
1074    let rows = t.rows();
1075    let cols = t.cols();
1076    if dim == 1 {
1077        let mut out = vec![1.0f64; cols];
1078        for (c, oc) in out.iter_mut().enumerate().take(cols) {
1079            let mut p = 1.0;
1080            for r in 0..rows {
1081                p *= t.data[r + c * rows];
1082            }
1083            *oc = p;
1084        }
1085        Ok(Value::Tensor(
1086            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("prod: {e}"))?,
1087        ))
1088    } else if dim == 2 {
1089        let mut out = vec![1.0f64; rows];
1090        for (r, orow) in out.iter_mut().enumerate().take(rows) {
1091            let mut p = 1.0;
1092            for c in 0..cols {
1093                p *= t.data[r + c * rows];
1094            }
1095            *orow = p;
1096        }
1097        Ok(Value::Tensor(
1098            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("prod: {e}"))?,
1099        ))
1100    } else {
1101        Err("prod: dim out of range".to_string())
1102    }
1103}
1104
1105#[runmat_macros::runtime_builtin(name = "prod")]
1106fn prod_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
1107    if rest.is_empty() {
1108        return prod_all_or_cols(a);
1109    }
1110    if rest.len() == 1 {
1111        match &rest[0] {
1112            Value::Num(d) => return prod_dim(a, *d),
1113            Value::Int(i) => return prod_dim(a, i.to_i64() as f64),
1114            _ => {}
1115        }
1116    }
1117    Err("prod: unsupported arguments".to_string())
1118}
1119
1120fn any_all_or_cols(a: Value) -> Result<Value, String> {
1121    match a {
1122        Value::Tensor(t) => {
1123            let rows = t.rows();
1124            let cols = t.cols();
1125            if rows > 1 && cols > 1 {
1126                let mut out = vec![0.0f64; cols];
1127                for (c, oc) in out.iter_mut().enumerate().take(cols) {
1128                    let mut v = 0.0;
1129                    for r in 0..rows {
1130                        if t.data[r + c * rows] != 0.0 {
1131                            v = 1.0;
1132                            break;
1133                        }
1134                    }
1135                    *oc = v;
1136                }
1137                Ok(Value::Tensor(
1138                    runmat_builtins::Tensor::new(out, vec![1, cols])
1139                        .map_err(|e| format!("any: {e}"))?,
1140                ))
1141            } else {
1142                Ok(Value::Num(if t.data.iter().any(|&x| x != 0.0) {
1143                    1.0
1144                } else {
1145                    0.0
1146                }))
1147            }
1148        }
1149        _ => Err("any: expected tensor".to_string()),
1150    }
1151}
1152
1153fn any_dim(a: Value, dim: f64) -> Result<Value, String> {
1154    let t = match a {
1155        Value::Tensor(t) => t,
1156        _ => return Err("any: expected tensor".to_string()),
1157    };
1158    let dim = if dim < 1.0 { 1usize } else { dim as usize };
1159    let rows = t.rows();
1160    let cols = t.cols();
1161    if dim == 1 {
1162        let mut out = vec![0.0f64; cols];
1163        for (c, oc) in out.iter_mut().enumerate().take(cols) {
1164            let mut v = 0.0;
1165            for r in 0..rows {
1166                if t.data[r + c * rows] != 0.0 {
1167                    v = 1.0;
1168                    break;
1169                }
1170            }
1171            *oc = v;
1172        }
1173        Ok(Value::Tensor(
1174            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("any: {e}"))?,
1175        ))
1176    } else if dim == 2 {
1177        let mut out = vec![0.0f64; rows];
1178        for (r, orow) in out.iter_mut().enumerate().take(rows) {
1179            let mut v = 0.0;
1180            for c in 0..cols {
1181                if t.data[r + c * rows] != 0.0 {
1182                    v = 1.0;
1183                    break;
1184                }
1185            }
1186            *orow = v;
1187        }
1188        Ok(Value::Tensor(
1189            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("any: {e}"))?,
1190        ))
1191    } else {
1192        Err("any: dim out of range".to_string())
1193    }
1194}
1195
1196#[runmat_macros::runtime_builtin(name = "any")]
1197fn any_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
1198    if rest.is_empty() {
1199        return any_all_or_cols(a);
1200    }
1201    if rest.len() == 1 {
1202        match &rest[0] {
1203            Value::Num(d) => return any_dim(a, *d),
1204            Value::Int(i) => return any_dim(a, i.to_i64() as f64),
1205            _ => {}
1206        }
1207    }
1208    Err("any: unsupported arguments".to_string())
1209}
1210
1211fn all_all_or_cols(a: Value) -> Result<Value, String> {
1212    match a {
1213        Value::Tensor(t) => {
1214            let rows = t.rows();
1215            let cols = t.cols();
1216            if rows > 1 && cols > 1 {
1217                let mut out = vec![0.0f64; cols];
1218                for (c, oc) in out.iter_mut().enumerate().take(cols) {
1219                    let mut v = 1.0;
1220                    for r in 0..rows {
1221                        if t.data[r + c * rows] == 0.0 {
1222                            v = 0.0;
1223                            break;
1224                        }
1225                    }
1226                    *oc = v;
1227                }
1228                Ok(Value::Tensor(
1229                    runmat_builtins::Tensor::new(out, vec![1, cols])
1230                        .map_err(|e| format!("all: {e}"))?,
1231                ))
1232            } else {
1233                Ok(Value::Num(if t.data.iter().all(|&x| x != 0.0) {
1234                    1.0
1235                } else {
1236                    0.0
1237                }))
1238            }
1239        }
1240        _ => Err("all: expected tensor".to_string()),
1241    }
1242}
1243
1244fn all_dim(a: Value, dim: f64) -> Result<Value, String> {
1245    let t = match a {
1246        Value::Tensor(t) => t,
1247        _ => return Err("all: expected tensor".to_string()),
1248    };
1249    let dim = if dim < 1.0 { 1usize } else { dim as usize };
1250    let rows = t.rows();
1251    let cols = t.cols();
1252    if dim == 1 {
1253        let mut out = vec![0.0f64; cols];
1254        for (c, oc) in out.iter_mut().enumerate().take(cols) {
1255            let mut v = 1.0;
1256            for r in 0..rows {
1257                if t.data[r + c * rows] == 0.0 {
1258                    v = 0.0;
1259                    break;
1260                }
1261            }
1262            *oc = v;
1263        }
1264        Ok(Value::Tensor(
1265            runmat_builtins::Tensor::new(out, vec![1, cols]).map_err(|e| format!("all: {e}"))?,
1266        ))
1267    } else if dim == 2 {
1268        let mut out = vec![0.0f64; rows];
1269        for (r, orow) in out.iter_mut().enumerate().take(rows) {
1270            let mut v = 1.0;
1271            for c in 0..cols {
1272                if t.data[r + c * rows] == 0.0 {
1273                    v = 0.0;
1274                    break;
1275                }
1276            }
1277            *orow = v;
1278        }
1279        Ok(Value::Tensor(
1280            runmat_builtins::Tensor::new(out, vec![rows, 1]).map_err(|e| format!("all: {e}"))?,
1281        ))
1282    } else {
1283        Err("all: dim out of range".to_string())
1284    }
1285}
1286
1287#[runmat_macros::runtime_builtin(name = "all")]
1288fn all_var_builtin(a: Value, rest: Vec<Value>) -> Result<Value, String> {
1289    if rest.is_empty() {
1290        return all_all_or_cols(a);
1291    }
1292    if rest.len() == 1 {
1293        match &rest[0] {
1294            Value::Num(d) => return all_dim(a, *d),
1295            Value::Int(i) => return all_dim(a, i.to_i64() as f64),
1296            _ => {}
1297        }
1298    }
1299    Err("all: unsupported arguments".to_string())
1300}
1301
1302#[runmat_macros::runtime_builtin(name = "warning", sink = true)]
1303fn warning_builtin(fmt: String, rest: Vec<Value>) -> Result<Value, String> {
1304    let s = format_variadic(&fmt, &rest)?;
1305    eprintln!("Warning: {s}");
1306    Ok(Value::Num(0.0))
1307}
1308
1309#[runmat_macros::runtime_builtin(name = "getmethod")]
1310fn getmethod_builtin(obj: Value, name: String) -> Result<Value, String> {
1311    match obj {
1312        Value::Object(o) => {
1313            // Return a closure capturing the receiver; feval will call runtime builtin call_method
1314            Ok(Value::Closure(runmat_builtins::Closure {
1315                function_name: "call_method".to_string(),
1316                captures: vec![Value::Object(o), Value::String(name)],
1317            }))
1318        }
1319        Value::ClassRef(cls) => Ok(Value::String(format!("@{cls}.{name}"))),
1320        other => Err(format!("getmethod unsupported on {other:?}")),
1321    }
1322}