Skip to main content

runmat_builtins/
lib.rs

1pub use inventory;
2pub mod symbolic;
3use runmat_gc_api::{GcHandle, Trace, Tracer};
4use runmat_thread_local::runmat_thread_local;
5use std::cell::RefCell;
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::convert::TryFrom;
9use std::fmt;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Mutex;
13use std::thread::ThreadId;
14pub use symbolic::{SymbolicExpr, SymbolicFunction};
15
16use indexmap::IndexMap;
17#[cfg(not(target_arch = "wasm32"))]
18use std::sync::OnceLock;
19
20#[cfg(target_arch = "wasm32")]
21pub mod wasm_registry {
22    use super::{BuiltinDoc, BuiltinFunction, Constant};
23    use std::cell::{Cell, RefCell};
24
25    thread_local! {
26        static FUNCTIONS: RefCell<Vec<&'static BuiltinFunction>> = const { RefCell::new(Vec::new()) };
27        static CONSTANTS: RefCell<Vec<&'static Constant>> = const { RefCell::new(Vec::new()) };
28        static DOCS: RefCell<Vec<&'static BuiltinDoc>> = const { RefCell::new(Vec::new()) };
29        static REGISTERED: Cell<bool> = const { Cell::new(false) };
30    }
31
32    fn leak<T>(value: T) -> &'static T {
33        Box::leak(Box::new(value))
34    }
35
36    pub fn submit_builtin_function(func: BuiltinFunction) {
37        let leaked = leak(func);
38        FUNCTIONS.with_borrow_mut(|functions| functions.push(leaked));
39    }
40
41    pub fn submit_constant(constant: Constant) {
42        let leaked = leak(constant);
43        CONSTANTS.with_borrow_mut(|constants| constants.push(leaked));
44    }
45
46    pub fn submit_builtin_doc(doc: BuiltinDoc) {
47        let leaked = leak(doc);
48        DOCS.with_borrow_mut(|docs| docs.push(leaked));
49    }
50
51    pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
52        FUNCTIONS.with_borrow(Clone::clone)
53    }
54
55    pub fn constants() -> Vec<&'static Constant> {
56        CONSTANTS.with_borrow(Clone::clone)
57    }
58
59    pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
60        DOCS.with_borrow(Clone::clone)
61    }
62
63    pub fn mark_registered() {
64        REGISTERED.set(true);
65    }
66
67    pub fn is_registered() -> bool {
68        REGISTERED.get()
69    }
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub enum Value {
74    Int(IntValue),
75    Num(f64),
76    /// Complex scalar value represented as (re, im)
77    Complex(f64, f64),
78    Bool(bool),
79    // Logical array (N-D of booleans). Scalars use Bool.
80    LogicalArray(LogicalArray),
81    String(String),
82    // String array (R2016b+): N-D array of string scalars
83    StringArray(StringArray),
84    // Char array (single-quoted): 2-D character array (rows x cols)
85    CharArray(CharArray),
86    Tensor(Tensor),
87    /// Real double sparse matrix in compressed sparse column form.
88    SparseTensor(SparseTensor),
89    /// Complex numeric array; same column-major shape semantics as `Tensor`
90    ComplexTensor(ComplexTensor),
91    /// Scalar symbolic expression used by `sym`, `syms`, and symbolic math builtins.
92    Symbolic(SymbolicExpr),
93    Cell(CellArray),
94    // Struct (scalar or nested). Struct arrays are represented in higher layers;
95    // this variant holds a single struct's fields.
96    Struct(StructValue),
97    // GPU-resident tensor handle (opaque; buffer managed by backend)
98    GpuTensor(runmat_accelerate_api::GpuTensorHandle),
99    // Simple object instance until full class system lands
100    Object(ObjectInstance),
101    /// Handle-object wrapper providing identity semantics and validity tracking
102    HandleObject(HandleRef),
103    /// Event listener handle for events
104    Listener(Listener),
105    /// Multiple outputs captured as a list (internal destructuring helper)
106    OutputList(Vec<Value>),
107    // Function handle pointing to a named function (builtin or user)
108    FunctionHandle(String),
109    // Function handle whose resolution must stay at the external boundary.
110    ExternalFunctionHandle(String),
111    // Function handle preserving typed method identity.
112    MethodFunctionHandle(String),
113    // Function handle with compiler/session semantic identity.
114    BoundFunctionHandle {
115        name: String,
116        function: usize,
117    },
118    Closure(Closure),
119    ClassRef(String),
120    MException(MException),
121}
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum IntValue {
124    I8(i8),
125    I16(i16),
126    I32(i32),
127    I64(i64),
128    U8(u8),
129    U16(u16),
130    U32(u32),
131    U64(u64),
132}
133
134impl IntValue {
135    pub fn to_i64(&self) -> i64 {
136        match self {
137            IntValue::I8(v) => *v as i64,
138            IntValue::I16(v) => *v as i64,
139            IntValue::I32(v) => *v as i64,
140            IntValue::I64(v) => *v,
141            IntValue::U8(v) => *v as i64,
142            IntValue::U16(v) => *v as i64,
143            IntValue::U32(v) => *v as i64,
144            IntValue::U64(v) => {
145                if *v > i64::MAX as u64 {
146                    i64::MAX
147                } else {
148                    *v as i64
149                }
150            }
151        }
152    }
153    pub fn to_f64(&self) -> f64 {
154        self.to_i64() as f64
155    }
156    pub fn is_zero(&self) -> bool {
157        self.to_i64() == 0
158    }
159    pub fn class_name(&self) -> &'static str {
160        match self {
161            IntValue::I8(_) => "int8",
162            IntValue::I16(_) => "int16",
163            IntValue::I32(_) => "int32",
164            IntValue::I64(_) => "int64",
165            IntValue::U8(_) => "uint8",
166            IntValue::U16(_) => "uint16",
167            IntValue::U32(_) => "uint32",
168            IntValue::U64(_) => "uint64",
169        }
170    }
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub struct StructValue {
175    pub fields: IndexMap<String, Value>,
176}
177
178impl StructValue {
179    pub fn new() -> Self {
180        Self {
181            fields: IndexMap::new(),
182        }
183    }
184
185    /// Insert a field, preserving insertion order when the name is new.
186    pub fn insert(&mut self, name: impl Into<String>, value: Value) -> Option<Value> {
187        self.fields.insert(name.into(), value)
188    }
189
190    /// Remove a field while preserving the relative order of remaining fields.
191    pub fn remove(&mut self, name: &str) -> Option<Value> {
192        self.fields.shift_remove(name)
193    }
194
195    /// Returns an iterator over field names in their stored order.
196    pub fn field_names(&self) -> impl Iterator<Item = &String> {
197        self.fields.keys()
198    }
199}
200
201impl Default for StructValue {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208pub enum NumericDType {
209    F64,
210    F32,
211    U8,
212    U16,
213}
214
215impl NumericDType {
216    pub fn class_name(self) -> &'static str {
217        match self {
218            NumericDType::F64 => "double",
219            NumericDType::F32 => "single",
220            NumericDType::U8 => "uint8",
221            NumericDType::U16 => "uint16",
222        }
223    }
224
225    pub fn byte_size(self) -> usize {
226        match self {
227            NumericDType::F64 => 8,
228            NumericDType::F32 => 4,
229            NumericDType::U8 => 1,
230            NumericDType::U16 => 2,
231        }
232    }
233}
234
235#[derive(Debug, Clone, PartialEq)]
236pub struct Tensor {
237    pub data: Vec<f64>,
238    pub shape: Vec<usize>, // Column-major layout
239    pub rows: usize,       // Compatibility for 2D usage
240    pub cols: usize,       // Compatibility for 2D usage
241    /// Logical numeric class of this tensor; host storage remains f64.
242    pub dtype: NumericDType,
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub struct SparseTensor {
247    pub rows: usize,
248    pub cols: usize,
249    /// Column pointers into `row_indices`/`values`; length is `cols + 1`.
250    pub col_ptrs: Vec<usize>,
251    /// Zero-based row indices, sorted within each column.
252    pub row_indices: Vec<usize>,
253    pub values: Vec<f64>,
254}
255
256#[derive(Debug, Clone, PartialEq)]
257pub struct ComplexTensor {
258    pub data: Vec<(f64, f64)>,
259    pub shape: Vec<usize>,
260    pub rows: usize,
261    pub cols: usize,
262}
263
264#[derive(Debug, Clone, PartialEq)]
265pub struct StringArray {
266    pub data: Vec<String>,
267    pub shape: Vec<usize>,
268    pub rows: usize,
269    pub cols: usize,
270}
271
272#[derive(Debug, Clone, PartialEq)]
273pub struct LogicalArray {
274    pub data: Vec<u8>, // 0 or 1 values; compact bitset can come later
275    pub shape: Vec<usize>,
276}
277
278impl LogicalArray {
279    pub fn new(data: Vec<u8>, shape: Vec<usize>) -> Result<Self, String> {
280        let expected: usize = shape.iter().product();
281        if data.len() != expected {
282            return Err(format!(
283                "LogicalArray data length {} doesn't match shape {:?} ({} elements)",
284                data.len(),
285                shape,
286                expected
287            ));
288        }
289        // Normalize to 0/1
290        let mut d = data;
291        for v in &mut d {
292            *v = if *v != 0 { 1 } else { 0 };
293        }
294        Ok(LogicalArray { data: d, shape })
295    }
296    pub fn zeros(shape: Vec<usize>) -> Self {
297        let expected: usize = shape.iter().product();
298        LogicalArray {
299            data: vec![0u8; expected],
300            shape,
301        }
302    }
303    pub fn len(&self) -> usize {
304        self.data.len()
305    }
306    pub fn is_empty(&self) -> bool {
307        self.data.is_empty()
308    }
309}
310
311#[derive(Debug, Clone, PartialEq)]
312pub struct CharArray {
313    pub data: Vec<char>,
314    pub rows: usize,
315    pub cols: usize,
316}
317
318impl CharArray {
319    pub fn new_row(s: &str) -> Self {
320        CharArray {
321            data: s.chars().collect(),
322            rows: 1,
323            cols: s.chars().count(),
324        }
325    }
326    pub fn new(data: Vec<char>, rows: usize, cols: usize) -> Result<Self, String> {
327        if rows * cols != data.len() {
328            return Err(format!(
329                "Char data length {} doesn't match dimensions {}x{}",
330                data.len(),
331                rows,
332                cols
333            ));
334        }
335        Ok(CharArray { data, rows, cols })
336    }
337}
338
339impl StringArray {
340    pub fn new(data: Vec<String>, shape: Vec<usize>) -> Result<Self, String> {
341        let expected: usize = shape.iter().product();
342        if data.len() != expected {
343            return Err(format!(
344                "StringArray data length {} doesn't match shape {:?} ({} elements)",
345                data.len(),
346                shape,
347                expected
348            ));
349        }
350        let (rows, cols) = if shape.len() >= 2 {
351            (shape[0], shape[1])
352        } else if shape.len() == 1 {
353            (1, shape[0])
354        } else {
355            (0, 0)
356        };
357        Ok(StringArray {
358            data,
359            shape,
360            rows,
361            cols,
362        })
363    }
364    pub fn new_2d(data: Vec<String>, rows: usize, cols: usize) -> Result<Self, String> {
365        Self::new(data, vec![rows, cols])
366    }
367    pub fn rows(&self) -> usize {
368        self.shape.first().copied().unwrap_or(1)
369    }
370    pub fn cols(&self) -> usize {
371        self.shape.get(1).copied().unwrap_or(1)
372    }
373}
374
375// GpuTensorHandle now lives in runmat-accel-api
376
377impl Tensor {
378    pub fn new(data: Vec<f64>, shape: Vec<usize>) -> Result<Self, String> {
379        let expected: usize = shape.iter().product();
380        if data.len() != expected {
381            return Err(format!(
382                "Tensor data length {} doesn't match shape {:?} ({} elements)",
383                data.len(),
384                shape,
385                expected
386            ));
387        }
388        let (rows, cols) = if shape.len() >= 2 {
389            (shape[0], shape[1])
390        } else if shape.len() == 1 {
391            (1, shape[0])
392        } else {
393            (0, 0)
394        };
395        Ok(Tensor {
396            data,
397            shape,
398            rows,
399            cols,
400            dtype: NumericDType::F64,
401        })
402    }
403
404    pub fn new_2d(data: Vec<f64>, rows: usize, cols: usize) -> Result<Self, String> {
405        Self::new(data, vec![rows, cols])
406    }
407
408    pub fn from_f32(data: Vec<f32>, shape: Vec<usize>) -> Result<Self, String> {
409        let converted: Vec<f64> = data.into_iter().map(|v| v as f64).collect();
410        Self::new_with_dtype(converted, shape, NumericDType::F32)
411    }
412
413    pub fn from_f32_slice(data: &[f32], shape: &[usize]) -> Result<Self, String> {
414        let converted: Vec<f64> = data.iter().map(|&v| v as f64).collect();
415        Self::new_with_dtype(converted, shape.to_vec(), NumericDType::F32)
416    }
417
418    pub fn new_with_dtype(
419        data: Vec<f64>,
420        shape: Vec<usize>,
421        dtype: NumericDType,
422    ) -> Result<Self, String> {
423        let mut t = Self::new(data, shape)?;
424        t.dtype = dtype;
425        Ok(t)
426    }
427
428    pub fn zeros(shape: Vec<usize>) -> Self {
429        let size: usize = shape.iter().product();
430        let (rows, cols) = if shape.len() >= 2 {
431            (shape[0], shape[1])
432        } else if shape.len() == 1 {
433            (1, shape[0])
434        } else {
435            (0, 0)
436        };
437        Tensor {
438            data: vec![0.0; size],
439            shape,
440            rows,
441            cols,
442            dtype: NumericDType::F64,
443        }
444    }
445
446    pub fn ones(shape: Vec<usize>) -> Self {
447        let size: usize = shape.iter().product();
448        let (rows, cols) = if shape.len() >= 2 {
449            (shape[0], shape[1])
450        } else if shape.len() == 1 {
451            (1, shape[0])
452        } else {
453            (0, 0)
454        };
455        Tensor {
456            data: vec![1.0; size],
457            shape,
458            rows,
459            cols,
460            dtype: NumericDType::F64,
461        }
462    }
463
464    // 2D helpers for transitional call sites
465    pub fn zeros2(rows: usize, cols: usize) -> Self {
466        Self::zeros(vec![rows, cols])
467    }
468    pub fn ones2(rows: usize, cols: usize) -> Self {
469        Self::ones(vec![rows, cols])
470    }
471
472    pub fn rows(&self) -> usize {
473        self.shape.first().copied().unwrap_or(1)
474    }
475    pub fn cols(&self) -> usize {
476        self.shape.get(1).copied().unwrap_or(1)
477    }
478
479    pub fn get2(&self, row: usize, col: usize) -> Result<f64, String> {
480        let rows = self.rows();
481        let cols = self.cols();
482        if row >= rows || col >= cols {
483            return Err(format!(
484                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
485            ));
486        }
487        // Column-major linearization: lin = row + col*rows
488        Ok(self.data[row + col * rows])
489    }
490
491    pub fn set2(&mut self, row: usize, col: usize, value: f64) -> Result<(), String> {
492        let rows = self.rows();
493        let cols = self.cols();
494        if row >= rows || col >= cols {
495            return Err(format!(
496                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
497            ));
498        }
499        // Column-major linearization
500        self.data[row + col * rows] = value;
501        Ok(())
502    }
503
504    pub fn scalar_to_tensor2(scalar: f64, rows: usize, cols: usize) -> Tensor {
505        Tensor {
506            data: vec![scalar; rows * cols],
507            shape: vec![rows, cols],
508            rows,
509            cols,
510            dtype: NumericDType::F64,
511        }
512    }
513    // No-compat constructors: prefer new/new_2d/zeros/zeros2/ones/ones2
514}
515
516impl SparseTensor {
517    pub fn new(
518        rows: usize,
519        cols: usize,
520        col_ptrs: Vec<usize>,
521        row_indices: Vec<usize>,
522        values: Vec<f64>,
523    ) -> Result<Self, String> {
524        if col_ptrs.len() != cols.saturating_add(1) {
525            return Err(format!(
526                "SparseTensor col_ptrs length {} doesn't match cols {}",
527                col_ptrs.len(),
528                cols
529            ));
530        }
531        if row_indices.len() != values.len() {
532            return Err(format!(
533                "SparseTensor row index length {} doesn't match value length {}",
534                row_indices.len(),
535                values.len()
536            ));
537        }
538        if col_ptrs.first().copied().unwrap_or(usize::MAX) != 0 {
539            return Err("SparseTensor col_ptrs must start at 0".to_string());
540        }
541        if col_ptrs.last().copied().unwrap_or(usize::MAX) != values.len() {
542            return Err("SparseTensor final col_ptr must equal nnz".to_string());
543        }
544        for window in col_ptrs.windows(2) {
545            if window[0] > window[1] {
546                return Err("SparseTensor col_ptrs must be nondecreasing".to_string());
547            }
548        }
549        for col in 0..cols {
550            let start = col_ptrs[col];
551            let end = col_ptrs[col + 1];
552            let mut prev: Option<usize> = None;
553            for &row in &row_indices[start..end] {
554                if row >= rows {
555                    return Err(format!("SparseTensor row index {row} exceeds rows {rows}"));
556                }
557                if prev.is_some_and(|p| p >= row) {
558                    return Err("SparseTensor row indices must be sorted and unique".to_string());
559                }
560                prev = Some(row);
561            }
562        }
563        Ok(Self {
564            rows,
565            cols,
566            col_ptrs,
567            row_indices,
568            values,
569        })
570    }
571
572    pub fn zeros(rows: usize, cols: usize) -> Self {
573        Self {
574            rows,
575            cols,
576            col_ptrs: vec![0; cols.saturating_add(1)],
577            row_indices: Vec::new(),
578            values: Vec::new(),
579        }
580    }
581
582    pub fn nnz(&self) -> usize {
583        self.values.len()
584    }
585
586    pub fn shape(&self) -> Vec<usize> {
587        vec![self.rows, self.cols]
588    }
589
590    pub fn to_dense(&self) -> Result<Tensor, String> {
591        let len = self
592            .rows
593            .checked_mul(self.cols)
594            .ok_or_else(|| "SparseTensor dense dimensions overflow usize".to_string())?;
595        let mut data = Vec::new();
596        data.try_reserve_exact(len)
597            .map_err(|err| format!("SparseTensor dense allocation failed: {err}"))?;
598        data.resize(len, 0.0);
599        for col in 0..self.cols {
600            for idx in self.col_ptrs[col]..self.col_ptrs[col + 1] {
601                let row = self.row_indices[idx];
602                data[row + col * self.rows] = self.values[idx];
603            }
604        }
605        Tensor::new(data, self.shape())
606    }
607
608    pub fn get(&self, row: usize, col: usize) -> Option<f64> {
609        if row >= self.rows || col >= self.cols {
610            return None;
611        }
612        let start = self.col_ptrs[col];
613        let end = self.col_ptrs[col + 1];
614        self.row_indices[start..end]
615            .binary_search(&row)
616            .ok()
617            .map(|offset| self.values[start + offset])
618    }
619}
620
621#[cfg(test)]
622mod sparse_tensor_tests {
623    use super::*;
624
625    #[test]
626    fn to_dense_rejects_overflowing_dimensions() {
627        let sparse = SparseTensor {
628            rows: usize::MAX,
629            cols: 2,
630            col_ptrs: vec![0, 0, 0],
631            row_indices: Vec::new(),
632            values: Vec::new(),
633        };
634
635        let err = sparse.to_dense().unwrap_err();
636        assert!(err.contains("overflow"));
637    }
638}
639
640impl ComplexTensor {
641    pub fn new(data: Vec<(f64, f64)>, shape: Vec<usize>) -> Result<Self, String> {
642        let expected: usize = shape.iter().product();
643        if data.len() != expected {
644            return Err(format!(
645                "ComplexTensor data length {} doesn't match shape {:?} ({} elements)",
646                data.len(),
647                shape,
648                expected
649            ));
650        }
651        let (rows, cols) = if shape.len() >= 2 {
652            (shape[0], shape[1])
653        } else if shape.len() == 1 {
654            (1, shape[0])
655        } else {
656            (0, 0)
657        };
658        Ok(ComplexTensor {
659            data,
660            shape,
661            rows,
662            cols,
663        })
664    }
665    pub fn new_2d(data: Vec<(f64, f64)>, rows: usize, cols: usize) -> Result<Self, String> {
666        Self::new(data, vec![rows, cols])
667    }
668    pub fn zeros(shape: Vec<usize>) -> Self {
669        let size: usize = shape.iter().product();
670        let (rows, cols) = if shape.len() >= 2 {
671            (shape[0], shape[1])
672        } else if shape.len() == 1 {
673            (1, shape[0])
674        } else {
675            (0, 0)
676        };
677        ComplexTensor {
678            data: vec![(0.0, 0.0); size],
679            shape,
680            rows,
681            cols,
682        }
683    }
684}
685
686const MAX_ND_DISPLAY_ELEMENTS: usize = 4096;
687
688fn should_expand_nd_display(shape: &[usize]) -> bool {
689    shape.len() > 2
690        && matches!(
691            total_len(shape),
692            Some(total) if total > 0 && total <= MAX_ND_DISPLAY_ELEMENTS
693        )
694}
695
696fn column_major_strides(shape: &[usize]) -> Vec<usize> {
697    let mut strides = Vec::with_capacity(shape.len());
698    let mut stride = 1usize;
699    for &dim in shape {
700        strides.push(stride);
701        stride = stride.saturating_mul(dim);
702    }
703    strides
704}
705
706fn decode_page_coords(mut page_index: usize, page_shape: &[usize]) -> Vec<usize> {
707    let mut coords = Vec::with_capacity(page_shape.len());
708    for &dim in page_shape {
709        if dim == 0 {
710            coords.push(0);
711        } else {
712            coords.push(page_index % dim);
713            page_index /= dim;
714        }
715    }
716    coords
717}
718
719fn write_nd_pages(
720    f: &mut fmt::Formatter<'_>,
721    shape: &[usize],
722    mut write_element: impl FnMut(&mut fmt::Formatter<'_>, usize) -> fmt::Result,
723) -> fmt::Result {
724    if shape.len() <= 2 {
725        return Ok(());
726    }
727    let rows = shape[0];
728    let cols = shape[1];
729    if rows == 0 || cols == 0 {
730        return write!(f, "[]");
731    }
732    let Some(page_count) = total_len(&shape[2..]) else {
733        return write!(f, "Tensor(shape={shape:?})");
734    };
735    if page_count == 0 {
736        return write!(f, "[]");
737    }
738    let strides = column_major_strides(shape);
739    for page_index in 0..page_count {
740        if page_index > 0 {
741            write!(f, "\n\n")?;
742        }
743        let coords = decode_page_coords(page_index, &shape[2..]);
744        write!(f, "(:, :")?;
745        for &coord in &coords {
746            write!(f, ", {}", coord + 1)?;
747        }
748        write!(f, ") =")?;
749
750        let mut page_base = 0usize;
751        for (offset, &coord) in coords.iter().enumerate() {
752            page_base += coord * strides[offset + 2];
753        }
754        for r in 0..rows {
755            writeln!(f)?;
756            write!(f, "  ")?;
757            for c in 0..cols {
758                if c > 0 {
759                    write!(f, "  ")?;
760                }
761                let linear = page_base + r + c * rows;
762                write_element(f, linear)?;
763            }
764        }
765    }
766    Ok(())
767}
768
769impl fmt::Display for Tensor {
770    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771        match self.shape.len() {
772            0 | 1 => {
773                // Treat as row vector for display
774                write!(f, "[")?;
775                for (i, v) in self.data.iter().enumerate() {
776                    if i > 0 {
777                        write!(f, " ")?;
778                    }
779                    write!(f, "{}", format_number(*v))?;
780                }
781                write!(f, "]")
782            }
783            2 => {
784                let rows = self.rows();
785                let cols = self.cols();
786                // Display as matrix
787                for r in 0..rows {
788                    writeln!(f)?;
789                    write!(f, "  ")?; // Indent
790                    for c in 0..cols {
791                        if c > 0 {
792                            write!(f, "  ")?;
793                        }
794                        let v = self.data[r + c * rows];
795                        write!(f, "{}", format_number(v))?;
796                    }
797                }
798                Ok(())
799            }
800            _ => {
801                if should_expand_nd_display(&self.shape) {
802                    write_nd_pages(f, &self.shape, |f, idx| {
803                        write!(f, "{}", format_number(self.data[idx]))
804                    })
805                } else {
806                    write!(f, "Tensor(shape={:?})", self.shape)
807                }
808            }
809        }
810    }
811}
812
813impl fmt::Display for SparseTensor {
814    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815        writeln!(
816            f,
817            "{}x{} sparse double matrix with {} nonzero entries",
818            self.rows,
819            self.cols,
820            self.nnz()
821        )?;
822        if self.nnz() == 0 {
823            return Ok(());
824        }
825        for col in 0..self.cols {
826            for idx in self.col_ptrs[col]..self.col_ptrs[col + 1] {
827                let row = self.row_indices[idx];
828                writeln!(
829                    f,
830                    "  ({},{})  {}",
831                    row + 1,
832                    col + 1,
833                    format_number(self.values[idx])
834                )?;
835            }
836        }
837        Ok(())
838    }
839}
840
841impl fmt::Display for StringArray {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        let (rows, cols) = match self.shape.len() {
844            0 => (0, 0),
845            1 => (1, self.shape[0]),
846            _ => (self.shape[0], self.shape[1]),
847        };
848        let count = self.data.len();
849        if count == 1 && rows == 1 && cols == 1 {
850            let v = &self.data[0];
851            if v == "<missing>" {
852                return write!(f, "<missing>");
853            }
854            let escaped = v.replace('"', "\\\"");
855            return write!(f, "\"{escaped}\"");
856        }
857        if self.shape.len() > 2 {
858            let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
859            return write!(f, "{} string array", dims.join("x"));
860        }
861        write!(f, "{rows}x{cols} string array")?;
862        if rows == 0 || cols == 0 {
863            return Ok(());
864        }
865        for r in 0..rows {
866            writeln!(f)?;
867            write!(f, "  ")?;
868            for c in 0..cols {
869                if c > 0 {
870                    write!(f, "  ")?;
871                }
872                let v = &self.data[r + c * rows];
873                if v == "<missing>" {
874                    write!(f, "<missing>")?;
875                } else {
876                    let escaped = v.replace('"', "\\\"");
877                    write!(f, "\"{escaped}\"")?;
878                }
879            }
880        }
881        Ok(())
882    }
883}
884
885impl fmt::Display for LogicalArray {
886    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
887        if self.data.len() == 1 {
888            return write!(f, "{}", if self.data[0] != 0 { 1 } else { 0 });
889        }
890        match self.shape.len() {
891            0 => write!(f, "[]"),
892            1 => {
893                write!(f, "[")?;
894                for (i, v) in self.data.iter().enumerate() {
895                    if i > 0 {
896                        write!(f, " ")?;
897                    }
898                    write!(f, "{}", if *v != 0 { 1 } else { 0 })?;
899                }
900                write!(f, "]")
901            }
902            2 => {
903                let rows = self.shape[0];
904                let cols = self.shape[1];
905                // Display as matrix
906                for r in 0..rows {
907                    writeln!(f)?;
908                    write!(f, "  ")?; // Indent
909                    for c in 0..cols {
910                        if c > 0 {
911                            write!(f, "  ")?;
912                        }
913                        let idx = r + c * rows;
914                        write!(f, "{}", if self.data[idx] != 0 { 1 } else { 0 })?;
915                    }
916                }
917                Ok(())
918            }
919            _ => {
920                if should_expand_nd_display(&self.shape) {
921                    write_nd_pages(f, &self.shape, |f, idx| {
922                        write!(f, "{}", if self.data[idx] != 0 { 1 } else { 0 })
923                    })
924                } else {
925                    let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
926                    write!(f, "{} logical array", dims.join("x"))
927                }
928            }
929        }
930    }
931}
932
933impl fmt::Display for CharArray {
934    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
935        for r in 0..self.rows {
936            writeln!(f)?;
937            write!(f, "  ")?; // Indent
938            for c in 0..self.cols {
939                let ch = self.data[r * self.cols + c];
940                write!(f, "{ch}")?;
941            }
942        }
943        Ok(())
944    }
945}
946
947// From implementations for Value
948impl From<i32> for Value {
949    fn from(i: i32) -> Self {
950        Value::Int(IntValue::I32(i))
951    }
952}
953impl From<i64> for Value {
954    fn from(i: i64) -> Self {
955        Value::Int(IntValue::I64(i))
956    }
957}
958impl From<u32> for Value {
959    fn from(i: u32) -> Self {
960        Value::Int(IntValue::U32(i))
961    }
962}
963impl From<u64> for Value {
964    fn from(i: u64) -> Self {
965        Value::Int(IntValue::U64(i))
966    }
967}
968impl From<i16> for Value {
969    fn from(i: i16) -> Self {
970        Value::Int(IntValue::I16(i))
971    }
972}
973impl From<i8> for Value {
974    fn from(i: i8) -> Self {
975        Value::Int(IntValue::I8(i))
976    }
977}
978impl From<u16> for Value {
979    fn from(i: u16) -> Self {
980        Value::Int(IntValue::U16(i))
981    }
982}
983impl From<u8> for Value {
984    fn from(i: u8) -> Self {
985        Value::Int(IntValue::U8(i))
986    }
987}
988
989impl From<f64> for Value {
990    fn from(f: f64) -> Self {
991        Value::Num(f)
992    }
993}
994
995impl From<bool> for Value {
996    fn from(b: bool) -> Self {
997        Value::Bool(b)
998    }
999}
1000
1001impl From<String> for Value {
1002    fn from(s: String) -> Self {
1003        Value::String(s)
1004    }
1005}
1006
1007impl From<&str> for Value {
1008    fn from(s: &str) -> Self {
1009        Value::String(s.to_string())
1010    }
1011}
1012
1013impl From<Tensor> for Value {
1014    fn from(m: Tensor) -> Self {
1015        Value::Tensor(m)
1016    }
1017}
1018
1019// Remove blanket From<Vec<Value>> to avoid losing shape information
1020
1021// TryFrom implementations for extracting native types
1022impl TryFrom<&Value> for i32 {
1023    type Error = String;
1024    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1025        match v {
1026            Value::Int(i) => Ok(i.to_i64() as i32),
1027            Value::Num(n) => Ok(*n as i32),
1028            _ => Err(format!("cannot convert {v:?} to i32")),
1029        }
1030    }
1031}
1032
1033impl TryFrom<&Value> for f64 {
1034    type Error = String;
1035    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1036        match v {
1037            Value::Num(n) => Ok(*n),
1038            Value::Int(i) => Ok(i.to_f64()),
1039            _ => Err(format!("cannot convert {v:?} to f64")),
1040        }
1041    }
1042}
1043
1044impl TryFrom<&Value> for bool {
1045    type Error = String;
1046    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1047        match v {
1048            Value::Bool(b) => Ok(*b),
1049            Value::Int(i) => Ok(!i.is_zero()),
1050            Value::Num(n) => Ok(*n != 0.0),
1051            _ => Err(format!("cannot convert {v:?} to bool")),
1052        }
1053    }
1054}
1055
1056impl TryFrom<&Value> for String {
1057    type Error = String;
1058    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1059        match v {
1060            Value::String(s) => Ok(s.clone()),
1061            Value::StringArray(sa) => {
1062                if sa.data.len() == 1 {
1063                    Ok(sa.data[0].clone())
1064                } else {
1065                    Err("cannot convert string array to scalar string".to_string())
1066                }
1067            }
1068            Value::CharArray(ca) => {
1069                // Convert full char array to one string if it is a single row; else error
1070                if ca.rows == 1 {
1071                    Ok(ca.data.iter().collect())
1072                } else {
1073                    Err("cannot convert multi-row char array to scalar string".to_string())
1074                }
1075            }
1076            Value::Int(i) => Ok(i.to_i64().to_string()),
1077            Value::Num(n) => Ok(n.to_string()),
1078            Value::Bool(b) => Ok(b.to_string()),
1079            _ => Err(format!("cannot convert {v:?} to String")),
1080        }
1081    }
1082}
1083
1084impl TryFrom<&Value> for Tensor {
1085    type Error = String;
1086    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1087        match v {
1088            Value::Tensor(m) => Ok(m.clone()),
1089            _ => Err(format!("cannot convert {v:?} to Tensor")),
1090        }
1091    }
1092}
1093
1094impl TryFrom<&Value> for Value {
1095    type Error = String;
1096    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1097        Ok(v.clone())
1098    }
1099}
1100
1101impl TryFrom<&Value> for Vec<Value> {
1102    type Error = String;
1103    fn try_from(v: &Value) -> Result<Self, Self::Error> {
1104        match v {
1105            Value::Cell(c) => Ok(c.data.clone()),
1106            _ => Err(format!("cannot convert {v:?} to Vec<Value>")),
1107        }
1108    }
1109}
1110
1111use serde::{Deserialize, Serialize};
1112
1113/// Enhanced type system used throughout RunMat for HIR and builtin functions
1114/// Designed to mirror Value variants for better type inference and LSP support
1115#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
1116pub enum Type {
1117    /// Integer number type
1118    Int,
1119    /// Floating-point number type  
1120    Num,
1121    /// Boolean type
1122    Bool,
1123    /// Logical array type (N-D boolean array) with optional shape information
1124    Logical {
1125        /// Optional full shape; None means unknown/dynamic; individual dims can be omitted by using None
1126        shape: Option<Vec<Option<usize>>>,
1127    },
1128    /// String type
1129    String,
1130    /// Tensor type with optional shape information (column-major semantics in runtime)
1131    Tensor {
1132        /// Optional full shape; None means unknown/dynamic; individual dims can be omitted by using None
1133        shape: Option<Vec<Option<usize>>>,
1134    },
1135    /// Scalar symbolic expression type.
1136    Symbolic,
1137    /// Cell array type with optional element type information
1138    Cell {
1139        /// Optional element type (None means mixed/unknown)
1140        element_type: Option<Box<Type>>,
1141        /// Optional length (None means unknown/dynamic)
1142        length: Option<usize>,
1143    },
1144    /// Function type with parameter and return types
1145    Function {
1146        /// Parameter types
1147        params: Vec<Type>,
1148        /// Return type
1149        returns: Box<Type>,
1150    },
1151    /// Void type (no value)
1152    Void,
1153    /// Unknown type (for type inference)
1154    Unknown,
1155    /// Union type (multiple possible types)
1156    Union(Vec<Type>),
1157    /// Struct-like type with optional known field set (purely for inference)
1158    Struct {
1159        /// Optional set of known field names observed via control-flow (None = unknown fields)
1160        known_fields: Option<Vec<String>>, // kept sorted unique for deterministic Eq
1161    },
1162    /// Multiple return values captured as a list (internal destructuring helper)
1163    OutputList(Vec<Type>),
1164}
1165
1166impl Type {
1167    /// Create a tensor type with unknown shape
1168    pub fn tensor() -> Self {
1169        Type::Tensor { shape: None }
1170    }
1171
1172    /// Create a logical type with unknown shape
1173    pub fn logical() -> Self {
1174        Type::Logical { shape: None }
1175    }
1176
1177    /// Create a logical type with known shape
1178    pub fn logical_with_shape(shape: Vec<usize>) -> Self {
1179        Type::Logical {
1180            shape: Some(shape.into_iter().map(Some).collect()),
1181        }
1182    }
1183
1184    /// Create a tensor type with known shape
1185    pub fn tensor_with_shape(shape: Vec<usize>) -> Self {
1186        Type::Tensor {
1187            shape: Some(shape.into_iter().map(Some).collect()),
1188        }
1189    }
1190
1191    /// Create a cell array type with unknown element type
1192    pub fn cell() -> Self {
1193        Type::Cell {
1194            element_type: None,
1195            length: None,
1196        }
1197    }
1198
1199    /// Create a cell array type with known element type
1200    pub fn cell_of(element_type: Type) -> Self {
1201        Type::Cell {
1202            element_type: Some(Box::new(element_type)),
1203            length: None,
1204        }
1205    }
1206
1207    /// Check if this type is compatible with another type
1208    pub fn is_compatible_with(&self, other: &Type) -> bool {
1209        match (self, other) {
1210            (Type::Unknown, _) | (_, Type::Unknown) => true,
1211            (Type::Int, Type::Num) | (Type::Num, Type::Int) => true, // Number compatibility
1212            (Type::Tensor { .. }, Type::Tensor { .. }) => true, // Tensor compatibility regardless of dims for now
1213            (Type::OutputList(a), Type::OutputList(b)) => a.len() == b.len(),
1214            (a, b) => a == b,
1215        }
1216    }
1217
1218    /// Get the most specific common type between two types
1219    pub fn unify(&self, other: &Type) -> Type {
1220        match (self, other) {
1221            (Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
1222            (Type::Int, Type::Num) | (Type::Num, Type::Int) => Type::Num,
1223            (Type::Tensor { shape: a }, Type::Tensor { shape: b }) => {
1224                let a_norm = match a {
1225                    Some(dims) if dims.is_empty() => None,
1226                    _ => a.clone(),
1227                };
1228                let b_norm = match b {
1229                    Some(dims) if dims.is_empty() => None,
1230                    _ => b.clone(),
1231                };
1232                let a_unknown = a_norm
1233                    .as_ref()
1234                    .map(|dims| dims.iter().all(|d| d.is_none()))
1235                    .unwrap_or(true);
1236                let b_unknown = b_norm
1237                    .as_ref()
1238                    .map(|dims| dims.iter().all(|d| d.is_none()))
1239                    .unwrap_or(true);
1240                if a_norm == b_norm
1241                    || (!a_unknown && b_unknown)
1242                    || (a_norm.is_some() && b_norm.is_none())
1243                {
1244                    Type::Tensor { shape: a_norm }
1245                } else if (a_unknown && !b_unknown) || (a_norm.is_none() && b_norm.is_some()) {
1246                    Type::Tensor { shape: b_norm }
1247                } else {
1248                    Type::tensor()
1249                }
1250            }
1251            (Type::Logical { shape: a }, Type::Logical { shape: b }) => {
1252                let a_norm = match a {
1253                    Some(dims) if dims.is_empty() => None,
1254                    _ => a.clone(),
1255                };
1256                let b_norm = match b {
1257                    Some(dims) if dims.is_empty() => None,
1258                    _ => b.clone(),
1259                };
1260                let a_unknown = a_norm
1261                    .as_ref()
1262                    .map(|dims| dims.iter().all(|d| d.is_none()))
1263                    .unwrap_or(true);
1264                let b_unknown = b_norm
1265                    .as_ref()
1266                    .map(|dims| dims.iter().all(|d| d.is_none()))
1267                    .unwrap_or(true);
1268                if a_norm == b_norm
1269                    || (!a_unknown && b_unknown)
1270                    || (a_norm.is_some() && b_norm.is_none())
1271                {
1272                    Type::Logical { shape: a_norm }
1273                } else if (a_unknown && !b_unknown) || (a_norm.is_none() && b_norm.is_some()) {
1274                    Type::Logical { shape: b_norm }
1275                } else {
1276                    Type::logical()
1277                }
1278            }
1279            (Type::Struct { known_fields: a }, Type::Struct { known_fields: b }) => match (a, b) {
1280                (None, None) => Type::Struct { known_fields: None },
1281                (Some(ka), None) | (None, Some(ka)) => Type::Struct {
1282                    known_fields: Some(ka.clone()),
1283                },
1284                (Some(ka), Some(kb)) => {
1285                    let mut set: std::collections::BTreeSet<String> = ka.iter().cloned().collect();
1286                    set.extend(kb.iter().cloned());
1287                    Type::Struct {
1288                        known_fields: Some(set.into_iter().collect()),
1289                    }
1290                }
1291            },
1292            (Type::OutputList(a), Type::OutputList(b)) => {
1293                if a.len() == b.len() {
1294                    let items = a
1295                        .iter()
1296                        .zip(b.iter())
1297                        .map(|(lhs, rhs)| lhs.unify(rhs))
1298                        .collect();
1299                    Type::OutputList(items)
1300                } else {
1301                    Type::OutputList(vec![Type::Unknown; a.len().max(b.len())])
1302                }
1303            }
1304            (a, b) if a == b => a.clone(),
1305            _ => Type::Union(vec![self.clone(), other.clone()]),
1306        }
1307    }
1308
1309    /// Infer type from a Value
1310    pub fn from_value(value: &Value) -> Type {
1311        match value {
1312            Value::Int(_) => Type::Int,
1313            Value::Num(_) => Type::Num,
1314            Value::Complex(_, _) => Type::Num, // treat as numeric double (complex) in type system for now
1315            Value::Bool(_) => Type::Bool,
1316            Value::LogicalArray(arr) => Type::Logical {
1317                shape: Some(arr.shape.iter().map(|&d| Some(d)).collect()),
1318            },
1319            Value::String(_) => Type::String,
1320            Value::StringArray(_sa) => {
1321                // Model as Cell of String for type system for now
1322                Type::cell_of(Type::String)
1323            }
1324            Value::Tensor(t) => Type::Tensor {
1325                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
1326            },
1327            Value::SparseTensor(t) => Type::Tensor {
1328                shape: Some(vec![Some(t.rows), Some(t.cols)]),
1329            },
1330            Value::ComplexTensor(t) => Type::Tensor {
1331                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
1332            },
1333            Value::Symbolic(_) => Type::Symbolic,
1334            Value::Cell(cells) => {
1335                if cells.data.is_empty() {
1336                    Type::cell()
1337                } else {
1338                    // Infer element type from first element
1339                    let element_type = Type::from_value(&cells.data[0]);
1340                    Type::Cell {
1341                        element_type: Some(Box::new(element_type)),
1342                        length: Some(cells.data.len()),
1343                    }
1344                }
1345            }
1346            Value::GpuTensor(h) => Type::Tensor {
1347                shape: Some(h.shape.iter().map(|&d| Some(d)).collect()),
1348            },
1349            Value::Object(_) => Type::Unknown,
1350            Value::HandleObject(_) => Type::Unknown,
1351            Value::Listener(_) => Type::Unknown,
1352            Value::Struct(_) => Type::Struct { known_fields: None },
1353            Value::FunctionHandle(_)
1354            | Value::ExternalFunctionHandle(_)
1355            | Value::MethodFunctionHandle(_)
1356            | Value::BoundFunctionHandle { .. } => Type::Function {
1357                params: vec![Type::Unknown],
1358                returns: Box::new(Type::Unknown),
1359            },
1360            Value::Closure(_) => Type::Function {
1361                params: vec![Type::Unknown],
1362                returns: Box::new(Type::Unknown),
1363            },
1364            Value::ClassRef(_) => Type::Unknown,
1365            Value::MException(_) => Type::Unknown,
1366            Value::CharArray(ca) => {
1367                // Treat as cell of char for type purposes; or a 2-D char matrix conceptually
1368                Type::Cell {
1369                    element_type: Some(Box::new(Type::String)),
1370                    length: Some(ca.rows * ca.cols),
1371                }
1372            }
1373            Value::OutputList(values) => {
1374                Type::OutputList(values.iter().map(Type::from_value).collect())
1375            }
1376        }
1377    }
1378}
1379
1380#[derive(Debug, Clone, PartialEq)]
1381pub struct Closure {
1382    pub function_name: String,
1383    pub bound_function: Option<usize>,
1384    pub captures: Vec<Value>,
1385}
1386
1387/// Acceleration metadata describing GPU-friendly characteristics of a builtin.
1388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1389pub enum AccelTag {
1390    Unary,
1391    Elementwise,
1392    Reduction,
1393    MatMul,
1394    Transpose,
1395    ArrayConstruct,
1396}
1397
1398/// Control-flow type for builtins that may suspend or error.
1399pub type BuiltinControlFlow = runmat_async::RuntimeError;
1400
1401/// Async result type for builtins.
1402pub type BuiltinFuture = Pin<Box<dyn Future<Output = Result<Value, BuiltinControlFlow>> + 'static>>;
1403
1404#[derive(Clone, Debug, Default)]
1405pub struct ResolveContext {
1406    pub literal_args: Vec<LiteralValue>,
1407}
1408
1409#[derive(Clone, Debug, PartialEq)]
1410pub enum LiteralValue {
1411    Number(f64),
1412    Bool(bool),
1413    String(String),
1414    Vector(Vec<LiteralValue>),
1415    Unknown,
1416}
1417
1418impl ResolveContext {
1419    pub fn new(literal_args: Vec<LiteralValue>) -> Self {
1420        Self { literal_args }
1421    }
1422
1423    pub fn numeric_dims(&self) -> Vec<Option<usize>> {
1424        self.numeric_dims_from(0)
1425    }
1426
1427    pub fn numeric_dims_from(&self, start: usize) -> Vec<Option<usize>> {
1428        let slice = self.literal_args.get(start..).unwrap_or(&[]);
1429        if let Some(LiteralValue::Vector(values)) = slice.first() {
1430            return values
1431                .iter()
1432                .map(Self::numeric_dimension_from_literal)
1433                .collect();
1434        }
1435        slice
1436            .iter()
1437            .map(Self::numeric_dimension_from_literal)
1438            .collect()
1439    }
1440
1441    pub fn literal_string_at(&self, index: usize) -> Option<String> {
1442        match self.literal_args.get(index) {
1443            Some(LiteralValue::String(value)) => Some(value.to_ascii_lowercase()),
1444            _ => None,
1445        }
1446    }
1447
1448    pub fn literal_bool_at(&self, index: usize) -> Option<bool> {
1449        match self.literal_args.get(index) {
1450            Some(LiteralValue::Bool(value)) => Some(*value),
1451            _ => None,
1452        }
1453    }
1454
1455    pub fn literal_vector_at(&self, index: usize) -> Option<Vec<LiteralValue>> {
1456        match self.literal_args.get(index) {
1457            Some(LiteralValue::Vector(values)) => Some(values.clone()),
1458            _ => None,
1459        }
1460    }
1461
1462    pub fn numeric_vector_at(&self, index: usize) -> Option<Vec<Option<usize>>> {
1463        let values = match self.literal_args.get(index) {
1464            Some(LiteralValue::Vector(values)) => values,
1465            _ => return None,
1466        };
1467        if values
1468            .iter()
1469            .any(|value| matches!(value, LiteralValue::Vector(_)))
1470        {
1471            return None;
1472        }
1473        Some(
1474            values
1475                .iter()
1476                .map(Self::numeric_dimension_from_literal)
1477                .collect(),
1478        )
1479    }
1480
1481    fn numeric_dimension_from_literal(value: &LiteralValue) -> Option<usize> {
1482        match value {
1483            LiteralValue::Number(num) => {
1484                if num.is_finite() {
1485                    let rounded = num.round();
1486                    if (num - rounded).abs() <= 1e-9 && rounded >= 0.0 {
1487                        return Some(rounded as usize);
1488                    }
1489                }
1490                None
1491            }
1492            _ => None,
1493        }
1494    }
1495}
1496
1497#[cfg(test)]
1498mod resolve_context_tests {
1499    use super::{LiteralValue, ResolveContext};
1500
1501    #[test]
1502    fn numeric_dims_reads_vector_literal() {
1503        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![
1504            LiteralValue::Number(2.0),
1505            LiteralValue::Number(3.0),
1506        ])]);
1507        assert_eq!(ctx.numeric_dims(), vec![Some(2), Some(3)]);
1508    }
1509
1510    #[test]
1511    fn numeric_dims_skips_non_numeric_entries() {
1512        let ctx = ResolveContext::new(vec![
1513            LiteralValue::Number(4.0),
1514            LiteralValue::String("like".to_string()),
1515            LiteralValue::Unknown,
1516        ]);
1517        assert_eq!(ctx.numeric_dims(), vec![Some(4), None, None]);
1518    }
1519
1520    #[test]
1521    fn numeric_dims_prefers_vector_even_with_trailing_args() {
1522        let ctx = ResolveContext::new(vec![
1523            LiteralValue::Vector(vec![LiteralValue::Number(1.0), LiteralValue::Number(5.0)]),
1524            LiteralValue::String("like".to_string()),
1525        ]);
1526        assert_eq!(ctx.numeric_dims(), vec![Some(1), Some(5)]);
1527    }
1528
1529    #[test]
1530    fn literal_string_is_lowercased() {
1531        let ctx = ResolveContext::new(vec![LiteralValue::String("OmItNaN".to_string())]);
1532        assert_eq!(ctx.literal_string_at(0), Some("omitnan".to_string()));
1533    }
1534
1535    #[test]
1536    fn literal_bool_is_available() {
1537        let ctx = ResolveContext::new(vec![LiteralValue::Bool(true)]);
1538        assert_eq!(ctx.literal_bool_at(0), Some(true));
1539    }
1540
1541    #[test]
1542    fn literal_vector_at_returns_clone() {
1543        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![
1544            LiteralValue::Number(7.0),
1545            LiteralValue::Unknown,
1546        ])]);
1547        assert_eq!(
1548            ctx.literal_vector_at(0),
1549            Some(vec![LiteralValue::Number(7.0), LiteralValue::Unknown])
1550        );
1551    }
1552
1553    #[test]
1554    fn numeric_vector_at_rejects_nested_vectors() {
1555        let ctx = ResolveContext::new(vec![LiteralValue::Vector(vec![LiteralValue::Vector(
1556            vec![LiteralValue::Number(1.0)],
1557        )])]);
1558        assert_eq!(ctx.numeric_vector_at(0), None);
1559    }
1560}
1561
1562pub type TypeResolver = fn(args: &[Type]) -> Type;
1563pub type TypeResolverWithContext = fn(args: &[Type], ctx: &ResolveContext) -> Type;
1564
1565#[derive(Clone, Copy, Debug)]
1566pub enum TypeResolverKind {
1567    Simple(TypeResolver),
1568    WithContext(TypeResolverWithContext),
1569}
1570
1571pub fn type_resolver_kind(resolver: TypeResolver) -> TypeResolverKind {
1572    TypeResolverKind::Simple(resolver)
1573}
1574
1575pub fn type_resolver_kind_ctx(resolver: TypeResolverWithContext) -> TypeResolverKind {
1576    TypeResolverKind::WithContext(resolver)
1577}
1578
1579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1580pub enum BuiltinOutputMode {
1581    Fixed,
1582    ByRequestedOutputCount,
1583}
1584
1585#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1586pub enum BuiltinCompletionPolicy {
1587    Public,
1588    MethodOnly,
1589    HiddenInternal,
1590}
1591
1592#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1593pub enum BuiltinParamArity {
1594    Required,
1595    Optional,
1596    Variadic,
1597}
1598
1599#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1600pub enum BuiltinParamType {
1601    Any,
1602    NumericScalar,
1603    IntegerScalar,
1604    StringScalar,
1605    NumericArray,
1606    LogicalArray,
1607    SizeArg,
1608    LikePrototype,
1609    AxesHandle,
1610    StyleSpec,
1611    PropertyName,
1612    PropertyValue,
1613}
1614
1615#[derive(Debug, Clone, Serialize)]
1616pub struct BuiltinParamDescriptor {
1617    pub name: &'static str,
1618    pub ty: BuiltinParamType,
1619    pub arity: BuiltinParamArity,
1620    pub default: Option<&'static str>,
1621    pub description: &'static str,
1622}
1623
1624#[derive(Debug, Clone, Serialize)]
1625pub struct BuiltinSignatureDescriptor {
1626    pub label: &'static str,
1627    pub inputs: &'static [BuiltinParamDescriptor],
1628    pub outputs: &'static [BuiltinParamDescriptor],
1629}
1630
1631#[derive(Debug, Clone, Serialize)]
1632pub struct BuiltinErrorDescriptor {
1633    pub code: &'static str,
1634    pub identifier: Option<&'static str>,
1635    pub when: &'static str,
1636    pub message: &'static str,
1637}
1638
1639#[derive(Debug, Clone, Serialize)]
1640pub struct BuiltinDescriptor {
1641    pub signatures: &'static [BuiltinSignatureDescriptor],
1642    pub output_mode: BuiltinOutputMode,
1643    pub completion_policy: BuiltinCompletionPolicy,
1644    pub errors: &'static [BuiltinErrorDescriptor],
1645}
1646
1647/// Simple builtin function definition using the unified type system
1648#[derive(Debug, Clone)]
1649pub struct BuiltinFunction {
1650    pub name: &'static str,
1651    pub description: &'static str,
1652    pub category: &'static str,
1653    pub doc: &'static str,
1654    pub examples: &'static str,
1655    pub param_types: Vec<Type>,
1656    pub return_type: Type,
1657    pub type_resolver: Option<TypeResolverKind>,
1658    pub implementation: fn(&[Value]) -> BuiltinFuture,
1659    pub accel_tags: &'static [AccelTag],
1660    pub is_sink: bool,
1661    pub suppress_auto_output: bool,
1662    pub descriptor: Option<&'static BuiltinDescriptor>,
1663}
1664
1665impl BuiltinFunction {
1666    #[allow(clippy::too_many_arguments)]
1667    pub fn new(
1668        name: &'static str,
1669        description: &'static str,
1670        category: &'static str,
1671        doc: &'static str,
1672        examples: &'static str,
1673        param_types: Vec<Type>,
1674        return_type: Type,
1675        type_resolver: Option<TypeResolverKind>,
1676        implementation: fn(&[Value]) -> BuiltinFuture,
1677        accel_tags: &'static [AccelTag],
1678        is_sink: bool,
1679        suppress_auto_output: bool,
1680    ) -> Self {
1681        Self {
1682            name,
1683            description,
1684            category,
1685            doc,
1686            examples,
1687            param_types,
1688            return_type,
1689            type_resolver,
1690            implementation,
1691            accel_tags,
1692            is_sink,
1693            suppress_auto_output,
1694            descriptor: None,
1695        }
1696    }
1697
1698    pub fn with_descriptor(mut self, descriptor: &'static BuiltinDescriptor) -> Self {
1699        self.descriptor = Some(descriptor);
1700        self
1701    }
1702
1703    pub fn with_descriptor_option(
1704        mut self,
1705        descriptor: Option<&'static BuiltinDescriptor>,
1706    ) -> Self {
1707        self.descriptor = descriptor;
1708        self
1709    }
1710
1711    pub fn infer_return_type(&self, args: &[Type]) -> Type {
1712        self.infer_return_type_with_context(args, &ResolveContext::default())
1713    }
1714
1715    pub fn infer_return_type_with_context(&self, args: &[Type], ctx: &ResolveContext) -> Type {
1716        if let Some(resolver) = self.type_resolver {
1717            return match resolver {
1718                TypeResolverKind::Simple(resolver) => resolver(args),
1719                TypeResolverKind::WithContext(resolver) => resolver(args, ctx),
1720            };
1721        }
1722        self.return_type.clone()
1723    }
1724
1725    pub fn semantics(&self) -> BuiltinSemantics {
1726        semantics::builtin_semantics_for(self)
1727    }
1728}
1729
1730/// A constant value that can be accessed as a variable
1731#[derive(Clone)]
1732pub struct Constant {
1733    pub name: &'static str,
1734    pub value: Value,
1735}
1736
1737pub mod semantics;
1738pub mod shape_rules;
1739
1740pub use semantics::{
1741    builtin_semantics_for, builtin_semantics_for_name, BuiltinAsyncBehavior, BuiltinCompatibility,
1742    BuiltinEffects, BuiltinEnvironmentEffect, BuiltinPurity, BuiltinSemanticKind, BuiltinSemantics,
1743    BuiltinWorkspaceEffect, ConcatKind, ShapeTransformKind,
1744};
1745
1746impl std::fmt::Debug for Constant {
1747    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1748        write!(
1749            f,
1750            "Constant {{ name: {:?}, value: {:?} }}",
1751            self.name, self.value
1752        )
1753    }
1754}
1755
1756#[cfg(not(target_arch = "wasm32"))]
1757inventory::collect!(BuiltinFunction);
1758#[cfg(not(target_arch = "wasm32"))]
1759inventory::collect!(Constant);
1760
1761#[cfg(not(target_arch = "wasm32"))]
1762pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
1763    inventory::iter::<BuiltinFunction>().collect()
1764}
1765
1766#[cfg(target_arch = "wasm32")]
1767pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
1768    wasm_registry::builtin_functions()
1769}
1770
1771#[cfg(not(target_arch = "wasm32"))]
1772static BUILTIN_LOOKUP: OnceLock<HashMap<String, &'static BuiltinFunction>> = OnceLock::new();
1773
1774#[cfg(not(target_arch = "wasm32"))]
1775fn builtin_lookup_map() -> &'static HashMap<String, &'static BuiltinFunction> {
1776    BUILTIN_LOOKUP.get_or_init(|| {
1777        let mut map = HashMap::new();
1778        for func in builtin_functions() {
1779            map.insert(func.name.to_ascii_lowercase(), func);
1780        }
1781        map
1782    })
1783}
1784
1785#[cfg(not(target_arch = "wasm32"))]
1786pub fn builtin_function_by_name(name: &str) -> Option<&'static BuiltinFunction> {
1787    builtin_lookup_map()
1788        .get(&name.to_ascii_lowercase())
1789        .copied()
1790}
1791
1792#[cfg(target_arch = "wasm32")]
1793pub fn builtin_function_by_name(name: &str) -> Option<&'static BuiltinFunction> {
1794    wasm_registry::builtin_functions()
1795        .into_iter()
1796        .find(|f| f.name.eq_ignore_ascii_case(name))
1797}
1798
1799pub fn suppresses_auto_output(name: &str) -> bool {
1800    builtin_function_by_name(name)
1801        .map(|f| f.suppress_auto_output)
1802        .unwrap_or(false)
1803}
1804
1805#[cfg(not(target_arch = "wasm32"))]
1806pub fn constants() -> Vec<&'static Constant> {
1807    inventory::iter::<Constant>().collect()
1808}
1809
1810#[cfg(target_arch = "wasm32")]
1811pub fn constants() -> Vec<&'static Constant> {
1812    wasm_registry::constants()
1813}
1814
1815// ----------------------
1816// Builtin documentation metadata (optional, registered by macros)
1817// ----------------------
1818
1819#[derive(Debug)]
1820pub struct BuiltinDoc {
1821    pub name: &'static str,
1822    pub category: Option<&'static str>,
1823    pub summary: Option<&'static str>,
1824    pub keywords: Option<&'static str>,
1825    pub errors: Option<&'static str>,
1826    pub related: Option<&'static str>,
1827    pub introduced: Option<&'static str>,
1828    pub status: Option<&'static str>,
1829    pub examples: Option<&'static str>,
1830}
1831
1832#[cfg(not(target_arch = "wasm32"))]
1833inventory::collect!(BuiltinDoc);
1834
1835#[cfg(not(target_arch = "wasm32"))]
1836pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
1837    inventory::iter::<BuiltinDoc>().collect()
1838}
1839
1840#[cfg(target_arch = "wasm32")]
1841pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
1842    wasm_registry::builtin_docs()
1843}
1844
1845// ----------------------
1846// Display implementations
1847// ----------------------
1848
1849/// Controls how numeric values are displayed in the console, mirroring MATLAB's `format` command.
1850#[derive(Debug, Clone, Copy, PartialEq, Default)]
1851pub enum FormatMode {
1852    /// 4 decimal places, fixed or scientific (MATLAB default).
1853    #[default]
1854    Short,
1855    /// 15 decimal places, fixed or scientific.
1856    Long,
1857    /// Always scientific notation, 4 decimal places.
1858    ShortE,
1859    /// Always scientific notation, 14 decimal places.
1860    LongE,
1861    /// Compact: shorter of fixed/scientific, 5 significant digits.
1862    ShortG,
1863    /// Compact: shorter of fixed/scientific, 15 significant digits.
1864    LongG,
1865    /// Rational approximation (p/q).
1866    Rational,
1867    /// IEEE 754 hexadecimal representation.
1868    Hex,
1869}
1870
1871runmat_thread_local! {
1872    static DISPLAY_FORMAT: RefCell<FormatMode> = const { RefCell::new(FormatMode::Short) };
1873}
1874
1875pub fn set_display_format(mode: FormatMode) {
1876    DISPLAY_FORMAT.with(|c| *c.borrow_mut() = mode);
1877}
1878
1879pub fn get_display_format() -> FormatMode {
1880    DISPLAY_FORMAT.with(|c| *c.borrow())
1881}
1882
1883/// Format a number using the current thread-local display format.
1884pub fn format_number(value: f64) -> String {
1885    if value.is_nan() {
1886        return "NaN".to_string();
1887    }
1888    if value.is_infinite() {
1889        return if value.is_sign_negative() {
1890            "-Inf"
1891        } else {
1892            "Inf"
1893        }
1894        .to_string();
1895    }
1896    let mode = get_display_format();
1897    if mode == FormatMode::Hex {
1898        return fmt_hex(value);
1899    }
1900    let v = if value == 0.0 { 0.0 } else { value };
1901    match mode {
1902        FormatMode::Short => fmt_short(v),
1903        FormatMode::Long => fmt_long(v),
1904        FormatMode::ShortE => fmt_sci(v, 4),
1905        FormatMode::LongE => fmt_sci(v, 14),
1906        FormatMode::ShortG => fmt_compact(v, 5),
1907        FormatMode::LongG => fmt_compact(v, 15),
1908        FormatMode::Rational => fmt_rational(v),
1909        FormatMode::Hex => unreachable!("hex mode handled before zero normalization"),
1910    }
1911}
1912
1913/// Reformat Rust's `e`-notation exponent into MATLAB style (`e+02`, `e-03`).
1914fn matlab_exp(s: &str) -> String {
1915    if let Some(e_pos) = s.find('e') {
1916        let mantissa = &s[..e_pos];
1917        let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0);
1918        let sign = if exp >= 0 { '+' } else { '-' };
1919        format!("{mantissa}e{sign}{:02}", exp.unsigned_abs())
1920    } else {
1921        s.to_string()
1922    }
1923}
1924
1925fn fmt_sci(v: f64, dec: usize) -> String {
1926    if v == 0.0 {
1927        return format!("0.{:0>dec$}e+00", 0, dec = dec);
1928    }
1929    let s = format!("{v:.dec$e}");
1930    matlab_exp(&s)
1931}
1932
1933fn fmt_short(v: f64) -> String {
1934    let abs = v.abs();
1935    if abs == 0.0 {
1936        return "0".to_string();
1937    }
1938    if v.fract() == 0.0 && abs < 1e15 {
1939        return format!("{}", v as i64);
1940    }
1941    if (0.001..10000.0).contains(&abs) {
1942        format!("{:.4}", v)
1943    } else {
1944        fmt_sci(v, 4)
1945    }
1946}
1947
1948fn fmt_long(v: f64) -> String {
1949    let abs = v.abs();
1950    if abs == 0.0 {
1951        return "0".to_string();
1952    }
1953    if v.fract() == 0.0 && abs < 1e15 {
1954        return format!("{}", v as i64);
1955    }
1956    if (0.001..10000.0).contains(&abs) {
1957        format!("{:.15}", v)
1958    } else {
1959        fmt_sci(v, 14)
1960    }
1961}
1962
1963fn fmt_compact(v: f64, sig_digits: usize) -> String {
1964    let abs = v.abs();
1965    if abs == 0.0 {
1966        return "0".to_string();
1967    }
1968    let use_scientific = !(1e-4..1e6).contains(&abs);
1969    if use_scientific {
1970        let dec = sig_digits - 1;
1971        let s = format!("{v:.dec$e}");
1972        // trim trailing zeros in mantissa then reformat exponent
1973        if let Some(e_pos) = s.find('e') {
1974            let exp_part = &s[e_pos..];
1975            let mut mantissa = s[..e_pos].to_string();
1976            if let Some(dot) = mantissa.find('.') {
1977                let mut end = mantissa.len();
1978                while end > dot + 1 && mantissa.as_bytes()[end - 1] == b'0' {
1979                    end -= 1;
1980                }
1981                if mantissa.as_bytes()[end - 1] == b'.' {
1982                    end -= 1;
1983                }
1984                mantissa.truncate(end);
1985            }
1986            return matlab_exp(&format!("{mantissa}{exp_part}"));
1987        }
1988        return matlab_exp(&s);
1989    }
1990    let exp10 = abs.log10().floor() as i32;
1991    let decimals = ((sig_digits as i32 - 1 - exp10).max(0)) as usize;
1992    let pow = 10f64.powi(decimals as i32);
1993    let rounded = (v * pow).round() / pow;
1994    let mut s = format!("{rounded:.decimals$}");
1995    if let Some(dot) = s.find('.') {
1996        let mut end = s.len();
1997        while end > dot + 1 && s.as_bytes()[end - 1] == b'0' {
1998            end -= 1;
1999        }
2000        if s.as_bytes()[end - 1] == b'.' {
2001            end -= 1;
2002        }
2003        s.truncate(end);
2004    }
2005    if s.is_empty() || s == "-0" {
2006        s = "0".to_string();
2007    }
2008    s
2009}
2010
2011fn fmt_rational(v: f64) -> String {
2012    if v == 0.0 {
2013        return "0".to_string();
2014    }
2015    let negative = v < 0.0;
2016    let abs = v.abs();
2017    if v.fract() == 0.0 && abs < 1e15 {
2018        return format!("{}", v as i64);
2019    }
2020    // Continued fraction convergents; stop at the first one within MATLAB's
2021    // 5e-7 relative tolerance (matches `format rational` behaviour for pi → 355/113).
2022    let tol = 5e-7 * abs;
2023    let max_d = 1_000_000i64;
2024    let mut n0: i64 = 1;
2025    let mut n1: i64 = abs.floor() as i64;
2026    let mut d0: i64 = 0;
2027    let mut d1: i64 = 1;
2028    let mut a = abs;
2029    let mut best_n = n1;
2030    let mut best_d = d1;
2031    for _ in 0..50 {
2032        if (abs - best_n as f64 / best_d as f64).abs() <= tol {
2033            break;
2034        }
2035        let f = a.fract();
2036        if f < 1e-10 {
2037            break;
2038        }
2039        a = 1.0 / f;
2040        let q = a.floor() as i64;
2041        let Some(n2) = q.checked_mul(n1).and_then(|v| v.checked_add(n0)) else {
2042            break;
2043        };
2044        let Some(d2) = q.checked_mul(d1).and_then(|v| v.checked_add(d0)) else {
2045            break;
2046        };
2047        if d2 > max_d {
2048            break;
2049        }
2050        best_n = n2;
2051        best_d = d2;
2052        n0 = n1;
2053        n1 = n2;
2054        d0 = d1;
2055        d1 = d2;
2056    }
2057    let sign = if negative { "-" } else { "" };
2058    if best_d == 1 {
2059        format!("{sign}{best_n}")
2060    } else {
2061        format!("{sign}{best_n}/{best_d}")
2062    }
2063}
2064
2065fn fmt_hex(v: f64) -> String {
2066    format!("{:016x}", v.to_bits())
2067}
2068
2069// -------- Exception type --------
2070#[derive(Debug, Clone, PartialEq)]
2071pub struct MException {
2072    pub identifier: String,
2073    pub message: String,
2074    pub stack: Vec<String>,
2075}
2076
2077impl MException {
2078    pub fn new(identifier: String, message: String) -> Self {
2079        Self {
2080            identifier,
2081            message,
2082            stack: Vec::new(),
2083        }
2084    }
2085}
2086
2087/// Reference to a GC-allocated object providing language handle semantics
2088#[derive(Debug, Clone)]
2089pub struct HandleRef {
2090    pub class_name: String,
2091    pub target: GcHandle,
2092    pub valid: bool,
2093}
2094
2095impl PartialEq for HandleRef {
2096    fn eq(&self, other: &Self) -> bool {
2097        self.target == other.target
2098    }
2099}
2100
2101/// Event listener handle for events
2102#[derive(Debug, Clone, PartialEq)]
2103pub struct Listener {
2104    pub id: u64,
2105    pub target: GcHandle,
2106    pub target_class_name: String,
2107    pub event_name: String,
2108    pub callback: GcHandle,
2109    pub enabled: bool,
2110    pub valid: bool,
2111}
2112
2113impl Listener {
2114    pub fn class_name(&self) -> String {
2115        self.target_class_name.clone()
2116    }
2117}
2118
2119impl Trace for CellArray {
2120    fn trace(&self, tracer: &mut dyn Tracer) {
2121        for value in &self.data {
2122            value.trace(tracer);
2123        }
2124    }
2125}
2126
2127impl Trace for StructValue {
2128    fn trace(&self, tracer: &mut dyn Tracer) {
2129        for value in self.fields.values() {
2130            value.trace(tracer);
2131        }
2132    }
2133}
2134
2135impl Trace for Closure {
2136    fn trace(&self, tracer: &mut dyn Tracer) {
2137        for value in &self.captures {
2138            value.trace(tracer);
2139        }
2140    }
2141}
2142
2143impl Trace for ObjectInstance {
2144    fn trace(&self, tracer: &mut dyn Tracer) {
2145        for value in self.properties.values() {
2146            value.trace(tracer);
2147        }
2148    }
2149}
2150
2151impl Trace for HandleRef {
2152    fn trace(&self, tracer: &mut dyn Tracer) {
2153        tracer.mark(self.target);
2154    }
2155}
2156
2157impl Trace for Listener {
2158    fn trace(&self, tracer: &mut dyn Tracer) {
2159        tracer.mark(self.target);
2160        tracer.mark(self.callback);
2161    }
2162}
2163
2164impl Trace for Value {
2165    fn trace(&self, tracer: &mut dyn Tracer) {
2166        match self {
2167            Value::Cell(cells) => cells.trace(tracer),
2168            Value::Struct(struct_value) => struct_value.trace(tracer),
2169            Value::HandleObject(handle) => handle.trace(tracer),
2170            Value::Listener(listener) => listener.trace(tracer),
2171            Value::Closure(closure) => closure.trace(tracer),
2172            Value::Object(object) => object.trace(tracer),
2173            Value::OutputList(values) => {
2174                for value in values {
2175                    value.trace(tracer);
2176                }
2177            }
2178            Value::Int(_)
2179            | Value::Num(_)
2180            | Value::Complex(_, _)
2181            | Value::Bool(_)
2182            | Value::LogicalArray(_)
2183            | Value::String(_)
2184            | Value::StringArray(_)
2185            | Value::CharArray(_)
2186            | Value::Tensor(_)
2187            | Value::SparseTensor(_)
2188            | Value::ComplexTensor(_)
2189            | Value::Symbolic(_)
2190            | Value::GpuTensor(_)
2191            | Value::FunctionHandle(_)
2192            | Value::ExternalFunctionHandle(_)
2193            | Value::MethodFunctionHandle(_)
2194            | Value::BoundFunctionHandle { .. }
2195            | Value::ClassRef(_)
2196            | Value::MException(_) => {}
2197        }
2198    }
2199}
2200
2201impl fmt::Display for Value {
2202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2203        match self {
2204            Value::Int(i) => write!(f, "{}", i.to_i64()),
2205            Value::Num(n) => write!(f, "{}", format_number(*n)),
2206            Value::Complex(re, im) => {
2207                if *im == 0.0 {
2208                    write!(f, "{}", format_number(*re))
2209                } else if *re == 0.0 {
2210                    write!(f, "{}i", format_number(*im))
2211                } else if *im < 0.0 {
2212                    write!(f, "{}-{}i", format_number(*re), format_number(im.abs()))
2213                } else {
2214                    write!(f, "{}+{}i", format_number(*re), format_number(*im))
2215                }
2216            }
2217            Value::Bool(b) => write!(f, "{}", if *b { 1 } else { 0 }),
2218            Value::LogicalArray(la) => write!(f, "{la}"),
2219            Value::String(s) => write!(f, "'{s}'"),
2220            Value::StringArray(sa) => write!(f, "{sa}"),
2221            Value::CharArray(ca) => write!(f, "{ca}"),
2222            Value::Tensor(m) => write!(f, "{m}"),
2223            Value::SparseTensor(m) => write!(f, "{m}"),
2224            Value::ComplexTensor(m) => write!(f, "{m}"),
2225            Value::Symbolic(expr) => write!(f, "{expr}"),
2226            Value::Cell(ca) => ca.fmt(f),
2227
2228            Value::GpuTensor(h) => write!(
2229                f,
2230                "GpuTensor(shape={:?}, device={}, buffer={})",
2231                h.shape, h.device_id, h.buffer_id
2232            ),
2233            Value::Object(obj) => write!(f, "{}(props={})", obj.class_name, obj.properties.len()),
2234            Value::HandleObject(h) => {
2235                write!(
2236                    f,
2237                    "<handle {} @0x{:x} valid={}>",
2238                    h.class_name,
2239                    h.target.addr(),
2240                    h.valid
2241                )
2242            }
2243            Value::Listener(l) => {
2244                write!(
2245                    f,
2246                    "<listener id={} {}@0x{:x} '{}' enabled={} valid={}>",
2247                    l.id,
2248                    l.class_name(),
2249                    l.target.addr(),
2250                    l.event_name,
2251                    l.enabled,
2252                    l.valid
2253                )
2254            }
2255            Value::Struct(st) => {
2256                write!(f, "struct {{")?;
2257                for (i, (key, val)) in st.fields.iter().enumerate() {
2258                    if i > 0 {
2259                        write!(f, ", ")?;
2260                    }
2261                    write!(f, "{}: {}", key, val)?;
2262                }
2263                write!(f, "}}")
2264            }
2265            Value::OutputList(values) => {
2266                write!(f, "[")?;
2267                for (i, value) in values.iter().enumerate() {
2268                    if i > 0 {
2269                        write!(f, ", ")?;
2270                    }
2271                    write!(f, "{}", value)?;
2272                }
2273                write!(f, "]")
2274            }
2275            Value::FunctionHandle(name)
2276            | Value::ExternalFunctionHandle(name)
2277            | Value::MethodFunctionHandle(name) => {
2278                write!(f, "@{name}")
2279            }
2280            Value::BoundFunctionHandle { name, .. } => write!(f, "@{name}"),
2281            Value::Closure(c) => write!(
2282                f,
2283                "<closure {} captures={}>",
2284                c.function_name,
2285                c.captures.len()
2286            ),
2287            Value::ClassRef(name) => write!(f, "<class {name}>"),
2288            Value::MException(e) => write!(
2289                f,
2290                "MException(identifier='{}', message='{}')",
2291                e.identifier, e.message
2292            ),
2293        }
2294    }
2295}
2296
2297impl fmt::Display for ComplexTensor {
2298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2299        match self.shape.len() {
2300            0 | 1 => {
2301                write!(f, "[")?;
2302                for (i, (re, im)) in self.data.iter().enumerate() {
2303                    if i > 0 {
2304                        write!(f, " ")?;
2305                    }
2306                    let s = Value::Complex(*re, *im).to_string();
2307                    write!(f, "{s}")?;
2308                }
2309                write!(f, "]")
2310            }
2311            2 => {
2312                let rows = self.rows;
2313                let cols = self.cols;
2314                write!(f, "[")?;
2315                for r in 0..rows {
2316                    for c in 0..cols {
2317                        if c > 0 {
2318                            write!(f, " ")?;
2319                        }
2320                        let (re, im) = self.data[r + c * rows];
2321                        let s = Value::Complex(re, im).to_string();
2322                        write!(f, "{s}")?;
2323                    }
2324                    if r + 1 < rows {
2325                        write!(f, "; ")?;
2326                    }
2327                }
2328                write!(f, "]")
2329            }
2330            _ => {
2331                if should_expand_nd_display(&self.shape) {
2332                    write_nd_pages(f, &self.shape, |f, idx| {
2333                        let (re, im) = self.data[idx];
2334                        write!(f, "{}", Value::Complex(re, im))
2335                    })
2336                } else {
2337                    write!(f, "ComplexTensor(shape={:?})", self.shape)
2338                }
2339            }
2340        }
2341    }
2342}
2343
2344#[cfg(test)]
2345mod display_tests {
2346    use super::{
2347        fmt_rational, format_number, set_display_format, ComplexTensor, FormatMode, LogicalArray,
2348        Tensor,
2349    };
2350
2351    #[test]
2352    fn fmt_rational_large_value_with_tiny_fract_does_not_overflow() {
2353        // abs ~1e15 with a small fractional part: q*n1 would overflow i64 without
2354        // checked arithmetic.
2355        let result = std::panic::catch_unwind(|| fmt_rational(1_000_000_000_000_000.000_1));
2356        assert!(
2357            result.is_ok(),
2358            "fmt_rational panicked on large value with tiny fract"
2359        );
2360
2361        // Negative counterpart.
2362        let result = std::panic::catch_unwind(|| fmt_rational(-1_000_000_000_000_000.000_1));
2363        assert!(
2364            result.is_ok(),
2365            "fmt_rational panicked on negative large value with tiny fract"
2366        );
2367    }
2368
2369    #[test]
2370    fn tensor_nd_display_uses_page_headers() {
2371        let tensor = Tensor::new(
2372            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],
2373            vec![2, 3, 2],
2374        )
2375        .expect("tensor");
2376        let rendered = tensor.to_string();
2377        assert!(rendered.contains("(:, :, 1) ="));
2378        assert!(rendered.contains("(:, :, 2) ="));
2379        assert!(rendered.contains("  1  0  0"));
2380    }
2381
2382    #[test]
2383    fn tensor_nd_display_falls_back_for_large_arrays() {
2384        let tensor = Tensor::new(vec![0.0; 4097], vec![1, 1, 4097]).expect("tensor");
2385        assert_eq!(tensor.to_string(), "Tensor(shape=[1, 1, 4097])");
2386    }
2387
2388    #[test]
2389    fn logical_nd_display_uses_headers_and_fallback_summary() {
2390        let logical =
2391            LogicalArray::new(vec![1, 0, 0, 1, 1, 0, 0, 1], vec![2, 2, 2]).expect("logical");
2392        let rendered = logical.to_string();
2393        assert!(rendered.contains("(:, :, 1) ="));
2394        assert!(rendered.contains("(:, :, 2) ="));
2395
2396        let large = LogicalArray::new(vec![1; 4097], vec![1, 1, 4097]).expect("large logical");
2397        assert_eq!(large.to_string(), "1x1x4097 logical array");
2398    }
2399
2400    #[test]
2401    fn complex_nd_display_uses_page_headers() {
2402        let complex = ComplexTensor::new(
2403            vec![(1.0, 0.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)],
2404            vec![2, 1, 2],
2405        )
2406        .expect("complex");
2407        let rendered = complex.to_string();
2408        assert!(rendered.contains("(:, :, 1) ="));
2409        assert!(rendered.contains("(:, :, 2) ="));
2410    }
2411
2412    #[test]
2413    fn format_hex_preserves_negative_zero_sign_bit() {
2414        set_display_format(FormatMode::Hex);
2415        assert_eq!(format_number(-0.0), "8000000000000000");
2416        assert_eq!(format_number(0.0), "0000000000000000");
2417        set_display_format(FormatMode::Short);
2418    }
2419}
2420
2421#[derive(Debug, Clone, PartialEq)]
2422pub struct CellArray {
2423    pub data: Vec<Value>,
2424    /// Full MATLAB-visible shape vector (column-major semantics).
2425    pub shape: Vec<usize>,
2426    /// Cached row count for 2-D interop; equals `shape[0]` when present.
2427    pub rows: usize,
2428    /// Cached column count for 2-D interop; equals `shape[1]` when present, otherwise 1 (or 0 for empty).
2429    pub cols: usize,
2430}
2431
2432impl CellArray {
2433    pub fn new(data: Vec<Value>, rows: usize, cols: usize) -> Result<Self, String> {
2434        Self::new_with_shape(data, vec![rows, cols])
2435    }
2436
2437    pub fn new_with_shape(data: Vec<Value>, shape: Vec<usize>) -> Result<Self, String> {
2438        let expected = total_len(&shape)
2439            .ok_or_else(|| "Cell data shape exceeds platform limits".to_string())?;
2440        if expected != data.len() {
2441            return Err(format!(
2442                "Cell data length {} doesn't match shape {:?} ({} elements)",
2443                data.len(),
2444                shape,
2445                expected
2446            ));
2447        }
2448        let (rows, cols) = shape_rows_cols(&shape);
2449        Ok(CellArray {
2450            data,
2451            shape,
2452            rows,
2453            cols,
2454        })
2455    }
2456
2457    pub fn get(&self, row: usize, col: usize) -> Result<Value, String> {
2458        if row >= self.rows || col >= self.cols {
2459            return Err(format!(
2460                "Cell index ({row}, {col}) out of bounds for {}x{} cell array",
2461                self.rows, self.cols
2462            ));
2463        }
2464        Ok(self.data[row * self.cols + col].clone())
2465    }
2466}
2467
2468fn total_len(shape: &[usize]) -> Option<usize> {
2469    if shape.is_empty() {
2470        return Some(0);
2471    }
2472    shape
2473        .iter()
2474        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
2475}
2476
2477fn shape_rows_cols(shape: &[usize]) -> (usize, usize) {
2478    if shape.is_empty() {
2479        return (0, 0);
2480    }
2481    if shape.len() == 1 {
2482        return (1, shape[0]);
2483    }
2484    (shape[0], shape[1])
2485}
2486
2487impl fmt::Display for CellArray {
2488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2489        let dims: Vec<String> = self.shape.iter().map(|d| d.to_string()).collect();
2490        if self.shape.len() > 2 {
2491            return write!(f, "{} cell array", dims.join("x"));
2492        }
2493        write!(f, "{}x{} cell array", self.rows, self.cols)?;
2494        if self.rows == 0 || self.cols == 0 {
2495            return Ok(());
2496        }
2497        for r in 0..self.rows {
2498            writeln!(f)?;
2499            write!(f, "  ")?;
2500            for c in 0..self.cols {
2501                if c > 0 {
2502                    write!(f, "  ")?;
2503                }
2504                let value = self.get(r, c).unwrap_or_else(|_| Value::Num(f64::NAN));
2505                write!(f, "{{{value}}}")?;
2506            }
2507        }
2508        Ok(())
2509    }
2510}
2511
2512#[derive(Debug, Clone, PartialEq)]
2513pub struct ObjectInstance {
2514    pub class_name: String,
2515    pub properties: HashMap<String, Value>,
2516}
2517
2518impl ObjectInstance {
2519    pub fn new(class_name: String) -> Self {
2520        Self {
2521            class_name,
2522            properties: HashMap::new(),
2523        }
2524    }
2525
2526    pub fn is_class(&self, name: &str) -> bool {
2527        self.class_name == name
2528    }
2529}
2530
2531// -------- Class registry (scaffolding) --------
2532#[derive(Debug, Clone, PartialEq, Eq)]
2533pub enum Access {
2534    Public,
2535    Private,
2536    Protected,
2537}
2538
2539#[derive(Debug, Clone)]
2540pub struct PropertyDef {
2541    pub name: String,
2542    pub is_static: bool,
2543    pub is_constant: bool,
2544    pub is_dependent: bool,
2545    pub get_access: Access,
2546    pub set_access: Access,
2547    pub default_value: Option<Value>,
2548}
2549
2550#[derive(Debug, Clone)]
2551pub struct MethodDef {
2552    pub name: String,
2553    pub is_static: bool,
2554    pub is_abstract: bool,
2555    pub is_sealed: bool,
2556    pub access: Access,
2557    pub function_name: String, // bound runtime builtin/user func name
2558    pub implicit_class_argument: Option<String>,
2559}
2560
2561#[derive(Debug, Clone)]
2562pub struct ClassDef {
2563    pub name: String, // namespaced e.g. pkg.Point
2564    pub parent: Option<String>,
2565    pub properties: HashMap<String, PropertyDef>,
2566    pub methods: HashMap<String, MethodDef>,
2567}
2568
2569thread_local! {
2570    static CLASS_REGISTRY: RefCell<HashMap<String, ClassDef>> =
2571        RefCell::new(primitive_class_registry());
2572    static SEALED_CLASS_REGISTRY: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
2573    static ABSTRACT_CLASS_REGISTRY: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
2574    static STATIC_VALUES: RefCell<HashMap<(String, String), Value>> = RefCell::new(HashMap::new());
2575    static STATIC_VALUE_THREAD_REGISTRATION: StaticValueThreadRegistration =
2576        const { StaticValueThreadRegistration };
2577    static ENUMERATION_REGISTRY: RefCell<HashMap<String, HashSet<String>>> =
2578        RefCell::new(HashMap::new());
2579}
2580
2581static STATIC_VALUE_THREADS: once_cell::sync::Lazy<Mutex<HashSet<ThreadId>>> =
2582    once_cell::sync::Lazy::new(|| Mutex::new(HashSet::new()));
2583
2584struct StaticValueThreadRegistration;
2585
2586impl Drop for StaticValueThreadRegistration {
2587    fn drop(&mut self) {
2588        if let Ok(mut threads) = STATIC_VALUE_THREADS.lock() {
2589            threads.remove(&std::thread::current().id());
2590        }
2591    }
2592}
2593
2594fn mark_static_values_thread_active() {
2595    STATIC_VALUE_THREAD_REGISTRATION.with(|_| {});
2596    if let Ok(mut threads) = STATIC_VALUE_THREADS.lock() {
2597        threads.insert(std::thread::current().id());
2598    }
2599}
2600
2601pub fn static_property_values_exist_on_other_threads() -> bool {
2602    let current = std::thread::current().id();
2603    STATIC_VALUE_THREADS
2604        .lock()
2605        .map(|threads| threads.iter().any(|thread_id| *thread_id != current))
2606        .unwrap_or(false)
2607}
2608
2609pub fn static_property_gc_roots() -> Vec<GcHandle> {
2610    struct RootCollector {
2611        roots: Vec<GcHandle>,
2612    }
2613
2614    impl Tracer for RootCollector {
2615        fn mark(&mut self, handle: GcHandle) {
2616            self.roots.push(handle);
2617        }
2618    }
2619
2620    STATIC_VALUES.with(|values| {
2621        let values = values.borrow();
2622        let mut collector = RootCollector { roots: Vec::new() };
2623        for value in values.values() {
2624            value.trace(&mut collector);
2625        }
2626        collector.roots
2627    })
2628}
2629
2630fn primitive_class_registry() -> HashMap<String, ClassDef> {
2631    ["double", "single", "logical"]
2632        .into_iter()
2633        .map(|class_name| {
2634            let mut methods = HashMap::new();
2635            methods.insert(
2636                "zeros".to_string(),
2637                MethodDef {
2638                    name: "zeros".to_string(),
2639                    is_static: true,
2640                    is_abstract: false,
2641                    is_sealed: false,
2642                    access: Access::Public,
2643                    function_name: "zeros".to_string(),
2644                    implicit_class_argument: Some(class_name.to_string()),
2645                },
2646            );
2647            (
2648                class_name.to_string(),
2649                ClassDef {
2650                    name: class_name.to_string(),
2651                    parent: None,
2652                    properties: HashMap::new(),
2653                    methods,
2654                },
2655            )
2656        })
2657        .collect()
2658}
2659
2660pub fn register_class(def: ClassDef) {
2661    register_class_with_modifiers(def, false, false);
2662}
2663
2664pub fn register_class_with_sealed(def: ClassDef, is_sealed: bool) {
2665    register_class_with_modifiers(def, is_sealed, false);
2666}
2667
2668pub fn register_class_with_modifiers(def: ClassDef, is_sealed: bool, is_abstract: bool) {
2669    let class_name = def.name.clone();
2670    CLASS_REGISTRY.with(|registry| {
2671        registry.borrow_mut().insert(class_name.clone(), def);
2672    });
2673    SEALED_CLASS_REGISTRY.with(|registry| {
2674        let mut registry = registry.borrow_mut();
2675        if is_sealed {
2676            registry.insert(class_name.clone());
2677        } else {
2678            registry.remove(&class_name);
2679        }
2680    });
2681    ABSTRACT_CLASS_REGISTRY.with(|registry| {
2682        let mut registry = registry.borrow_mut();
2683        if is_abstract {
2684            registry.insert(class_name.clone());
2685        } else {
2686            registry.remove(&class_name);
2687        }
2688    });
2689    ENUMERATION_REGISTRY.with(|registry| {
2690        registry.borrow_mut().entry(class_name).or_default();
2691    });
2692}
2693
2694pub fn register_class_enumerations(class_name: &str, members: impl IntoIterator<Item = String>) {
2695    ENUMERATION_REGISTRY.with(|registry| {
2696        let mut registry = registry.borrow_mut();
2697        let entry = registry.entry(class_name.to_string()).or_default();
2698        entry.clear();
2699        entry.extend(members);
2700    });
2701}
2702
2703pub fn class_has_enumeration_member(class_name: &str, member: &str) -> bool {
2704    ENUMERATION_REGISTRY.with(|registry| {
2705        registry
2706            .borrow()
2707            .get(class_name)
2708            .is_some_and(|members| members.contains(member))
2709    })
2710}
2711
2712pub fn get_class(name: &str) -> Option<ClassDef> {
2713    CLASS_REGISTRY.with(|registry| registry.borrow().get(name).cloned())
2714}
2715
2716pub fn class_names() -> Vec<String> {
2717    CLASS_REGISTRY.with(|registry| registry.borrow().keys().cloned().collect())
2718}
2719
2720pub fn is_class_sealed(name: &str) -> bool {
2721    SEALED_CLASS_REGISTRY.with(|registry| registry.borrow().contains(name))
2722}
2723
2724pub fn is_class_abstract(name: &str) -> bool {
2725    ABSTRACT_CLASS_REGISTRY.with(|registry| registry.borrow().contains(name))
2726}
2727
2728pub fn is_class_or_subclass(class_name: &str, ancestor_name: &str) -> bool {
2729    if class_name == ancestor_name {
2730        return true;
2731    }
2732    CLASS_REGISTRY.with(|registry| {
2733        let registry = registry.borrow();
2734        let mut current = Some(class_name.to_string());
2735        let mut visited = std::collections::HashSet::new();
2736        while let Some(name) = current {
2737            if !visited.insert(name.clone()) {
2738                break;
2739            }
2740            if name == ancestor_name {
2741                return true;
2742            }
2743            current = registry
2744                .get(&name)
2745                .and_then(|class_def| class_def.parent.clone());
2746        }
2747        false
2748    })
2749}
2750
2751/// Resolve a property through the inheritance chain, returning the property definition and
2752/// the name of the class where it was defined.
2753pub fn lookup_property(class_name: &str, prop: &str) -> Option<(PropertyDef, String)> {
2754    CLASS_REGISTRY.with(|registry| {
2755        let registry = registry.borrow();
2756        let mut current = Some(class_name.to_string());
2757        let mut visited = std::collections::HashSet::new();
2758        while let Some(name) = current {
2759            if !visited.insert(name.clone()) {
2760                break;
2761            }
2762            if let Some(cls) = registry.get(&name) {
2763                if let Some(p) = cls.properties.get(prop) {
2764                    return Some((p.clone(), name));
2765                }
2766                current = cls.parent.clone();
2767            } else {
2768                break;
2769            }
2770        }
2771        None
2772    })
2773}
2774
2775/// Resolve a method through the inheritance chain, returning the method definition and
2776/// the name of the class where it was defined.
2777pub fn lookup_method(class_name: &str, method: &str) -> Option<(MethodDef, String)> {
2778    CLASS_REGISTRY.with(|registry| {
2779        let registry = registry.borrow();
2780        let mut current = Some(class_name.to_string());
2781        let mut visited = std::collections::HashSet::new();
2782        while let Some(name) = current {
2783            if !visited.insert(name.clone()) {
2784                break;
2785            }
2786            if let Some(cls) = registry.get(&name) {
2787                if let Some(m) = cls.methods.get(method) {
2788                    return Some((m.clone(), name));
2789                }
2790                current = cls.parent.clone();
2791            } else {
2792                break;
2793            }
2794        }
2795        None
2796    })
2797}
2798
2799pub fn get_static_property_value(class_name: &str, prop: &str) -> Option<Value> {
2800    STATIC_VALUES.with(|values| {
2801        values
2802            .borrow()
2803            .get(&(class_name.to_string(), prop.to_string()))
2804            .cloned()
2805    })
2806}
2807
2808pub fn set_static_property_value(class_name: &str, prop: &str, value: Value) {
2809    mark_static_values_thread_active();
2810    STATIC_VALUES.with(|values| {
2811        values
2812            .borrow_mut()
2813            .insert((class_name.to_string(), prop.to_string()), value);
2814    });
2815}
2816
2817/// Set a static property, resolving the defining ancestor class for storage.
2818pub fn set_static_property_value_in_owner(
2819    class_name: &str,
2820    prop: &str,
2821    value: Value,
2822) -> Result<(), String> {
2823    if let Some((_p, owner)) = lookup_property(class_name, prop) {
2824        set_static_property_value(&owner, prop, value);
2825        Ok(())
2826    } else {
2827        Err(format!("Unknown static property '{class_name}.{prop}'"))
2828    }
2829}
2830
2831#[cfg(test)]
2832mod class_registry_tests {
2833    use super::{
2834        get_class, lookup_method, lookup_property, register_class, Access, ClassDef, MethodDef,
2835        PropertyDef,
2836    };
2837    use std::collections::HashMap;
2838    use std::sync::atomic::{AtomicU64, Ordering};
2839
2840    static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
2841
2842    fn unique_class_name(prefix: &str) -> String {
2843        let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
2844        format!("{}_{}", prefix, id)
2845    }
2846
2847    #[test]
2848    fn primitive_classes_expose_static_zeros_method_metadata() {
2849        for class_name in ["double", "single", "logical"] {
2850            let class_def = get_class(class_name).expect("primitive class should be registered");
2851            let method = class_def
2852                .methods
2853                .get("zeros")
2854                .expect("primitive class should expose zeros static method");
2855            assert!(method.is_static, "zeros should be static on {class_name}");
2856            assert_eq!(method.function_name, "zeros");
2857            assert_eq!(method.implicit_class_argument.as_deref(), Some(class_name));
2858
2859            let (resolved, owner) =
2860                lookup_method(class_name, "zeros").expect("lookup should find primitive zeros");
2861            assert_eq!(owner, class_name);
2862            assert_eq!(resolved.function_name, "zeros");
2863            assert_eq!(
2864                resolved.implicit_class_argument.as_deref(),
2865                Some(class_name)
2866            );
2867        }
2868    }
2869
2870    #[test]
2871    fn method_lookup_uses_parent_class_metadata_chain() {
2872        let parent_name = unique_class_name("plan6_parent");
2873        let child_name = unique_class_name("plan6_child");
2874
2875        let mut parent_methods = HashMap::new();
2876        parent_methods.insert(
2877            "parentOnly".to_string(),
2878            MethodDef {
2879                name: "parentOnly".to_string(),
2880                is_static: false,
2881                is_abstract: false,
2882                is_sealed: false,
2883                access: Access::Public,
2884                function_name: "parentOnly_impl".to_string(),
2885                implicit_class_argument: None,
2886            },
2887        );
2888        register_class(ClassDef {
2889            name: parent_name.clone(),
2890            parent: None,
2891            properties: HashMap::new(),
2892            methods: parent_methods,
2893        });
2894        register_class(ClassDef {
2895            name: child_name.clone(),
2896            parent: Some(parent_name.clone()),
2897            properties: HashMap::new(),
2898            methods: HashMap::new(),
2899        });
2900
2901        let (method, owner) = lookup_method(&child_name, "parentOnly")
2902            .expect("child lookup should resolve inherited method through parent metadata");
2903        assert_eq!(owner, parent_name);
2904        assert_eq!(method.function_name, "parentOnly_impl");
2905    }
2906
2907    #[test]
2908    fn method_lookup_handles_parent_cycle() {
2909        let class_a = unique_class_name("plan6_cycle_method_a");
2910        let class_b = unique_class_name("plan6_cycle_method_b");
2911
2912        register_class(ClassDef {
2913            name: class_a.clone(),
2914            parent: Some(class_b.clone()),
2915            properties: HashMap::new(),
2916            methods: HashMap::new(),
2917        });
2918        register_class(ClassDef {
2919            name: class_b.clone(),
2920            parent: Some(class_a.clone()),
2921            properties: HashMap::new(),
2922            methods: HashMap::new(),
2923        });
2924
2925        assert!(
2926            lookup_method(&class_a, "missing").is_none(),
2927            "cyclic parent metadata should terminate missing method lookup"
2928        );
2929    }
2930
2931    #[test]
2932    fn property_lookup_uses_parent_class_metadata_chain() {
2933        let parent_name = unique_class_name("plan6_property_parent");
2934        let child_name = unique_class_name("plan6_property_child");
2935
2936        let mut parent_properties = HashMap::new();
2937        parent_properties.insert(
2938            "parentFlag".to_string(),
2939            PropertyDef {
2940                name: "parentFlag".to_string(),
2941                is_static: false,
2942                is_constant: false,
2943                is_dependent: false,
2944                get_access: Access::Public,
2945                set_access: Access::Public,
2946                default_value: None,
2947            },
2948        );
2949        register_class(ClassDef {
2950            name: parent_name.clone(),
2951            parent: None,
2952            properties: parent_properties,
2953            methods: HashMap::new(),
2954        });
2955        register_class(ClassDef {
2956            name: child_name.clone(),
2957            parent: Some(parent_name.clone()),
2958            properties: HashMap::new(),
2959            methods: HashMap::new(),
2960        });
2961
2962        let (property, owner) = lookup_property(&child_name, "parentFlag")
2963            .expect("child property lookup should resolve inherited property through parent");
2964        assert_eq!(owner, parent_name);
2965        assert_eq!(property.name, "parentFlag");
2966        assert!(!property.is_static);
2967    }
2968
2969    #[test]
2970    fn property_lookup_handles_parent_cycle() {
2971        let class_a = unique_class_name("plan6_cycle_property_a");
2972        let class_b = unique_class_name("plan6_cycle_property_b");
2973
2974        register_class(ClassDef {
2975            name: class_a.clone(),
2976            parent: Some(class_b.clone()),
2977            properties: HashMap::new(),
2978            methods: HashMap::new(),
2979        });
2980        register_class(ClassDef {
2981            name: class_b.clone(),
2982            parent: Some(class_a.clone()),
2983            properties: HashMap::new(),
2984            methods: HashMap::new(),
2985        });
2986
2987        assert!(
2988            lookup_property(&class_a, "missing").is_none(),
2989            "cyclic parent metadata should terminate missing property lookup"
2990        );
2991    }
2992}