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