1use super::introspection::{
13 state_args_stub, state_caller_stub, state_capture_all_stub, state_capture_call_stub,
14 state_capture_module_stub, state_capture_stub, state_locals_stub, state_resume_frame_stub,
15 state_resume_stub,
16};
17use shape_runtime::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
18use shape_runtime::state_diff;
19use shape_runtime::type_schema::{FieldType, TypeSchema};
20use shape_value::ValueWord;
21use std::sync::Arc;
22
23pub fn create_state_module() -> ModuleExports {
29 let mut module = ModuleExports::new("std::core::state");
30 module.description = "Content-addressed VM state primitives".to_string();
31
32 module.add_type_schema(TypeSchema::new(
35 "FunctionRef",
36 vec![
37 ("name".to_string(), FieldType::String),
38 ("hash".to_string(), FieldType::String),
39 ],
40 ));
41
42 module.add_type_schema(TypeSchema::new(
43 "FrameState",
44 vec![
45 ("function_name".to_string(), FieldType::String),
46 ("blob_hash".to_string(), FieldType::String),
47 ("ip".to_string(), FieldType::I64),
48 ("locals".to_string(), FieldType::Any),
49 ("args".to_string(), FieldType::Any),
50 ("upvalues".to_string(), FieldType::Any),
51 ],
52 ));
53
54 module.add_type_schema(TypeSchema::new(
55 "VmState",
56 vec![
57 ("frames".to_string(), FieldType::Any),
58 ("module_bindings".to_string(), FieldType::Any),
59 ("instruction_count".to_string(), FieldType::I64),
60 ],
61 ));
62
63 module.add_type_schema(TypeSchema::new(
64 "ModuleState",
65 vec![("bindings".to_string(), FieldType::Any)],
66 ));
67
68 module.add_type_schema(TypeSchema::new(
69 "CallPayload",
70 vec![
71 ("hash".to_string(), FieldType::String),
72 ("args".to_string(), FieldType::Any),
73 ],
74 ));
75
76 module.add_function_with_schema(
79 "hash",
80 state_hash,
81 ModuleFunction {
82 description: "SHA-256 content hash of any value".to_string(),
83 params: vec![ModuleParam {
84 name: "value".into(),
85 type_name: "any".into(),
86 required: true,
87 description: "Value to hash".into(),
88 ..Default::default()
89 }],
90 return_type: Some("string".into()),
91 },
92 );
93
94 module.add_function_with_schema(
95 "fn_hash",
96 state_fn_hash,
97 ModuleFunction {
98 description: "Get a function's content hash from its FunctionBlob".to_string(),
99 params: vec![ModuleParam {
100 name: "f".into(),
101 type_name: "any".into(),
102 required: true,
103 description: "Function value".into(),
104 ..Default::default()
105 }],
106 return_type: Some("string".into()),
107 },
108 );
109
110 module.add_function_with_schema(
111 "schema_hash",
112 state_schema_hash,
113 ModuleFunction {
114 description: "Content hash of a type's schema definition".to_string(),
115 params: vec![ModuleParam {
116 name: "type_name".into(),
117 type_name: "string".into(),
118 required: true,
119 description: "Name of the type to hash".into(),
120 ..Default::default()
121 }],
122 return_type: Some("string".into()),
123 },
124 );
125
126 module.add_function_with_schema(
129 "serialize",
130 state_serialize,
131 ModuleFunction {
132 description: "Serialize a value to MessagePack bytes".to_string(),
133 params: vec![ModuleParam {
134 name: "value".into(),
135 type_name: "any".into(),
136 required: true,
137 description: "Value to serialize".into(),
138 ..Default::default()
139 }],
140 return_type: Some("Array<int>".into()),
141 },
142 );
143
144 module.add_function_with_schema(
145 "deserialize",
146 state_deserialize,
147 ModuleFunction {
148 description: "Deserialize MessagePack bytes back to a value".to_string(),
149 params: vec![ModuleParam {
150 name: "bytes".into(),
151 type_name: "Array<int>".into(),
152 required: true,
153 description: "MessagePack byte array".into(),
154 ..Default::default()
155 }],
156 return_type: Some("any".into()),
157 },
158 );
159
160 module.add_function_with_schema(
163 "diff",
164 state_diff,
165 ModuleFunction {
166 description: "Compute delta between two values using content-hash trees".to_string(),
167 params: vec![
168 ModuleParam {
169 name: "old".into(),
170 type_name: "any".into(),
171 required: true,
172 description: "Old value".into(),
173 ..Default::default()
174 },
175 ModuleParam {
176 name: "new".into(),
177 type_name: "any".into(),
178 required: true,
179 description: "New value".into(),
180 ..Default::default()
181 },
182 ],
183 return_type: Some("Delta".into()),
184 },
185 );
186
187 module.add_function_with_schema(
188 "patch",
189 state_patch,
190 ModuleFunction {
191 description: "Apply a delta to a base value, producing the updated value".to_string(),
192 params: vec![
193 ModuleParam {
194 name: "base".into(),
195 type_name: "any".into(),
196 required: true,
197 description: "Base value".into(),
198 ..Default::default()
199 },
200 ModuleParam {
201 name: "delta".into(),
202 type_name: "Delta".into(),
203 required: true,
204 description: "Delta to apply".into(),
205 ..Default::default()
206 },
207 ],
208 return_type: Some("any".into()),
209 },
210 );
211
212 module.add_function_with_schema(
215 "capture",
216 state_capture_stub,
217 ModuleFunction {
218 description: "Capture current function's frame state".to_string(),
219 params: vec![],
220 return_type: Some("FrameState".into()),
221 },
222 );
223
224 module.add_function_with_schema(
225 "capture_all",
226 state_capture_all_stub,
227 ModuleFunction {
228 description: "Capture full VM execution state".to_string(),
229 params: vec![],
230 return_type: Some("VmState".into()),
231 },
232 );
233
234 module.add_function_with_schema(
235 "capture_module",
236 state_capture_module_stub,
237 ModuleFunction {
238 description: "Capture module-level bindings and type schemas".to_string(),
239 params: vec![],
240 return_type: Some("ModuleState".into()),
241 },
242 );
243
244 module.add_function_with_schema(
245 "capture_call",
246 state_capture_call_stub,
247 ModuleFunction {
248 description: "Build a ready-to-call payload without executing".to_string(),
249 params: vec![
250 ModuleParam {
251 name: "f".into(),
252 type_name: "any".into(),
253 required: true,
254 description: "Function to capture".into(),
255 ..Default::default()
256 },
257 ModuleParam {
258 name: "args".into(),
259 type_name: "Array<any>".into(),
260 required: true,
261 description: "Arguments for the call".into(),
262 ..Default::default()
263 },
264 ],
265 return_type: Some("CallPayload".into()),
266 },
267 );
268
269 module.add_function_with_schema(
272 "resume",
273 state_resume_stub,
274 ModuleFunction {
275 description: "Resume full VM state (does not return)".to_string(),
276 params: vec![ModuleParam {
277 name: "vm".into(),
278 type_name: "VmState".into(),
279 required: true,
280 description: "VM state to resume".into(),
281 ..Default::default()
282 }],
283 return_type: None,
284 },
285 );
286
287 module.add_function_with_schema(
288 "resume_frame",
289 state_resume_frame_stub,
290 ModuleFunction {
291 description: "Re-enter a captured function frame and return its result".to_string(),
292 params: vec![ModuleParam {
293 name: "f".into(),
294 type_name: "FrameState".into(),
295 required: true,
296 description: "Frame state to resume".into(),
297 ..Default::default()
298 }],
299 return_type: Some("any".into()),
300 },
301 );
302
303 module.add_function_with_schema(
306 "caller",
307 state_caller_stub,
308 ModuleFunction {
309 description: "Get a reference to the calling function".to_string(),
310 params: vec![],
311 return_type: Some("FunctionRef?".into()),
312 },
313 );
314
315 module.add_function_with_schema(
316 "args",
317 state_args_stub,
318 ModuleFunction {
319 description: "Get the current function's arguments as an array".to_string(),
320 params: vec![],
321 return_type: Some("Array<any>".into()),
322 },
323 );
324
325 module.add_function_with_schema(
326 "locals",
327 state_locals_stub,
328 ModuleFunction {
329 description: "Get the current scope's local variables as a map".to_string(),
330 params: vec![],
331 return_type: Some("Map<string, any>".into()),
332 },
333 );
334
335 module.add_function_with_schema(
336 "snapshot",
337 state_capture_all_stub,
338 ModuleFunction {
339 description: "Create a snapshot of the current execution state. This is a suspension point: the engine saves all state and returns Snapshot::Hash(id). When resumed from a snapshot, execution continues here and returns Snapshot::Resumed.".to_string(),
340 params: vec![],
341 return_type: Some("Snapshot".into()),
342 },
343 );
344
345 module
346}
347
348pub(crate) fn state_hash(args: &[ValueWord], ctx: &ModuleContext) -> Result<ValueWord, String> {
357 let value = args.first().ok_or("state.hash requires 1 argument")?;
358 let digest = state_diff::content_hash_value(value, ctx.schemas);
359 Ok(ValueWord::from_string(Arc::new(digest.hex().to_string())))
360}
361
362pub(crate) fn state_fn_hash(args: &[ValueWord], ctx: &ModuleContext) -> Result<ValueWord, String> {
369 let f = args.first().ok_or("state.fn_hash requires 1 argument")?;
370
371 let func_id = if let Some(fid) = f.as_function() {
372 Some(fid as usize)
373 } else if let Some(heap_ref) = f.as_heap_ref() {
374 if let shape_value::HeapValue::Closure { function_id, .. } = heap_ref {
375 Some(*function_id as usize)
376 } else {
377 None
378 }
379 } else {
380 None
381 };
382
383 let func_id = func_id.ok_or("state.fn_hash: argument is not a function")?;
384
385 if let Some(hashes) = ctx.function_hashes {
387 if let Some(Some(hash_bytes)) = hashes.get(func_id) {
388 let hex: String = hash_bytes
389 .iter()
390 .fold(String::with_capacity(64), |mut acc, b| {
391 use std::fmt::Write;
392 let _ = write!(acc, "{:02x}", b);
393 acc
394 });
395 return Ok(ValueWord::from_string(Arc::new(hex)));
396 }
397 }
398
399 Ok(ValueWord::from_string(Arc::new(format!("fn:{}", func_id))))
401}
402
403pub(crate) fn state_schema_hash(
408 args: &[ValueWord],
409 ctx: &ModuleContext,
410) -> Result<ValueWord, String> {
411 let name_nb = args
412 .first()
413 .ok_or("state.schema_hash requires 1 argument")?;
414 let type_name = name_nb
415 .as_str()
416 .ok_or("state.schema_hash: argument must be a string")?;
417
418 let schema = ctx.schemas.get(type_name).ok_or_else(|| {
419 format!(
420 "state.schema_hash: type '{}' not found in registry",
421 type_name
422 )
423 })?;
424
425 let hash_bytes = match schema.content_hash {
427 Some(hash) => hash,
428 None => {
429 let mut schema_clone = schema.clone();
431 schema_clone.content_hash()
432 }
433 };
434
435 let hex = hash_bytes
437 .iter()
438 .fold(String::with_capacity(64), |mut acc, b| {
439 use std::fmt::Write;
440 let _ = write!(acc, "{:02x}", b);
441 acc
442 });
443
444 Ok(ValueWord::from_string(Arc::new(hex)))
445}
446
447pub(crate) fn state_serialize(
456 args: &[ValueWord],
457 _ctx: &ModuleContext,
458) -> Result<ValueWord, String> {
459 let value = args.first().ok_or("state.serialize requires 1 argument")?;
460
461 let tmp = std::env::temp_dir().join("shape_state_serialize");
465 let store = shape_runtime::snapshot::SnapshotStore::new(&tmp)
466 .map_err(|e| format!("state.serialize: failed to create temp store: {}", e))?;
467 let serializable = shape_runtime::snapshot::nanboxed_to_serializable(value, &store)
468 .map_err(|e| format!("state.serialize: {}", e))?;
469
470 let bytes = rmp_serde::to_vec(&serializable)
471 .map_err(|e| format!("state.serialize: msgpack encoding failed: {}", e))?;
472
473 let arr: Vec<ValueWord> = bytes
475 .iter()
476 .map(|&b| ValueWord::from_i64(b as i64))
477 .collect();
478 Ok(ValueWord::from_array(Arc::new(arr)))
479}
480
481pub(crate) fn state_deserialize(
485 args: &[ValueWord],
486 _ctx: &ModuleContext,
487) -> Result<ValueWord, String> {
488 let bytes_nb = args
489 .first()
490 .ok_or("state.deserialize requires 1 argument")?;
491 let arr = bytes_nb
492 .as_any_array()
493 .ok_or("state.deserialize: argument must be an Array<int>")?
494 .to_generic();
495
496 let mut bytes = Vec::with_capacity(arr.len());
498 for nb in arr.iter() {
499 let b = nb
500 .as_i64()
501 .or_else(|| nb.as_f64().map(|f| f as i64))
502 .ok_or("state.deserialize: array elements must be integers")?;
503 if !(0..=255).contains(&b) {
504 return Err(format!(
505 "state.deserialize: byte value {} out of range 0..255",
506 b
507 ));
508 }
509 bytes.push(b as u8);
510 }
511
512 let serializable: shape_runtime::snapshot::SerializableVMValue = rmp_serde::from_slice(&bytes)
514 .map_err(|e| format!("state.deserialize: msgpack decoding failed: {}", e))?;
515
516 let tmp = std::env::temp_dir().join("shape_state_deserialize");
517 let store = shape_runtime::snapshot::SnapshotStore::new(&tmp)
518 .map_err(|e| format!("state.deserialize: failed to create temp store: {}", e))?;
519 let nb = shape_runtime::snapshot::serializable_to_nanboxed(&serializable, &store)
520 .map_err(|e| format!("state.deserialize: {}", e))?;
521
522 Ok(nb)
523}
524
525pub(crate) fn state_diff(args: &[ValueWord], ctx: &ModuleContext) -> Result<ValueWord, String> {
540 let old = args.first().ok_or("state.diff requires 2 arguments")?;
541 let new = args.get(1).ok_or("state.diff requires 2 arguments")?;
542
543 let delta = state_diff::diff_values(old, new, ctx.schemas);
544
545 let mut keys = Vec::with_capacity(delta.changed.len());
547 let mut values = Vec::with_capacity(delta.changed.len());
548 for (path, value) in delta.changed.iter() {
549 keys.push(ValueWord::from_string(Arc::new(path.clone())));
550 values.push(value.clone());
551 }
552 let changed_map = ValueWord::from_hashmap_pairs(keys, values);
553
554 let removed_arr: Vec<ValueWord> = delta
556 .removed
557 .iter()
558 .map(|s| ValueWord::from_string(Arc::new(s.clone())))
559 .collect();
560 let removed = ValueWord::from_array(Arc::new(removed_arr));
561
562 if let Some(schema) = ctx.schemas.get("Delta") {
564 use shape_value::heap_value::HeapValue;
565 use shape_value::slot::ValueSlot;
566
567 let schema_id = schema.id as u64;
568 let slots = vec![
571 ValueSlot::from_heap(changed_map.as_heap_ref().unwrap().clone()),
572 ValueSlot::from_heap(removed.as_heap_ref().unwrap().clone()),
573 ];
574 let heap_mask: u64 = 0b11; return Ok(ValueWord::from_heap_value(HeapValue::TypedObject {
577 schema_id,
578 slots: slots.into_boxed_slice(),
579 heap_mask,
580 }));
581 }
582
583 Ok(shape_runtime::type_schema::typed_object_from_pairs(&[
585 ("changed", changed_map),
586 ("removed", removed),
587 ]))
588}
589
590pub(crate) fn state_patch(args: &[ValueWord], ctx: &ModuleContext) -> Result<ValueWord, String> {
598 let base = args.first().ok_or("state.patch requires 2 arguments")?;
599 let delta_nb = args.get(1).ok_or("state.patch requires 2 arguments")?;
600
601 let mut delta = state_diff::Delta::empty();
602
603 if let Some((schema_id, slots, heap_mask)) = delta_nb.as_typed_object() {
605 let is_delta = ctx
606 .schemas
607 .get_by_id(schema_id as u32)
608 .map(|s| s.name == "Delta")
609 .unwrap_or(false);
610
611 if is_delta && slots.len() >= 2 {
612 let changed_nb = if heap_mask & 1 != 0 {
614 slots[0].as_heap_nb()
615 } else {
616 return Err("state.patch: Delta.changed slot is not a heap value".to_string());
617 };
618
619 let removed_nb = if heap_mask & 2 != 0 {
621 slots[1].as_heap_nb()
622 } else {
623 return Err("state.patch: Delta.removed slot is not a heap value".to_string());
624 };
625
626 if let Some((keys, values, _index)) = changed_nb.as_hashmap() {
628 for (k, v) in keys.iter().zip(values.iter()) {
629 let key = k
630 .as_str()
631 .ok_or("state.patch: changed key must be a string")?
632 .to_string();
633 delta.changed.insert(key, v.clone());
634 }
635 } else {
636 return Err("state.patch: Delta.changed must be a Map".to_string());
637 }
638
639 if let Some(view) = removed_nb.as_any_array() {
641 let arr = view.to_generic();
642 for nb in arr.iter() {
643 let key = nb
644 .as_str()
645 .ok_or("state.patch: removed entry must be a string")?
646 .to_string();
647 delta.removed.push(key);
648 }
649 } else {
650 return Err("state.patch: Delta.removed must be an Array".to_string());
651 }
652
653 let result = state_diff::patch_value(base, &delta, ctx.schemas);
654 return Ok(result);
655 }
656 }
657
658 let delta_arr = delta_nb
660 .as_any_array()
661 .ok_or("state.patch: delta must be a Delta TypedObject or [changed, removed] array")?
662 .to_generic();
663
664 if delta_arr.len() < 2 {
665 return Err(
666 "state.patch: delta must be a Delta TypedObject or [changed, removed] array"
667 .to_string(),
668 );
669 }
670
671 let changed_pairs = delta_arr[0]
672 .as_any_array()
673 .ok_or("state.patch: changed must be an array of [key, value] pairs")?
674 .to_generic();
675 let removed_arr = delta_arr[1]
676 .as_any_array()
677 .ok_or("state.patch: removed must be an array of strings")?
678 .to_generic();
679
680 for pair in changed_pairs.iter() {
681 let pair_arr = pair
682 .as_any_array()
683 .ok_or("state.patch: each changed entry must be a [key, value] pair")?
684 .to_generic();
685 if pair_arr.len() < 2 {
686 return Err("state.patch: each changed entry must be a [key, value] pair".to_string());
687 }
688 let key = pair_arr[0]
689 .as_str()
690 .ok_or("state.patch: changed key must be a string")?
691 .to_string();
692 delta.changed.insert(key, pair_arr[1].clone());
693 }
694
695 for nb in removed_arr.iter() {
696 let key = nb
697 .as_str()
698 .ok_or("state.patch: removed entry must be a string")?
699 .to_string();
700 delta.removed.push(key);
701 }
702
703 let result = state_diff::patch_value(base, &delta, ctx.schemas);
704 Ok(result)
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use shape_runtime::type_schema::FieldType;
711
712 fn find_schema<'a>(
714 module: &'a ModuleExports,
715 name: &str,
716 ) -> &'a shape_runtime::type_schema::TypeSchema {
717 module
718 .type_schemas
719 .iter()
720 .find(|s| s.name == name)
721 .unwrap_or_else(|| panic!("schema '{}' not found", name))
722 }
723
724 #[test]
725 fn test_state_schemas_have_concrete_field_types() {
726 let module = create_state_module();
727
728 let func_ref = find_schema(&module, "FunctionRef");
730 assert_eq!(
731 func_ref.get_field("name").unwrap().field_type,
732 FieldType::String
733 );
734 assert_eq!(
735 func_ref.get_field("hash").unwrap().field_type,
736 FieldType::String
737 );
738
739 let frame = find_schema(&module, "FrameState");
741 assert_eq!(
742 frame.get_field("function_name").unwrap().field_type,
743 FieldType::String
744 );
745 assert_eq!(
746 frame.get_field("blob_hash").unwrap().field_type,
747 FieldType::String
748 );
749 assert_eq!(frame.get_field("ip").unwrap().field_type, FieldType::I64);
750 assert_eq!(
751 frame.get_field("locals").unwrap().field_type,
752 FieldType::Any
753 );
754 assert_eq!(frame.get_field("args").unwrap().field_type, FieldType::Any);
755 assert_eq!(
756 frame.get_field("upvalues").unwrap().field_type,
757 FieldType::Any
758 );
759
760 let vm_state = find_schema(&module, "VmState");
762 assert_eq!(
763 vm_state.get_field("instruction_count").unwrap().field_type,
764 FieldType::I64
765 );
766 assert_eq!(
767 vm_state.get_field("frames").unwrap().field_type,
768 FieldType::Any
769 );
770 assert_eq!(
771 vm_state.get_field("module_bindings").unwrap().field_type,
772 FieldType::Any
773 );
774
775 let mod_state = find_schema(&module, "ModuleState");
777 assert_eq!(
778 mod_state.get_field("bindings").unwrap().field_type,
779 FieldType::Any
780 );
781
782 let call = find_schema(&module, "CallPayload");
784 assert_eq!(
785 call.get_field("hash").unwrap().field_type,
786 FieldType::String
787 );
788 assert_eq!(call.get_field("args").unwrap().field_type, FieldType::Any);
789 }
790}