Skip to main content

runmat_runtime/object/
resolve.rs

1//! Executor-neutral member and static-member resolution.
2//!
3//! This module owns MATLAB struct/object/handle member access, class access
4//! control, dependent and dynamic properties, `subsref`/`subsasgn` overloads,
5//! graphics-handle compatibility, and canonical GC-barrier stores. Executors
6//! provide only their current function identity for access checks and attach
7//! their own frame/source context to returned runtime errors.
8
9use 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
574/// Store a member while applying the canonical GC write barrier.
575pub 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
593/// Store a dynamically named member while applying the canonical GC barrier.
594pub 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}