Skip to main content

runmat_runtime/
class_registry.rs

1//! Session-scoped class registrations, executable method bindings, and static values.
2//!
3//! Immutable class/member declarations belong to `runmat-types`. These records
4//! are the live runtime projection: they may contain executable names and live
5//! default/static values and therefore intentionally remain outside the static
6//! declaration schema.
7
8use std::cell::RefCell;
9use std::collections::{HashMap, HashSet};
10use std::sync::Mutex;
11use std::sync::Once;
12use std::thread::ThreadId;
13
14use runmat_gc_api::{GcHandle, Trace, Tracer};
15use runmat_types::MemberAccess;
16use runmat_value::Value;
17
18#[derive(Debug, Clone)]
19pub struct RuntimeProperty {
20    pub name: String,
21    pub is_static: bool,
22    pub is_constant: bool,
23    pub is_dependent: bool,
24    pub get_access: MemberAccess,
25    pub set_access: MemberAccess,
26    pub default_value: Option<Value>,
27}
28
29#[derive(Debug, Clone)]
30pub struct RuntimeMethod {
31    pub name: String,
32    pub is_static: bool,
33    pub is_abstract: bool,
34    pub is_sealed: bool,
35    pub access: MemberAccess,
36    pub function_name: String,
37    pub implicit_class_argument: Option<String>,
38}
39
40#[derive(Debug, Clone)]
41pub struct RuntimeClass {
42    pub name: String,
43    pub parent: Option<String>,
44    pub properties: HashMap<String, RuntimeProperty>,
45    pub methods: HashMap<String, RuntimeMethod>,
46}
47
48/// Session-aware registration check for lazily installed builtin classes.
49///
50/// Unlike a thread-local boolean, this consults the active runtime context's
51/// class registry, so two interleaved sessions on one native or WASM thread
52/// cannot cause one another to skip registration.
53pub struct ClassRegistration {
54    class_name: &'static str,
55}
56
57impl ClassRegistration {
58    pub const fn new(class_name: &'static str) -> Self {
59        Self { class_name }
60    }
61
62    fn get(&self) -> bool {
63        with_state(|state| state.registrations.contains(self.class_name))
64    }
65
66    pub fn ensure(&self, register: impl FnOnce()) {
67        if self.get() {
68            return;
69        }
70        register();
71        with_state_mut(|state| {
72            state.registrations.insert(self.class_name);
73        });
74    }
75}
76
77thread_local! {
78    static FALLBACK_STATE: RefCell<RuntimeClassState> = RefCell::new(RuntimeClassState::default());
79    static CONTEXT_STATES: RefCell<Vec<std::rc::Weak<crate::context::RuntimeContextState>>> =
80        const { RefCell::new(Vec::new()) };
81    static STATIC_VALUE_THREAD_REGISTRATION: StaticValueThreadRegistration =
82        const { StaticValueThreadRegistration };
83}
84
85#[derive(Debug)]
86pub(crate) struct RuntimeClassState {
87    classes: HashMap<String, RuntimeClass>,
88    sealed: HashSet<String>,
89    abstract_classes: HashSet<String>,
90    static_values: HashMap<(String, String), Value>,
91    enumerations: HashMap<String, HashSet<String>>,
92    registrations: HashSet<&'static str>,
93}
94
95impl Default for RuntimeClassState {
96    fn default() -> Self {
97        Self {
98            classes: primitive_class_registry(),
99            sealed: HashSet::new(),
100            abstract_classes: HashSet::new(),
101            static_values: HashMap::new(),
102            enumerations: HashMap::new(),
103            registrations: HashSet::new(),
104        }
105    }
106}
107
108pub(crate) fn register_context_state(state: &std::rc::Rc<crate::context::RuntimeContextState>) {
109    CONTEXT_STATES.with(|states| {
110        let mut states = states.borrow_mut();
111        states.retain(|state| state.strong_count() > 0);
112        if !states
113            .iter()
114            .filter_map(std::rc::Weak::upgrade)
115            .any(|existing| std::rc::Rc::ptr_eq(&existing, state))
116        {
117            states.push(std::rc::Rc::downgrade(state));
118        }
119    });
120}
121
122static STATIC_VALUE_THREADS: once_cell::sync::Lazy<Mutex<HashSet<ThreadId>>> =
123    once_cell::sync::Lazy::new(|| Mutex::new(HashSet::new()));
124
125struct StaticValueThreadRegistration;
126
127fn ensure_gc_root_provider() {
128    static REGISTER: Once = Once::new();
129    REGISTER.call_once(|| {
130        runmat_gc::register_external_root_provider(
131            "runmat-runtime-class-statics",
132            static_property_gc_roots,
133            static_property_values_exist_on_other_threads,
134        );
135    });
136}
137
138impl Drop for StaticValueThreadRegistration {
139    fn drop(&mut self) {
140        if let Ok(mut threads) = STATIC_VALUE_THREADS.lock() {
141            threads.remove(&std::thread::current().id());
142        }
143    }
144}
145
146fn mark_static_values_thread_active() {
147    STATIC_VALUE_THREAD_REGISTRATION.with(|_| {});
148    if let Ok(mut threads) = STATIC_VALUE_THREADS.lock() {
149        threads.insert(std::thread::current().id());
150    }
151}
152
153pub fn static_property_values_exist_on_other_threads() -> bool {
154    let current = std::thread::current().id();
155    STATIC_VALUE_THREADS
156        .lock()
157        .map(|threads| threads.iter().any(|thread_id| *thread_id != current))
158        .unwrap_or(false)
159}
160
161pub fn static_property_gc_roots() -> Vec<GcHandle> {
162    struct RootCollector(Vec<GcHandle>);
163    impl Tracer for RootCollector {
164        fn mark(&mut self, handle: GcHandle) {
165            self.0.push(handle);
166        }
167    }
168    let mut collector = RootCollector(Vec::new());
169    FALLBACK_STATE.with(|state| {
170        for value in state.borrow().static_values.values() {
171            value.trace(&mut collector);
172        }
173    });
174    CONTEXT_STATES.with(|states| {
175        let mut states = states.borrow_mut();
176        states.retain(|state| state.strong_count() > 0);
177        for state in states.iter().filter_map(std::rc::Weak::upgrade) {
178            for value in state.classes.borrow().static_values.values() {
179                value.trace(&mut collector);
180            }
181        }
182    });
183    collector.0
184}
185
186fn primitive_class_registry() -> HashMap<String, RuntimeClass> {
187    let mut registry = HashMap::new();
188    for class_name in [
189        "double", "single", "logical", "int8", "int16", "int32", "int64", "uint8", "uint16",
190        "uint32", "uint64",
191    ] {
192        let method = RuntimeMethod {
193            name: "zeros".into(),
194            is_static: true,
195            is_abstract: false,
196            is_sealed: false,
197            access: MemberAccess::Public,
198            function_name: "zeros".into(),
199            implicit_class_argument: Some(class_name.into()),
200        };
201        registry.insert(
202            class_name.into(),
203            RuntimeClass {
204                name: class_name.into(),
205                parent: None,
206                properties: HashMap::new(),
207                methods: HashMap::from([("zeros".into(), method)]),
208            },
209        );
210    }
211    for (name, parent) in [
212        ("handle", None),
213        ("dynamicprops", Some("handle")),
214        ("matlab.metadata.Property", None),
215        ("matlab.metadata.DynamicProperty", Some("handle")),
216    ] {
217        registry.insert(
218            name.into(),
219            RuntimeClass {
220                name: name.into(),
221                parent: parent.map(str::to_owned),
222                properties: HashMap::new(),
223                methods: HashMap::new(),
224            },
225        );
226    }
227    registry.insert(
228        "gpuArray".into(),
229        RuntimeClass {
230            name: "gpuArray".into(),
231            parent: None,
232            properties: HashMap::new(),
233            methods: runmat_builtins::GPU_ARRAY_PUBLIC_METHODS
234                .iter()
235                .map(|method| {
236                    (
237                        (*method).to_owned(),
238                        RuntimeMethod {
239                            name: (*method).to_owned(),
240                            is_static: false,
241                            is_abstract: false,
242                            is_sealed: false,
243                            access: MemberAccess::Public,
244                            function_name: format!("gpuArray.{method}"),
245                            implicit_class_argument: None,
246                        },
247                    )
248                })
249                .collect(),
250        },
251    );
252    if let Some(class) = registry.get_mut("matlab.metadata.DynamicProperty") {
253        class.methods.insert(
254            "delete".into(),
255            RuntimeMethod {
256                name: "delete".into(),
257                is_static: false,
258                is_abstract: false,
259                is_sealed: false,
260                access: MemberAccess::Public,
261                function_name: "matlab.metadata.DynamicProperty.delete".into(),
262                implicit_class_argument: None,
263            },
264        );
265    }
266    registry
267}
268
269pub fn register_class(def: RuntimeClass) {
270    register_class_with_modifiers(def, false, false);
271}
272
273pub fn register_class_with_sealed(def: RuntimeClass, is_sealed: bool) {
274    register_class_with_modifiers(def, is_sealed, false);
275}
276
277pub fn register_class_with_modifiers(def: RuntimeClass, is_sealed: bool, is_abstract: bool) {
278    let class_name = def.name.clone();
279    with_state_mut(|state| {
280        state.classes.insert(class_name.clone(), def);
281        set_membership(&mut state.sealed, &class_name, is_sealed);
282        set_membership(&mut state.abstract_classes, &class_name, is_abstract);
283        state.enumerations.entry(class_name).or_default();
284    });
285}
286
287fn set_membership(registry: &mut HashSet<String>, name: &str, present: bool) {
288    if present {
289        registry.insert(name.to_owned());
290    } else {
291        registry.remove(name);
292    }
293}
294
295pub fn register_class_enumerations(class_name: &str, members: impl IntoIterator<Item = String>) {
296    with_state_mut(|state| {
297        let entry = state.enumerations.entry(class_name.to_owned()).or_default();
298        entry.clear();
299        entry.extend(members);
300    });
301}
302
303pub fn class_has_enumeration_member(class_name: &str, member: &str) -> bool {
304    with_state(|state| {
305        state
306            .enumerations
307            .get(class_name)
308            .is_some_and(|members| members.contains(member))
309    })
310}
311
312pub fn get_class(name: &str) -> Option<RuntimeClass> {
313    with_state(|state| state.classes.get(name).cloned())
314}
315
316pub fn class_names() -> Vec<String> {
317    with_state(|state| state.classes.keys().cloned().collect())
318}
319
320/// Resolve the declaring class context for a runtime function name. Executors
321/// use this shared lookup to apply identical private/protected member access.
322pub fn class_context_for_function(function_name: &str) -> Option<String> {
323    if function_name.is_empty() {
324        return None;
325    }
326    if let Some((class_name, method_name)) = function_name.rsplit_once('.') {
327        if !class_name.is_empty() && !method_name.is_empty() && get_class(class_name).is_some() {
328            return Some(class_name.to_string());
329        }
330    }
331    class_names().into_iter().find(|class_name| {
332        get_class(class_name).is_some_and(|class_def| {
333            class_def.methods.values().any(|method| {
334                method.function_name == function_name
335                    || method
336                        .function_name
337                        .strip_prefix(class_name)
338                        .is_some_and(|suffix| {
339                            suffix
340                                .strip_prefix('.')
341                                .is_some_and(|name| name == function_name)
342                        })
343                    || method
344                        .function_name
345                        .rsplit_once('.')
346                        .is_some_and(|(_, name)| name == function_name)
347            })
348        })
349    })
350}
351
352pub fn is_class_sealed(name: &str) -> bool {
353    with_state(|state| state.sealed.contains(name))
354}
355
356pub fn is_class_abstract(name: &str) -> bool {
357    with_state(|state| state.abstract_classes.contains(name))
358}
359
360pub fn is_class_or_subclass(class_name: &str, ancestor_name: &str) -> bool {
361    if class_name == ancestor_name {
362        return true;
363    }
364    with_state(|state| {
365        let registry = &state.classes;
366        let mut current = Some(class_name.to_owned());
367        let mut visited = HashSet::new();
368        while let Some(name) = current {
369            if !visited.insert(name.clone()) {
370                return false;
371            }
372            if name == ancestor_name {
373                return true;
374            }
375            current = registry.get(&name).and_then(|class| class.parent.clone());
376        }
377        false
378    })
379}
380
381pub fn superclass_chain(class_name: &str) -> Option<Vec<String>> {
382    with_state(|state| {
383        let registry = &state.classes;
384        if !registry.contains_key(class_name) {
385            return None;
386        }
387        let mut current = registry
388            .get(class_name)
389            .and_then(|class| class.parent.clone());
390        let mut visited = HashSet::from([class_name.to_owned()]);
391        let mut result = Vec::new();
392        while let Some(name) = current {
393            if !visited.insert(name.clone()) {
394                break;
395            }
396            result.push(name.clone());
397            current = registry.get(&name).and_then(|class| class.parent.clone());
398        }
399        Some(result)
400    })
401}
402
403pub fn lookup_property(class_name: &str, property: &str) -> Option<(RuntimeProperty, String)> {
404    lookup_member(class_name, |class| class.properties.get(property).cloned())
405}
406
407pub fn lookup_method(class_name: &str, method: &str) -> Option<(RuntimeMethod, String)> {
408    lookup_member(class_name, |class| class.methods.get(method).cloned())
409}
410
411fn lookup_member<T>(
412    class_name: &str,
413    mut member: impl FnMut(&RuntimeClass) -> Option<T>,
414) -> Option<(T, String)> {
415    with_state(|state| {
416        let registry = &state.classes;
417        let mut current = Some(class_name.to_owned());
418        let mut visited = HashSet::new();
419        while let Some(name) = current {
420            if !visited.insert(name.clone()) {
421                break;
422            }
423            let class = registry.get(&name)?;
424            if let Some(value) = member(class) {
425                return Some((value, name));
426            }
427            current = class.parent.clone();
428        }
429        None
430    })
431}
432
433pub fn get_static_property_value(class_name: &str, property: &str) -> Option<Value> {
434    with_state(|state| {
435        state
436            .static_values
437            .get(&(class_name.to_owned(), property.to_owned()))
438            .cloned()
439    })
440}
441
442pub fn set_static_property_value(class_name: &str, property: &str, value: Value) {
443    ensure_gc_root_provider();
444    mark_static_values_thread_active();
445    with_state_mut(|state| {
446        state
447            .static_values
448            .insert((class_name.to_owned(), property.to_owned()), value);
449    });
450}
451
452fn with_state<R>(callback: impl FnOnce(&RuntimeClassState) -> R) -> R {
453    if let Some(context) = crate::context::legacy::active() {
454        callback(&context.state().classes.borrow())
455    } else {
456        FALLBACK_STATE.with(|state| callback(&state.borrow()))
457    }
458}
459
460fn with_state_mut<R>(callback: impl FnOnce(&mut RuntimeClassState) -> R) -> R {
461    if let Some(context) = crate::context::legacy::active() {
462        callback(&mut context.state().classes.borrow_mut())
463    } else {
464        FALLBACK_STATE.with(|state| callback(&mut state.borrow_mut()))
465    }
466}
467
468pub fn set_static_property_value_in_owner(
469    class_name: &str,
470    property: &str,
471    value: Value,
472) -> Result<(), String> {
473    let Some((_, owner)) = lookup_property(class_name, property) else {
474        return Err(format!("Unknown static property '{class_name}.{property}'"));
475    };
476    set_static_property_value(&owner, property, value);
477    Ok(())
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use std::sync::atomic::{AtomicU64, Ordering};
484
485    static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
486
487    fn unique_class_name(prefix: &str) -> String {
488        format!(
489            "{prefix}_{}",
490            TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed)
491        )
492    }
493
494    fn empty_class(name: String, parent: Option<String>) -> RuntimeClass {
495        RuntimeClass {
496            name,
497            parent,
498            properties: HashMap::new(),
499            methods: HashMap::new(),
500        }
501    }
502
503    #[test]
504    fn primitives_and_inheritance_are_session_local_and_resolvable() {
505        assert_eq!(
506            lookup_method("uint64", "zeros").unwrap().0.function_name,
507            "zeros"
508        );
509        assert!(is_class_or_subclass("dynamicprops", "handle"));
510        assert_eq!(
511            superclass_chain("dynamicprops"),
512            Some(vec!["handle".into()])
513        );
514    }
515
516    #[test]
517    fn every_primitive_exposes_class_preserving_static_zeros_binding() {
518        for name in [
519            "double", "single", "logical", "int8", "int16", "int32", "int64", "uint8", "uint16",
520            "uint32", "uint64",
521        ] {
522            let (method, owner) = lookup_method(name, "zeros").unwrap();
523            assert_eq!(owner, name);
524            assert!(method.is_static);
525            assert_eq!(method.function_name, "zeros");
526            assert_eq!(method.implicit_class_argument.as_deref(), Some(name));
527        }
528    }
529
530    #[test]
531    fn superclass_chain_is_nearest_first_and_handles_missing_parents_and_cycles() {
532        let grand = unique_class_name("grand");
533        let parent = unique_class_name("parent");
534        let child = unique_class_name("child");
535        register_class(empty_class(grand.clone(), None));
536        register_class(empty_class(parent.clone(), Some(grand.clone())));
537        register_class(empty_class(child.clone(), Some(parent.clone())));
538        assert_eq!(
539            superclass_chain(&child),
540            Some(vec![parent.clone(), grand.clone()])
541        );
542        assert_eq!(superclass_chain(&grand), Some(Vec::new()));
543        assert_eq!(superclass_chain("missing-class"), None);
544
545        let orphan = unique_class_name("orphan");
546        let missing_parent = unique_class_name("missing_parent");
547        register_class(empty_class(orphan.clone(), Some(missing_parent.clone())));
548        assert_eq!(superclass_chain(&orphan), Some(vec![missing_parent]));
549
550        let first = unique_class_name("cycle_first");
551        let second = unique_class_name("cycle_second");
552        register_class(empty_class(first.clone(), Some(second.clone())));
553        register_class(empty_class(second.clone(), Some(first.clone())));
554        assert_eq!(superclass_chain(&first), Some(vec![second]));
555        assert!(!is_class_or_subclass(&first, "missing-ancestor"));
556    }
557
558    #[test]
559    fn inherited_method_and_property_lookup_terminate_on_cycles() {
560        let parent = unique_class_name("lookup_parent");
561        let child = unique_class_name("lookup_child");
562        let mut parent_class = empty_class(parent.clone(), None);
563        parent_class.methods.insert(
564            "parentOnly".into(),
565            RuntimeMethod {
566                name: "parentOnly".into(),
567                is_static: false,
568                is_abstract: false,
569                is_sealed: false,
570                access: MemberAccess::Public,
571                function_name: "parentOnly_impl".into(),
572                implicit_class_argument: None,
573            },
574        );
575        parent_class.properties.insert(
576            "parentFlag".into(),
577            RuntimeProperty {
578                name: "parentFlag".into(),
579                is_static: false,
580                is_constant: false,
581                is_dependent: false,
582                get_access: MemberAccess::Public,
583                set_access: MemberAccess::Public,
584                default_value: None,
585            },
586        );
587        register_class(parent_class);
588        register_class(empty_class(child.clone(), Some(parent.clone())));
589        assert_eq!(lookup_method(&child, "parentOnly").unwrap().1, parent);
590        assert_eq!(
591            lookup_property(&child, "parentFlag").unwrap().0.name,
592            "parentFlag"
593        );
594
595        let first = unique_class_name("lookup_cycle_first");
596        let second = unique_class_name("lookup_cycle_second");
597        register_class(empty_class(first.clone(), Some(second.clone())));
598        register_class(empty_class(second, Some(first.clone())));
599        assert!(lookup_method(&first, "missing").is_none());
600        assert!(lookup_property(&first, "missing").is_none());
601    }
602
603    #[test]
604    fn modifiers_enumerations_and_inherited_static_values_share_one_registry_owner() {
605        let parent = unique_class_name("state_parent");
606        let child = unique_class_name("state_child");
607        let mut parent_class = empty_class(parent.clone(), None);
608        parent_class.properties.insert(
609            "Count".into(),
610            RuntimeProperty {
611                name: "Count".into(),
612                is_static: true,
613                is_constant: false,
614                is_dependent: false,
615                get_access: MemberAccess::Public,
616                set_access: MemberAccess::Private,
617                default_value: Some(Value::Num(0.0)),
618            },
619        );
620        register_class_with_modifiers(parent_class, true, true);
621        register_class(empty_class(child.clone(), Some(parent.clone())));
622        register_class_enumerations(&parent, ["Ready".to_owned(), "Done".to_owned()]);
623
624        assert!(is_class_sealed(&parent));
625        assert!(is_class_abstract(&parent));
626        assert!(class_has_enumeration_member(&parent, "Ready"));
627        assert!(!class_has_enumeration_member(&parent, "Missing"));
628        set_static_property_value_in_owner(&child, "Count", Value::Num(7.0)).unwrap();
629        assert_eq!(
630            get_static_property_value(&parent, "Count"),
631            Some(Value::Num(7.0))
632        );
633        assert_eq!(get_static_property_value(&child, "Count"), None);
634        assert!(set_static_property_value_in_owner(&child, "Missing", Value::Num(1.0)).is_err());
635    }
636}