1use crate::builtins::introspection::dynamicprops;
10use crate::call::identity::external_qualified_display_name;
11use crate::object::dispatch::{
12 call_object_member_subsasgn, call_object_member_subsref,
13 call_object_property_getter_with_outputs, call_object_property_setter_with_outputs,
14 class_defines_member_subsasgn, class_defines_member_subsref,
15};
16use crate::object::indexing::ObjectIndexOp;
17use crate::RuntimeError;
18use runmat_value::{Closure, StructValue, Tensor, Value};
19
20const IDENT_PROPERTY_PRIVATE_ACCESS: &str = "RunMat:PropertyPrivateAccess";
21const IDENT_PROPERTY_READ_ONLY: &str = "RunMat:PropertyReadOnly";
22
23fn mex(identifier: &str, message: &str) -> RuntimeError {
24 crate::runtime_error::semantic_error(identifier, message)
25}
26
27fn has_builtin_member_subsref_protocol(class_name: &str) -> bool {
28 let qualified = format!("{class_name}.{}", ObjectIndexOp::Subsref.protocol_name());
29 runmat_builtins::builtin_name_is_known(&qualified)
30}
31
32fn caller_has_internal_class_access(caller_function_name: Option<&str>, class_name: &str) -> bool {
33 if let Some(caller_name) = caller_function_name {
34 if let Some((caller_class, _)) = caller_name.rsplit_once('.') {
35 if !caller_class.is_empty()
36 && crate::class_registry::get_class(caller_class).is_some()
37 && (crate::class_registry::is_class_or_subclass(caller_class, class_name)
38 || crate::class_registry::is_class_or_subclass(class_name, caller_class))
39 {
40 return true;
41 }
42 }
43 }
44 caller_class_for_function(caller_function_name).is_some_and(|caller_class| {
45 crate::class_registry::is_class_or_subclass(&caller_class, class_name)
46 || crate::class_registry::is_class_or_subclass(class_name, &caller_class)
47 })
48}
49
50fn caller_is_index_overload(
51 caller_function_name: Option<&str>,
52 class_name: &str,
53 op: ObjectIndexOp,
54) -> bool {
55 let Some(caller) = caller_function_name else {
56 return false;
57 };
58 let method_name = op.protocol_name();
59 if caller == method_name {
60 return true;
61 }
62 if let Some((method, owner)) = crate::class_registry::lookup_method(class_name, method_name) {
63 if caller == method.function_name {
64 return true;
65 }
66 if caller == format!("{owner}.{method_name}") {
67 return true;
68 }
69 }
70 if let Some((caller_class, caller_method)) = caller.rsplit_once('.') {
71 if caller_method == method_name
72 && crate::class_registry::is_class_or_subclass(class_name, caller_class)
73 {
74 return true;
75 }
76 }
77 if let Some(caller_class) = caller_class_for_function(Some(caller)) {
78 if let Some((method, _owner)) =
79 crate::class_registry::lookup_method(&caller_class, method_name)
80 {
81 if method.function_name == caller
82 && (crate::class_registry::is_class_or_subclass(class_name, &caller_class)
83 || crate::class_registry::is_class_or_subclass(&caller_class, class_name))
84 {
85 return true;
86 }
87 }
88 }
89 false
90}
91
92fn caller_class_for_function(caller_function_name: Option<&str>) -> Option<String> {
93 let caller_function_name = caller_function_name?;
94 if crate::class_registry::get_class(caller_function_name).is_some() {
95 return Some(caller_function_name.to_string());
96 }
97 if let Some(owner) = crate::class_registry::class_names()
98 .into_iter()
99 .find(|class_name| {
100 crate::class_registry::get_class(class_name).is_some_and(|class_def| {
101 class_def
102 .methods
103 .values()
104 .any(|method| method.function_name == caller_function_name)
105 })
106 })
107 {
108 return Some(owner);
109 }
110 if let Some((class_name, method_name)) = caller_function_name.rsplit_once('.') {
111 if !class_name.is_empty()
112 && !method_name.is_empty()
113 && crate::class_registry::get_class(class_name).is_some()
114 {
115 return Some(class_name.to_string());
116 }
117 }
118 None
119}
120
121fn access_permitted(
122 owner: &str,
123 access: &runmat_types::MemberAccess,
124 caller_function_name: Option<&str>,
125) -> bool {
126 match access {
127 runmat_types::MemberAccess::Public => true,
128 runmat_types::MemberAccess::Private => {
129 caller_class_for_function(caller_function_name).as_deref() == Some(owner)
130 }
131 runmat_types::MemberAccess::Protected => caller_class_for_function(caller_function_name)
132 .is_some_and(|caller_class| {
133 crate::class_registry::is_class_or_subclass(&caller_class, owner)
134 }),
135 }
136}
137
138pub async fn load_member(
139 base: Value,
140 field: String,
141 allow_init: bool,
142 caller_function_name: Option<&str>,
143) -> Result<Value, RuntimeError> {
144 match base {
145 Value::ObjectArray(array) => {
146 let mut values = Vec::with_capacity(array.len());
147 for value in array.data() {
148 values.push(
149 Box::pin(load_member(
150 value.clone(),
151 field.clone(),
152 allow_init,
153 caller_function_name,
154 ))
155 .await?,
156 );
157 }
158 if values.len() == 1 {
159 Ok(values.remove(0))
160 } else {
161 Ok(Value::OutputList(values))
162 }
163 }
164 Value::Object(obj) => {
165 if let Some(cls) = crate::class_registry::get_class(&obj.class_name) {
166 if class_defines_member_subsref(&cls)
167 && !caller_is_index_overload(
168 caller_function_name,
169 &obj.class_name,
170 ObjectIndexOp::Subsref,
171 )
172 && !caller_has_internal_class_access(caller_function_name, &obj.class_name)
173 {
174 return call_object_member_subsref(Value::Object(obj), field).await;
175 }
176 }
177 if let Some((p, owner)) =
178 crate::class_registry::lookup_property(&obj.class_name, &field)
179 {
180 if p.is_static {
181 return Err(mex(
182 "RunMat:PropertyStaticAccess",
183 &format!(
184 "Property '{}' is static; use classref('{}').{}",
185 field, obj.class_name, field
186 ),
187 ));
188 }
189 if !access_permitted(&owner, &p.get_access, caller_function_name) {
190 return Err(mex(
191 IDENT_PROPERTY_PRIVATE_ACCESS,
192 &format!("Property '{}' is private", field),
193 ));
194 }
195 if p.is_dependent {
196 if let Ok(v) = call_object_property_getter_with_outputs(
197 Value::Object(obj.clone()),
198 &field,
199 1,
200 )
201 .await
202 {
203 return Ok(v);
204 }
205 }
206 }
207 if let Some(v) = dynamicprops::dynamic_property_read(&obj, &field)? {
208 Ok(v)
209 } else if let Some(v) = obj.properties.get(&field) {
210 Ok(v.clone())
211 } else if let Some((p2, owner)) =
212 crate::class_registry::lookup_property(&obj.class_name, &field)
213 {
214 if !access_permitted(&owner, &p2.get_access, caller_function_name) {
215 return Err(mex(
216 IDENT_PROPERTY_PRIVATE_ACCESS,
217 &format!("Property '{}' is private", field),
218 ));
219 }
220 if p2.is_dependent {
221 let backing = format!("{field}_backing");
222 if let Some(vb) = obj.properties.get(&backing) {
223 return Ok(vb.clone());
224 }
225 }
226 Err(format!(
227 "Undefined property '{}' for class {}",
228 field, obj.class_name
229 )
230 .into())
231 } else if let Some(cls) = crate::class_registry::get_class(&obj.class_name) {
232 if class_defines_member_subsref(&cls) {
233 call_object_member_subsref(Value::Object(obj), field).await
234 } else {
235 Err(format!(
236 "Undefined property '{}' for class {}",
237 field, obj.class_name
238 )
239 .into())
240 }
241 } else {
242 Err(format!("Unknown class {}", obj.class_name).into())
243 }
244 }
245 Value::HandleObject(handle) => {
246 if let Some(cls) = crate::class_registry::get_class(&handle.class_name) {
247 if class_defines_member_subsref(&cls)
248 && !caller_is_index_overload(
249 caller_function_name,
250 &handle.class_name,
251 ObjectIndexOp::Subsref,
252 )
253 && !caller_has_internal_class_access(caller_function_name, &handle.class_name)
254 {
255 return call_object_member_subsref(Value::HandleObject(handle), field).await;
256 }
257 }
258 if has_builtin_member_subsref_protocol(&handle.class_name) {
259 return call_object_member_subsref(Value::HandleObject(handle), field).await;
260 }
261 crate::builtins::structs::core::getfield::get_member_value(
262 Value::HandleObject(handle),
263 &field,
264 )
265 .await
266 }
267 Value::Listener(listener) => {
268 crate::builtins::structs::core::getfield::get_member_value(
269 Value::Listener(listener),
270 &field,
271 )
272 .await
273 }
274 Value::ClassRef(cls) => load_static_member(&cls, &field, caller_function_name),
275 base @ (Value::Num(_) | Value::Int(_)) => {
276 if !is_possible_graphics_handle_value(&base) {
277 return Err(mex("LoadMember", "LoadMember on non-object"));
278 }
279 load_graphics_member(base, &field).await.map_err(|err| {
280 if is_invalid_graphics_handle_error(&err) {
281 mex("LoadMember", "LoadMember on non-object")
282 } else {
283 err
284 }
285 })
286 }
287 Value::Struct(st) => {
288 if let Some(v) = st.fields.get(&field) {
289 Ok(v.clone())
290 } else if allow_init {
291 Ok(Value::Struct(StructValue::new()))
292 } else {
293 Err(format!("Undefined field '{}'", field).into())
294 }
295 }
296 Value::Cell(ca) => crate::object::cell::gather_cell_member(&ca, &field),
297 Value::MException(mexn) => {
298 let value = match field.as_str() {
299 "identifier" => Value::String(mexn.identifier.clone()),
300 "message" => Value::String(mexn.message.clone()),
301 "stack" => {
302 let values: Vec<Value> = mexn
303 .stack
304 .iter()
305 .map(|s| Value::String(s.clone()))
306 .collect();
307 let rows = values.len();
308 let cell = runmat_value::CellArray::new(values, rows, 1)
309 .map_err(|e| format!("MException.stack: {e}"))?;
310 Value::Cell(cell)
311 }
312 other => return Err(format!("Reference to non-existent field '{}'.", other).into()),
313 };
314 Ok(value)
315 }
316 _ => Err(mex("LoadMember", "LoadMember on non-object")),
317 }
318}
319
320pub async fn load_member_dynamic(
321 base: Value,
322 name: String,
323 allow_init: bool,
324 caller_function_name: Option<&str>,
325) -> Result<Value, RuntimeError> {
326 load_member(base, name, allow_init, caller_function_name).await
327}
328
329pub fn load_static_member(
330 cls: &str,
331 field: &str,
332 caller_function_name: Option<&str>,
333) -> Result<Value, RuntimeError> {
334 if let Some((p, owner)) = crate::class_registry::lookup_property(cls, field) {
335 if !p.is_static {
336 return Err(mex(
337 "RunMat:PropertyStaticAccess",
338 &format!("Property '{}' is not static", field),
339 ));
340 }
341 if !access_permitted(&owner, &p.get_access, caller_function_name) {
342 return Err(mex(
343 IDENT_PROPERTY_PRIVATE_ACCESS,
344 &format!("Property '{}' is private", field),
345 ));
346 }
347 if let Some(v) = crate::class_registry::get_static_property_value(&owner, field) {
348 Ok(v)
349 } else if let Some(v) = &p.default_value {
350 Ok(v.clone())
351 } else {
352 Ok(Value::Tensor(
353 Tensor::new(vec![], vec![0, 0]).expect("empty tensor"),
354 ))
355 }
356 } else if let Some((m, _owner)) = crate::class_registry::lookup_method(cls, field) {
357 if !m.is_static {
358 return Err(mex(
359 "RunMat:MethodStaticAccess",
360 &format!("Method '{}' is not static", field),
361 ));
362 }
363 Ok(Value::Closure(Closure {
364 function_name: m.function_name,
365 bound_function: None,
366 captures: vec![],
367 }))
368 } else if crate::class_registry::class_has_enumeration_member(cls, field) {
369 let mut value = runmat_value::ObjectInstance::new(cls.to_string());
370 value.properties.insert(
371 "__enum_member__".to_string(),
372 Value::String(field.to_string()),
373 );
374 Ok(Value::Object(value))
375 } else {
376 let qualified = external_qualified_display_name(cls, field);
377 if runmat_builtins::builtin_functions()
378 .iter()
379 .any(|b| b.name == qualified)
380 {
381 Ok(Value::Closure(Closure {
382 function_name: qualified,
383 bound_function: None,
384 captures: vec![],
385 }))
386 } else {
387 Err(format!("Unknown property '{}' on class {}", field, cls).into())
388 }
389 }
390}
391
392pub async fn store_member<OnWrite>(
393 base: Value,
394 field: String,
395 rhs: Value,
396 allow_init: bool,
397 caller_function_name: Option<&str>,
398 mut on_write: OnWrite,
399) -> Result<Value, RuntimeError>
400where
401 OnWrite: FnMut(&Value, &Value),
402{
403 match base {
404 Value::Object(mut obj) => {
405 if let Some(cls) = crate::class_registry::get_class(&obj.class_name) {
406 if class_defines_member_subsasgn(&cls)
407 && !caller_is_index_overload(
408 caller_function_name,
409 &obj.class_name,
410 ObjectIndexOp::Subsasgn,
411 )
412 && !caller_has_internal_class_access(caller_function_name, &obj.class_name)
413 {
414 return call_object_member_subsasgn(Value::Object(obj), field, rhs).await;
415 }
416 }
417 if let Some((p, owner)) =
418 crate::class_registry::lookup_property(&obj.class_name, &field)
419 {
420 if p.is_static {
421 return Err(mex(
422 "RunMat:PropertyStaticAccess",
423 &format!(
424 "Property '{}' is static; use classref('{}').{}",
425 field, obj.class_name, field
426 ),
427 ));
428 }
429 if p.is_constant {
430 return Err(mex(
431 IDENT_PROPERTY_READ_ONLY,
432 &format!("Property '{}' is constant", field),
433 ));
434 }
435 if !access_permitted(&owner, &p.set_access, caller_function_name) {
436 return Err(mex(
437 IDENT_PROPERTY_PRIVATE_ACCESS,
438 &format!("Property '{}' is private", field),
439 ));
440 }
441 if p.is_dependent {
442 if let Ok(v) = call_object_property_setter_with_outputs(
443 Value::Object(obj.clone()),
444 &field,
445 rhs.clone(),
446 1,
447 )
448 .await
449 {
450 return Ok(v);
451 }
452 }
453 if let Some(oldv) = obj.properties.get(&field) {
454 on_write(oldv, &rhs);
455 }
456 obj.properties.insert(field, rhs);
457 Ok(Value::Object(obj))
458 } else if dynamicprops::dynamic_property_exists(&obj, &field) {
459 if let Some(oldv) = obj.properties.get(&field) {
460 on_write(oldv, &rhs);
461 }
462 dynamicprops::dynamic_property_assign(&mut obj, &field, rhs)?;
463 Ok(Value::Object(obj))
464 } else if let Some(cls) = crate::class_registry::get_class(&obj.class_name) {
465 if class_defines_member_subsasgn(&cls) {
466 call_object_member_subsasgn(Value::Object(obj), field, rhs).await
467 } else {
468 Err(format!("Undefined property '{}' for class {}", field, cls.name).into())
469 }
470 } else {
471 Err(format!("Unknown class {}", obj.class_name).into())
472 }
473 }
474 Value::ClassRef(cls) => {
475 if let Some((p, owner)) = crate::class_registry::lookup_property(&cls, &field) {
476 if !p.is_static {
477 return Err(mex(
478 "RunMat:PropertyStaticAccess",
479 &format!("Property '{}' is not static", field),
480 ));
481 }
482 if p.is_constant {
483 return Err(mex(
484 IDENT_PROPERTY_READ_ONLY,
485 &format!("Property '{}' is constant", field),
486 ));
487 }
488 if !access_permitted(&owner, &p.set_access, caller_function_name) {
489 return Err(mex(
490 IDENT_PROPERTY_PRIVATE_ACCESS,
491 &format!("Property '{}' is private", field),
492 ));
493 }
494 crate::class_registry::set_static_property_value_in_owner(&owner, &field, rhs)?;
495 Ok(Value::ClassRef(cls))
496 } else {
497 Err(format!("Unknown property '{}' on class {}", field, cls).into())
498 }
499 }
500 Value::HandleObject(handle) => {
501 if let Some(cls) = crate::class_registry::get_class(&handle.class_name) {
502 if class_defines_member_subsasgn(&cls)
503 && !caller_is_index_overload(
504 caller_function_name,
505 &handle.class_name,
506 ObjectIndexOp::Subsasgn,
507 )
508 && !caller_has_internal_class_access(caller_function_name, &handle.class_name)
509 {
510 return call_object_member_subsasgn(Value::HandleObject(handle), field, rhs)
511 .await;
512 }
513 }
514 crate::call_builtin_async_with_outputs(
515 "setfield",
516 &[Value::HandleObject(handle), Value::String(field), rhs],
517 1,
518 )
519 .await
520 }
521 Value::Listener(listener) => {
522 crate::call_builtin_async_with_outputs(
523 "setfield",
524 &[Value::Listener(listener), Value::String(field), rhs],
525 1,
526 )
527 .await
528 }
529 Value::Num(0.0) if allow_init => {
530 let mut st = StructValue::new();
531 st.fields.insert(field, rhs);
532 Ok(Value::Struct(st))
533 }
534 base @ (Value::Num(_) | Value::Int(_)) => {
535 if !is_possible_graphics_handle_value(&base) {
536 return Err(mex("StoreMember", "StoreMember on non-object"));
537 }
538 store_graphics_member(base, &field, rhs)
539 .await
540 .map_err(|err| {
541 if is_invalid_graphics_handle_error(&err) {
542 mex("StoreMember", "StoreMember on non-object")
543 } else {
544 err
545 }
546 })
547 }
548 Value::Struct(mut st) => {
549 if let Some(oldv) = st.fields.get(&field) {
550 on_write(oldv, &rhs);
551 }
552 st.fields.insert(field, rhs);
553 Ok(Value::Struct(st))
554 }
555 Value::Cell(ca) => crate::object::cell::assign_cell_member(ca, field, rhs, on_write),
556 _ => Err(mex("StoreMember", "StoreMember on non-object")),
557 }
558}
559
560pub async fn store_member_dynamic<OnWrite>(
561 base: Value,
562 name: String,
563 rhs: Value,
564 allow_init: bool,
565 caller_function_name: Option<&str>,
566 on_write: OnWrite,
567) -> Result<Value, RuntimeError>
568where
569 OnWrite: FnMut(&Value, &Value),
570{
571 store_member(base, name, rhs, allow_init, caller_function_name, on_write).await
572}
573
574pub async fn store_member_traced(
576 base: Value,
577 field: String,
578 rhs: Value,
579 allow_init: bool,
580 caller_function_name: Option<&str>,
581) -> Result<Value, RuntimeError> {
582 store_member(
583 base,
584 field,
585 rhs,
586 allow_init,
587 caller_function_name,
588 runmat_gc::gc_record_write,
589 )
590 .await
591}
592
593pub async fn store_member_dynamic_traced(
595 base: Value,
596 name: String,
597 rhs: Value,
598 allow_init: bool,
599 caller_function_name: Option<&str>,
600) -> Result<Value, RuntimeError> {
601 store_member_dynamic(
602 base,
603 name,
604 rhs,
605 allow_init,
606 caller_function_name,
607 runmat_gc::gc_record_write,
608 )
609 .await
610}
611
612async fn load_graphics_member(base: Value, field: &str) -> Result<Value, RuntimeError> {
613 crate::call_builtin_async("get", &[base, Value::String(field.to_string())]).await
614}
615
616async fn store_graphics_member(
617 base: Value,
618 field: &str,
619 rhs: Value,
620) -> Result<Value, RuntimeError> {
621 crate::call_builtin_async(
622 "set",
623 &[base.clone(), Value::String(field.to_string()), rhs],
624 )
625 .await?;
626 Ok(base)
627}
628
629fn is_invalid_graphics_handle_error(err: &RuntimeError) -> bool {
630 let text = err.to_string().to_ascii_lowercase();
631 text.contains("unsupported or invalid plotting handle")
632 || text.contains("invalid plotting handle")
633 || text.contains("invalid figure handle")
634 || text.contains("invalid axes handle")
635}
636
637fn is_possible_graphics_handle_value(value: &Value) -> bool {
638 match value {
639 Value::Num(v) => v.is_finite() && *v >= 0.0,
640 Value::Int(i) => i.try_to_u64().is_some(),
641 _ => false,
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::{is_possible_graphics_handle_value, load_member, load_static_member, store_member};
648 use runmat_types::MemberAccess;
649 use runmat_value::{IntValue, ObjectArray, ObjectInstance, Value};
650 use std::collections::HashMap;
651 use std::sync::atomic::{AtomicU64, Ordering};
652
653 static TEST_CLASS_COUNTER: AtomicU64 = AtomicU64::new(0);
654
655 #[test]
656 fn graphics_handle_predicate_preserves_wide_integer_sign() {
657 assert!(is_possible_graphics_handle_value(&Value::Int(
658 IntValue::U64(u64::MAX)
659 )));
660 assert!(!is_possible_graphics_handle_value(&Value::Int(
661 IntValue::I64(-1)
662 )));
663 }
664
665 fn unique_class_name(prefix: &str) -> String {
666 let id = TEST_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed);
667 format!("{}_{}", prefix, id)
668 }
669
670 #[test]
671 fn object_array_member_access_returns_comma_separated_values_in_order() {
672 let values = ["first", "second"]
673 .into_iter()
674 .map(|name| {
675 let mut object = ObjectInstance::new("matlab.unittest.TestResult".into());
676 object
677 .properties
678 .insert("Name".into(), Value::String(name.into()));
679 Value::Object(object)
680 })
681 .collect();
682 let array =
683 Value::ObjectArray(ObjectArray::row("matlab.unittest.TestResult", values).unwrap());
684 let loaded =
685 futures::executor::block_on(load_member(array, "Name".into(), false, None)).unwrap();
686 assert_eq!(
687 loaded,
688 Value::OutputList(vec![
689 Value::String("first".into()),
690 Value::String("second".into())
691 ])
692 );
693 }
694
695 #[test]
696 fn load_static_member_resolves_inherited_static_property_value() {
697 let parent_name = unique_class_name("vm_static_parent");
698 let child_name = unique_class_name("vm_static_child");
699
700 let mut parent_properties = HashMap::new();
701 parent_properties.insert(
702 "version".to_string(),
703 crate::class_registry::RuntimeProperty {
704 name: "version".to_string(),
705 is_static: true,
706 is_constant: false,
707 is_dependent: false,
708 get_access: MemberAccess::Public,
709 set_access: MemberAccess::Public,
710 default_value: Some(Value::Num(1.0)),
711 },
712 );
713 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
714 name: parent_name.clone(),
715 parent: None,
716 properties: parent_properties,
717 methods: HashMap::new(),
718 });
719 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
720 name: child_name.clone(),
721 parent: Some(parent_name.clone()),
722 properties: HashMap::new(),
723 methods: HashMap::new(),
724 });
725
726 crate::class_registry::set_static_property_value(&parent_name, "version", Value::Num(3.0));
727 let value = load_static_member(&child_name, "version", None)
728 .expect("inherited static property should resolve through parent metadata owner");
729 assert_eq!(value, Value::Num(3.0));
730 }
731
732 #[test]
733 fn store_member_updates_inherited_static_property_owner_slot() {
734 let parent_name = unique_class_name("vm_store_static_parent");
735 let child_name = unique_class_name("vm_store_static_child");
736
737 let mut parent_properties = HashMap::new();
738 parent_properties.insert(
739 "version".to_string(),
740 crate::class_registry::RuntimeProperty {
741 name: "version".to_string(),
742 is_static: true,
743 is_constant: false,
744 is_dependent: false,
745 get_access: MemberAccess::Public,
746 set_access: MemberAccess::Public,
747 default_value: Some(Value::Num(1.0)),
748 },
749 );
750 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
751 name: parent_name.clone(),
752 parent: None,
753 properties: parent_properties,
754 methods: HashMap::new(),
755 });
756 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
757 name: child_name.clone(),
758 parent: Some(parent_name.clone()),
759 properties: HashMap::new(),
760 methods: HashMap::new(),
761 });
762
763 let out = futures::executor::block_on(store_member(
764 Value::ClassRef(child_name.clone()),
765 "version".to_string(),
766 Value::Num(9.0),
767 false,
768 None,
769 |_old, _new| {},
770 ))
771 .expect("storing inherited static property via child class ref should succeed");
772 assert_eq!(out, Value::ClassRef(child_name));
773 assert_eq!(
774 crate::class_registry::get_static_property_value(&parent_name, "version"),
775 Some(Value::Num(9.0))
776 );
777 }
778
779 #[test]
780 fn load_static_member_resolves_inherited_static_method() {
781 let parent_name = unique_class_name("vm_method_parent");
782 let child_name = unique_class_name("vm_method_child");
783
784 let mut parent_methods = HashMap::new();
785 parent_methods.insert(
786 "build".to_string(),
787 crate::class_registry::RuntimeMethod {
788 name: "build".to_string(),
789 is_static: true,
790 is_abstract: false,
791 is_sealed: false,
792 access: MemberAccess::Public,
793 function_name: "build_impl".to_string(),
794 implicit_class_argument: None,
795 },
796 );
797 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
798 name: parent_name.clone(),
799 parent: None,
800 properties: HashMap::new(),
801 methods: parent_methods,
802 });
803 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
804 name: child_name.clone(),
805 parent: Some(parent_name.clone()),
806 properties: HashMap::new(),
807 methods: HashMap::new(),
808 });
809
810 let value = load_static_member(&child_name, "build", None)
811 .expect("inherited static method should resolve through parent metadata");
812 let Value::Closure(closure) = value else {
813 panic!("expected static method lookup to return closure");
814 };
815 assert_eq!(closure.function_name, "build_impl");
816 }
817
818 #[test]
819 fn load_member_uses_inherited_subsref_for_missing_property() {
820 let parent_name = unique_class_name("vm_subsref_parent");
821 let child_name = unique_class_name("vm_subsref_child");
822
823 let mut parent_methods = HashMap::new();
824 parent_methods.insert(
825 "subsref".to_string(),
826 crate::class_registry::RuntimeMethod {
827 name: "subsref".to_string(),
828 is_static: false,
829 is_abstract: false,
830 is_sealed: false,
831 access: MemberAccess::Public,
832 function_name: "OverIdx.subsref".to_string(),
833 implicit_class_argument: None,
834 },
835 );
836 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
837 name: parent_name.clone(),
838 parent: None,
839 properties: HashMap::new(),
840 methods: parent_methods,
841 });
842 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
843 name: child_name.clone(),
844 parent: Some(parent_name),
845 properties: HashMap::new(),
846 methods: HashMap::new(),
847 });
848
849 let obj = Value::Object(ObjectInstance::new(child_name));
850 let value =
851 futures::executor::block_on(load_member(obj, "missing".to_string(), false, None))
852 .expect("missing member should dispatch to inherited subsref");
853 assert_eq!(value, Value::Num(77.0));
854 }
855
856 #[test]
857 fn store_member_uses_inherited_subsasgn_for_missing_property() {
858 let parent_name = unique_class_name("vm_subsasgn_parent");
859 let child_name = unique_class_name("vm_subsasgn_child");
860
861 let mut parent_methods = HashMap::new();
862 parent_methods.insert(
863 "subsasgn".to_string(),
864 crate::class_registry::RuntimeMethod {
865 name: "subsasgn".to_string(),
866 is_static: false,
867 is_abstract: false,
868 is_sealed: false,
869 access: MemberAccess::Public,
870 function_name: "OverIdx.subsasgn".to_string(),
871 implicit_class_argument: None,
872 },
873 );
874 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
875 name: parent_name.clone(),
876 parent: None,
877 properties: HashMap::new(),
878 methods: parent_methods,
879 });
880 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
881 name: child_name.clone(),
882 parent: Some(parent_name),
883 properties: HashMap::new(),
884 methods: HashMap::new(),
885 });
886
887 let out = futures::executor::block_on(store_member(
888 Value::Object(ObjectInstance::new(child_name)),
889 "missing".to_string(),
890 Value::Num(13.0),
891 false,
892 None,
893 |_old, _new| {},
894 ))
895 .expect("missing member store should dispatch to inherited subsasgn");
896 let Value::Object(obj) = out else {
897 panic!("expected object result from inherited subsasgn");
898 };
899 assert_eq!(obj.properties.get("missing"), Some(&Value::Num(13.0)));
900 }
901}