Skip to main content

runmat_runtime/builtins/introspection/
object_serialization.rs

1//! Object save/load customization hooks.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6    CellArray, ObjectInstance, StructValue, Value,
7};
8use runmat_macros::runtime_builtin;
9
10pub(crate) const SAVEOBJ_METHOD: &str = "saveobj";
11pub(crate) const LOADOBJ_METHOD: &str = "loadobj";
12
13const SERIALIZED_CLASS_FIELD: &str = "__runmat_serialized_object_class__";
14const SERIALIZED_PAYLOAD_FIELD: &str = "__runmat_serialized_object_payload__";
15const SERIALIZED_KIND_FIELD: &str = "__runmat_serialized_object_kind__";
16const SERIALIZED_HAD_SAVEOBJ_FIELD: &str = "__runmat_serialized_object_had_saveobj__";
17const SERIALIZED_KIND_VALUE: &str = "value";
18const SERIALIZED_KIND_HANDLE: &str = "handle";
19const MAX_SERIALIZATION_DEPTH: usize = 32;
20
21const SAVEOBJ_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
22    name: "b",
23    ty: BuiltinParamType::Any,
24    arity: BuiltinParamArity::Required,
25    default: None,
26    description: "Serialized object representation.",
27}];
28
29const SAVEOBJ_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
30    name: "a",
31    ty: BuiltinParamType::Any,
32    arity: BuiltinParamArity::Required,
33    default: None,
34    description: "Object to serialize.",
35}];
36
37const SAVEOBJ_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
38    label: "b = saveobj(a)",
39    inputs: &SAVEOBJ_INPUTS,
40    outputs: &SAVEOBJ_OUTPUT,
41}];
42
43const LOADOBJ_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
44    name: "b",
45    ty: BuiltinParamType::Any,
46    arity: BuiltinParamArity::Required,
47    default: None,
48    description: "Deserialized object.",
49}];
50
51const LOADOBJ_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
52    name: "a",
53    ty: BuiltinParamType::Any,
54    arity: BuiltinParamArity::Required,
55    default: None,
56    description: "Serialized object representation.",
57}];
58
59const LOADOBJ_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
60    label: "b = loadobj(a)",
61    inputs: &LOADOBJ_INPUTS,
62    outputs: &LOADOBJ_OUTPUT,
63}];
64
65const SERIALIZATION_ERROR_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
66    code: "RM.OBJECT_SERIALIZATION.INVALID_ARGUMENT",
67    identifier: Some("RunMat:ObjectSerialization:InvalidArgument"),
68    when: "The input is not an object or serialized object envelope.",
69    message: "object serialization: invalid argument",
70};
71
72const SERIALIZATION_ERROR_DISPATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
73    code: "RM.OBJECT_SERIALIZATION.DISPATCH",
74    identifier: Some("RunMat:ObjectSerialization:Dispatch"),
75    when: "A saveobj or loadobj method cannot be resolved or executed.",
76    message: "object serialization: method dispatch failed",
77};
78
79const SERIALIZATION_ERROR_RECURSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
80    code: "RM.OBJECT_SERIALIZATION.RECURSION",
81    identifier: Some("RunMat:ObjectSerialization:RecursionLimit"),
82    when: "Object serialization nesting exceeds the supported recursion limit.",
83    message: "object serialization: recursion limit exceeded",
84};
85
86const SERIALIZATION_ERRORS: [BuiltinErrorDescriptor; 3] = [
87    SERIALIZATION_ERROR_ARGUMENT,
88    SERIALIZATION_ERROR_DISPATCH,
89    SERIALIZATION_ERROR_RECURSION,
90];
91
92pub const SAVEOBJ_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
93    signatures: &SAVEOBJ_SIGNATURES,
94    output_mode: BuiltinOutputMode::Fixed,
95    completion_policy: BuiltinCompletionPolicy::Public,
96    errors: &SERIALIZATION_ERRORS,
97};
98
99pub const LOADOBJ_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
100    signatures: &LOADOBJ_SIGNATURES,
101    output_mode: BuiltinOutputMode::Fixed,
102    completion_policy: BuiltinCompletionPolicy::Public,
103    errors: &SERIALIZATION_ERRORS,
104};
105
106fn serialization_error(
107    builtin: &'static str,
108    descriptor: &'static BuiltinErrorDescriptor,
109    detail: impl Into<String>,
110) -> crate::RuntimeError {
111    crate::runtime_descriptor_error_with_detail(builtin, descriptor, detail.into())
112}
113
114fn object_class_name(value: &Value) -> Option<String> {
115    crate::object_receiver_class_name(value)
116}
117
118async fn dispatch_registered_method(
119    class_name: &str,
120    method_name: &str,
121    args: Vec<Value>,
122    builtin: &'static str,
123) -> crate::BuiltinResult<Option<Value>> {
124    if let Some((method, owner)) = runmat_builtins::lookup_method(class_name, method_name) {
125        let owner_member = format!("{owner}.{method_name}");
126        let mut candidates = Vec::with_capacity(2);
127        if !method.function_name.trim().is_empty() {
128            candidates.push(method.function_name);
129        }
130        if !candidates
131            .iter()
132            .any(|candidate| candidate == &owner_member)
133        {
134            candidates.push(owner_member);
135        }
136
137        let mut undefined = None;
138        for candidate in candidates {
139            let (identity, fallback_policy) = crate::callable_identity_for_handle_name(&candidate);
140            match crate::dispatch_callable_with_policy(identity, fallback_policy, args.clone(), 1)
141                .await
142            {
143                Ok(value) => return Ok(Some(value)),
144                Err(err) if crate::is_undefined_function_error(&err) => undefined = Some(err),
145                Err(err) => return Err(err),
146            }
147        }
148        return Err(undefined.unwrap_or_else(|| {
149            serialization_error(
150                builtin,
151                &SERIALIZATION_ERROR_DISPATCH,
152                format!("{class_name}.{method_name} did not resolve to a callable"),
153            )
154        }));
155    }
156
157    if args
158        .first()
159        .is_some_and(|arg| matches!(arg, Value::Object(_) | Value::HandleObject(_)))
160    {
161        match crate::dispatch_object_external_member(class_name.to_string(), method_name, args, 1)
162            .await
163        {
164            Ok(value) => Ok(Some(value)),
165            Err(err) if crate::is_undefined_function_error(&err) => Ok(None),
166            Err(err) => Err(err),
167        }
168    } else {
169        Ok(None)
170    }
171}
172
173async fn call_saveobj_if_available(value: Value) -> crate::BuiltinResult<Option<Value>> {
174    let Some(class_name) = object_class_name(&value) else {
175        return Ok(None);
176    };
177    dispatch_registered_method(&class_name, SAVEOBJ_METHOD, vec![value], SAVEOBJ_METHOD).await
178}
179
180async fn call_loadobj_if_available(
181    class_name: &str,
182    payload: Value,
183) -> crate::BuiltinResult<Option<Value>> {
184    dispatch_registered_method(class_name, LOADOBJ_METHOD, vec![payload], LOADOBJ_METHOD).await
185}
186
187fn object_properties_payload(value: &Value) -> crate::BuiltinResult<StructValue> {
188    match value {
189        Value::Object(object) => {
190            let mut payload = StructValue::new();
191            for (name, property) in &object.properties {
192                payload.insert(name.clone(), property.clone());
193            }
194            Ok(payload)
195        }
196        Value::HandleObject(handle) => runmat_gc::gc_with_value(&handle.target, |target| {
197            if let Value::Object(object) = target {
198                let mut payload = StructValue::new();
199                for (name, property) in &object.properties {
200                    payload.insert(name.clone(), property.clone());
201                }
202                Ok(payload)
203            } else {
204                Err(serialization_error(
205                    SAVEOBJ_METHOD,
206                    &SERIALIZATION_ERROR_ARGUMENT,
207                    "handle target is not an object",
208                ))
209            }
210        })
211        .map_err(|err| {
212            serialization_error(
213                SAVEOBJ_METHOD,
214                &SERIALIZATION_ERROR_ARGUMENT,
215                format!("failed to read handle target: {err}"),
216            )
217        })?,
218        other => Err(serialization_error(
219            SAVEOBJ_METHOD,
220            &SERIALIZATION_ERROR_ARGUMENT,
221            format!("expected object, got {other:?}"),
222        )),
223    }
224}
225
226fn serialized_object_envelope(
227    class_name: String,
228    kind: &'static str,
229    had_saveobj: bool,
230    payload: Value,
231) -> Value {
232    let mut st = StructValue::new();
233    st.insert(SERIALIZED_CLASS_FIELD, Value::String(class_name));
234    st.insert(SERIALIZED_KIND_FIELD, Value::String(kind.to_string()));
235    st.insert(SERIALIZED_HAD_SAVEOBJ_FIELD, Value::Bool(had_saveobj));
236    st.insert(SERIALIZED_PAYLOAD_FIELD, payload);
237    Value::Struct(st)
238}
239
240fn serialized_envelope(value: &Value) -> Option<(String, Value)> {
241    let Value::Struct(st) = value else {
242        return None;
243    };
244    let class_name = st
245        .fields
246        .get(SERIALIZED_CLASS_FIELD)
247        .and_then(|value| String::try_from(value).ok())?;
248    let payload = st.fields.get(SERIALIZED_PAYLOAD_FIELD)?.clone();
249    if st.fields.get(SERIALIZED_KIND_FIELD).is_none()
250        || st.fields.get(SERIALIZED_HAD_SAVEOBJ_FIELD).is_none()
251    {
252        return None;
253    }
254    Some((class_name, payload))
255}
256
257async fn prepare_value_for_save_depth(value: Value, depth: usize) -> crate::BuiltinResult<Value> {
258    if depth > MAX_SERIALIZATION_DEPTH {
259        return Err(serialization_error(
260            SAVEOBJ_METHOD,
261            &SERIALIZATION_ERROR_RECURSION,
262            "nested object serialization exceeded the supported depth",
263        ));
264    }
265
266    match value {
267        receiver @ (Value::Object(_) | Value::HandleObject(_)) => {
268            let class_name = object_class_name(&receiver).ok_or_else(|| {
269                serialization_error(
270                    SAVEOBJ_METHOD,
271                    &SERIALIZATION_ERROR_ARGUMENT,
272                    "object receiver is missing class metadata",
273                )
274            })?;
275            let kind = if matches!(receiver, Value::HandleObject(_)) {
276                SERIALIZED_KIND_HANDLE
277            } else {
278                SERIALIZED_KIND_VALUE
279            };
280            let (payload, had_saveobj) =
281                if let Some(saved) = call_saveobj_if_available(receiver.clone()).await? {
282                    (saved, true)
283                } else {
284                    (Value::Struct(object_properties_payload(&receiver)?), false)
285                };
286            let payload = Box::pin(prepare_value_for_save_depth(payload, depth + 1)).await?;
287            Ok(serialized_object_envelope(
288                class_name,
289                kind,
290                had_saveobj,
291                payload,
292            ))
293        }
294        Value::Struct(st) => {
295            let mut converted = StructValue::new();
296            for (name, field) in st.fields.into_iter() {
297                converted.insert(
298                    name,
299                    Box::pin(prepare_value_for_save_depth(field, depth + 1)).await?,
300                );
301            }
302            Ok(Value::Struct(converted))
303        }
304        Value::Cell(cell) => {
305            let mut converted = Vec::with_capacity(cell.data.len());
306            for value in cell.data {
307                converted.push(Box::pin(prepare_value_for_save_depth(value, depth + 1)).await?);
308            }
309            CellArray::new_with_shape(converted, cell.shape)
310                .map(Value::Cell)
311                .map_err(|err| {
312                    serialization_error(
313                        SAVEOBJ_METHOD,
314                        &SERIALIZATION_ERROR_ARGUMENT,
315                        format!("failed to rebuild serialized cell payload: {err}"),
316                    )
317                })
318        }
319        other => Ok(other),
320    }
321}
322
323pub(crate) async fn prepare_value_for_mat_save(value: Value) -> crate::BuiltinResult<Value> {
324    prepare_value_for_save_depth(value, 0).await
325}
326
327async fn restore_value_after_load_depth(value: Value, depth: usize) -> crate::BuiltinResult<Value> {
328    if depth > MAX_SERIALIZATION_DEPTH {
329        return Err(serialization_error(
330            LOADOBJ_METHOD,
331            &SERIALIZATION_ERROR_RECURSION,
332            "nested object deserialization exceeded the supported depth",
333        ));
334    }
335
336    if let Some((class_name, payload)) = serialized_envelope(&value) {
337        let restored_payload = Box::pin(restore_value_after_load_depth(payload, depth + 1)).await?;
338        if let Some(restored) =
339            call_loadobj_if_available(&class_name, restored_payload.clone()).await?
340        {
341            return Ok(restored);
342        }
343        if let Value::Struct(fields) = restored_payload {
344            return Ok(Value::Object(ObjectInstance {
345                class_name,
346                properties: fields.fields.into_iter().collect(),
347                dynamic_properties: None,
348            }));
349        }
350        return Ok(restored_payload);
351    }
352
353    match value {
354        Value::Struct(st) => {
355            let mut converted = StructValue::new();
356            for (name, field) in st.fields.into_iter() {
357                converted.insert(
358                    name,
359                    Box::pin(restore_value_after_load_depth(field, depth + 1)).await?,
360                );
361            }
362            Ok(Value::Struct(converted))
363        }
364        Value::Cell(cell) => {
365            let mut converted = Vec::with_capacity(cell.data.len());
366            for value in cell.data {
367                converted.push(Box::pin(restore_value_after_load_depth(value, depth + 1)).await?);
368            }
369            CellArray::new_with_shape(converted, cell.shape)
370                .map(Value::Cell)
371                .map_err(|err| {
372                    serialization_error(
373                        LOADOBJ_METHOD,
374                        &SERIALIZATION_ERROR_ARGUMENT,
375                        format!("failed to rebuild deserialized cell payload: {err}"),
376                    )
377                })
378        }
379        other => Ok(other),
380    }
381}
382
383pub(crate) async fn restore_value_from_mat_load(value: Value) -> crate::BuiltinResult<Value> {
384    restore_value_after_load_depth(value, 0).await
385}
386
387#[runtime_builtin(
388    name = "saveobj",
389    category = "introspection",
390    summary = "Return the serialized representation for an object.",
391    keywords = "saveobj,loadobj,object,serialization,mat",
392    descriptor(crate::builtins::introspection::object_serialization::SAVEOBJ_DESCRIPTOR),
393    builtin_path = "crate::builtins::introspection::object_serialization"
394)]
395pub async fn saveobj_builtin(value: Value) -> crate::BuiltinResult<Value> {
396    match value {
397        receiver @ (Value::Object(_) | Value::HandleObject(_)) => {
398            if let Some(saved) = call_saveobj_if_available(receiver.clone()).await? {
399                Ok(saved)
400            } else {
401                Ok(Value::Struct(object_properties_payload(&receiver)?))
402            }
403        }
404        other => Err(serialization_error(
405            SAVEOBJ_METHOD,
406            &SERIALIZATION_ERROR_ARGUMENT,
407            format!("expected object, got {other:?}"),
408        )),
409    }
410}
411
412#[runtime_builtin(
413    name = "loadobj",
414    category = "introspection",
415    summary = "Restore an object from a serialized representation.",
416    keywords = "loadobj,saveobj,object,serialization,mat",
417    descriptor(crate::builtins::introspection::object_serialization::LOADOBJ_DESCRIPTOR),
418    builtin_path = "crate::builtins::introspection::object_serialization"
419)]
420pub async fn loadobj_builtin(value: Value) -> crate::BuiltinResult<Value> {
421    restore_value_from_mat_load(value).await
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use futures::executor::block_on;
428    use tempfile::tempdir;
429
430    #[test]
431    fn saveobj_without_overload_returns_property_struct() {
432        let mut object = ObjectInstance::new("NoIdx".to_string());
433        object.properties.insert("x".to_string(), Value::Num(4.0));
434        let saved = block_on(saveobj_builtin(Value::Object(object))).expect("saveobj");
435        let Value::Struct(st) = saved else {
436            panic!("expected struct payload");
437        };
438        assert_eq!(st.fields.get("x"), Some(&Value::Num(4.0)));
439    }
440
441    #[test]
442    fn saveobj_dispatches_registered_method() {
443        block_on(crate::register_test_classes_builtin()).expect("test classes");
444        let mut object = ObjectInstance::new("OverIdx".to_string());
445        object.properties.insert("k".to_string(), Value::Num(9.0));
446        object
447            .properties
448            .insert("nargs".to_string(), Value::Num(5.0));
449
450        let saved = block_on(saveobj_builtin(Value::Object(object))).expect("saveobj");
451        let Value::Struct(st) = saved else {
452            panic!("expected saveobj struct");
453        };
454        assert_eq!(st.fields.get("k"), Some(&Value::Num(9.0)));
455        assert_eq!(
456            st.fields.get("saved_by"),
457            Some(&Value::String("OverIdx.saveobj".to_string()))
458        );
459    }
460
461    #[test]
462    fn loadobj_restores_serialized_envelope_through_registered_method() {
463        block_on(crate::register_test_classes_builtin()).expect("test classes");
464        let mut object = ObjectInstance::new("OverIdx".to_string());
465        object.properties.insert("k".to_string(), Value::Num(11.0));
466
467        let envelope =
468            block_on(prepare_value_for_mat_save(Value::Object(object))).expect("prepare");
469        let restored = block_on(restore_value_from_mat_load(envelope)).expect("restore");
470        let Value::Object(restored_object) = restored else {
471            panic!("expected restored object");
472        };
473        assert_eq!(restored_object.class_name, "OverIdx");
474        assert_eq!(restored_object.properties.get("k"), Some(&Value::Num(11.0)));
475        assert_eq!(
476            restored_object.properties.get("loaded_by"),
477            Some(&Value::String("OverIdx.loadobj".to_string()))
478        );
479    }
480
481    #[test]
482    fn mat_roundtrip_calls_saveobj_and_loadobj() {
483        block_on(crate::register_test_classes_builtin()).expect("test classes");
484        let dir = tempdir().expect("tempdir");
485        let path = dir.path().join("object_roundtrip.mat");
486
487        let mut object = ObjectInstance::new("OverIdx".to_string());
488        object.properties.insert("k".to_string(), Value::Num(13.0));
489        object
490            .properties
491            .insert("nargs".to_string(), Value::Num(2.0));
492
493        let bytes = block_on(
494            crate::builtins::io::mat::save::encode_workspace_to_mat_bytes(&[(
495                "obj".to_string(),
496                Value::Object(object),
497            )]),
498        )
499        .expect("encode");
500        std::fs::write(&path, bytes).expect("write mat file");
501
502        let entries =
503            block_on(crate::builtins::io::mat::load::read_mat_file(&path)).expect("load mat file");
504        let loaded = entries
505            .iter()
506            .find(|(name, _)| name == "obj")
507            .map(|(_, value)| value)
508            .expect("obj entry");
509        let Value::Object(restored_object) = loaded else {
510            panic!("expected restored object");
511        };
512        assert_eq!(restored_object.class_name, "OverIdx");
513        assert_eq!(restored_object.properties.get("k"), Some(&Value::Num(13.0)));
514        assert_eq!(
515            restored_object.properties.get("loaded_by"),
516            Some(&Value::String("OverIdx.loadobj".to_string()))
517        );
518
519        let decoded = crate::builtins::io::mat::load::decode_workspace_from_mat_bytes(
520            &std::fs::read(&path).unwrap(),
521        )
522        .expect("decode workspace bytes");
523        let decoded_value = decoded
524            .iter()
525            .find(|(name, _)| name == "obj")
526            .map(|(_, value)| value)
527            .expect("decoded obj entry");
528        let Value::Object(decoded_object) = decoded_value else {
529            panic!("expected decoded object");
530        };
531        assert_eq!(decoded_object.class_name, "OverIdx");
532        assert_eq!(
533            decoded_object.properties.get("loaded_by"),
534            Some(&Value::String("OverIdx.loadobj".to_string()))
535        );
536    }
537}