runmat_builtins/
lib.rs

1pub use inventory;
2use runmat_gc_api::GcPtr;
3use std::collections::HashMap;
4use std::convert::TryFrom;
5use std::fmt;
6
7use indexmap::IndexMap;
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum Value {
11    Int(IntValue),
12    Num(f64),
13    /// Complex scalar value represented as (re, im)
14    Complex(f64, f64),
15    Bool(bool),
16    // Logical array (N-D of booleans). Scalars use Bool.
17    LogicalArray(LogicalArray),
18    String(String),
19    // String array (R2016b+): N-D array of string scalars
20    StringArray(StringArray),
21    // Char array (single-quoted): 2-D character array (rows x cols)
22    CharArray(CharArray),
23    Tensor(Tensor),
24    /// Complex numeric array; same column-major shape semantics as `Tensor`
25    ComplexTensor(ComplexTensor),
26    Cell(CellArray),
27    // Struct (scalar or nested). Struct arrays are represented in higher layers;
28    // this variant holds a single struct's fields.
29    Struct(StructValue),
30    // GPU-resident tensor handle (opaque; buffer managed by backend)
31    GpuTensor(runmat_accelerate_api::GpuTensorHandle),
32    // Simple object instance until full class system lands
33    Object(ObjectInstance),
34    /// Handle-object wrapper providing identity semantics and validity tracking
35    HandleObject(HandleRef),
36    /// Event listener handle for events
37    Listener(Listener),
38    // Function handle pointing to a named function (builtin or user)
39    FunctionHandle(String),
40    Closure(Closure),
41    ClassRef(String),
42    MException(MException),
43}
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum IntValue {
46    I8(i8),
47    I16(i16),
48    I32(i32),
49    I64(i64),
50    U8(u8),
51    U16(u16),
52    U32(u32),
53    U64(u64),
54}
55
56impl IntValue {
57    pub fn to_i64(&self) -> i64 {
58        match self {
59            IntValue::I8(v) => *v as i64,
60            IntValue::I16(v) => *v as i64,
61            IntValue::I32(v) => *v as i64,
62            IntValue::I64(v) => *v,
63            IntValue::U8(v) => *v as i64,
64            IntValue::U16(v) => *v as i64,
65            IntValue::U32(v) => *v as i64,
66            IntValue::U64(v) => {
67                if *v > i64::MAX as u64 {
68                    i64::MAX
69                } else {
70                    *v as i64
71                }
72            }
73        }
74    }
75    pub fn to_f64(&self) -> f64 {
76        self.to_i64() as f64
77    }
78    pub fn is_zero(&self) -> bool {
79        self.to_i64() == 0
80    }
81    pub fn class_name(&self) -> &'static str {
82        match self {
83            IntValue::I8(_) => "int8",
84            IntValue::I16(_) => "int16",
85            IntValue::I32(_) => "int32",
86            IntValue::I64(_) => "int64",
87            IntValue::U8(_) => "uint8",
88            IntValue::U16(_) => "uint16",
89            IntValue::U32(_) => "uint32",
90            IntValue::U64(_) => "uint64",
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq)]
96pub struct StructValue {
97    pub fields: IndexMap<String, Value>,
98}
99
100impl StructValue {
101    pub fn new() -> Self {
102        Self {
103            fields: IndexMap::new(),
104        }
105    }
106
107    /// Insert a field, preserving insertion order when the name is new.
108    pub fn insert(&mut self, name: impl Into<String>, value: Value) -> Option<Value> {
109        self.fields.insert(name.into(), value)
110    }
111
112    /// Remove a field while preserving the relative order of remaining fields.
113    pub fn remove(&mut self, name: &str) -> Option<Value> {
114        self.fields.shift_remove(name)
115    }
116
117    /// Returns an iterator over field names in their stored order.
118    pub fn field_names(&self) -> impl Iterator<Item = &String> {
119        self.fields.keys()
120    }
121}
122
123impl Default for StructValue {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130pub enum NumericDType {
131    F64,
132    F32,
133}
134
135#[derive(Debug, Clone, PartialEq)]
136pub struct Tensor {
137    pub data: Vec<f64>,
138    pub shape: Vec<usize>, // Column-major layout
139    pub rows: usize,       // Compatibility for 2D usage
140    pub cols: usize,       // Compatibility for 2D usage
141    /// Logical numeric class of this tensor; host storage remains f64.
142    pub dtype: NumericDType,
143}
144
145#[derive(Debug, Clone, PartialEq)]
146pub struct ComplexTensor {
147    pub data: Vec<(f64, f64)>,
148    pub shape: Vec<usize>,
149    pub rows: usize,
150    pub cols: usize,
151}
152
153#[derive(Debug, Clone, PartialEq)]
154pub struct StringArray {
155    pub data: Vec<String>,
156    pub shape: Vec<usize>,
157    pub rows: usize,
158    pub cols: usize,
159}
160
161#[derive(Debug, Clone, PartialEq)]
162pub struct LogicalArray {
163    pub data: Vec<u8>, // 0 or 1 values; compact bitset can come later
164    pub shape: Vec<usize>,
165}
166
167impl LogicalArray {
168    pub fn new(data: Vec<u8>, shape: Vec<usize>) -> Result<Self, String> {
169        let expected: usize = shape.iter().product();
170        if data.len() != expected {
171            return Err(format!(
172                "LogicalArray data length {} doesn't match shape {:?} ({} elements)",
173                data.len(),
174                shape,
175                expected
176            ));
177        }
178        // Normalize to 0/1
179        let mut d = data;
180        for v in &mut d {
181            *v = if *v != 0 { 1 } else { 0 };
182        }
183        Ok(LogicalArray { data: d, shape })
184    }
185    pub fn zeros(shape: Vec<usize>) -> Self {
186        let expected: usize = shape.iter().product();
187        LogicalArray {
188            data: vec![0u8; expected],
189            shape,
190        }
191    }
192    pub fn len(&self) -> usize {
193        self.data.len()
194    }
195    pub fn is_empty(&self) -> bool {
196        self.data.is_empty()
197    }
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct CharArray {
202    pub data: Vec<char>,
203    pub rows: usize,
204    pub cols: usize,
205}
206
207impl CharArray {
208    pub fn new_row(s: &str) -> Self {
209        CharArray {
210            data: s.chars().collect(),
211            rows: 1,
212            cols: s.chars().count(),
213        }
214    }
215    pub fn new(data: Vec<char>, rows: usize, cols: usize) -> Result<Self, String> {
216        if rows * cols != data.len() {
217            return Err(format!(
218                "Char data length {} doesn't match dimensions {}x{}",
219                data.len(),
220                rows,
221                cols
222            ));
223        }
224        Ok(CharArray { data, rows, cols })
225    }
226}
227
228impl StringArray {
229    pub fn new(data: Vec<String>, shape: Vec<usize>) -> Result<Self, String> {
230        let expected: usize = shape.iter().product();
231        if data.len() != expected {
232            return Err(format!(
233                "StringArray data length {} doesn't match shape {:?} ({} elements)",
234                data.len(),
235                shape,
236                expected
237            ));
238        }
239        let (rows, cols) = if shape.len() >= 2 {
240            (shape[0], shape[1])
241        } else if shape.len() == 1 {
242            (1, shape[0])
243        } else {
244            (0, 0)
245        };
246        Ok(StringArray {
247            data,
248            shape,
249            rows,
250            cols,
251        })
252    }
253    pub fn new_2d(data: Vec<String>, rows: usize, cols: usize) -> Result<Self, String> {
254        Self::new(data, vec![rows, cols])
255    }
256    pub fn rows(&self) -> usize {
257        self.shape.first().copied().unwrap_or(1)
258    }
259    pub fn cols(&self) -> usize {
260        self.shape.get(1).copied().unwrap_or(1)
261    }
262}
263
264// GpuTensorHandle now lives in runmat-accel-api
265
266impl Tensor {
267    pub fn new(data: Vec<f64>, shape: Vec<usize>) -> Result<Self, String> {
268        let expected: usize = shape.iter().product();
269        if data.len() != expected {
270            return Err(format!(
271                "Tensor data length {} doesn't match shape {:?} ({} elements)",
272                data.len(),
273                shape,
274                expected
275            ));
276        }
277        let (rows, cols) = if shape.len() >= 2 {
278            (shape[0], shape[1])
279        } else if shape.len() == 1 {
280            (1, shape[0])
281        } else {
282            (0, 0)
283        };
284        Ok(Tensor {
285            data,
286            shape,
287            rows,
288            cols,
289            dtype: NumericDType::F64,
290        })
291    }
292
293    pub fn new_2d(data: Vec<f64>, rows: usize, cols: usize) -> Result<Self, String> {
294        Self::new(data, vec![rows, cols])
295    }
296
297    pub fn from_f32(data: Vec<f32>, shape: Vec<usize>) -> Result<Self, String> {
298        let converted: Vec<f64> = data.into_iter().map(|v| v as f64).collect();
299        Self::new_with_dtype(converted, shape, NumericDType::F32)
300    }
301
302    pub fn from_f32_slice(data: &[f32], shape: &[usize]) -> Result<Self, String> {
303        let converted: Vec<f64> = data.iter().map(|&v| v as f64).collect();
304        Self::new_with_dtype(converted, shape.to_vec(), NumericDType::F32)
305    }
306
307    pub fn new_with_dtype(
308        data: Vec<f64>,
309        shape: Vec<usize>,
310        dtype: NumericDType,
311    ) -> Result<Self, String> {
312        let mut t = Self::new(data, shape)?;
313        t.dtype = dtype;
314        Ok(t)
315    }
316
317    pub fn zeros(shape: Vec<usize>) -> Self {
318        let size: usize = shape.iter().product();
319        let (rows, cols) = if shape.len() >= 2 {
320            (shape[0], shape[1])
321        } else if shape.len() == 1 {
322            (1, shape[0])
323        } else {
324            (0, 0)
325        };
326        Tensor {
327            data: vec![0.0; size],
328            shape,
329            rows,
330            cols,
331            dtype: NumericDType::F64,
332        }
333    }
334
335    pub fn ones(shape: Vec<usize>) -> Self {
336        let size: usize = shape.iter().product();
337        let (rows, cols) = if shape.len() >= 2 {
338            (shape[0], shape[1])
339        } else if shape.len() == 1 {
340            (1, shape[0])
341        } else {
342            (0, 0)
343        };
344        Tensor {
345            data: vec![1.0; size],
346            shape,
347            rows,
348            cols,
349            dtype: NumericDType::F64,
350        }
351    }
352
353    // 2D helpers for transitional call sites
354    pub fn zeros2(rows: usize, cols: usize) -> Self {
355        Self::zeros(vec![rows, cols])
356    }
357    pub fn ones2(rows: usize, cols: usize) -> Self {
358        Self::ones(vec![rows, cols])
359    }
360
361    pub fn rows(&self) -> usize {
362        self.shape.first().copied().unwrap_or(1)
363    }
364    pub fn cols(&self) -> usize {
365        self.shape.get(1).copied().unwrap_or(1)
366    }
367
368    pub fn get2(&self, row: usize, col: usize) -> Result<f64, String> {
369        let rows = self.rows();
370        let cols = self.cols();
371        if row >= rows || col >= cols {
372            return Err(format!(
373                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
374            ));
375        }
376        // Column-major linearization: lin = row + col*rows
377        Ok(self.data[row + col * rows])
378    }
379
380    pub fn set2(&mut self, row: usize, col: usize, value: f64) -> Result<(), String> {
381        let rows = self.rows();
382        let cols = self.cols();
383        if row >= rows || col >= cols {
384            return Err(format!(
385                "Index ({row}, {col}) out of bounds for {rows}x{cols} tensor"
386            ));
387        }
388        // Column-major linearization
389        self.data[row + col * rows] = value;
390        Ok(())
391    }
392
393    pub fn scalar_to_tensor2(scalar: f64, rows: usize, cols: usize) -> Tensor {
394        Tensor {
395            data: vec![scalar; rows * cols],
396            shape: vec![rows, cols],
397            rows,
398            cols,
399            dtype: NumericDType::F64,
400        }
401    }
402    // No-compat constructors: prefer new/new_2d/zeros/zeros2/ones/ones2
403}
404
405impl ComplexTensor {
406    pub fn new(data: Vec<(f64, f64)>, shape: Vec<usize>) -> Result<Self, String> {
407        let expected: usize = shape.iter().product();
408        if data.len() != expected {
409            return Err(format!(
410                "ComplexTensor data length {} doesn't match shape {:?} ({} elements)",
411                data.len(),
412                shape,
413                expected
414            ));
415        }
416        let (rows, cols) = if shape.len() >= 2 {
417            (shape[0], shape[1])
418        } else if shape.len() == 1 {
419            (1, shape[0])
420        } else {
421            (0, 0)
422        };
423        Ok(ComplexTensor {
424            data,
425            shape,
426            rows,
427            cols,
428        })
429    }
430    pub fn new_2d(data: Vec<(f64, f64)>, rows: usize, cols: usize) -> Result<Self, String> {
431        Self::new(data, vec![rows, cols])
432    }
433    pub fn zeros(shape: Vec<usize>) -> Self {
434        let size: usize = shape.iter().product();
435        let (rows, cols) = if shape.len() >= 2 {
436            (shape[0], shape[1])
437        } else if shape.len() == 1 {
438            (1, shape[0])
439        } else {
440            (0, 0)
441        };
442        ComplexTensor {
443            data: vec![(0.0, 0.0); size],
444            shape,
445            rows,
446            cols,
447        }
448    }
449}
450
451impl fmt::Display for Tensor {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        match self.shape.len() {
454            0 | 1 => {
455                // Treat as row vector for display
456                write!(f, "[")?;
457                for (i, v) in self.data.iter().enumerate() {
458                    if i > 0 {
459                        write!(f, " ")?;
460                    }
461                    write!(f, "{}", format_number_short_g(*v))?;
462                }
463                write!(f, "]")
464            }
465            2 => {
466                let rows = self.rows();
467                let cols = self.cols();
468                write!(f, "[")?;
469                for r in 0..rows {
470                    for c in 0..cols {
471                        if c > 0 {
472                            write!(f, " ")?;
473                        }
474                        let v = self.data[r + c * rows];
475                        write!(f, "{}", format_number_short_g(v))?;
476                    }
477                    if r + 1 < rows {
478                        write!(f, "; ")?;
479                    }
480                }
481                write!(f, "]")
482            }
483            _ => write!(f, "Tensor(shape={:?})", self.shape),
484        }
485    }
486}
487
488impl fmt::Display for StringArray {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        match self.shape.len() {
491            0 | 1 => {
492                write!(f, "[")?;
493                for (i, v) in self.data.iter().enumerate() {
494                    if i > 0 {
495                        write!(f, " ")?;
496                    }
497                    let escaped = v.replace('"', "\\\"");
498                    write!(f, "\"{escaped}\"")?;
499                }
500                write!(f, "]")
501            }
502            2 => {
503                let rows = self.rows();
504                let cols = self.cols();
505                write!(f, "[")?;
506                for r in 0..rows {
507                    for c in 0..cols {
508                        if c > 0 {
509                            write!(f, " ")?;
510                        }
511                        let v = &self.data[r + c * rows];
512                        let escaped = v.replace('"', "\\\"");
513                        write!(f, "\"{escaped}\"")?;
514                    }
515                    if r + 1 < rows {
516                        write!(f, "; ")?;
517                    }
518                }
519                write!(f, "]")
520            }
521            _ => write!(f, "StringArray(shape={:?})", self.shape),
522        }
523    }
524}
525
526impl fmt::Display for LogicalArray {
527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528        match self.shape.len() {
529            0 => write!(f, "[]"),
530            1 => {
531                write!(f, "[")?;
532                for (i, v) in self.data.iter().enumerate() {
533                    if i > 0 {
534                        write!(f, " ")?;
535                    }
536                    write!(f, "{}", if *v != 0 { 1 } else { 0 })?;
537                }
538                write!(f, "]")
539            }
540            2 => {
541                let rows = self.shape[0];
542                let cols = self.shape[1];
543                write!(f, "[")?;
544                for r in 0..rows {
545                    for c in 0..cols {
546                        if c > 0 {
547                            write!(f, " ")?;
548                        }
549                        let idx = r + c * rows;
550                        write!(f, "{}", if self.data[idx] != 0 { 1 } else { 0 })?;
551                    }
552                    if r + 1 < rows {
553                        write!(f, "; ")?;
554                    }
555                }
556                write!(f, "]")
557            }
558            _ => write!(f, "LogicalArray(shape={:?})", self.shape),
559        }
560    }
561}
562
563impl fmt::Display for CharArray {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        // Display as single-quoted rows separated by ;
566        write!(f, "[")?;
567        for r in 0..self.rows {
568            if r > 0 {
569                write!(f, "; ")?;
570            }
571            write!(f, "'")?;
572            for c in 0..self.cols {
573                let ch = self.data[r * self.cols + c];
574                if ch == '\'' {
575                    write!(f, "''")?;
576                } else {
577                    write!(f, "{ch}")?;
578                }
579            }
580            write!(f, "'")?;
581        }
582        write!(f, "]")
583    }
584}
585
586// From implementations for Value
587impl From<i32> for Value {
588    fn from(i: i32) -> Self {
589        Value::Int(IntValue::I32(i))
590    }
591}
592impl From<i64> for Value {
593    fn from(i: i64) -> Self {
594        Value::Int(IntValue::I64(i))
595    }
596}
597impl From<u32> for Value {
598    fn from(i: u32) -> Self {
599        Value::Int(IntValue::U32(i))
600    }
601}
602impl From<u64> for Value {
603    fn from(i: u64) -> Self {
604        Value::Int(IntValue::U64(i))
605    }
606}
607impl From<i16> for Value {
608    fn from(i: i16) -> Self {
609        Value::Int(IntValue::I16(i))
610    }
611}
612impl From<i8> for Value {
613    fn from(i: i8) -> Self {
614        Value::Int(IntValue::I8(i))
615    }
616}
617impl From<u16> for Value {
618    fn from(i: u16) -> Self {
619        Value::Int(IntValue::U16(i))
620    }
621}
622impl From<u8> for Value {
623    fn from(i: u8) -> Self {
624        Value::Int(IntValue::U8(i))
625    }
626}
627
628impl From<f64> for Value {
629    fn from(f: f64) -> Self {
630        Value::Num(f)
631    }
632}
633
634impl From<bool> for Value {
635    fn from(b: bool) -> Self {
636        Value::Bool(b)
637    }
638}
639
640impl From<String> for Value {
641    fn from(s: String) -> Self {
642        Value::String(s)
643    }
644}
645
646impl From<&str> for Value {
647    fn from(s: &str) -> Self {
648        Value::String(s.to_string())
649    }
650}
651
652impl From<Tensor> for Value {
653    fn from(m: Tensor) -> Self {
654        Value::Tensor(m)
655    }
656}
657
658// Remove blanket From<Vec<Value>> to avoid losing shape information
659
660// TryFrom implementations for extracting native types
661impl TryFrom<&Value> for i32 {
662    type Error = String;
663    fn try_from(v: &Value) -> Result<Self, Self::Error> {
664        match v {
665            Value::Int(i) => Ok(i.to_i64() as i32),
666            Value::Num(n) => Ok(*n as i32),
667            _ => Err(format!("cannot convert {v:?} to i32")),
668        }
669    }
670}
671
672impl TryFrom<&Value> for f64 {
673    type Error = String;
674    fn try_from(v: &Value) -> Result<Self, Self::Error> {
675        match v {
676            Value::Num(n) => Ok(*n),
677            Value::Int(i) => Ok(i.to_f64()),
678            _ => Err(format!("cannot convert {v:?} to f64")),
679        }
680    }
681}
682
683impl TryFrom<&Value> for bool {
684    type Error = String;
685    fn try_from(v: &Value) -> Result<Self, Self::Error> {
686        match v {
687            Value::Bool(b) => Ok(*b),
688            Value::Int(i) => Ok(!i.is_zero()),
689            Value::Num(n) => Ok(*n != 0.0),
690            _ => Err(format!("cannot convert {v:?} to bool")),
691        }
692    }
693}
694
695impl TryFrom<&Value> for String {
696    type Error = String;
697    fn try_from(v: &Value) -> Result<Self, Self::Error> {
698        match v {
699            Value::String(s) => Ok(s.clone()),
700            Value::StringArray(sa) => {
701                if sa.data.len() == 1 {
702                    Ok(sa.data[0].clone())
703                } else {
704                    Err("cannot convert string array to scalar string".to_string())
705                }
706            }
707            Value::CharArray(ca) => {
708                // Convert full char array to one string if it is a single row; else error
709                if ca.rows == 1 {
710                    Ok(ca.data.iter().collect())
711                } else {
712                    Err("cannot convert multi-row char array to scalar string".to_string())
713                }
714            }
715            Value::Int(i) => Ok(i.to_i64().to_string()),
716            Value::Num(n) => Ok(n.to_string()),
717            Value::Bool(b) => Ok(b.to_string()),
718            _ => Err(format!("cannot convert {v:?} to String")),
719        }
720    }
721}
722
723impl TryFrom<&Value> for Tensor {
724    type Error = String;
725    fn try_from(v: &Value) -> Result<Self, Self::Error> {
726        match v {
727            Value::Tensor(m) => Ok(m.clone()),
728            _ => Err(format!("cannot convert {v:?} to Tensor")),
729        }
730    }
731}
732
733impl TryFrom<&Value> for Value {
734    type Error = String;
735    fn try_from(v: &Value) -> Result<Self, Self::Error> {
736        Ok(v.clone())
737    }
738}
739
740impl TryFrom<&Value> for Vec<Value> {
741    type Error = String;
742    fn try_from(v: &Value) -> Result<Self, Self::Error> {
743        match v {
744            Value::Cell(c) => Ok(c.data.iter().map(|p| (**p).clone()).collect()),
745            _ => Err(format!("cannot convert {v:?} to Vec<Value>")),
746        }
747    }
748}
749
750use serde::{Deserialize, Serialize};
751
752/// Enhanced type system used throughout RunMat for HIR and builtin functions
753/// Designed to mirror Value variants for better type inference and LSP support
754#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
755pub enum Type {
756    /// Integer number type
757    Int,
758    /// Floating-point number type  
759    Num,
760    /// Boolean type
761    Bool,
762    /// Logical array type (N-D boolean array)
763    Logical,
764    /// String type
765    String,
766    /// Tensor type with optional shape information (column-major semantics in runtime)
767    Tensor {
768        /// Optional full shape; None means unknown/dynamic; individual dims can be omitted by using None
769        shape: Option<Vec<Option<usize>>>,
770    },
771    /// Cell array type with optional element type information
772    Cell {
773        /// Optional element type (None means mixed/unknown)
774        element_type: Option<Box<Type>>,
775        /// Optional length (None means unknown/dynamic)
776        length: Option<usize>,
777    },
778    /// Function type with parameter and return types
779    Function {
780        /// Parameter types
781        params: Vec<Type>,
782        /// Return type
783        returns: Box<Type>,
784    },
785    /// Void type (no value)
786    Void,
787    /// Unknown type (for type inference)
788    Unknown,
789    /// Union type (multiple possible types)
790    Union(Vec<Type>),
791    /// Struct-like type with optional known field set (purely for inference)
792    Struct {
793        /// Optional set of known field names observed via control-flow (None = unknown fields)
794        known_fields: Option<Vec<String>>, // kept sorted unique for deterministic Eq
795    },
796}
797
798impl Type {
799    /// Create a tensor type with unknown shape
800    pub fn tensor() -> Self {
801        Type::Tensor { shape: None }
802    }
803
804    /// Create a tensor type with known shape
805    pub fn tensor_with_shape(shape: Vec<usize>) -> Self {
806        Type::Tensor {
807            shape: Some(shape.into_iter().map(Some).collect()),
808        }
809    }
810
811    /// Create a cell array type with unknown element type
812    pub fn cell() -> Self {
813        Type::Cell {
814            element_type: None,
815            length: None,
816        }
817    }
818
819    /// Create a cell array type with known element type
820    pub fn cell_of(element_type: Type) -> Self {
821        Type::Cell {
822            element_type: Some(Box::new(element_type)),
823            length: None,
824        }
825    }
826
827    /// Check if this type is compatible with another type
828    pub fn is_compatible_with(&self, other: &Type) -> bool {
829        match (self, other) {
830            (Type::Unknown, _) | (_, Type::Unknown) => true,
831            (Type::Int, Type::Num) | (Type::Num, Type::Int) => true, // Number compatibility
832            (Type::Tensor { .. }, Type::Tensor { .. }) => true, // Tensor compatibility regardless of dims for now
833            (a, b) => a == b,
834        }
835    }
836
837    /// Get the most specific common type between two types
838    pub fn unify(&self, other: &Type) -> Type {
839        match (self, other) {
840            (Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
841            (Type::Int, Type::Num) | (Type::Num, Type::Int) => Type::Num,
842            (Type::Tensor { .. }, Type::Tensor { .. }) => Type::tensor(), // Lose shape info for now
843            (Type::Struct { known_fields: a }, Type::Struct { known_fields: b }) => match (a, b) {
844                (None, None) => Type::Struct { known_fields: None },
845                (Some(ka), None) | (None, Some(ka)) => Type::Struct {
846                    known_fields: Some(ka.clone()),
847                },
848                (Some(ka), Some(kb)) => {
849                    let mut set: std::collections::BTreeSet<String> = ka.iter().cloned().collect();
850                    set.extend(kb.iter().cloned());
851                    Type::Struct {
852                        known_fields: Some(set.into_iter().collect()),
853                    }
854                }
855            },
856            (a, b) if a == b => a.clone(),
857            _ => Type::Union(vec![self.clone(), other.clone()]),
858        }
859    }
860
861    /// Infer type from a Value
862    pub fn from_value(value: &Value) -> Type {
863        match value {
864            Value::Int(_) => Type::Int,
865            Value::Num(_) => Type::Num,
866            Value::Complex(_, _) => Type::Num, // treat as numeric double (complex) in type system for now
867            Value::Bool(_) => Type::Bool,
868            Value::LogicalArray(_) => Type::Logical,
869            Value::String(_) => Type::String,
870            Value::StringArray(_sa) => {
871                // Model as Cell of String for type system for now
872                Type::cell_of(Type::String)
873            }
874            Value::Tensor(t) => Type::Tensor {
875                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
876            },
877            Value::ComplexTensor(t) => Type::Tensor {
878                shape: Some(t.shape.iter().map(|&d| Some(d)).collect()),
879            },
880            Value::Cell(cells) => {
881                if cells.data.is_empty() {
882                    Type::cell()
883                } else {
884                    // Infer element type from first element
885                    let element_type = Type::from_value(&cells.data[0]);
886                    Type::Cell {
887                        element_type: Some(Box::new(element_type)),
888                        length: Some(cells.data.len()),
889                    }
890                }
891            }
892            Value::GpuTensor(h) => Type::Tensor {
893                shape: Some(h.shape.iter().map(|&d| Some(d)).collect()),
894            },
895            Value::Object(_) => Type::Unknown,
896            Value::HandleObject(_) => Type::Unknown,
897            Value::Listener(_) => Type::Unknown,
898            Value::Struct(_) => Type::Struct { known_fields: None },
899            Value::FunctionHandle(_) => Type::Function {
900                params: vec![Type::Unknown],
901                returns: Box::new(Type::Unknown),
902            },
903            Value::Closure(_) => Type::Function {
904                params: vec![Type::Unknown],
905                returns: Box::new(Type::Unknown),
906            },
907            Value::ClassRef(_) => Type::Unknown,
908            Value::MException(_) => Type::Unknown,
909            Value::CharArray(ca) => {
910                // Treat as cell of char for type purposes; or a 2-D char matrix conceptually
911                Type::Cell {
912                    element_type: Some(Box::new(Type::String)),
913                    length: Some(ca.rows * ca.cols),
914                }
915            }
916        }
917    }
918}
919
920#[derive(Debug, Clone, PartialEq)]
921pub struct Closure {
922    pub function_name: String,
923    pub captures: Vec<Value>,
924}
925
926/// Acceleration metadata describing GPU-friendly characteristics of a builtin.
927#[derive(Debug, Clone, Copy, PartialEq, Eq)]
928pub enum AccelTag {
929    Unary,
930    Elementwise,
931    Reduction,
932    MatMul,
933    Transpose,
934    ArrayConstruct,
935}
936
937/// Simple builtin function definition using the unified type system
938#[derive(Debug, Clone)]
939pub struct BuiltinFunction {
940    pub name: &'static str,
941    pub description: &'static str,
942    pub category: &'static str,
943    pub doc: &'static str,
944    pub examples: &'static str,
945    pub param_types: Vec<Type>,
946    pub return_type: Type,
947    pub implementation: fn(&[Value]) -> Result<Value, String>,
948    pub accel_tags: &'static [AccelTag],
949    pub is_sink: bool,
950}
951
952impl BuiltinFunction {
953    #[allow(clippy::too_many_arguments)]
954    pub fn new(
955        name: &'static str,
956        description: &'static str,
957        category: &'static str,
958        doc: &'static str,
959        examples: &'static str,
960        param_types: Vec<Type>,
961        return_type: Type,
962        implementation: fn(&[Value]) -> Result<Value, String>,
963        accel_tags: &'static [AccelTag],
964        is_sink: bool,
965    ) -> Self {
966        Self {
967            name,
968            description,
969            category,
970            doc,
971            examples,
972            param_types,
973            return_type,
974            implementation,
975            accel_tags,
976            is_sink,
977        }
978    }
979}
980
981/// A constant value that can be accessed as a variable
982#[derive(Clone)]
983pub struct Constant {
984    pub name: &'static str,
985    pub value: Value,
986}
987
988impl std::fmt::Debug for Constant {
989    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
990        write!(
991            f,
992            "Constant {{ name: {:?}, value: {:?} }}",
993            self.name, self.value
994        )
995    }
996}
997
998inventory::collect!(BuiltinFunction);
999inventory::collect!(Constant);
1000
1001pub fn builtin_functions() -> Vec<&'static BuiltinFunction> {
1002    inventory::iter::<BuiltinFunction>().collect()
1003}
1004
1005pub fn constants() -> Vec<&'static Constant> {
1006    inventory::iter::<Constant>().collect()
1007}
1008
1009// ----------------------
1010// Builtin documentation metadata (optional, registered by macros)
1011// ----------------------
1012
1013#[derive(Debug)]
1014pub struct BuiltinDoc {
1015    pub name: &'static str,
1016    pub category: Option<&'static str>,
1017    pub summary: Option<&'static str>,
1018    pub keywords: Option<&'static str>,
1019    pub errors: Option<&'static str>,
1020    pub related: Option<&'static str>,
1021    pub introduced: Option<&'static str>,
1022    pub status: Option<&'static str>,
1023    pub examples: Option<&'static str>,
1024}
1025
1026inventory::collect!(BuiltinDoc);
1027
1028pub fn builtin_docs() -> Vec<&'static BuiltinDoc> {
1029    inventory::iter::<BuiltinDoc>().collect()
1030}
1031
1032// ----------------------
1033// Display implementations
1034// ----------------------
1035
1036fn format_number_short_g(value: f64) -> String {
1037    if value.is_nan() {
1038        return "NaN".to_string();
1039    }
1040    if value.is_infinite() {
1041        return if value.is_sign_negative() {
1042            "-Inf"
1043        } else {
1044            "Inf"
1045        }
1046        .to_string();
1047    }
1048    // Normalize -0.0 to 0
1049    let mut v = value;
1050    if v == 0.0 {
1051        v = 0.0;
1052    }
1053
1054    let abs = v.abs();
1055    if abs == 0.0 {
1056        return "0".to_string();
1057    }
1058
1059    // Decide between fixed and scientific notation roughly like short g
1060    let use_scientific = !(1e-4..1e6).contains(&abs);
1061
1062    if use_scientific {
1063        // 5 significant digits in scientific notation for short g style
1064        let s = format!("{v:.5e}");
1065        // Trim trailing zeros in fraction part
1066        if let Some(idx) = s.find('e') {
1067            let (mut mantissa, exp) = s.split_at(idx);
1068            // mantissa like "-1.23450"
1069            if let Some(dot_idx) = mantissa.find('.') {
1070                // Trim trailing zeros
1071                let mut end = mantissa.len();
1072                while end > dot_idx + 1 && mantissa.as_bytes()[end - 1] == b'0' {
1073                    end -= 1;
1074                }
1075                if end > 0 && mantissa.as_bytes()[end - 1] == b'.' {
1076                    end -= 1;
1077                }
1078                mantissa = &mantissa[..end];
1079            }
1080            return format!("{mantissa}{exp}");
1081        }
1082        return s;
1083    }
1084
1085    // Fixed notation with up to 12 significant digits, trim trailing zeros
1086    // Compute number of decimals to retain to reach ~12 significant digits
1087    let exp10 = abs.log10().floor() as i32; // position of most significant digit
1088    let sig_digits: i32 = 12;
1089    let decimals = (sig_digits - 1 - exp10).clamp(0, 12) as usize;
1090    // Round to that many decimals
1091    let pow = 10f64.powi(decimals as i32);
1092    let rounded = (v * pow).round() / pow;
1093    let mut s = format!("{rounded:.decimals$}");
1094    if let Some(dot) = s.find('.') {
1095        // Trim trailing zeros
1096        let mut end = s.len();
1097        while end > dot + 1 && s.as_bytes()[end - 1] == b'0' {
1098            end -= 1;
1099        }
1100        if end > 0 && s.as_bytes()[end - 1] == b'.' {
1101            end -= 1;
1102        }
1103        s.truncate(end);
1104    }
1105    if s.is_empty() || s == "-0" {
1106        s = "0".to_string();
1107    }
1108    s
1109}
1110
1111// -------- Exception type --------
1112#[derive(Debug, Clone, PartialEq)]
1113pub struct MException {
1114    pub identifier: String,
1115    pub message: String,
1116    pub stack: Vec<String>,
1117}
1118
1119impl MException {
1120    pub fn new(identifier: String, message: String) -> Self {
1121        Self {
1122            identifier,
1123            message,
1124            stack: Vec::new(),
1125        }
1126    }
1127}
1128
1129/// Reference to a GC-allocated object providing language handle semantics
1130#[derive(Debug, Clone)]
1131pub struct HandleRef {
1132    pub class_name: String,
1133    pub target: GcPtr<Value>,
1134    pub valid: bool,
1135}
1136
1137impl PartialEq for HandleRef {
1138    fn eq(&self, other: &Self) -> bool {
1139        let a = unsafe { self.target.as_raw() } as usize;
1140        let b = unsafe { other.target.as_raw() } as usize;
1141        a == b
1142    }
1143}
1144
1145/// Event listener handle for events
1146#[derive(Debug, Clone, PartialEq)]
1147pub struct Listener {
1148    pub id: u64,
1149    pub target: GcPtr<Value>,
1150    pub event_name: String,
1151    pub callback: GcPtr<Value>,
1152    pub enabled: bool,
1153    pub valid: bool,
1154}
1155
1156impl Listener {
1157    pub fn class_name(&self) -> String {
1158        match unsafe { &*self.target.as_raw() } {
1159            Value::Object(o) => o.class_name.clone(),
1160            Value::HandleObject(h) => h.class_name.clone(),
1161            _ => String::new(),
1162        }
1163    }
1164}
1165
1166impl fmt::Display for Value {
1167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1168        match self {
1169            Value::Int(i) => write!(f, "{}", i.to_i64()),
1170            Value::Num(n) => write!(f, "{}", format_number_short_g(*n)),
1171            Value::Complex(re, im) => {
1172                if *im == 0.0 {
1173                    write!(f, "{}", format_number_short_g(*re))
1174                } else if *re == 0.0 {
1175                    write!(f, "{}i", format_number_short_g(*im))
1176                } else if *im < 0.0 {
1177                    write!(
1178                        f,
1179                        "{}-{}i",
1180                        format_number_short_g(*re),
1181                        format_number_short_g(im.abs())
1182                    )
1183                } else {
1184                    write!(
1185                        f,
1186                        "{}+{}i",
1187                        format_number_short_g(*re),
1188                        format_number_short_g(*im)
1189                    )
1190                }
1191            }
1192            Value::Bool(b) => write!(f, "{}", if *b { 1 } else { 0 }),
1193            Value::LogicalArray(la) => write!(f, "{la}"),
1194            Value::String(s) => write!(f, "'{s}'"),
1195            Value::StringArray(sa) => write!(f, "{sa}"),
1196            Value::CharArray(ca) => write!(f, "{ca}"),
1197            Value::Tensor(m) => write!(f, "{m}"),
1198            Value::ComplexTensor(m) => write!(f, "{m}"),
1199            Value::Cell(ca) => ca.fmt(f),
1200
1201            Value::GpuTensor(h) => write!(
1202                f,
1203                "GpuTensor(shape={:?}, device={}, buffer={})",
1204                h.shape, h.device_id, h.buffer_id
1205            ),
1206            Value::Object(obj) => write!(f, "{}(props={})", obj.class_name, obj.properties.len()),
1207            Value::HandleObject(h) => {
1208                let ptr = unsafe { h.target.as_raw() } as usize;
1209                write!(
1210                    f,
1211                    "<handle {} @0x{:x} valid={}>",
1212                    h.class_name, ptr, h.valid
1213                )
1214            }
1215            Value::Listener(l) => {
1216                let ptr = unsafe { l.target.as_raw() } as usize;
1217                write!(
1218                    f,
1219                    "<listener id={} {}@0x{:x} '{}' enabled={} valid={}>",
1220                    l.id,
1221                    l.class_name(),
1222                    ptr,
1223                    l.event_name,
1224                    l.enabled,
1225                    l.valid
1226                )
1227            }
1228            Value::Struct(st) => write!(f, "struct(fields={})", st.fields.len()),
1229            Value::FunctionHandle(name) => write!(f, "@{name}"),
1230            Value::Closure(c) => write!(
1231                f,
1232                "<closure {} captures={}>",
1233                c.function_name,
1234                c.captures.len()
1235            ),
1236            Value::ClassRef(name) => write!(f, "<class {name}>"),
1237            Value::MException(e) => write!(
1238                f,
1239                "MException(identifier='{}', message='{}')",
1240                e.identifier, e.message
1241            ),
1242        }
1243    }
1244}
1245
1246impl fmt::Display for ComplexTensor {
1247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1248        match self.shape.len() {
1249            0 | 1 => {
1250                write!(f, "[")?;
1251                for (i, (re, im)) in self.data.iter().enumerate() {
1252                    if i > 0 {
1253                        write!(f, " ")?;
1254                    }
1255                    let s = Value::Complex(*re, *im).to_string();
1256                    write!(f, "{s}")?;
1257                }
1258                write!(f, "]")
1259            }
1260            2 => {
1261                let rows = self.rows;
1262                let cols = self.cols;
1263                write!(f, "[")?;
1264                for r in 0..rows {
1265                    for c in 0..cols {
1266                        if c > 0 {
1267                            write!(f, " ")?;
1268                        }
1269                        let (re, im) = self.data[r + c * rows];
1270                        let s = Value::Complex(re, im).to_string();
1271                        write!(f, "{s}")?;
1272                    }
1273                    if r + 1 < rows {
1274                        write!(f, "; ")?;
1275                    }
1276                }
1277                write!(f, "]")
1278            }
1279            _ => write!(f, "ComplexTensor(shape={:?})", self.shape),
1280        }
1281    }
1282}
1283
1284#[derive(Debug, Clone, PartialEq)]
1285pub struct CellArray {
1286    pub data: Vec<GcPtr<Value>>,
1287    /// Full MATLAB-visible shape vector (column-major semantics).
1288    pub shape: Vec<usize>,
1289    /// Cached row count for 2-D interop; equals `shape[0]` when present.
1290    pub rows: usize,
1291    /// Cached column count for 2-D interop; equals `shape[1]` when present, otherwise 1 (or 0 for empty).
1292    pub cols: usize,
1293}
1294
1295impl CellArray {
1296    pub fn new_handles(
1297        handles: Vec<GcPtr<Value>>,
1298        rows: usize,
1299        cols: usize,
1300    ) -> Result<Self, String> {
1301        Self::new_handles_with_shape(handles, vec![rows, cols])
1302    }
1303
1304    pub fn new_handles_with_shape(
1305        handles: Vec<GcPtr<Value>>,
1306        shape: Vec<usize>,
1307    ) -> Result<Self, String> {
1308        let expected = total_len(&shape)
1309            .ok_or_else(|| "Cell data shape exceeds platform limits".to_string())?;
1310        if expected != handles.len() {
1311            return Err(format!(
1312                "Cell data length {} doesn't match shape {:?} ({} elements)",
1313                handles.len(),
1314                shape,
1315                expected
1316            ));
1317        }
1318        let (rows, cols) = shape_rows_cols(&shape);
1319        Ok(CellArray {
1320            data: handles,
1321            shape,
1322            rows,
1323            cols,
1324        })
1325    }
1326
1327    pub fn new(data: Vec<Value>, rows: usize, cols: usize) -> Result<Self, String> {
1328        Self::new_with_shape(data, vec![rows, cols])
1329    }
1330
1331    pub fn new_with_shape(data: Vec<Value>, shape: Vec<usize>) -> Result<Self, String> {
1332        let expected = total_len(&shape)
1333            .ok_or_else(|| "Cell data shape exceeds platform limits".to_string())?;
1334        if expected != data.len() {
1335            return Err(format!(
1336                "Cell data length {} doesn't match shape {:?} ({} elements)",
1337                data.len(),
1338                shape,
1339                expected
1340            ));
1341        }
1342        // Note: data will be allocated into GC handles by callers (runtime/ignition) to avoid builtins↔gc cycles
1343        let handles: Vec<GcPtr<Value>> = data
1344            .into_iter()
1345            .map(|v| unsafe { GcPtr::from_raw(Box::into_raw(Box::new(v))) })
1346            .collect();
1347        Self::new_handles_with_shape(handles, shape)
1348    }
1349
1350    pub fn get(&self, row: usize, col: usize) -> Result<Value, String> {
1351        if row >= self.rows || col >= self.cols {
1352            return Err(format!(
1353                "Cell index ({row}, {col}) out of bounds for {}x{} cell array",
1354                self.rows, self.cols
1355            ));
1356        }
1357        Ok((*self.data[row * self.cols + col]).clone())
1358    }
1359}
1360
1361fn total_len(shape: &[usize]) -> Option<usize> {
1362    if shape.is_empty() {
1363        return Some(0);
1364    }
1365    shape
1366        .iter()
1367        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
1368}
1369
1370fn shape_rows_cols(shape: &[usize]) -> (usize, usize) {
1371    if shape.is_empty() {
1372        return (0, 0);
1373    }
1374    let rows = shape[0];
1375    let cols = if shape.len() >= 2 { shape[1] } else { 1 };
1376    (rows, cols)
1377}
1378
1379impl fmt::Display for CellArray {
1380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1381        if self.shape.len() > 2 {
1382            return write!(f, "CellArray(shape={:?})", self.shape);
1383        }
1384        write!(f, "{{")?;
1385        for r in 0..self.rows {
1386            for c in 0..self.cols {
1387                if c > 0 {
1388                    write!(f, ", ")?;
1389                }
1390                let v = &self.data[r * self.cols + c];
1391                write!(f, "{}", **v)?;
1392            }
1393            if r + 1 < self.rows {
1394                write!(f, "; ")?;
1395            }
1396        }
1397        write!(f, "}}")
1398    }
1399}
1400
1401#[derive(Debug, Clone, PartialEq)]
1402pub struct ObjectInstance {
1403    pub class_name: String,
1404    pub properties: HashMap<String, Value>,
1405}
1406
1407impl ObjectInstance {
1408    pub fn new(class_name: String) -> Self {
1409        Self {
1410            class_name,
1411            properties: HashMap::new(),
1412        }
1413    }
1414}
1415
1416// -------- Class registry (scaffolding) --------
1417#[derive(Debug, Clone, PartialEq, Eq)]
1418pub enum Access {
1419    Public,
1420    Private,
1421}
1422
1423#[derive(Debug, Clone)]
1424pub struct PropertyDef {
1425    pub name: String,
1426    pub is_static: bool,
1427    pub is_dependent: bool,
1428    pub get_access: Access,
1429    pub set_access: Access,
1430    pub default_value: Option<Value>,
1431}
1432
1433#[derive(Debug, Clone)]
1434pub struct MethodDef {
1435    pub name: String,
1436    pub is_static: bool,
1437    pub access: Access,
1438    pub function_name: String, // bound runtime builtin/user func name
1439}
1440
1441#[derive(Debug, Clone)]
1442pub struct ClassDef {
1443    pub name: String, // namespaced e.g. pkg.Point
1444    pub parent: Option<String>,
1445    pub properties: HashMap<String, PropertyDef>,
1446    pub methods: HashMap<String, MethodDef>,
1447}
1448
1449use std::sync::{Mutex, OnceLock};
1450
1451static CLASS_REGISTRY: OnceLock<Mutex<HashMap<String, ClassDef>>> = OnceLock::new();
1452static STATIC_VALUES: OnceLock<Mutex<HashMap<(String, String), Value>>> = OnceLock::new();
1453
1454fn registry() -> &'static Mutex<HashMap<String, ClassDef>> {
1455    CLASS_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
1456}
1457
1458pub fn register_class(def: ClassDef) {
1459    let mut m = registry().lock().unwrap();
1460    m.insert(def.name.clone(), def);
1461}
1462
1463pub fn get_class(name: &str) -> Option<ClassDef> {
1464    registry().lock().unwrap().get(name).cloned()
1465}
1466
1467/// Resolve a property through the inheritance chain, returning the property definition and
1468/// the name of the class where it was defined.
1469pub fn lookup_property(class_name: &str, prop: &str) -> Option<(PropertyDef, String)> {
1470    let reg = registry().lock().unwrap();
1471    let mut current = Some(class_name.to_string());
1472    let guard: Option<std::sync::MutexGuard<'_, std::collections::HashMap<String, ClassDef>>> =
1473        None;
1474    drop(guard);
1475    while let Some(name) = current {
1476        if let Some(cls) = reg.get(&name) {
1477            if let Some(p) = cls.properties.get(prop) {
1478                return Some((p.clone(), name));
1479            }
1480            current = cls.parent.clone();
1481        } else {
1482            break;
1483        }
1484    }
1485    None
1486}
1487
1488/// Resolve a method through the inheritance chain, returning the method definition and
1489/// the name of the class where it was defined.
1490pub fn lookup_method(class_name: &str, method: &str) -> Option<(MethodDef, String)> {
1491    let reg = registry().lock().unwrap();
1492    let mut current = Some(class_name.to_string());
1493    while let Some(name) = current {
1494        if let Some(cls) = reg.get(&name) {
1495            if let Some(m) = cls.methods.get(method) {
1496                return Some((m.clone(), name));
1497            }
1498            current = cls.parent.clone();
1499        } else {
1500            break;
1501        }
1502    }
1503    None
1504}
1505
1506fn static_values() -> &'static Mutex<HashMap<(String, String), Value>> {
1507    STATIC_VALUES.get_or_init(|| Mutex::new(HashMap::new()))
1508}
1509
1510pub fn get_static_property_value(class_name: &str, prop: &str) -> Option<Value> {
1511    static_values()
1512        .lock()
1513        .unwrap()
1514        .get(&(class_name.to_string(), prop.to_string()))
1515        .cloned()
1516}
1517
1518pub fn set_static_property_value(class_name: &str, prop: &str, value: Value) {
1519    static_values()
1520        .lock()
1521        .unwrap()
1522        .insert((class_name.to_string(), prop.to_string()), value);
1523}
1524
1525/// Set a static property, resolving the defining ancestor class for storage.
1526pub fn set_static_property_value_in_owner(
1527    class_name: &str,
1528    prop: &str,
1529    value: Value,
1530) -> Result<(), String> {
1531    if let Some((_p, owner)) = lookup_property(class_name, prop) {
1532        set_static_property_value(&owner, prop, value);
1533        Ok(())
1534    } else {
1535        Err(format!("Unknown static property '{class_name}.{prop}'"))
1536    }
1537}