1use 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}