Skip to main content

runmat_builtins/
lib.rs

1pub use inventory;
2use runmat_gc_api::GcPtr;
3use std::collections::HashMap;
4use std::convert::TryFrom;
5use std::fmt;
6use std::future::Future;
7use std::pin::Pin;
8
9use indexmap::IndexMap;
10use std::sync::OnceLock;
11
12#[cfg(target_arch = "wasm32")]
13pub mod wasm_registry {
14    use super::{BuiltinDoc, BuiltinFunction, Constant};
15    use once_cell::sync::Lazy;
16    use std::sync::Mutex;
17
18    static FUNCTIONS: Lazy<Mutex<Vec<&'static BuiltinFunction>>> =
19        Lazy::new(|| Mutex::new(Vec::new()));
20    static CONSTANTS: Lazy<Mutex<Vec<&'static Constant>>> = Lazy::new(|| Mutex::new(Vec::new()));
21    static DOCS: Lazy<Mutex<Vec<&'static BuiltinDoc>>> = Lazy::new(|| Mutex::new(Vec::new()));
22    static REGISTERED: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
23
24    fn leak<T>(value: T) -> &'static T {
25        Box::leak(Box::new(value))
26    }
27
28    pub fn submit_builtin_function(func: BuiltinFunction) {
29        let leaked = leak(func);
30        FUNCTIONS.lock().unwrap().push(leaked);
31    }
32
33    pub fn submit_constant(constant: Constant) {
34        let leaked = leak(constant);
35        CONSTANTS.lock().unwrap().push(leaked);
36    }
37
38    pub fn submit_builtin_doc(doc: BuiltinDoc) {
39        let leaked = leak(doc);
40        DOCS.lock().unwrap().push(leaked);
41    }
42
43    pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
44        FUNCTIONS.lock().unwrap().clone()
45    }
46
47    pub fn constants() -> Vec<&'static Constant> {
48        CONSTANTS.lock().unwrap().clone()
49    }
50
51    pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
52        DOCS.lock().unwrap().clone()
53    }
54
55    pub fn mark_registered() {
56        *REGISTERED.lock().unwrap() = true;
57    }
58
59    pub fn is_registered() -> bool {
60        *REGISTERED.lock().unwrap()
61    }
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub enum Value {
66    Int(IntValue),
67    Num(f64),
68    /// Complex scalar value represented as (re, im)
69    Complex(f64, f64),
70    Bool(bool),
71    // Logical array (N-D of booleans). Scalars use Bool.
72    LogicalArray(LogicalArray),
73    String(String),
74    // String array (R2016b+): N-D array of string scalars
75    StringArray(StringArray),
76    // Char array (single-quoted): 2-D character array (rows x cols)
77    CharArray(CharArray),
78    Tensor(Tensor),
79    /// Complex numeric array; same column-major shape semantics as `Tensor`
80    ComplexTensor(ComplexTensor),
81    Cell(CellArray),
82    // Struct (scalar or nested). Struct arrays are represented in higher layers;
83    // this variant holds a single struct's fields.
84    Struct(StructValue),
85    // GPU-resident tensor handle (opaque; buffer managed by backend)
86    GpuTensor(runmat_accelerate_api::GpuTensorHandle),
87    // Simple object instance until full class system lands
88    Object(ObjectInstance),
89    /// Handle-object wrapper providing identity semantics and validity tracking
90    HandleObject(HandleRef),
91    /// Event listener handle for events
92    Listener(Listener),
93    /// Multiple outputs captured as a list (internal destructuring helper)
94    OutputList(Vec<Value>),
95    // Function handle pointing to a named function (builtin or user)
96    FunctionHandle(String),
97    Closure(Closure),
98    ClassRef(String),
99    MException(MException),
100}
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum IntValue {
103    I8(i8),
104    I16(i16),
105    I32(i32),
106    I64(i64),
107    U8(u8),
108    U16(u16),
109    U32(u32),
110    U64(u64),
111}
112
113impl IntValue {
114    pub fn to_i64(&self) -> i64 {
115        match self {
116            IntValue::I8(v) => *v as i64,
117            IntValue::I16(v) => *v as i64,
118            IntValue::I32(v) => *v as i64,
119            IntValue::I64(v) => *v,
120            IntValue::U8(v) => *v as i64,
121            IntValue::U16(v) => *v as i64,
122            IntValue::U32(v) => *v as i64,
123            IntValue::U64(v) => {
124                if *v > i64::MAX as u64 {
125                    i64::MAX
126                } else {
127                    *v as i64
128                }
129            }
130        }
131    }
132    pub fn to_f64(&self) -> f64 {
133        self.to_i64() as f64
134    }
135    pub fn is_zero(&self) -> bool {
136        self.to_i64() == 0
137    }
138    pub fn class_name(&self) -> &'static str {
139        match self {
140            IntValue::I8(_) => "int8",
141            IntValue::I16(_) => "int16",
142            IntValue::I32(_) => "int32",
143            IntValue::I64(_) => "int64",
144            IntValue::U8(_) => "uint8",
145            IntValue::U16(_) => "uint16",
146            IntValue::U32(_) => "uint32",
147            IntValue::U64(_) => "uint64",
148        }
149    }
150}
151
152#[derive(Debug, Clone, PartialEq)]
153pub struct StructValue {
154    pub fields: IndexMap<String, Value>,
155}
156
157impl StructValue {
158    pub fn new() -> Self {
159        Self {
160            fields: IndexMap::new(),
161        }
162    }
163
164    /// Insert a field, preserving insertion order when the name is new.
165    pub fn insert(&mut self, name: impl Into<String>, value: Value) -> Option<Value> {
166        self.fields.insert(name.into(), value)
167    }
168
169    /// Remove a field while preserving the relative order of remaining fields.
170    pub fn remove(&mut self, name: &str) -> Option<Value> {
171        self.fields.shift_remove(name)
172    }
173
174    /// Returns an iterator over field names in their stored order.
175    pub fn field_names(&self) -> impl Iterator<Item = &String> {
176        self.fields.keys()
177    }
178}
179
180impl Default for StructValue {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187pub enum NumericDType {
188    F64,
189    F32,
190}
191
192#[derive(Debug, Clone, PartialEq)]
193pub struct Tensor {
194    pub data: Vec<f64>,
195    pub shape: Vec<usize>, // Column-major layout
196    pub rows: usize,       // Compatibility for 2D usage
197    pub cols: usize,       // Compatibility for 2D usage
198    /// Logical numeric class of this tensor; host storage remains f64.
199    pub dtype: NumericDType,
200}
201
202#[derive(Debug, Clone, PartialEq)]
203pub struct ComplexTensor {
204    pub data: Vec<(f64, f64)>,
205    pub shape: Vec<usize>,
206    pub rows: usize,
207    pub cols: usize,
208}
209
210#[derive(Debug, Clone, PartialEq)]
211pub struct StringArray {
212    pub data: Vec<String>,
213    pub shape: Vec<usize>,
214    pub rows: usize,
215    pub cols: usize,
216}
217
218#[derive(Debug, Clone, PartialEq)]
219pub struct LogicalArray {
220    pub data: Vec<u8>, // 0 or 1 values; compact bitset can come later
221    pub shape: Vec<usize>,
222}
223
224impl LogicalArray {
225    pub fn new(data: Vec<u8>, shape: Vec<usize>) -> Result<Self, String> {
226        let expected: usize = shape.iter().product();
227        if data.len() != expected {
228            return Err(format!(
229                "LogicalArray data length {} doesn't match shape {:?} ({} elements)",
230                data.len(),
231                shape,
232                expected
233            ));
234        }
235        // Normalize to 0/1
236        let mut d = data;
237        for v in &mut d {
238            *v = if *v != 0 { 1 } else { 0 };
239        }
240        Ok(LogicalArray { data: d, shape })
241    }
242    pub fn zeros(shape: Vec<usize>) -> Self {
243        let expected: usize = shape.iter().product();
244        LogicalArray {
245            data: vec![0u8; expected],
246            shape,
247        }
248    }
249    pub fn len(&self) -> usize {
250        self.data.len()
251    }
252    pub fn is_empty(&self) -> bool {
253        self.data.is_empty()
254    }
255}
256
257#[derive(Debug, Clone, PartialEq)]
258pub struct CharArray {
259    pub data: Vec<char>,
260    pub rows: usize,
261    pub cols: usize,
262}
263
264impl CharArray {
265    pub fn new_row(s: &str) -> Self {
266        CharArray {
267            data: s.chars().collect(),
268            rows: 1,
269            cols: s.chars().count(),
270        }
271    }
272    pub fn new(data: Vec<char>, rows: usize, cols: usize) -> Result<Self, String> {
273        if rows * cols != data.len() {
274            return Err(format!(
275                "Char data length {} doesn't match dimensions {}x{}",
276                data.len(),
277                rows,
278                cols
279            ));
280        }
281        Ok(CharArray { data, rows, cols })
282    }
283}
284
285impl StringArray {
286    pub fn new(data: Vec<String>, shape: Vec<usize>) -> Result<Self, String> {
287        let expected: usize = shape.iter().product();
288        if data.len() != expected {
289            return Err(format!(
290                "StringArray data length {} doesn't match shape {:?} ({} elements)",
291                data.len(),
292                shape,
293                expected
294            ));
295        }
296        let (rows, cols) = if shape.len() >= 2 {
297            (shape[0], shape[1])
298        } else if shape.len() == 1 {
299            (1, shape[0])
300        } else {
301            (0, 0)
302        };
303        Ok(StringArray {
304            data,
305            shape,
306            rows,
307            cols,
308        })
309    }
310    pub fn new_2d(data: Vec<String>, rows: usize, cols: usize) -> Result<Self, String> {
311        Self::new(data, vec![rows, cols])
312    }
313    pub fn rows(&self) -> usize {
314        self.shape.first().copied().unwrap_or(1)
315    }
316    pub fn cols(&self) -> usize {
317        self.shape.get(1).copied().unwrap_or(1)
318    }
319}
320
321// GpuTensorHandle now lives in runmat-accel-api
322
323impl Tensor {
324    pub fn new(data: Vec<f64>, shape: Vec<usize>) -> Result<Self, String> {
325        let expected: usize = shape.iter().product();
326        if data.len() != expected {
327            return Err(format!(
328                "Tensor data length {} doesn't match shape {:?} ({} elements)",
329                data.len(),
330                shape,
331                expected
332            ));
333        }
334        let (rows, cols) = if shape.len() >= 2 {
335            (shape[0], shape[1])
336        } else if shape.len() == 1 {
337            (1, shape[0])
338        } else {
339            (0, 0)
340        };
341        Ok(Tensor {
342            data,
343            shape,
344            rows,
345            cols,
346            dtype: NumericDType::F64,
347        })
348    }
349
350    pub fn new_2d(data: Vec<f64>, rows: usize, cols: usize) -> Result<Self, String> {
351        Self::new(data, vec![rows, cols])
352    }
353
354    pub fn from_f32(data: Vec<f32>, shape: Vec<usize>) -> Result<Self, String> {
355        let converted: Vec<f64> = data.into_iter().map(|v| v as f64).collect();
356        Self::new_with_dtype(converted, shape, NumericDType::F32)
357    }
358
359    pub fn from_f32_slice(data: &[f32], shape: &[usize]) -> Result<Self, String> {
360        let converted: Vec<f64> = data.iter().map(|&v| v as f64).collect();
361        Self::new_with_dtype(converted, shape.to_vec(), NumericDType::F32)
362    }
363
364    pub fn new_with_dtype(
365        data: Vec<f64>,
366        shape: Vec<usize>,
367        dtype: NumericDType,
368    ) -> Result<Self, String> {
369        let mut t = Self::new(data, shape)?;
370        t.dtype = dtype;
371        Ok(t)
372    }
373
374    pub fn zeros(shape: Vec<usize>) -> Self {
375        let size: usize = shape.iter().product();
376        let (rows, cols) = if shape.len() >= 2 {
377            (shape[0], shape[1])
378        } else if shape.len() == 1 {
379            (1, shape[0])
380        } else {
381            (0, 0)
382        };
383        Tensor {
384            data: vec![0.0; size],
385            shape,
386            rows,
387            cols,
388            dtype: NumericDType::F64,
389        }
390    }
391
392    pub fn ones(shape: Vec<usize>) -> Self {
393        let size: usize = shape.iter().product();
394        let (rows, cols) = if shape.len() >= 2 {
395            (shape[0], shape[1])
396        } else if shape.len() == 1 {
397            (1, shape[0])
398        } else {
399            (0, 0)
400        };
401        Tensor {
402            data: vec![1.0; size],
403            shape,
404            rows,
405            cols,
406            dtype: NumericDType::F64,
407        }
408    }
409
410    // 2D helpers for transitional call sites
411    pub fn zeros2(rows: usize, cols: usize) -> Self {
412        Self::zeros(vec![rows, cols])
413    }
414    pub fn ones2(rows: usize, cols: usize) -> Self {
415        Self::ones(vec![rows, cols])
416    }
417
418    pub fn rows(&self) -> usize {
419        self.shape.first().copied().unwrap_or(1)
420    }
421    pub fn cols(&self) -> usize {
422        self.shape.get(1).copied().unwrap_or(1)
423    }
424
425    pub fn get2(&self, row: usize, col: usize) -> Result<f64, String> {
426        let rows = self.rows();
427        let cols = self.cols();
428        if row >= rows || col >= cols {
429            return Err(format!(
430                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
431            ));
432        }
433        // Column-major linearization: lin = row + col*rows
434        Ok(self.data[row + col * rows])
435    }
436
437    pub fn set2(&mut self, row: usize, col: usize, value: f64) -> Result<(), String> {
438        let rows = self.rows();
439        let cols = self.cols();
440        if row >= rows || col >= cols {
441            return Err(format!(
442                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
443            ));
444        }
445        // Column-major linearization
446        self.data[row + col * rows] = value;
447        Ok(())
448    }
449
450    pub fn scalar_to_tensor2(scalar: f64, rows: usize, cols: usize) -> Tensor {
451        Tensor {
452            data: vec![scalar; rows * cols],
453            shape: vec![rows, cols],
454            rows,
455            cols,
456            dtype: NumericDType::F64,
457        }
458    }
459    // No-compat constructors: prefer new/new_2d/zeros/zeros2/ones/ones2
460}
461
462impl ComplexTensor {
463    pub fn new(data: Vec<(f64, f64)>, shape: Vec<usize>) -> Result<Self, String> {
464        let expected: usize = shape.iter().product();
465        if data.len() != expected {
466            return Err(format!(
467                "ComplexTensor data length {} doesn't match shape {:?} ({} elements)",
468                data.len(),
469                shape,
470                expected
471            ));
472        }
473        let (rows, cols) = if shape.len() >= 2 {
474            (shape[0], shape[1])
475        } else if shape.len() == 1 {
476            (1, shape[0])
477        } else {
478            (0, 0)
479        };
480        Ok(ComplexTensor {
481            data,
482            shape,
483            rows,
484            cols,
485        })
486    }
487    pub fn new_2d(data: Vec<(f64, f64)>, rows: usize, cols: usize) -> Result<Self, String> {
488        Self::new(data, vec![rows, cols])
489    }
490    pub fn zeros(shape: Vec<usize>) -> Self {
491        let size: usize = shape.iter().product();
492        let (rows, cols) = if shape.len() >= 2 {
493            (shape[0], shape[1])
494        } else if shape.len() == 1 {
495            (1, shape[0])
496        } else {
497            (0, 0)
498        };
499        ComplexTensor {
500            data: vec![(0.0, 0.0); size],
501            shape,
502            rows,
503            cols,
504        }
505    }
506}
507
508const MAX_ND_DISPLAY_ELEMENTS: usize = 4096;
509
510fn should_expand_nd_display(shape: &[usize]) -> bool {
511    shape.len() > 2
512        && matches!(
513            total_len(shape),
514            Some(total) if total > 0 && total <= MAX_ND_DISPLAY_ELEMENTS
515        )
516}
517
518fn column_major_strides(shape: &[usize]) -> Vec<usize> {
519    let mut strides = Vec::with_capacity(shape.len());
520    let mut stride = 1usize;
521    for &dim in shape {
522        strides.push(stride);
523        stride = stride.saturating_mul(dim);
524    }
525    strides
526}
527
528fn decode_page_coords(mut page_index: usize, page_shape: &[usize]) -> Vec<usize> {
529    let mut coords = Vec::with_capacity(page_shape.len());
530    for &dim in page_shape {
531        if dim == 0 {
532            coords.push(0);
533        } else {
534            coords.push(page_index % dim);
535            page_index /= dim;
536        }
537    }
538    coords
539}
540
541fn write_nd_pages(
542    f: &mut fmt::Formatter<'_>,
543    shape: &[usize],
544    mut write_element: impl FnMut(&mut fmt::Formatter<'_>, usize) -> fmt::Result,
545) -> fmt::Result {
546    if shape.len() <= 2 {
547        return Ok(());
548    }
549    let rows = shape[0];
550    let cols = shape[1];
551    if rows == 0 || cols == 0 {
552        return write!(f, "[]");
553    }
554    let Some(page_count) = total_len(&shape[2..]) else {
555        return write!(f, "Tensor(shape={shape:?})");
556    };
557    if page_count == 0 {
558        return write!(f, "[]");
559    }
560    let strides = column_major_strides(shape);
561    for page_index in 0..page_count {
562        if page_index > 0 {
563            write!(f, "\n\n")?;
564        }
565        let coords = decode_page_coords(page_index, &shape[2..]);
566        write!(f, "(:, :")?;
567        for &coord in &coords {
568            write!(f, ", {}", coord + 1)?;
569        }
570        write!(f, ") =")?;
571
572        let mut page_base = 0usize;
573        for (offset, &coord) in coords.iter().enumerate() {
574            page_base += coord * strides[offset + 2];
575        }
576        for r in 0..rows {
577            writeln!(f)?;
578            write!(f, "  ")?;
579            for c in 0..cols {
580                if c > 0 {
581                    write!(f, "  ")?;
582                }
583                let linear = page_base + r + c * rows;
584                write_element(f, linear)?;
585            }
586        }
587    }
588    Ok(())
589}
590
591impl fmt::Display for Tensor {
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        match self.shape.len() {
594            0 | 1 => {
595                // Treat as row vector for display
596                write!(f, "[")?;
597                for (i, v) in self.data.iter().enumerate() {
598                    if i > 0 {
599                        write!(f, " ")?;
600                    }
601                    write!(f, "{}", format_number_short_g(*v))?;
602                }
603                write!(f, "]")
604            }
605            2 => {
606                let rows = self.rows();
607                let cols = self.cols();
608                // Display as matrix
609                for r in 0..rows {
610                    writeln!(f)?;
611                    write!(f, "  ")?; // Indent
612                    for c in 0..cols {
613                        if c > 0 {
614                            write!(f, "  ")?;
615                        }
616                        let v = self.data[r + c * rows];
617                        write!(f, "{}", format_number_short_g(v))?;
618                    }
619                }
620                Ok(())
621            }
622            _ => {
623                if should_expand_nd_display(&self.shape) {
624                    write_nd_pages(f, &self.shape, |f, idx| {
625                        write!(f, "{}", format_number_short_g(self.data[idx]))
626                    })
627                } else {
628                    write!(f, "Tensor(shape={:?})", self.shape)
629                }
630            }
631        }
632    }
633}
634
635impl fmt::Display for StringArray {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637        let (rows, cols) = match self.shape.len() {
638            0 => (0, 0),
639            1 => (1, self.shape[0]),
640            _ => (self.shape[0], self.shape[1]),
641        };
642        let count = self.data.len();
643        if count == 1 && rows == 1 && cols == 1 {
644            let v = &self.data[0];
645            if v == "<missing>" {
646                return write!(f, "<missing>");
647            }
648            let escaped = v.replace('"', "\\\"");
649            return write!(f, "\"{escaped}\"");
650        }
651        if self.shape.len() > 2 {
652            let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
653            return write!(f, "{} string array", dims.join("x"));
654        }
655        write!(f, "{rows}x{cols} string array")?;
656        if rows == 0 || cols == 0 {
657            return Ok(());
658        }
659        for r in 0..rows {
660            writeln!(f)?;
661            write!(f, "  ")?;
662            for c in 0..cols {
663                if c > 0 {
664                    write!(f, "  ")?;
665                }
666                let v = &self.data[r + c * rows];
667                if v == "<missing>" {
668                    write!(f, "<missing>")?;
669                } else {
670                    let escaped = v.replace('"', "\\\"");
671                    write!(f, "\"{escaped}\"")?;
672                }
673            }
674        }
675        Ok(())
676    }
677}
678
679impl fmt::Display for LogicalArray {
680    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681        if self.data.len() == 1 {
682            return write!(f, "{}", if self.data[0] != 0 { 1 } else { 0 });
683        }
684        match self.shape.len() {
685            0 => write!(f, "[]"),
686            1 => {
687                write!(f, "[")?;
688                for (i, v) in self.data.iter().enumerate() {
689                    if i > 0 {
690                        write!(f, " ")?;
691                    }
692                    write!(f, "{}", if *v != 0 { 1 } else { 0 })?;
693                }
694                write!(f, "]")
695            }
696            2 => {
697                let rows = self.shape[0];
698                let cols = self.shape[1];
699                // Display as matrix
700                for r in 0..rows {
701                    writeln!(f)?;
702                    write!(f, "  ")?; // Indent
703                    for c in 0..cols {
704                        if c > 0 {
705                            write!(f, "  ")?;
706                        }
707                        let idx = r + c * rows;
708                        write!(f, "{}", if self.data[idx] != 0 { 1 } else { 0 })?;
709                    }
710                }
711                Ok(())
712            }
713            _ => {
714                if should_expand_nd_display(&self.shape) {
715                    write_nd_pages(f, &self.shape, |f, idx| {
716                        write!(f, "{}", if self.data[idx] != 0 { 1 } else { 0 })
717                    })
718                } else {
719                    let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
720                    write!(f, "{} logical array", dims.join("x"))
721                }
722            }
723        }
724    }
725}
726
727impl fmt::Display for CharArray {
728    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729        for r in 0..self.rows {
730            writeln!(f)?;
731            write!(f, "  ")?; // Indent
732            for c in 0..self.cols {
733                let ch = self.data[r * self.cols + c];
734                write!(f, "{ch}")?;
735            }
736        }
737        Ok(())
738    }
739}
740
741// From implementations for Value
742impl From<i32> for Value {
743    fn from(i: i32) -> Self {
744        Value::Int(IntValue::I32(i))
745    }
746}
747impl From<i64> for Value {
748    fn from(i: i64) -> Self {
749        Value::Int(IntValue::I64(i))
750    }
751}
752impl From<u32> for Value {
753    fn from(i: u32) -> Self {
754        Value::Int(IntValue::U32(i))
755    }
756}
757impl From<u64> for Value {
758    fn from(i: u64) -> Self {
759        Value::Int(IntValue::U64(i))
760    }
761}
762impl From<i16> for Value {
763    fn from(i: i16) -> Self {
764        Value::Int(IntValue::I16(i))
765    }
766}
767impl From<i8> for Value {
768    fn from(i: i8) -> Self {
769        Value::Int(IntValue::I8(i))
770    }
771}
772impl From<u16> for Value {
773    fn from(i: u16) -> Self {
774        Value::Int(IntValue::U16(i))
775    }
776}
777impl From<u8> for Value {
778    fn from(i: u8) -> Self {
779        Value::Int(IntValue::U8(i))
780    }
781}
782
783impl From<f64> for Value {
784    fn from(f: f64) -> Self {
785        Value::Num(f)
786    }
787}
788
789impl From<bool> for Value {
790    fn from(b: bool) -> Self {
791        Value::Bool(b)
792    }
793}
794
795impl From<String> for Value {
796    fn from(s: String) -> Self {
797        Value::String(s)
798    }
799}
800
801impl From<&str> for Value {
802    fn from(s: &str) -> Self {
803        Value::String(s.to_string())
804    }
805}
806
807impl From<Tensor> for Value {
808    fn from(m: Tensor) -> Self {
809        Value::Tensor(m)
810    }
811}
812
813// Remove blanket From<Vec<Value>> to avoid losing shape information
814
815// TryFrom implementations for extracting native types
816impl TryFrom<&Value> for i32 {
817    type Error = String;
818    fn try_from(v: &Value) -> Result<Self, Self::Error> {
819        match v {
820            Value::Int(i) => Ok(i.to_i64() as i32),
821            Value::Num(n) => Ok(*n as i32),
822            _ => Err(format!("cannot convert {v:?} to i32")),
823        }
824    }
825}
826
827impl TryFrom<&Value> for f64 {
828    type Error = String;
829    fn try_from(v: &Value) -> Result<Self, Self::Error> {
830        match v {
831            Value::Num(n) => Ok(*n),
832            Value::Int(i) => Ok(i.to_f64()),
833            _ => Err(format!("cannot convert {v:?} to f64")),
834        }
835    }
836}
837
838impl TryFrom<&Value> for bool {
839    type Error = String;
840    fn try_from(v: &Value) -> Result<Self, Self::Error> {
841        match v {
842            Value::Bool(b) => Ok(*b),
843            Value::Int(i) => Ok(!i.is_zero()),
844            Value::Num(n) => Ok(*n != 0.0),
845            _ => Err(format!("cannot convert {v:?} to bool")),
846        }
847    }
848}
849
850impl TryFrom<&Value> for String {
851    type Error = String;
852    fn try_from(v: &Value) -> Result<Self, Self::Error> {
853        match v {
854            Value::String(s) => Ok(s.clone()),
855            Value::StringArray(sa) => {
856                if sa.data.len() == 1 {
857                    Ok(sa.data[0].clone())
858                } else {
859                    Err("cannot convert string array to scalar string".to_string())
860                }
861            }
862            Value::CharArray(ca) => {
863                // Convert full char array to one string if it is a single row; else error
864                if ca.rows == 1 {
865                    Ok(ca.data.iter().collect())
866                } else {
867                    Err("cannot convert multi-row char array to scalar string".to_string())
868                }
869            }
870            Value::Int(i) => Ok(i.to_i64().to_string()),
871            Value::Num(n) => Ok(n.to_string()),
872            Value::Bool(b) => Ok(b.to_string()),
873            _ => Err(format!("cannot convert {v:?} to String")),
874        }
875    }
876}
877
878impl TryFrom<&Value> for Tensor {
879    type Error = String;
880    fn try_from(v: &Value) -> Result<Self, Self::Error> {
881        match v {
882            Value::Tensor(m) => Ok(m.clone()),
883            _ => Err(format!("cannot convert {v:?} to Tensor")),
884        }
885    }
886}
887
888impl TryFrom<&Value> for Value {
889    type Error = String;
890    fn try_from(v: &Value) -> Result<Self, Self::Error> {
891        Ok(v.clone())
892    }
893}
894
895impl TryFrom<&Value> for Vec<Value> {
896    type Error = String;
897    fn try_from(v: &Value) -> Result<Self, Self::Error> {
898        match v {
899            Value::Cell(c) => Ok(c.data.iter().map(|p| (**p).clone()).collect()),
900            _ => Err(format!("cannot convert {v:?} to Vec<Value>")),
901        }
902    }
903}
904
905use serde::{Deserialize, Serialize};
906
907/// Enhanced type system used throughout RunMat for HIR and builtin functions
908/// Designed to mirror Value variants for better type inference and LSP support
909#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
910pub enum Type {
911    /// Integer number type
912    Int,
913    /// Floating-point number type  
914    Num,
915    /// Boolean type
916    Bool,
917    /// Logical array type (N-D boolean array) with optional shape information
918    Logical {
919        /// Optional full shape; None means unknown/dynamic; individual dims can be omitted by using None
920        shape: Option<Vec<Option<usize>>>,
921    },
922    /// String type
923    String,
924    /// Tensor type with optional shape information (column-major semantics in runtime)
925    Tensor {
926        /// Optional full shape; None means unknown/dynamic; individual dims can be omitted by using None
927        shape: Option<Vec<Option<usize>>>,
928    },
929    /// Cell array type with optional element type information
930    Cell {
931        /// Optional element type (None means mixed/unknown)
932        element_type: Option<Box<Type>>,
933        /// Optional length (None means unknown/dynamic)
934        length: Option<usize>,
935    },
936    /// Function type with parameter and return types
937    Function {
938        /// Parameter types
939        params: Vec<Type>,
940        /// Return type
941        returns: Box<Type>,
942    },
943    /// Void type (no value)
944    Void,
945    /// Unknown type (for type inference)
946    Unknown,
947    /// Union type (multiple possible types)
948    Union(Vec<Type>),
949    /// Struct-like type with optional known field set (purely for inference)
950    Struct {
951        /// Optional set of known field names observed via control-flow (None = unknown fields)
952        known_fields: Option<Vec<String>>, // kept sorted unique for deterministic Eq
953    },
954    /// Multiple return values captured as a list (internal destructuring helper)
955    OutputList(Vec<Type>),
956    /// Dataset handle with optional compile-time schema information
957    DataDataset {
958        arrays: Option<std::collections::BTreeMap<String, DataArrayTypeInfo>>,
959    },
960    /// Data array handle with optional dtype/shape metadata
961    DataArray {
962        dtype: Option<String>,
963        shape: Option<Vec<Option<usize>>>,
964        chunk_shape: Option<Vec<Option<usize>>>,
965        codec: Option<String>,
966    },
967    /// Data transaction handle
968    DataTransaction,
969}
970
971#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
972pub struct DataArrayTypeInfo {
973    pub dtype: Option<String>,
974    pub shape: Option<Vec<Option<usize>>>,
975    pub chunk_shape: Option<Vec<Option<usize>>>,
976    pub codec: Option<String>,
977}
978
979impl Type {
980    /// Create a tensor type with unknown shape
981    pub fn tensor() -> Self {
982        Type::Tensor { shape: None }
983    }
984
985    /// Create a logical type with unknown shape
986    pub fn logical() -> Self {
987        Type::Logical { shape: None }
988    }
989
990    /// Create a logical type with known shape
991    pub fn logical_with_shape(shape: Vec<usize>) -> Self {
992        Type::Logical {
993            shape: Some(shape.into_iter().map(Some).collect()),
994        }
995    }
996
997    /// Create a tensor type with known shape
998    pub fn tensor_with_shape(shape: Vec<usize>) -> Self {
999        Type::Tensor {
1000            shape: Some(shape.into_iter().map(Some).collect()),
1001        }
1002    }
1003
1004    /// Create a cell array type with unknown element type
1005    pub fn cell() -> Self {
1006        Type::Cell {
1007            element_type: None,
1008            length: None,
1009        }
1010    }
1011
1012    /// Create a cell array type with known element type
1013    pub fn cell_of(element_type: Type) -> Self {
1014        Type::Cell {
1015            element_type: Some(Box::new(element_type)),
1016            length: None,
1017        }
1018    }
1019
1020    /// Check if this type is compatible with another type
1021    pub fn is_compatible_with(&self, other: &Type) -> bool {
1022        match (self, other) {
1023            (Type::Unknown, _) | (_, Type::Unknown) => true,
1024            (Type::Int, Type::Num) | (Type::Num, Type::Int) => true, // Number compatibility
1025            (Type::Tensor { .. }, Type::Tensor { .. }) => true, // Tensor compatibility regardless of dims for now
1026            (Type::OutputList(a), Type::OutputList(b)) => a.len() == b.len(),
1027            (Type::DataDataset { .. }, Type::DataDataset { .. }) => true,
1028            (Type::DataArray { .. }, Type::DataArray { .. }) => true,
1029            (Type::DataTransaction, Type::DataTransaction) => true,
1030            (a, b) => a == b,
1031        }
1032    }
1033
1034    /// Get the most specific common type between two types
1035    pub fn unify(&self, other: &Type) -> Type {
1036        match (self, other) {
1037            (Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
1038            (Type::Int, Type::Num) | (Type::Num, Type::Int) => Type::Num,
1039            (Type::Tensor { shape: a }, Type::Tensor { shape: b }) => {
1040                let a_norm = match a {
1041                    Some(dims) if dims.is_empty() => None,
1042                    _ => a.clone(),
1043                };
1044                let b_norm = match b {
1045                    Some(dims) if dims.is_empty() => None,
1046                    _ => b.clone(),
1047                };
1048                let a_unknown = a_norm
1049                    .as_ref()
1050                    .map(|dims| dims.iter().all(|d| d.is_none()))
1051                    .unwrap_or(true);
1052                let b_unknown = b_norm
1053                    .as_ref()
1054                    .map(|dims| dims.iter().all(|d| d.is_none()))
1055                    .unwrap_or(true);
1056                if a_norm == b_norm
1057                    || (!a_unknown && b_unknown)
1058                    || (a_norm.is_some() && b_norm.is_none())
1059                {
1060                    Type::Tensor { shape: a_norm }
1061                } else if (a_unknown && !b_unknown) || (a_norm.is_none() && b_norm.is_some()) {
1062                    Type::Tensor { shape: b_norm }
1063                } else {
1064                    Type::tensor()
1065                }
1066            }
1067            (Type::Logical { shape: a }, Type::Logical { shape: b }) => {
1068                let a_norm = match a {
1069                    Some(dims) if dims.is_empty() => None,
1070                    _ => a.clone(),
1071                };
1072                let b_norm = match b {
1073                    Some(dims) if dims.is_empty() => None,
1074                    _ => b.clone(),
1075                };
1076                let a_unknown = a_norm
1077                    .as_ref()
1078                    .map(|dims| dims.iter().all(|d| d.is_none()))
1079                    .unwrap_or(true);
1080                let b_unknown = b_norm
1081                    .as_ref()
1082                    .map(|dims| dims.iter().all(|d| d.is_none()))
1083                    .unwrap_or(true);
1084                if a_norm == b_norm
1085                    || (!a_unknown && b_unknown)
1086                    || (a_norm.is_some() && b_norm.is_none())
1087                {
1088                    Type::Logical { shape: a_norm }
1089                } else if (a_unknown && !b_unknown) || (a_norm.is_none() && b_norm.is_some()) {
1090                    Type::Logical { shape: b_norm }
1091                } else {
1092                    Type::logical()
1093                }
1094            }
1095            (Type::Struct { known_fields: a }, Type::Struct { known_fields: b }) => match (a, b) {
1096                (None, None) => Type::Struct { known_fields: None },
1097                (Some(ka), None) | (None, Some(ka)) => Type::Struct {
1098                    known_fields: Some(ka.clone()),
1099                },
1100                (Some(ka), Some(kb)) => {
1101                    let mut set: std::collections::BTreeSet<String> = ka.iter().cloned().collect();
1102                    set.extend(kb.iter().cloned());
1103                    Type::Struct {
1104                        known_fields: Some(set.into_iter().collect()),
1105                    }
1106                }
1107            },
1108            (Type::OutputList(a), Type::OutputList(b)) => {
1109                if a.len() == b.len() {
1110                    let items = a
1111                        .iter()
1112                        .zip(b.iter())
1113                        .map(|(lhs, rhs)| lhs.unify(rhs))
1114                        .collect();
1115                    Type::OutputList(items)
1116                } else {
1117                    Type::OutputList(vec![Type::Unknown; a.len().max(b.len())])
1118                }
1119            }
1120            (Type::DataDataset { arrays: a }, Type::DataDataset { arrays: b }) => {
1121                let merged = match (a, b) {
1122                    (None, None) => None,
1123                    (Some(sa), None) | (None, Some(sa)) => Some(sa.clone()),
1124                    (Some(sa), Some(sb)) => {
1125                        let mut out = sa.clone();
1126                        for (name, right) in sb {
1127                            out.entry(name.clone())
1128                                .and_modify(|left| {
1129                                    *left = unify_array_type_info(left, right);
1130                                })
1131                                .or_insert_with(|| right.clone());
1132                        }
1133                        Some(out)
1134                    }
1135                };
1136                Type::DataDataset { arrays: merged }
1137            }
1138            (
1139                Type::DataArray {
1140                    dtype: ad,
1141                    shape: ashp,
1142                    chunk_shape: ach,
1143                    codec: ac,
1144                },
1145                Type::DataArray {
1146                    dtype: bd,
1147                    shape: bshp,
1148                    chunk_shape: bch,
1149                    codec: bc,
1150                },
1151            ) => Type::DataArray {
1152                dtype: ad.clone().or_else(|| bd.clone()),
1153                shape: unify_optional_dims(ashp, bshp),
1154                chunk_shape: unify_optional_dims(ach, bch),
1155                codec: ac.clone().or_else(|| bc.clone()),
1156            },
1157            (Type::DataTransaction, Type::DataTransaction) => Type::DataTransaction,
1158            (a, b) if a == b => a.clone(),
1159            _ => Type::Union(vec![self.clone(), other.clone()]),
1160        }
1161    }
1162
1163    /// Infer type from a Value
1164    pub fn from_value(value: &Value) -> Type {
1165        match value {
1166            Value::Int(_) => Type::Int,
1167            Value::Num(_) => Type::Num,
1168            Value::Complex(_, _) => Type::Num, // treat as numeric double (complex) in type system for now
1169            Value::Bool(_) => Type::Bool,
1170            Value::LogicalArray(arr) => Type::Logical {
1171                shape: Some(arr.shape.iter().map(|&d| Some(d)).collect()),
1172            },
1173            Value::String(_) => Type::String,
1174            Value::StringArray(_sa) => {
1175                // Model as Cell of String for type system for now
1176                Type::cell_of(Type::String)
1177            }
1178            Value::Tensor(t) => Type::Tensor {
1179                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
1180            },
1181            Value::ComplexTensor(t) => Type::Tensor {
1182                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
1183            },
1184            Value::Cell(cells) => {
1185                if cells.data.is_empty() {
1186                    Type::cell()
1187                } else {
1188                    // Infer element type from first element
1189                    let element_type = Type::from_value(&cells.data[0]);
1190                    Type::Cell {
1191                        element_type: Some(Box::new(element_type)),
1192                        length: Some(cells.data.len()),
1193                    }
1194                }
1195            }
1196            Value::GpuTensor(h) => Type::Tensor {
1197                shape: Some(h.shape.iter().map(|&d| Some(d)).collect()),
1198            },
1199            Value::Object(_) => Type::Unknown,
1200            Value::HandleObject(_) => Type::Unknown,
1201            Value::Listener(_) => Type::Unknown,
1202            Value::Struct(_) => Type::Struct { known_fields: None },
1203            Value::FunctionHandle(_) => Type::Function {
1204                params: vec![Type::Unknown],
1205                returns: Box::new(Type::Unknown),
1206            },
1207            Value::Closure(_) => Type::Function {
1208                params: vec![Type::Unknown],
1209                returns: Box::new(Type::Unknown),
1210            },
1211            Value::ClassRef(_) => Type::Unknown,
1212            Value::MException(_) => Type::Unknown,
1213            Value::CharArray(ca) => {
1214                // Treat as cell of char for type purposes; or a 2-D char matrix conceptually
1215                Type::Cell {
1216                    element_type: Some(Box::new(Type::String)),
1217                    length: Some(ca.rows * ca.cols),
1218                }
1219            }
1220            Value::OutputList(values) => {
1221                Type::OutputList(values.iter().map(Type::from_value).collect())
1222            }
1223        }
1224    }
1225}
1226
1227fn unify_optional_dims(
1228    lhs: &Option<Vec<Option<usize>>>,
1229    rhs: &Option<Vec<Option<usize>>>,
1230) -> Option<Vec<Option<usize>>> {
1231    match (lhs, rhs) {
1232        (None, None) => None,
1233        (Some(a), None) | (None, Some(a)) => Some(a.clone()),
1234        (Some(a), Some(b)) if a == b => Some(a.clone()),
1235        (Some(a), Some(b)) if a.len() == b.len() => Some(
1236            a.iter()
1237                .zip(b.iter())
1238                .map(|(x, y)| if x == y { *x } else { None })
1239                .collect(),
1240        ),
1241        (Some(_), Some(_)) => None,
1242    }
1243}
1244
1245fn unify_array_type_info(lhs: &DataArrayTypeInfo, rhs: &DataArrayTypeInfo) -> DataArrayTypeInfo {
1246    DataArrayTypeInfo {
1247        dtype: lhs.dtype.clone().or_else(|| rhs.dtype.clone()),
1248        shape: unify_optional_dims(&lhs.shape, &rhs.shape),
1249        chunk_shape: unify_optional_dims(&lhs.chunk_shape, &rhs.chunk_shape),
1250        codec: lhs.codec.clone().or_else(|| rhs.codec.clone()),
1251    }
1252}
1253
1254#[derive(Debug, Clone, PartialEq)]
1255pub struct Closure {
1256    pub function_name: String,
1257    pub captures: Vec<Value>,
1258}
1259
1260/// Acceleration metadata describing GPU-friendly characteristics of a builtin.
1261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262pub enum AccelTag {
1263    Unary,
1264    Elementwise,
1265    Reduction,
1266    MatMul,
1267    Transpose,
1268    ArrayConstruct,
1269}
1270
1271/// Control-flow type for builtins that may suspend or error.
1272pub type BuiltinControlFlow = runmat_async::RuntimeError;
1273
1274/// Async result type for builtins.
1275pub type BuiltinFuture = Pin<Box<dyn Future<Output = Result<Value, BuiltinControlFlow>> + 'static>>;
1276
1277#[derive(Clone, Debug, Default)]
1278pub struct ResolveContext {
1279    pub literal_args: Vec<LiteralValue>,
1280}
1281
1282#[derive(Clone, Debug, PartialEq)]
1283pub enum LiteralValue {
1284    Number(f64),
1285    Bool(bool),
1286    String(String),
1287    Vector(Vec<LiteralValue>),
1288    Unknown,
1289}
1290
1291impl ResolveContext {
1292    pub fn new(literal_args: Vec<LiteralValue>) -> Self {
1293        Self { literal_args }
1294    }
1295
1296    pub fn numeric_dims(&self) -> Vec<Option<usize>> {
1297        self.numeric_dims_from(0)
1298    }
1299
1300    pub fn numeric_dims_from(&self, start: usize) -> Vec<Option<usize>> {
1301        let slice = self.literal_args.get(start..).unwrap_or(&[]);
1302        if let Some(LiteralValue::Vector(values)) = slice.first() {
1303            return values
1304                .iter()
1305                .map(Self::numeric_dimension_from_literal)
1306                .collect();
1307        }
1308        slice
1309            .iter()
1310            .map(Self::numeric_dimension_from_literal)
1311            .collect()
1312    }
1313
1314    pub fn literal_string_at(&self, index: usize) -> Option<String> {
1315        match self.literal_args.get(index) {
1316            Some(LiteralValue::String(value)) => Some(value.to_ascii_lowercase()),
1317            _ => None,
1318        }
1319    }
1320
1321    pub fn literal_bool_at(&self, index: usize) -> Option<bool> {
1322        match self.literal_args.get(index) {
1323            Some(LiteralValue::Bool(value)) => Some(*value),
1324            _ => None,
1325        }
1326    }
1327
1328    pub fn literal_vector_at(&self, index: usize) -> Option<Vec<LiteralValue>> {
1329        match self.literal_args.get(index) {
1330            Some(LiteralValue::Vector(values)) => Some(values.clone()),
1331            _ => None,
1332        }
1333    }
1334
1335    pub fn numeric_vector_at(&self, index: usize) -> Option<Vec<Option<usize>>> {
1336        let values = match self.literal_args.get(index) {
1337            Some(LiteralValue::Vector(values)) => values,
1338            _ => return None,
1339        };
1340        if values
1341            .iter()
1342            .any(|value| matches!(value, LiteralValue::Vector(_)))
1343        {
1344            return None;
1345        }
1346        Some(
1347            values
1348                .iter()
1349                .map(Self::numeric_dimension_from_literal)
1350                .collect(),
1351        )
1352    }
1353
1354    fn numeric_dimension_from_literal(value: &LiteralValue) -> Option<usize> {
1355        match value {
1356            LiteralValue::Number(num) => {
1357                if num.is_finite() {
1358                    let rounded = num.round();
1359                    if (num - rounded).abs() <= 1e-9 && rounded >= 0.0 {
1360                        return Some(rounded as usize);
1361                    }
1362                }
1363                None
1364            }
1365            _ => None,
1366        }
1367    }
1368}
1369
1370#[cfg(test)]
1371mod resolve_context_tests {
1372    use super::{LiteralValue, ResolveContext};
1373
1374    #[test]
1375    fn numeric_dims_reads_vector_literal() {
1376        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![
1377            LiteralValue::Number(2.0),
1378            LiteralValue::Number(3.0),
1379        ])]);
1380        assert_eq!(ctx.numeric_dims(), vec![Some(2), Some(3)]);
1381    }
1382
1383    #[test]
1384    fn numeric_dims_skips_non_numeric_entries() {
1385        let ctx = ResolveContext::new(vec![
1386            LiteralValue::Number(4.0),
1387            LiteralValue::String("like".to_string()),
1388            LiteralValue::Unknown,
1389        ]);
1390        assert_eq!(ctx.numeric_dims(), vec![Some(4), None, None]);
1391    }
1392
1393    #[test]
1394    fn numeric_dims_prefers_vector_even_with_trailing_args() {
1395        let ctx = ResolveContext::new(vec![
1396            LiteralValue::Vector(vec![LiteralValue::Number(1.0), LiteralValue::Number(5.0)]),
1397            LiteralValue::String("like".to_string()),
1398        ]);
1399        assert_eq!(ctx.numeric_dims(), vec![Some(1), Some(5)]);
1400    }
1401
1402    #[test]
1403    fn literal_string_is_lowercased() {
1404        let ctx = ResolveContext::new(vec![LiteralValue::String("OmItNaN".to_string())]);
1405        assert_eq!(ctx.literal_string_at(0), Some("omitnan".to_string()));
1406    }
1407
1408    #[test]
1409    fn literal_bool_is_available() {
1410        let ctx = ResolveContext::new(vec![LiteralValue::Bool(true)]);
1411        assert_eq!(ctx.literal_bool_at(0), Some(true));
1412    }
1413
1414    #[test]
1415    fn literal_vector_at_returns_clone() {
1416        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![
1417            LiteralValue::Number(7.0),
1418            LiteralValue::Unknown,
1419        ])]);
1420        assert_eq!(
1421            ctx.literal_vector_at(0),
1422            Some(vec![LiteralValue::Number(7.0), LiteralValue::Unknown])
1423        );
1424    }
1425
1426    #[test]
1427    fn numeric_vector_at_rejects_nested_vectors() {
1428        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![LiteralValue::Vector(
1429            vec![LiteralValue::Number(1.0)],
1430        )])]);
1431        assert_eq!(ctx.numeric_vector_at(0), None);
1432    }
1433}
1434
1435pub type TypeResolver = fn(args: &[Type]) -> Type;
1436pub type TypeResolverWithContext = fn(args: &[Type], ctx: &ResolveContext) -> Type;
1437
1438#[derive(Clone, Copy, Debug)]
1439pub enum TypeResolverKind {
1440    Legacy(TypeResolver),
1441    WithContext(TypeResolverWithContext),
1442}
1443
1444pub fn type_resolver_kind(resolver: TypeResolver) -> TypeResolverKind {
1445    TypeResolverKind::Legacy(resolver)
1446}
1447
1448pub fn type_resolver_kind_ctx(resolver: TypeResolverWithContext) -> TypeResolverKind {
1449    TypeResolverKind::WithContext(resolver)
1450}
1451
1452/// Simple builtin function definition using the unified type system
1453#[derive(Debug, Clone)]
1454pub struct BuiltinFunction {
1455    pub name: &'static str,
1456    pub description: &'static str,
1457    pub category: &'static str,
1458    pub doc: &'static str,
1459    pub examples: &'static str,
1460    pub param_types: Vec<Type>,
1461    pub return_type: Type,
1462    pub type_resolver: Option<TypeResolverKind>,
1463    pub implementation: fn(&[Value]) -> BuiltinFuture,
1464    pub accel_tags: &'static [AccelTag],
1465    pub is_sink: bool,
1466    pub suppress_auto_output: bool,
1467}
1468
1469impl BuiltinFunction {
1470    #[allow(clippy::too_many_arguments)]
1471    pub fn new(
1472        name: &'static str,
1473        description: &'static str,
1474        category: &'static str,
1475        doc: &'static str,
1476        examples: &'static str,
1477        param_types: Vec<Type>,
1478        return_type: Type,
1479        type_resolver: Option<TypeResolverKind>,
1480        implementation: fn(&[Value]) -> BuiltinFuture,
1481        accel_tags: &'static [AccelTag],
1482        is_sink: bool,
1483        suppress_auto_output: bool,
1484    ) -> Self {
1485        Self {
1486            name,
1487            description,
1488            category,
1489            doc,
1490            examples,
1491            param_types,
1492            return_type,
1493            type_resolver,
1494            implementation,
1495            accel_tags,
1496            is_sink,
1497            suppress_auto_output,
1498        }
1499    }
1500
1501    pub fn infer_return_type(&self, args: &[Type]) -> Type {
1502        self.infer_return_type_with_context(args, &ResolveContext::default())
1503    }
1504
1505    pub fn infer_return_type_with_context(&self, args: &[Type], ctx: &ResolveContext) -> Type {
1506        if let Some(resolver) = self.type_resolver {
1507            return match resolver {
1508                TypeResolverKind::Legacy(resolver) => resolver(args),
1509                TypeResolverKind::WithContext(resolver) => resolver(args, ctx),
1510            };
1511        }
1512        self.return_type.clone()
1513    }
1514}
1515
1516/// A constant value that can be accessed as a variable
1517#[derive(Clone)]
1518pub struct Constant {
1519    pub name: &'static str,
1520    pub value: Value,
1521}
1522
1523pub mod shape_rules;
1524
1525impl std::fmt::Debug for Constant {
1526    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1527        write!(
1528            f,
1529            "Constant {{ name: {:?}, value: {:?} }}",
1530            self.name, self.value
1531        )
1532    }
1533}
1534
1535#[cfg(not(target_arch = "wasm32"))]
1536inventory::collect!(BuiltinFunction);
1537#[cfg(not(target_arch = "wasm32"))]
1538inventory::collect!(Constant);
1539
1540#[cfg(not(target_arch = "wasm32"))]
1541pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
1542    inventory::iter::<BuiltinFunction>().collect()
1543}
1544
1545#[cfg(target_arch = "wasm32")]
1546pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
1547    wasm_registry::builtin_functions()
1548}
1549
1550#[cfg(not(target_arch = "wasm32"))]
1551static BUILTIN_LOOKUP: OnceLock<HashMap<String, &'static BuiltinFunction>> = OnceLock::new();
1552
1553#[cfg(not(target_arch = "wasm32"))]
1554fn builtin_lookup_map() -> &'static HashMap<String, &'static BuiltinFunction> {
1555    BUILTIN_LOOKUP.get_or_init(|| {
1556        let mut map = HashMap::new();
1557        for func in builtin_functions() {
1558            map.insert(func.name.to_ascii_lowercase(), func);
1559        }
1560        map
1561    })
1562}
1563
1564#[cfg(not(target_arch = "wasm32"))]
1565pub fn builtin_function_by_name(name: &str) -> Option<&'static BuiltinFunction> {
1566    builtin_lookup_map()
1567        .get(&name.to_ascii_lowercase())
1568        .copied()
1569}
1570
1571#[cfg(target_arch = "wasm32")]
1572pub fn builtin_function_by_name(name: &str) -> Option<&'static BuiltinFunction> {
1573    wasm_registry::builtin_functions()
1574        .into_iter()
1575        .find(|f| f.name.eq_ignore_ascii_case(name))
1576}
1577
1578pub fn suppresses_auto_output(name: &str) -> bool {
1579    builtin_function_by_name(name)
1580        .map(|f| f.suppress_auto_output)
1581        .unwrap_or(false)
1582}
1583
1584#[cfg(not(target_arch = "wasm32"))]
1585pub fn constants() -> Vec<&'static Constant> {
1586    inventory::iter::<Constant>().collect()
1587}
1588
1589#[cfg(target_arch = "wasm32")]
1590pub fn constants() -> Vec<&'static Constant> {
1591    wasm_registry::constants()
1592}
1593
1594// ----------------------
1595// Builtin documentation metadata (optional, registered by macros)
1596// ----------------------
1597
1598#[derive(Debug)]
1599pub struct BuiltinDoc {
1600    pub name: &'static str,
1601    pub category: Option<&'static str>,
1602    pub summary: Option<&'static str>,
1603    pub keywords: Option<&'static str>,
1604    pub errors: Option<&'static str>,
1605    pub related: Option<&'static str>,
1606    pub introduced: Option<&'static str>,
1607    pub status: Option<&'static str>,
1608    pub examples: Option<&'static str>,
1609}
1610
1611#[cfg(not(target_arch = "wasm32"))]
1612inventory::collect!(BuiltinDoc);
1613
1614#[cfg(not(target_arch = "wasm32"))]
1615pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
1616    inventory::iter::<BuiltinDoc>().collect()
1617}
1618
1619#[cfg(target_arch = "wasm32")]
1620pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
1621    wasm_registry::builtin_docs()
1622}
1623
1624// ----------------------
1625// Display implementations
1626// ----------------------
1627
1628fn format_number_short_g(value: f64) -> String {
1629    if value.is_nan() {
1630        return "NaN".to_string();
1631    }
1632    if value.is_infinite() {
1633        return if value.is_sign_negative() {
1634            "-Inf"
1635        } else {
1636            "Inf"
1637        }
1638        .to_string();
1639    }
1640    // Normalize -0.0 to 0
1641    let mut v = value;
1642    if v == 0.0 {
1643        v = 0.0;
1644    }
1645
1646    let abs = v.abs();
1647    if abs == 0.0 {
1648        return "0".to_string();
1649    }
1650
1651    // Decide between fixed and scientific notation roughly like short g
1652    let use_scientific = !(1e-4..1e6).contains(&abs);
1653
1654    if use_scientific {
1655        // 5 significant digits in scientific notation for short g style
1656        let s = format!("{v:.4e}");
1657        // Trim trailing zeros in fraction part
1658        if let Some(idx) = s.find('e') {
1659            let (mut mantissa, exp) = s.split_at(idx);
1660            // mantissa like "-1.23450"
1661            if let Some(dot_idx) = mantissa.find('.') {
1662                // Trim trailing zeros
1663                let mut end = mantissa.len();
1664                while end > dot_idx + 1 && mantissa.as_bytes()[end - 1] == b'0' {
1665                    end -= 1;
1666                }
1667                if end > 0 && mantissa.as_bytes()[end - 1] == b'.' {
1668                    end -= 1;
1669                }
1670                mantissa = &mantissa[..end];
1671            }
1672            return format!("{mantissa}{exp}");
1673        }
1674        return s;
1675    }
1676
1677    // Fixed notation with up to 12 significant digits, trim trailing zeros
1678    // Compute number of decimals to retain to reach ~12 significant digits
1679    let exp10 = abs.log10().floor() as i32; // position of most significant digit
1680    let sig_digits: i32 = 12;
1681    let decimals = (sig_digits - 1 - exp10).clamp(0, 12) as usize;
1682    // Round to that many decimals
1683    let pow = 10f64.powi(decimals as i32);
1684    let rounded = (v * pow).round() / pow;
1685    let mut s = format!("{rounded:.decimals$}");
1686    if let Some(dot) = s.find('.') {
1687        // Trim trailing zeros
1688        let mut end = s.len();
1689        while end > dot + 1 && s.as_bytes()[end - 1] == b'0' {
1690            end -= 1;
1691        }
1692        if end > 0 && s.as_bytes()[end - 1] == b'.' {
1693            end -= 1;
1694        }
1695        s.truncate(end);
1696    }
1697    if s.is_empty() || s == "-0" {
1698        s = "0".to_string();
1699    }
1700    s
1701}
1702
1703// -------- Exception type --------
1704#[derive(Debug, Clone, PartialEq)]
1705pub struct MException {
1706    pub identifier: String,
1707    pub message: String,
1708    pub stack: Vec<String>,
1709}
1710
1711impl MException {
1712    pub fn new(identifier: String, message: String) -> Self {
1713        Self {
1714            identifier,
1715            message,
1716            stack: Vec::new(),
1717        }
1718    }
1719}
1720
1721/// Reference to a GC-allocated object providing language handle semantics
1722#[derive(Debug, Clone)]
1723pub struct HandleRef {
1724    pub class_name: String,
1725    pub target: GcPtr<Value>,
1726    pub valid: bool,
1727}
1728
1729impl PartialEq for HandleRef {
1730    fn eq(&self, other: &Self) -> bool {
1731        let a = unsafe { self.target.as_raw() } as usize;
1732        let b = unsafe { other.target.as_raw() } as usize;
1733        a == b
1734    }
1735}
1736
1737/// Event listener handle for events
1738#[derive(Debug, Clone, PartialEq)]
1739pub struct Listener {
1740    pub id: u64,
1741    pub target: GcPtr<Value>,
1742    pub event_name: String,
1743    pub callback: GcPtr<Value>,
1744    pub enabled: bool,
1745    pub valid: bool,
1746}
1747
1748impl Listener {
1749    pub fn class_name(&self) -> String {
1750        match unsafe { &*self.target.as_raw() } {
1751            Value::Object(o) => o.class_name.clone(),
1752            Value::HandleObject(h) => h.class_name.clone(),
1753            _ => String::new(),
1754        }
1755    }
1756}
1757
1758impl fmt::Display for Value {
1759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1760        match self {
1761            Value::Int(i) => write!(f, "{}", i.to_i64()),
1762            Value::Num(n) => write!(f, "{}", format_number_short_g(*n)),
1763            Value::Complex(re, im) => {
1764                if *im == 0.0 {
1765                    write!(f, "{}", format_number_short_g(*re))
1766                } else if *re == 0.0 {
1767                    write!(f, "{}i", format_number_short_g(*im))
1768                } else if *im < 0.0 {
1769                    write!(
1770                        f,
1771                        "{}-{}i",
1772                        format_number_short_g(*re),
1773                        format_number_short_g(im.abs())
1774                    )
1775                } else {
1776                    write!(
1777                        f,
1778                        "{}+{}i",
1779                        format_number_short_g(*re),
1780                        format_number_short_g(*im)
1781                    )
1782                }
1783            }
1784            Value::Bool(b) => write!(f, "{}", if *b { 1 } else { 0 }),
1785            Value::LogicalArray(la) => write!(f, "{la}"),
1786            Value::String(s) => write!(f, "'{s}'"),
1787            Value::StringArray(sa) => write!(f, "{sa}"),
1788            Value::CharArray(ca) => write!(f, "{ca}"),
1789            Value::Tensor(m) => write!(f, "{m}"),
1790            Value::ComplexTensor(m) => write!(f, "{m}"),
1791            Value::Cell(ca) => ca.fmt(f),
1792
1793            Value::GpuTensor(h) => write!(
1794                f,
1795                "GpuTensor(shape={:?}, device={}, buffer={})",
1796                h.shape, h.device_id, h.buffer_id
1797            ),
1798            Value::Object(obj) => write!(f, "{}(props={})", obj.class_name, obj.properties.len()),
1799            Value::HandleObject(h) => {
1800                let ptr = unsafe { h.target.as_raw() } as usize;
1801                write!(
1802                    f,
1803                    "<handle {} @0x{:x} valid={}>",
1804                    h.class_name, ptr, h.valid
1805                )
1806            }
1807            Value::Listener(l) => {
1808                let ptr = unsafe { l.target.as_raw() } as usize;
1809                write!(
1810                    f,
1811                    "<listener id={} {}@0x{:x} '{}' enabled={} valid={}>",
1812                    l.id,
1813                    l.class_name(),
1814                    ptr,
1815                    l.event_name,
1816                    l.enabled,
1817                    l.valid
1818                )
1819            }
1820            Value::Struct(st) => {
1821                write!(f, "struct {{")?;
1822                for (i, (key, val)) in st.fields.iter().enumerate() {
1823                    if i > 0 {
1824                        write!(f, ", ")?;
1825                    }
1826                    write!(f, "{}: {}", key, val)?;
1827                }
1828                write!(f, "}}")
1829            }
1830            Value::OutputList(values) => {
1831                write!(f, "[")?;
1832                for (i, value) in values.iter().enumerate() {
1833                    if i > 0 {
1834                        write!(f, ", ")?;
1835                    }
1836                    write!(f, "{}", value)?;
1837                }
1838                write!(f, "]")
1839            }
1840            Value::FunctionHandle(name) => write!(f, "@{name}"),
1841            Value::Closure(c) => write!(
1842                f,
1843                "<closure {} captures={}>",
1844                c.function_name,
1845                c.captures.len()
1846            ),
1847            Value::ClassRef(name) => write!(f, "<class {name}>"),
1848            Value::MException(e) => write!(
1849                f,
1850                "MException(identifier='{}', message='{}')",
1851                e.identifier, e.message
1852            ),
1853        }
1854    }
1855}
1856
1857impl fmt::Display for ComplexTensor {
1858    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1859        match self.shape.len() {
1860            0 | 1 => {
1861                write!(f, "[")?;
1862                for (i, (re, im)) in self.data.iter().enumerate() {
1863                    if i > 0 {
1864                        write!(f, " ")?;
1865                    }
1866                    let s = Value::Complex(*re, *im).to_string();
1867                    write!(f, "{s}")?;
1868                }
1869                write!(f, "]")
1870            }
1871            2 => {
1872                let rows = self.rows;
1873                let cols = self.cols;
1874                write!(f, "[")?;
1875                for r in 0..rows {
1876                    for c in 0..cols {
1877                        if c > 0 {
1878                            write!(f, " ")?;
1879                        }
1880                        let (re, im) = self.data[r + c * rows];
1881                        let s = Value::Complex(re, im).to_string();
1882                        write!(f, "{s}")?;
1883                    }
1884                    if r + 1 < rows {
1885                        write!(f, "; ")?;
1886                    }
1887                }
1888                write!(f, "]")
1889            }
1890            _ => {
1891                if should_expand_nd_display(&self.shape) {
1892                    write_nd_pages(f, &self.shape, |f, idx| {
1893                        let (re, im) = self.data[idx];
1894                        write!(f, "{}", Value::Complex(re, im))
1895                    })
1896                } else {
1897                    write!(f, "ComplexTensor(shape={:?})", self.shape)
1898                }
1899            }
1900        }
1901    }
1902}
1903
1904#[cfg(test)]
1905mod display_tests {
1906    use super::{ComplexTensor, LogicalArray, Tensor};
1907
1908    #[test]
1909    fn tensor_nd_display_uses_page_headers() {
1910        let tensor = Tensor::new(
1911            vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
1912            vec![2, 3, 2],
1913        )
1914        .expect("tensor");
1915        let rendered = tensor.to_string();
1916        assert!(rendered.contains("(:, :, 1) ="));
1917        assert!(rendered.contains("(:, :, 2) ="));
1918        assert!(rendered.contains("  1  0  0"));
1919    }
1920
1921    #[test]
1922    fn tensor_nd_display_falls_back_for_large_arrays() {
1923        let tensor = Tensor::new(vec![0.0; 4097], vec![1, 1, 4097]).expect("tensor");
1924        assert_eq!(tensor.to_string(), "Tensor(shape=[1, 1, 4097])");
1925    }
1926
1927    #[test]
1928    fn logical_nd_display_uses_headers_and_fallback_summary() {
1929        let logical =
1930            LogicalArray::new(vec![1, 0, 0, 1, 1, 0, 0, 1], vec![2, 2, 2]).expect("logical");
1931        let rendered = logical.to_string();
1932        assert!(rendered.contains("(:, :, 1) ="));
1933        assert!(rendered.contains("(:, :, 2) ="));
1934
1935        let large = LogicalArray::new(vec![1; 4097], vec![1, 1, 4097]).expect("large logical");
1936        assert_eq!(large.to_string(), "1x1x4097 logical array");
1937    }
1938
1939    #[test]
1940    fn complex_nd_display_uses_page_headers() {
1941        let complex = ComplexTensor::new(
1942            vec![(1.0, 0.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)],
1943            vec![2, 1, 2],
1944        )
1945        .expect("complex");
1946        let rendered = complex.to_string();
1947        assert!(rendered.contains("(:, :, 1) ="));
1948        assert!(rendered.contains("(:, :, 2) ="));
1949    }
1950}
1951
1952#[derive(Debug, Clone, PartialEq)]
1953pub struct CellArray {
1954    pub data: Vec<GcPtr<Value>>,
1955    /// Full MATLAB-visible shape vector (column-major semantics).
1956    pub shape: Vec<usize>,
1957    /// Cached row count for 2-D interop; equals `shape[0]` when present.
1958    pub rows: usize,
1959    /// Cached column count for 2-D interop; equals `shape[1]` when present, otherwise 1 (or 0 for empty).
1960    pub cols: usize,
1961}
1962
1963impl CellArray {
1964    pub fn new_handles(
1965        handles: Vec<GcPtr<Value>>,
1966        rows: usize,
1967        cols: usize,
1968    ) -> Result<Self, String> {
1969        Self::new_handles_with_shape(handles, vec![rows, cols])
1970    }
1971
1972    pub fn new_handles_with_shape(
1973        handles: Vec<GcPtr<Value>>,
1974        shape: Vec<usize>,
1975    ) -> Result<Self, String> {
1976        let expected = total_len(&shape)
1977            .ok_or_else(|| "Cell data shape exceeds platform limits".to_string())?;
1978        if expected != handles.len() {
1979            return Err(format!(
1980                "Cell data length {} doesn't match shape {:?} ({} elements)",
1981                handles.len(),
1982                shape,
1983                expected
1984            ));
1985        }
1986        let (rows, cols) = shape_rows_cols(&shape);
1987        Ok(CellArray {
1988            data: handles,
1989            shape,
1990            rows,
1991            cols,
1992        })
1993    }
1994
1995    pub fn new(data: Vec<Value>, rows: usize, cols: usize) -> Result<Self, String> {
1996        Self::new_with_shape(data, vec![rows, cols])
1997    }
1998
1999    pub fn new_with_shape(data: Vec<Value>, shape: Vec<usize>) -> Result<Self, String> {
2000        let expected = total_len(&shape)
2001            .ok_or_else(|| "Cell data shape exceeds platform limits".to_string())?;
2002        if expected != data.len() {
2003            return Err(format!(
2004                "Cell data length {} doesn't match shape {:?} ({} elements)",
2005                data.len(),
2006                shape,
2007                expected
2008            ));
2009        }
2010        // Note: data will be allocated into GC handles by callers (runtime/ignition) to avoid builtins↔gc cycles
2011        let handles: Vec<GcPtr<Value>> = data
2012            .into_iter()
2013            .map(|v| unsafe { GcPtr::from_raw(Box::into_raw(Box::new(v))) })
2014            .collect();
2015        Self::new_handles_with_shape(handles, shape)
2016    }
2017
2018    pub fn get(&self, row: usize, col: usize) -> Result<Value, String> {
2019        if row >= self.rows || col >= self.cols {
2020            return Err(format!(
2021                "Cell index ({row}, {col}) out of bounds for {}x{} cell array",
2022                self.rows, self.cols
2023            ));
2024        }
2025        Ok((*self.data[row * self.cols + col]).clone())
2026    }
2027}
2028
2029fn total_len(shape: &[usize]) -> Option<usize> {
2030    if shape.is_empty() {
2031        return Some(0);
2032    }
2033    shape
2034        .iter()
2035        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
2036}
2037
2038fn shape_rows_cols(shape: &[usize]) -> (usize, usize) {
2039    if shape.is_empty() {
2040        return (0, 0);
2041    }
2042    if shape.len() == 1 {
2043        return (1, shape[0]);
2044    }
2045    (shape[0], shape[1])
2046}
2047
2048impl fmt::Display for CellArray {
2049    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2050        let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
2051        if self.shape.len() > 2 {
2052            return write!(f, "{} cell array", dims.join("x"));
2053        }
2054        write!(f, "{}x{} cell array", self.rows, self.cols)?;
2055        if self.rows == 0 || self.cols == 0 {
2056            return Ok(());
2057        }
2058        for r in 0..self.rows {
2059            writeln!(f)?;
2060            write!(f, "  ")?;
2061            for c in 0..self.cols {
2062                if c > 0 {
2063                    write!(f, "  ")?;
2064                }
2065                let value = self.get(r, c).unwrap_or_else(|_| Value::Num(f64::NAN));
2066                write!(f, "{{{value}}}")?;
2067            }
2068        }
2069        Ok(())
2070    }
2071}
2072
2073#[derive(Debug, Clone, PartialEq)]
2074pub struct ObjectInstance {
2075    pub class_name: String,
2076    pub properties: HashMap<String, Value>,
2077}
2078
2079impl ObjectInstance {
2080    pub fn new(class_name: String) -> Self {
2081        Self {
2082            class_name,
2083            properties: HashMap::new(),
2084        }
2085    }
2086}
2087
2088// -------- Class registry (scaffolding) --------
2089#[derive(Debug, Clone, PartialEq, Eq)]
2090pub enum Access {
2091    Public,
2092    Private,
2093}
2094
2095#[derive(Debug, Clone)]
2096pub struct PropertyDef {
2097    pub name: String,
2098    pub is_static: bool,
2099    pub is_dependent: bool,
2100    pub get_access: Access,
2101    pub set_access: Access,
2102    pub default_value: Option<Value>,
2103}
2104
2105#[derive(Debug, Clone)]
2106pub struct MethodDef {
2107    pub name: String,
2108    pub is_static: bool,
2109    pub access: Access,
2110    pub function_name: String, // bound runtime builtin/user func name
2111}
2112
2113#[derive(Debug, Clone)]
2114pub struct ClassDef {
2115    pub name: String, // namespaced e.g. pkg.Point
2116    pub parent: Option<String>,
2117    pub properties: HashMap<String, PropertyDef>,
2118    pub methods: HashMap<String, MethodDef>,
2119}
2120
2121use std::sync::Mutex;
2122
2123static CLASS_REGISTRY: OnceLock<Mutex<HashMap<String, ClassDef>>> = OnceLock::new();
2124static STATIC_VALUES: OnceLock<Mutex<HashMap<(String, String), Value>>> = OnceLock::new();
2125
2126fn registry() -> &'static Mutex<HashMap<String, ClassDef>> {
2127    CLASS_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
2128}
2129
2130pub fn register_class(def: ClassDef) {
2131    let mut m = registry().lock().unwrap();
2132    m.insert(def.name.clone(), def);
2133}
2134
2135pub fn get_class(name: &str) -> Option<ClassDef> {
2136    registry().lock().unwrap().get(name).cloned()
2137}
2138
2139/// Resolve a property through the inheritance chain, returning the property definition and
2140/// the name of the class where it was defined.
2141pub fn lookup_property(class_name: &str, prop: &str) -> Option<(PropertyDef, String)> {
2142    let reg = registry().lock().unwrap();
2143    let mut current = Some(class_name.to_string());
2144    let guard: Option<std::sync::MutexGuard<'_, std::collections::HashMap<String, ClassDef>>> =
2145        None;
2146    drop(guard);
2147    while let Some(name) = current {
2148        if let Some(cls) = reg.get(&name) {
2149            if let Some(p) = cls.properties.get(prop) {
2150                return Some((p.clone(), name));
2151            }
2152            current = cls.parent.clone();
2153        } else {
2154            break;
2155        }
2156    }
2157    None
2158}
2159
2160/// Resolve a method through the inheritance chain, returning the method definition and
2161/// the name of the class where it was defined.
2162pub fn lookup_method(class_name: &str, method: &str) -> Option<(MethodDef, String)> {
2163    let reg = registry().lock().unwrap();
2164    let mut current = Some(class_name.to_string());
2165    while let Some(name) = current {
2166        if let Some(cls) = reg.get(&name) {
2167            if let Some(m) = cls.methods.get(method) {
2168                return Some((m.clone(), name));
2169            }
2170            current = cls.parent.clone();
2171        } else {
2172            break;
2173        }
2174    }
2175    None
2176}
2177
2178fn static_values() -> &'static Mutex<HashMap<(String, String), Value>> {
2179    STATIC_VALUES.get_or_init(|| Mutex::new(HashMap::new()))
2180}
2181
2182pub fn get_static_property_value(class_name: &str, prop: &str) -> Option<Value> {
2183    static_values()
2184        .lock()
2185        .unwrap()
2186        .get(&(class_name.to_string(), prop.to_string()))
2187        .cloned()
2188}
2189
2190pub fn set_static_property_value(class_name: &str, prop: &str, value: Value) {
2191    static_values()
2192        .lock()
2193        .unwrap()
2194        .insert((class_name.to_string(), prop.to_string()), value);
2195}
2196
2197/// Set a static property, resolving the defining ancestor class for storage.
2198pub fn set_static_property_value_in_owner(
2199    class_name: &str,
2200    prop: &str,
2201    value: Value,
2202) -> Result<(), String> {
2203    if let Some((_p, owner)) = lookup_property(class_name, prop) {
2204        set_static_property_value(&owner, prop, value);
2205        Ok(())
2206    } else {
2207        Err(format!("Unknown static property '{class_name}.{prop}'"))
2208    }
2209}