1use 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
48pub 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
320pub 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}