1use super::introspection::{
25 state_args_stub, state_caller_stub, state_capture_all_stub, state_capture_call_stub,
26 state_capture_module_stub, state_capture_stub, state_locals_stub, state_resume_frame_stub,
27 state_resume_stub,
28};
29use shape_runtime::module_exports::{ModuleContext, ModuleExports, ModuleParam};
30use shape_runtime::type_schema::{FieldType, TypeSchema};
31use shape_runtime::typed_module_exports::{ConcreteType, TypedReturn};
32use shape_runtime::marshal::register_typed_function;
33use shape_value::KindedSlot;
34
35pub fn create_state_module() -> ModuleExports {
46 let mut module = ModuleExports::new("std::core::state");
47 module.description = "Content-addressed VM state primitives".to_string();
48
49 module.add_type_schema(TypeSchema::new(
52 "FunctionRef",
53 vec![
54 ("name".to_string(), FieldType::String),
55 ("hash".to_string(), FieldType::String),
56 ],
57 ));
58
59 module.add_type_schema(TypeSchema::new(
60 "FrameState",
61 vec![
62 ("function_name".to_string(), FieldType::String),
63 ("blob_hash".to_string(), FieldType::String),
64 ("ip".to_string(), FieldType::I64),
65 ("locals".to_string(), FieldType::Any),
66 ("args".to_string(), FieldType::Any),
67 ("upvalues".to_string(), FieldType::Any),
68 ],
69 ));
70
71 module.add_type_schema(TypeSchema::new(
72 "VmState",
73 vec![
74 ("frames".to_string(), FieldType::Any),
75 ("module_bindings".to_string(), FieldType::Any),
76 ("instruction_count".to_string(), FieldType::I64),
77 ],
78 ));
79
80 module.add_type_schema(TypeSchema::new(
81 "ModuleState",
82 vec![("bindings".to_string(), FieldType::Any)],
83 ));
84
85 module.add_type_schema(TypeSchema::new(
86 "CallPayload",
87 vec![
88 ("hash".to_string(), FieldType::String),
89 ("args".to_string(), FieldType::Any),
90 ],
91 ));
92
93 register_typed_function(
96 &mut module,
97 "hash",
98 "SHA-256 content hash of any value",
99 vec![ModuleParam {
100 name: "value".into(),
101 type_name: "any".into(),
102 required: true,
103 description: "Value to hash".into(),
104 ..Default::default()
105 }],
106 ConcreteType::String,
107 state_hash,
108 );
109
110 register_typed_function(
111 &mut module,
112 "fn_hash",
113 "Get a function's content hash from its FunctionBlob",
114 vec![ModuleParam {
115 name: "f".into(),
116 type_name: "any".into(),
117 required: true,
118 description: "Function value".into(),
119 ..Default::default()
120 }],
121 ConcreteType::String,
122 state_fn_hash,
123 );
124
125 register_typed_function(
126 &mut module,
127 "schema_hash",
128 "Content hash of a type's schema definition",
129 vec![ModuleParam {
130 name: "type_name".into(),
131 type_name: "string".into(),
132 required: true,
133 description: "Name of the type to hash".into(),
134 ..Default::default()
135 }],
136 ConcreteType::String,
137 state_schema_hash,
138 );
139
140 register_typed_function(
143 &mut module,
144 "serialize",
145 "Serialize a value to MessagePack bytes",
146 vec![ModuleParam {
147 name: "value".into(),
148 type_name: "any".into(),
149 required: true,
150 description: "Value to serialize".into(),
151 ..Default::default()
152 }],
153 ConcreteType::ArrayInt,
154 state_serialize,
155 );
156
157 register_typed_function(
158 &mut module,
159 "deserialize",
160 "Deserialize MessagePack bytes back to a value",
161 vec![ModuleParam {
162 name: "bytes".into(),
163 type_name: "Array<int>".into(),
164 required: true,
165 description: "MessagePack byte array".into(),
166 ..Default::default()
167 }],
168 ConcreteType::Any,
169 state_deserialize,
170 );
171
172 register_typed_function(
175 &mut module,
176 "diff",
177 "Compute delta between two values using content-hash trees",
178 vec![
179 ModuleParam {
180 name: "old".into(),
181 type_name: "any".into(),
182 required: true,
183 description: "Old value".into(),
184 ..Default::default()
185 },
186 ModuleParam {
187 name: "new".into(),
188 type_name: "any".into(),
189 required: true,
190 description: "New value".into(),
191 ..Default::default()
192 },
193 ],
194 ConcreteType::Named("Delta".into()),
195 state_diff,
196 );
197
198 register_typed_function(
199 &mut module,
200 "patch",
201 "Apply a delta to a base value, producing the updated value",
202 vec![
203 ModuleParam {
204 name: "base".into(),
205 type_name: "any".into(),
206 required: true,
207 description: "Base value".into(),
208 ..Default::default()
209 },
210 ModuleParam {
211 name: "delta".into(),
212 type_name: "Delta".into(),
213 required: true,
214 description: "Delta to apply".into(),
215 ..Default::default()
216 },
217 ],
218 ConcreteType::Any,
219 state_patch,
220 );
221
222 register_typed_function(
225 &mut module,
226 "capture",
227 "Capture current function's frame state",
228 vec![],
229 ConcreteType::Named("FrameState".into()),
230 state_capture_stub,
231 );
232
233 register_typed_function(
234 &mut module,
235 "capture_all",
236 "Capture full VM execution state",
237 vec![],
238 ConcreteType::Named("VmState".into()),
239 state_capture_all_stub,
240 );
241
242 register_typed_function(
243 &mut module,
244 "capture_module",
245 "Capture module-level bindings and type schemas",
246 vec![],
247 ConcreteType::Named("ModuleState".into()),
248 state_capture_module_stub,
249 );
250
251 register_typed_function(
252 &mut module,
253 "capture_call",
254 "Build a ready-to-call payload without executing",
255 vec![
256 ModuleParam {
257 name: "f".into(),
258 type_name: "any".into(),
259 required: true,
260 description: "Function to capture".into(),
261 ..Default::default()
262 },
263 ModuleParam {
264 name: "args".into(),
265 type_name: "Array<any>".into(),
266 required: true,
267 description: "Arguments for the call".into(),
268 ..Default::default()
269 },
270 ],
271 ConcreteType::Named("CallPayload".into()),
272 state_capture_call_stub,
273 );
274
275 register_typed_function(
284 &mut module,
285 "resume",
286 "Resume full VM state (does not return)",
287 vec![ModuleParam {
288 name: "vm".into(),
289 type_name: "VmState".into(),
290 required: true,
291 description: "VM state to resume".into(),
292 ..Default::default()
293 }],
294 ConcreteType::Named("never".into()),
295 state_resume_stub,
296 );
297
298 register_typed_function(
299 &mut module,
300 "resume_frame",
301 "Re-enter a captured function frame and return its result",
302 vec![ModuleParam {
303 name: "f".into(),
304 type_name: "FrameState".into(),
305 required: true,
306 description: "Frame state to resume".into(),
307 ..Default::default()
308 }],
309 ConcreteType::Any,
310 state_resume_frame_stub,
311 );
312
313 register_typed_function(
316 &mut module,
317 "caller",
318 "Get a reference to the calling function",
319 vec![],
320 ConcreteType::Named("FunctionRef?".into()),
321 state_caller_stub,
322 );
323
324 register_typed_function(
325 &mut module,
326 "args",
327 "Get the current function's arguments as an array",
328 vec![],
329 ConcreteType::Named("Array<any>".into()),
330 state_args_stub,
331 );
332
333 register_typed_function(
334 &mut module,
335 "locals",
336 "Get the current scope's local variables as a map",
337 vec![],
338 ConcreteType::Named("Map<string, any>".into()),
339 state_locals_stub,
340 );
341
342 register_typed_function(
343 &mut module,
344 "snapshot",
345 "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.",
346 vec![],
347 ConcreteType::Named("Snapshot".into()),
348 state_capture_all_stub,
349 );
350
351 module
352}
353
354fn content_surface(op: &str) -> String {
373 format!(
374 "{op}: W17-snapshot-resume surface — kind-threaded \
375 slot_to_serializable / serializable_to_slot replacement for the \
376 deleted nanboxed_to_serializable / serializable_to_nanboxed \
377 pair has not landed; state.diff / state.patch additionally \
378 depend on the deleted 1486-LoC `state_diff` runtime module's \
379 kind-threaded rebuild. Tracked as W17-snapshot-resume per \
380 docs/cluster-audits/phase-2d-playbook.md §3. \
381 ADR-006 §2.7.4 (snapshot serialization deferral) + §2.7.5.1 \
382 (post-proof wire-format shape for new HeapKinds).",
383 )
384}
385
386fn ephemeral_store() -> Result<shape_runtime::snapshot::SnapshotStore, String> {
399 let tmp = tempfile::tempdir().map_err(|e| {
400 format!(
401 "W17-snapshot-resume surface — tempdir creation failed: {e}. \
402 ADR-006 §2.7.4."
403 )
404 })?;
405 let store = shape_runtime::snapshot::SnapshotStore::new(tmp.path()).map_err(|e| {
406 format!(
407 "W17-snapshot-resume surface — SnapshotStore::new failed: {e}. \
408 ADR-006 §2.7.4."
409 )
410 })?;
411 std::mem::forget(tmp);
418 Ok(store)
419}
420
421fn slot_to_serialized_bytes(slot: &KindedSlot) -> Result<Vec<u8>, String> {
426 use shape_runtime::snapshot::slot_to_serializable;
427 let store = ephemeral_store()?;
428 let sv = slot_to_serializable(slot.slot().raw(), slot.kind(), &store)?;
429 let bytes = bincode::serialize(&sv).map_err(|e| {
430 format!(
431 "state.serialize: W17-snapshot-resume surface — bincode \
432 serialization failed: {e}. ADR-006 §2.7.5.1."
433 )
434 })?;
435 Ok(bytes)
436}
437
438pub(crate) fn state_hash(
445 args: &[KindedSlot],
446 _ctx: &ModuleContext,
447) -> Result<TypedReturn, String> {
448 let Some(arg) = args.first() else {
449 return Err(content_surface("state.hash"));
450 };
451 let bytes = slot_to_serialized_bytes(arg)?;
452 let digest = shape_runtime::hash_bytes(&bytes);
453 Ok(TypedReturn::Concrete(
454 shape_runtime::typed_module_exports::ConcreteReturn::String(digest.hex().to_string()),
455 ))
456}
457
458pub(crate) fn state_fn_hash(
466 args: &[KindedSlot],
467 ctx: &ModuleContext,
468) -> Result<TypedReturn, String> {
469 use shape_value::{HeapKind, NativeKind};
470
471 let Some(arg) = args.first() else {
472 return Err(content_surface("state.fn_hash"));
473 };
474 let bits = arg.slot().raw();
479 let function_id = match arg.kind() {
480 NativeKind::Int64 | NativeKind::UInt64 => Some(bits as u16),
481 NativeKind::Ptr(HeapKind::Closure) => {
482 if bits == 0 {
483 None
484 } else {
485 let ptr = bits as *const u8;
487 Some(unsafe {
488 shape_value::v2::closure_raw::typed_closure_function_id(ptr)
489 })
490 }
491 }
492 _ => None,
493 };
494 let Some(fid) = function_id else {
495 return Err(format!(
496 "state.fn_hash: W17-snapshot-resume surface — argument is not a \
497 function value (kind={:?}); function-handle decoding for \
498 HeapKind::FunctionRef / TraitObject not yet wired. ADR-006 \
499 §2.7.4.",
500 arg.kind()
501 ));
502 };
503 let Some(hashes) = ctx.function_hashes else {
505 return Err(format!(
506 "state.fn_hash: W17-snapshot-resume surface — \
507 ctx.function_hashes is None at this dispatch surface; \
508 content-addressed metadata not propagated through \
509 invoke_module_fn_id_stub. ADR-006 §2.7.4."
510 ));
511 };
512 let Some(maybe_hash) = hashes.get(fid as usize) else {
513 return Err(format!(
514 "state.fn_hash: function_id {fid} out of range \
515 (program has {} functions). ADR-006 §2.7.4.",
516 hashes.len()
517 ));
518 };
519 let Some(hash_bytes) = maybe_hash else {
520 return Err(format!(
521 "state.fn_hash: W17-snapshot-resume surface — function_id {fid} \
522 has no content-addressed hash entry (compiled without \
523 content-addressed metadata). ADR-006 §2.7.4."
524 ));
525 };
526 Ok(TypedReturn::Concrete(
527 shape_runtime::typed_module_exports::ConcreteReturn::String(hex::encode(hash_bytes)),
528 ))
529}
530
531pub(crate) fn state_schema_hash(
537 args: &[KindedSlot],
538 ctx: &ModuleContext,
539) -> Result<TypedReturn, String> {
540 let Some(arg) = args.first() else {
541 return Err(content_surface("state.schema_hash"));
542 };
543 let type_name = match arg.kind() {
547 shape_value::NativeKind::String
548 | shape_value::NativeKind::Ptr(shape_value::HeapKind::String) => {
549 let bits = arg.slot().raw();
550 if bits == 0 {
551 return Err(format!(
552 "state.schema_hash: W17-snapshot-resume surface — null \
553 string bits. ADR-006 §2.7.6."
554 ));
555 }
556 unsafe {
558 let arc = std::sync::Arc::<String>::from_raw(bits as *const String);
559 let s: String = (*arc).clone();
560 let _ = std::sync::Arc::into_raw(arc); s
562 }
563 }
564 other => {
565 return Err(format!(
566 "state.schema_hash: W17-snapshot-resume surface — first \
567 argument must be string (got kind={other:?}). ADR-006 §2.7.4."
568 ));
569 }
570 };
571 let Some(schema) = ctx.schemas.get(&type_name) else {
572 return Err(format!(
573 "state.schema_hash: unknown type '{type_name}'. ADR-006 §2.7.4."
574 ));
575 };
576 let bytes = bincode::serialize(schema).map_err(|e| {
577 format!(
578 "state.schema_hash: W17-snapshot-resume surface — bincode \
579 serialization failed: {e}. ADR-006 §2.7.5.1."
580 )
581 })?;
582 let digest = shape_runtime::hash_bytes(&bytes);
583 Ok(TypedReturn::Concrete(
584 shape_runtime::typed_module_exports::ConcreteReturn::String(digest.hex().to_string()),
585 ))
586}
587
588pub(crate) fn state_serialize(
600 args: &[KindedSlot],
601 _ctx: &ModuleContext,
602) -> Result<TypedReturn, String> {
603 let Some(arg) = args.first() else {
604 return Err(content_surface("state.serialize"));
605 };
606 let _bytes = slot_to_serialized_bytes(arg)?;
607 Err(format!(
608 "state.serialize: W17-snapshot-resume surface — body computed \
609 {} bytes via slot_to_serializable but the Array<int>/Bytes return \
610 arm needs the W17-marshal-return-arms follow-up at \
611 project_typed_return. ADR-006 §2.7.4 + §2.7.5.1.",
612 _bytes.len()
613 ))
614}
615
616pub(crate) fn state_deserialize(
622 _args: &[KindedSlot],
623 _ctx: &ModuleContext,
624) -> Result<TypedReturn, String> {
625 Err(content_surface("state.deserialize"))
626}
627
628pub(crate) fn state_diff(
640 _args: &[KindedSlot],
641 _ctx: &ModuleContext,
642) -> Result<TypedReturn, String> {
643 Err(content_surface("state.diff"))
644}
645
646pub(crate) fn state_patch(
652 _args: &[KindedSlot],
653 _ctx: &ModuleContext,
654) -> Result<TypedReturn, String> {
655 Err(content_surface("state.patch"))
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use shape_runtime::type_schema::FieldType;
662
663 fn find_schema<'a>(
665 module: &'a ModuleExports,
666 name: &str,
667 ) -> &'a shape_runtime::type_schema::TypeSchema {
668 module
669 .type_schemas
670 .iter()
671 .find(|s| s.name == name)
672 .unwrap_or_else(|| panic!("schema '{}' not found", name))
673 }
674
675 #[test]
680 fn test_state_schemas_have_concrete_field_types() {
681 let module = create_state_module();
682
683 let func_ref = find_schema(&module, "FunctionRef");
685 assert_eq!(
686 func_ref.get_field("name").unwrap().field_type,
687 FieldType::String
688 );
689 assert_eq!(
690 func_ref.get_field("hash").unwrap().field_type,
691 FieldType::String
692 );
693
694 let frame = find_schema(&module, "FrameState");
696 assert_eq!(
697 frame.get_field("function_name").unwrap().field_type,
698 FieldType::String
699 );
700 assert_eq!(
701 frame.get_field("blob_hash").unwrap().field_type,
702 FieldType::String
703 );
704 assert_eq!(frame.get_field("ip").unwrap().field_type, FieldType::I64);
705 assert_eq!(
706 frame.get_field("locals").unwrap().field_type,
707 FieldType::Any
708 );
709 assert_eq!(frame.get_field("args").unwrap().field_type, FieldType::Any);
710 assert_eq!(
711 frame.get_field("upvalues").unwrap().field_type,
712 FieldType::Any
713 );
714
715 let vm_state = find_schema(&module, "VmState");
717 assert_eq!(
718 vm_state.get_field("instruction_count").unwrap().field_type,
719 FieldType::I64
720 );
721 assert_eq!(
722 vm_state.get_field("frames").unwrap().field_type,
723 FieldType::Any
724 );
725 assert_eq!(
726 vm_state.get_field("module_bindings").unwrap().field_type,
727 FieldType::Any
728 );
729
730 let mod_state = find_schema(&module, "ModuleState");
732 assert_eq!(
733 mod_state.get_field("bindings").unwrap().field_type,
734 FieldType::Any
735 );
736
737 let call = find_schema(&module, "CallPayload");
739 assert_eq!(
740 call.get_field("hash").unwrap().field_type,
741 FieldType::String
742 );
743 assert_eq!(call.get_field("args").unwrap().field_type, FieldType::Any);
744 }
745}