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