1use std::sync::Arc;
8
9use crate::VMExecutionResult;
10use crate::bytecode::BytecodeProgram;
11use crate::compiler::BytecodeCompiler;
12use crate::configuration::BytecodeExecutor;
13use crate::executor::SNAPSHOT_FUTURE_ID;
14use crate::executor::debugger_integration::DebuggerIntegration;
15use crate::executor::{ForeignFunctionHandle, VMConfig, VirtualMachine};
16use shape_value::{HeapValue, ValueWord};
17
18use shape_ast::Program;
19use shape_runtime::context::ExecutionContext;
20use shape_runtime::engine::{ExecutionType, ProgramExecutor, ShapeEngine};
21use shape_runtime::error::Result;
22use shape_runtime::event_queue::{SuspensionState, WaitCondition};
23use shape_value::{EnumPayload, EnumValue};
24use shape_wire::{AnyError as WireAnyError, WireValue, render_any_error_plain};
25
26impl BytecodeExecutor {
27 fn load_module_bindings_from_context(
29 vm: &mut VirtualMachine,
30 ctx: &ExecutionContext,
31 module_binding_registry: &Arc<std::sync::RwLock<shape_runtime::ModuleBindingRegistry>>,
32 module_binding_names: &[String],
33 ) {
34 for (idx, name) in module_binding_names.iter().enumerate() {
35 if name.is_empty() {
36 continue;
37 }
38
39 if let Some(value) = module_binding_registry.read().unwrap().get_by_name(name) {
41 if value
43 .as_heap_ref()
44 .is_some_and(|h| matches!(h, HeapValue::Closure { .. }))
45 {
46 continue;
47 }
48 vm.set_module_binding(idx, value);
49 continue;
50 }
51
52 if let Ok(Some(value)) = ctx.get_variable(name) {
54 if value
56 .as_heap_ref()
57 .is_some_and(|h| matches!(h, HeapValue::Closure { .. }))
58 {
59 continue;
60 }
61 vm.set_module_binding(idx, value);
62 }
63 }
64 }
65
66 fn save_module_bindings_to_context(
68 vm: &VirtualMachine,
69 ctx: &mut ExecutionContext,
70 module_binding_names: &[String],
71 ) {
72 let module_bindings = vm.module_binding_values();
73 for (idx, name) in module_binding_names.iter().enumerate() {
74 if name.is_empty() {
75 continue;
76 }
77 if idx < module_bindings.len() {
78 let value = module_bindings[idx].clone();
79 let _ = ctx.set_variable(name, value);
83 }
84 }
85 }
86
87 fn extract_and_store_format_hints(_program: &Program, _ctx: Option<&mut ExecutionContext>) {
92 }
94
95 fn run_vm_loop(
101 &self,
102 vm: &mut VirtualMachine,
103 engine: &mut ShapeEngine,
104 module_binding_names: &[String],
105 bytecode_for_snapshot: &BytecodeProgram,
106 initial_push: Option<ValueWord>,
107 ) -> Result<ValueWord> {
108 engine.get_runtime_mut().clear_last_runtime_error();
109
110 let mut first_run = initial_push.is_some();
111 let initial_value = initial_push;
112
113 let result = loop {
114 let runtime = engine.get_runtime_mut();
115 let mut ctx = runtime.persistent_context_mut();
116
117 if first_run {
118 if let Some(ref val) = initial_value {
119 let _ = vm.push_vw(val.clone());
120 }
121 first_run = false;
122 }
123
124 match vm.execute_with_suspend(ctx.as_deref_mut()) {
125 Ok(VMExecutionResult::Completed(value)) => break value,
126 Ok(VMExecutionResult::Suspended {
127 future_id,
128 resume_ip,
129 }) => {
130 let wait = if future_id == SNAPSHOT_FUTURE_ID {
131 WaitCondition::Snapshot
132 } else {
133 WaitCondition::Future { id: future_id }
134 };
135
136 if let Some(ctx) = ctx.as_mut() {
137 Self::save_module_bindings_to_context(vm, ctx, module_binding_names);
138 ctx.set_suspension_state(SuspensionState::new(wait, resume_ip));
139 }
140
141 drop(ctx);
142
143 if future_id == SNAPSHOT_FUTURE_ID {
144 let store = engine.snapshot_store().ok_or_else(|| {
145 shape_runtime::error::ShapeError::RuntimeError {
146 message: "Snapshot store not configured".to_string(),
147 location: None,
148 }
149 })?;
150 let vm_snapshot = vm.snapshot(store).map_err(|e| {
151 shape_runtime::error::ShapeError::RuntimeError {
152 message: e.to_string(),
153 location: None,
154 }
155 })?;
156 let vm_hash = engine.store_snapshot_blob(&vm_snapshot)?;
157 let bytecode_hash = engine.store_snapshot_blob(bytecode_for_snapshot)?;
158 let snapshot_hash =
159 engine.snapshot_with_hashes(Some(vm_hash), Some(bytecode_hash))?;
160
161 let hash_str_nb =
162 ValueWord::from_string(Arc::new(snapshot_hash.hex().to_string()));
163 let hash_nb = vm
164 .create_typed_enum_nb("Snapshot", "Hash", vec![hash_str_nb.clone()])
165 .unwrap_or_else(|| {
166 let hash_nb = ValueWord::from_string(Arc::new(
167 snapshot_hash.hex().to_string(),
168 ));
169 ValueWord::from_enum(EnumValue {
170 enum_name: "Snapshot".to_string(),
171 variant: "Hash".to_string(),
172 payload: EnumPayload::Tuple(vec![hash_nb]),
173 })
174 });
175 let _ = vm.push_vw(hash_nb);
176 continue;
177 }
178
179 break ValueWord::none();
180 }
181 Err(shape_value::VMError::Interrupted) => {
182 drop(ctx);
183 let snapshot_hash = if let Some(store) = engine.snapshot_store() {
184 match vm.snapshot(store) {
185 Ok(vm_snapshot) => {
186 let vm_hash = engine.store_snapshot_blob(&vm_snapshot).ok();
187 let bc_hash =
188 engine.store_snapshot_blob(bytecode_for_snapshot).ok();
189 if let (Some(vh), Some(bh)) = (vm_hash, bc_hash) {
190 engine
191 .snapshot_with_hashes(Some(vh), Some(bh))
192 .ok()
193 .map(|h| h.hex().to_string())
194 } else {
195 None
196 }
197 }
198 Err(_) => None,
199 }
200 } else {
201 None
202 };
203 return Err(shape_runtime::error::ShapeError::Interrupted { snapshot_hash });
204 }
205 Err(e) => {
206 let mut location = vm.last_error_line().map(|line| {
207 let mut loc = shape_ast::error::SourceLocation::new(line as usize, 1);
208 if let Some(file) = vm.last_error_file() {
209 loc = loc.with_file(file.to_string());
210 }
211 loc
212 });
213 let mut message = e.to_string();
214 let mut runtime_error_payload = None;
215
216 if let Some(any_error_nb) = vm.take_last_uncaught_exception() {
217 let any_error_wire = if let Some(exec_ctx) = ctx.as_deref() {
218 shape_runtime::wire_conversion::nb_to_wire(&any_error_nb, exec_ctx)
219 } else {
220 let fallback_ctx =
221 shape_runtime::context::ExecutionContext::new_empty();
222 shape_runtime::wire_conversion::nb_to_wire(&any_error_nb, &fallback_ctx)
223 };
224 runtime_error_payload = Some(any_error_wire.clone());
225
226 if let Some(rendered) = render_any_error_plain(&any_error_wire) {
227 message = rendered;
228 }
229
230 if let Some(parsed) = WireAnyError::from_wire(&any_error_wire)
231 && let Some(frame) = parsed.primary_location()
232 && let Some(line) = frame.line
233 {
234 let mut loc = shape_ast::error::SourceLocation::new(
235 line,
236 frame.column.unwrap_or(1),
237 );
238 if let Some(file) = frame.file {
239 loc = loc.with_file(file);
240 }
241 location = Some(loc);
242 }
243 }
244
245 drop(ctx);
246 engine
247 .get_runtime_mut()
248 .set_last_runtime_error(runtime_error_payload);
249
250 return Err(shape_runtime::error::ShapeError::RuntimeError {
251 message,
252 location,
253 });
254 }
255 }
256 };
257
258 Ok(result)
259 }
260
261 fn finalize_result(
263 vm: &VirtualMachine,
264 engine: &mut ShapeEngine,
265 module_binding_names: &[String],
266 result_nb: &ValueWord,
267 ) -> (
268 WireValue,
269 Option<shape_wire::metadata::TypeInfo>,
270 Option<serde_json::Value>,
271 Option<String>,
272 Option<String>,
273 ) {
274 let (content_json, content_html, content_terminal) =
275 shape_runtime::wire_conversion::nb_extract_content(result_nb);
276
277 let runtime = engine.get_runtime_mut();
278 let mut ctx = runtime.persistent_context_mut();
279 let mut type_info = None;
280 let wire_value = if let Some(ctx) = ctx.as_mut() {
281 Self::save_module_bindings_to_context(vm, ctx, module_binding_names);
282 let type_name = result_nb.type_name();
283 type_info = Some(
284 shape_runtime::wire_conversion::nb_to_envelope(result_nb, type_name, ctx).type_info,
285 );
286 shape_runtime::wire_conversion::nb_to_wire(result_nb, ctx)
287 } else {
288 WireValue::Null
289 };
290 (
291 wire_value,
292 type_info,
293 content_json,
294 content_html,
295 content_terminal,
296 )
297 }
298
299 pub fn resume_snapshot(
301 &self,
302 engine: &mut ShapeEngine,
303 vm_snapshot: shape_runtime::snapshot::VmSnapshot,
304 mut bytecode: BytecodeProgram,
305 ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
306 let store = engine.snapshot_store().ok_or_else(|| {
307 shape_runtime::error::ShapeError::RuntimeError {
308 message: "Snapshot store not configured".to_string(),
309 location: None,
310 }
311 })?;
312
313 let mut vm =
315 VirtualMachine::from_snapshot(bytecode.clone(), &vm_snapshot, store).map_err(|e| {
316 shape_runtime::error::ShapeError::RuntimeError {
317 message: e.to_string(),
318 location: None,
319 }
320 })?;
321 vm.set_interrupt(self.interrupt.clone());
322
323 for ext in &self.extensions {
325 vm.register_extension(ext.clone());
326 }
327 vm.populate_module_objects();
328
329 let module_binding_names = bytecode.module_binding_names.clone();
330 let bytecode_for_snapshot = bytecode;
331
332 let resumed = vm
334 .create_typed_enum_nb("Snapshot", "Resumed", vec![])
335 .unwrap_or_else(|| {
336 ValueWord::from_enum(EnumValue {
337 enum_name: "Snapshot".to_string(),
338 variant: "Resumed".to_string(),
339 payload: EnumPayload::Unit,
340 })
341 });
342
343 let result = self.run_vm_loop(
344 &mut vm,
345 engine,
346 &module_binding_names,
347 &bytecode_for_snapshot,
348 Some(resumed),
349 )?;
350 let (wire_value, type_info, content_json, content_html, content_terminal) =
351 Self::finalize_result(&vm, engine, &module_binding_names, &result);
352
353 Ok(shape_runtime::engine::ProgramExecutorResult {
354 wire_value,
355 type_info,
356 execution_type: ExecutionType::Script,
357 content_json,
358 content_html,
359 content_terminal,
360 })
361 }
362
363 pub(crate) fn compile_program_impl(
369 &self,
370 engine: &mut ShapeEngine,
371 program: &Program,
372 ) -> Result<BytecodeProgram> {
373 let source_for_compilation = engine.current_source().map(|s| s.to_string());
374
375 if let (Some(cache), Some(source)) = (&self.bytecode_cache, &source_for_compilation) {
377 if let Some(cached) = cache.get(source) {
378 return Ok(cached);
379 }
380 }
381
382 let runtime = engine.get_runtime_mut();
383
384 let known_bindings: Vec<String> = if let Some(ctx) = runtime.persistent_context() {
385 let names = ctx.root_scope_binding_names();
386 if names.is_empty() {
387 crate::stdlib::core_binding_names()
388 } else {
389 names
390 }
391 } else {
392 crate::stdlib::core_binding_names()
393 };
394
395 Self::extract_and_store_format_hints(program, runtime.persistent_context_mut());
396
397 let module_binding_registry = runtime.module_binding_registry();
398 let imported_program = Self::create_program_from_imports(&module_binding_registry)?;
399
400 let mut merged_program = imported_program;
401 merged_program.items.extend(program.items.clone());
402 crate::module_resolution::prepend_prelude_items(&mut merged_program);
403 self.append_imported_module_items(&mut merged_program);
404
405 let mut compiler = BytecodeCompiler::new();
406 compiler.register_known_bindings(&known_bindings);
407
408 if !self.extensions.is_empty() {
409 compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
410 }
411
412 if let Ok(cwd) = std::env::current_dir() {
413 compiler.set_source_dir(cwd);
414 }
415
416 let bytecode = if let Some(source) = &source_for_compilation {
417 compiler.compile_with_source(&merged_program, source)?
418 } else {
419 compiler.compile(&merged_program)?
420 };
421
422 if let (Some(cache), Some(source)) = (&self.bytecode_cache, &source_for_compilation) {
424 let _ = cache.put(source, &bytecode);
425 }
426
427 Ok(bytecode)
428 }
429
430 pub fn compile_program_for_inspection(
435 &self,
436 engine: &mut ShapeEngine,
437 program: &Program,
438 ) -> Result<BytecodeProgram> {
439 self.compile_program_impl(engine, program)
440 }
441
442 pub fn recompile_and_resume(
449 &self,
450 engine: &mut ShapeEngine,
451 mut vm_snapshot: shape_runtime::snapshot::VmSnapshot,
452 old_bytecode: BytecodeProgram,
453 program: &Program,
454 ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
455 use crate::bytecode::{BuiltinFunction, OpCode, Operand};
456
457 let new_bytecode = self.compile_program_impl(engine, program)?;
458
459 let old_snapshot_ips: Vec<usize> = old_bytecode
461 .instructions
462 .iter()
463 .enumerate()
464 .filter(|(_, instr)| {
465 instr.opcode == OpCode::BuiltinCall
466 && matches!(
467 &instr.operand,
468 Some(Operand::Builtin(BuiltinFunction::Snapshot))
469 )
470 })
471 .map(|(i, _)| i)
472 .collect();
473
474 let new_snapshot_ips: Vec<usize> = new_bytecode
476 .instructions
477 .iter()
478 .enumerate()
479 .filter(|(_, instr)| {
480 instr.opcode == OpCode::BuiltinCall
481 && matches!(
482 &instr.operand,
483 Some(Operand::Builtin(BuiltinFunction::Snapshot))
484 )
485 })
486 .map(|(i, _)| i)
487 .collect();
488
489 let old_snapshot_idx = old_snapshot_ips
492 .iter()
493 .position(|&ip| ip + 1 == vm_snapshot.ip)
494 .ok_or_else(|| shape_runtime::error::ShapeError::RuntimeError {
495 message: format!(
496 "Could not find snapshot() call in original bytecode at IP {} \
497 (snapshot calls found at: {:?})",
498 vm_snapshot.ip, old_snapshot_ips
499 ),
500 location: None,
501 })?;
502
503 let &new_snapshot_ip = new_snapshot_ips.get(old_snapshot_idx).ok_or_else(|| {
505 shape_runtime::error::ShapeError::RuntimeError {
506 message: format!(
507 "Recompiled source has {} snapshot() call(s) but resuming from \
508 snapshot #{} (0-indexed)",
509 new_snapshot_ips.len(),
510 old_snapshot_idx
511 ),
512 location: None,
513 }
514 })?;
515
516 if !vm_snapshot.call_stack.is_empty() {
519 return Err(shape_runtime::error::ShapeError::RuntimeError {
520 message: "Recompile-and-resume is only supported when snapshot() is called \
521 at the top level (call stack is non-empty)"
522 .to_string(),
523 location: None,
524 });
525 }
526
527 vm_snapshot.ip = new_snapshot_ip + 1;
529
530 eprintln!(
531 "Remapped snapshot IP: {} -> {} (snapshot #{})",
532 old_snapshot_ips[old_snapshot_idx] + 1,
533 vm_snapshot.ip,
534 old_snapshot_idx
535 );
536
537 self.resume_snapshot(engine, vm_snapshot, new_bytecode)
538 }
539}
540
541impl shape_runtime::engine::ExpressionEvaluator for BytecodeExecutor {
542 fn eval_statements(
543 &self,
544 stmts: &[shape_ast::Statement],
545 ctx: &mut ExecutionContext,
546 ) -> Result<ValueWord> {
547 let items: Vec<shape_ast::Item> = stmts
549 .iter()
550 .map(|s| shape_ast::Item::Statement(s.clone(), shape_ast::Span::DUMMY))
551 .collect();
552 let mut program = Program { items };
553
554 crate::module_resolution::prepend_prelude_items(&mut program);
556
557 let compiler = BytecodeCompiler::new();
559 let bytecode = compiler.compile(&program)?;
560
561 let module_binding_names = bytecode.module_binding_names.clone();
562 let mut vm = VirtualMachine::new(VMConfig::default());
563 vm.load_program(bytecode);
564 for ext in &self.extensions {
566 vm.register_extension(ext.clone());
567 }
568 vm.populate_module_objects();
569
570 for (idx, name) in module_binding_names.iter().enumerate() {
572 if name.is_empty() {
573 continue;
574 }
575 if let Ok(Some(value)) = ctx.get_variable(name) {
576 let is_closure = value
577 .as_heap_ref()
578 .is_some_and(|h| matches!(h, HeapValue::Closure { .. }));
579 if !is_closure {
580 vm.set_module_binding(idx, value);
581 }
582 }
583 }
584
585 let result_nb =
586 vm.execute(Some(ctx))
587 .map_err(|e| shape_runtime::error::ShapeError::RuntimeError {
588 message: e.to_string(),
589 location: None,
590 })?;
591
592 Self::save_module_bindings_to_context(&vm, ctx, &module_binding_names);
594
595 Ok(result_nb.clone())
596 }
597
598 fn eval_expr(&self, expr: &shape_ast::Expr, ctx: &mut ExecutionContext) -> Result<ValueWord> {
599 let stmt = shape_ast::Statement::Expression(expr.clone(), shape_ast::Span::DUMMY);
601 self.eval_statements(&[stmt], ctx)
602 }
603}
604
605impl ProgramExecutor for BytecodeExecutor {
606 fn execute_program(
607 &self,
608 engine: &mut ShapeEngine,
609 program: &Program,
610 ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
611 let source_for_compilation = engine.current_source().map(|s| s.to_string());
613
614 let (mut vm, module_binding_names, bytecode_for_snapshot) = {
616 let runtime = engine.get_runtime_mut();
617
618 let known_bindings: Vec<String> = if let Some(ctx) = runtime.persistent_context() {
620 ctx.root_scope_binding_names()
621 } else {
622 Vec::new()
623 };
624
625 Self::extract_and_store_format_hints(program, runtime.persistent_context_mut());
628
629 let module_binding_registry = runtime.module_binding_registry();
631 let imported_program = Self::create_program_from_imports(&module_binding_registry)?;
632
633 let mut merged_program = imported_program;
635 merged_program.items.extend(program.items.clone());
636 crate::module_resolution::prepend_prelude_items(&mut merged_program);
637 self.append_imported_module_items(&mut merged_program);
638
639 let mut compiler = BytecodeCompiler::new();
641 compiler.register_known_bindings(&known_bindings);
642
643 if !self.extensions.is_empty() {
645 compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
646 }
647
648 if let Ok(cwd) = std::env::current_dir() {
650 compiler.set_source_dir(cwd);
651 }
652
653 let bytecode = if let Some(source) = &source_for_compilation {
655 compiler.compile_with_source(&merged_program, source)?
656 } else {
657 compiler.compile(&merged_program)?
658 };
659
660 let module_binding_names = bytecode.module_binding_names.clone();
662
663 let mut vm = VirtualMachine::new(VMConfig::default());
665 vm.set_interrupt(self.interrupt.clone());
666 let bytecode_for_snapshot = bytecode.clone();
667 vm.load_program(bytecode);
668 for ext in &self.extensions {
669 vm.register_extension(ext.clone());
670 }
671 vm.populate_module_objects();
672
673 vm.foreign_fn_handles.clear();
675
676 if !vm.program.foreign_functions.is_empty() {
678 let entries = vm.program.foreign_functions.clone();
679 let mut handles = Vec::with_capacity(entries.len());
680 let mut native_library_cache: std::collections::HashMap<
681 String,
682 std::sync::Arc<libloading::Library>,
683 > = std::collections::HashMap::new();
684 let runtime_ctx = runtime.persistent_context();
685
686 for (idx, entry) in entries.iter().enumerate() {
687 if let Some(native_spec) = &entry.native_abi {
688 let linked = crate::executor::native_abi::link_native_function(
689 native_spec,
690 &vm.program.native_struct_layouts,
691 &mut native_library_cache,
692 )
693 .map_err(|e| {
694 shape_runtime::error::ShapeError::RuntimeError {
695 message: format!(
696 "Failed to link native function '{}': {}",
697 entry.name, e
698 ),
699 location: None,
700 }
701 })?;
702
703 vm.program.foreign_functions[idx].dynamic_errors = false;
705 handles.push(Some(ForeignFunctionHandle::Native(std::sync::Arc::new(
706 linked,
707 ))));
708 continue;
709 }
710
711 let Some(ctx) = runtime_ctx.as_ref() else {
712 return Err(shape_runtime::error::ShapeError::RuntimeError {
713 message: format!(
714 "No runtime context available to link foreign function '{}'",
715 entry.name
716 ),
717 location: None,
718 });
719 };
720
721 if let Some(lang_runtime) = ctx.get_language_runtime(&entry.language) {
722 vm.program.foreign_functions[idx].dynamic_errors =
725 lang_runtime.has_dynamic_errors();
726
727 let compiled = lang_runtime.compile(
728 &entry.name,
729 &entry.body_text,
730 &entry.param_names,
731 &entry.param_types,
732 entry.return_type.as_deref(),
733 entry.is_async,
734 )?;
735 handles.push(Some(ForeignFunctionHandle::Runtime {
736 runtime: lang_runtime,
737 compiled,
738 }));
739 } else {
740 return Err(shape_runtime::error::ShapeError::RuntimeError {
741 message: format!(
742 "No language runtime registered for '{}'. \
743 Install the {} extension to use `fn {} ...` blocks.",
744 entry.language, entry.language, entry.language
745 ),
746 location: None,
747 });
748 }
749 }
750 vm.foreign_fn_handles = handles;
751 }
752
753 let module_binding_registry = runtime.module_binding_registry();
754 let mut ctx = runtime.persistent_context_mut();
755
756 if let Some(ctx) = ctx.as_mut() {
758 Self::load_module_bindings_from_context(
759 &mut vm,
760 ctx,
761 &module_binding_registry,
762 &module_binding_names,
763 );
764 }
765
766 (vm, module_binding_names, bytecode_for_snapshot)
767 }; let result = self.run_vm_loop(
771 &mut vm,
772 engine,
773 &module_binding_names,
774 &bytecode_for_snapshot,
775 None,
776 )?;
777
778 let (wire_value, type_info, content_json, content_html, content_terminal) =
780 Self::finalize_result(&vm, engine, &module_binding_names, &result);
781
782 Ok(shape_runtime::engine::ProgramExecutorResult {
783 wire_value,
784 type_info,
785 execution_type: ExecutionType::Script,
786 content_json,
787 content_html,
788 content_terminal,
789 })
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::bytecode::OpCode;
797 use crate::bytecode::Operand;
798 use crate::executor::VirtualMachine;
799 use shape_runtime::snapshot::{SnapshotStore, VmSnapshot};
800
801 #[test]
802 fn snapshot_resume_keeps_snapshot_enum_matching_after_bytecode_roundtrip() {
803 let source = r#"
804from std::core::snapshot use { Snapshot }
805
806function checkpointed(x) {
807 let snap = snapshot()
808 match snap {
809 Snapshot::Hash(id) => id,
810 Snapshot::Resumed => x + 1
811 }
812}
813
814checkpointed(41)
815"#;
816
817 let temp = tempfile::tempdir().expect("tempdir");
818 let store = SnapshotStore::new(temp.path()).expect("snapshot store");
819
820 let mut engine = ShapeEngine::new().expect("engine");
821 engine.load_stdlib().expect("load stdlib");
822 engine.enable_snapshot_store(store.clone());
823
824 let executor_first = BytecodeExecutor::new();
825 let first_result = engine
826 .execute(&executor_first, source)
827 .expect("first execute should succeed");
828 assert!(
829 first_result.value.as_str().is_some(),
830 "first run should return snapshot hash string from Snapshot::Hash arm, got {:?}",
831 first_result.value
832 );
833
834 let snapshot_id = engine
835 .last_snapshot()
836 .cloned()
837 .expect("snapshot id should be recorded");
838 let (semantic, context, vm_hash, bytecode_hash) = engine
839 .load_snapshot(&snapshot_id)
840 .expect("load snapshot metadata");
841 engine
842 .apply_snapshot(semantic, context)
843 .expect("apply snapshot context");
844
845 let vm_hash = vm_hash.expect("vm hash should be present");
846 let bytecode_hash = bytecode_hash.expect("bytecode hash should be present");
847 let vm_snapshot: VmSnapshot = store.get_struct(&vm_hash).expect("deserialize vm snapshot");
848 let bytecode: BytecodeProgram = store
849 .get_struct(&bytecode_hash)
850 .expect("deserialize bytecode");
851 let resume_ip = vm_snapshot.ip;
852 assert!(
853 resume_ip < bytecode.instructions.len(),
854 "snapshot resume ip should be within instruction stream"
855 );
856 assert_eq!(
857 bytecode.instructions[resume_ip].opcode,
858 OpCode::StoreLocal,
859 "snapshot resume ip should point to StoreLocal consuming snapshot() value"
860 );
861
862 let snapshot_schema = bytecode
863 .type_schema_registry
864 .get("Snapshot")
865 .expect("bytecode should contain Snapshot schema");
866 let snapshot_schema_id = snapshot_schema.id as u16;
867 let snapshot_by_id = bytecode
868 .type_schema_registry
869 .get_by_id(snapshot_schema.id)
870 .expect("Snapshot schema id should resolve");
871 assert_eq!(
872 snapshot_by_id.name, "Snapshot",
873 "schema id mapping should resolve back to Snapshot"
874 );
875 let resumed_variant_id = snapshot_schema
876 .get_enum_info()
877 .and_then(|info| info.variant_id("Resumed"))
878 .expect("Snapshot::Resumed variant should exist");
879
880 let typed_field_type_ids: Vec<u16> = bytecode
881 .instructions
882 .iter()
883 .filter_map(|instruction| match instruction.operand {
884 Some(Operand::TypedField {
885 type_id, field_idx, ..
886 }) if field_idx == 0 => Some(type_id),
887 _ => None,
888 })
889 .collect();
890 assert!(
891 typed_field_type_ids.contains(&snapshot_schema_id),
892 "match bytecode should reference Snapshot schema id {} (found typed field ids {:?})",
893 snapshot_schema_id,
894 typed_field_type_ids
895 );
896
897 let vm_probe = VirtualMachine::from_snapshot(bytecode.clone(), &vm_snapshot, &store)
898 .expect("vm probe");
899 let resumed_probe = vm_probe
900 .create_typed_enum_nb("Snapshot", "Resumed", vec![])
901 .expect("create typed Snapshot::Resumed");
902 let (probe_schema_id, probe_slots, _) = resumed_probe
903 .as_typed_object()
904 .expect("resumed marker should be typed object");
905 assert_eq!(
906 probe_schema_id as u16, snapshot_schema_id,
907 "resume marker schema should match compiled Snapshot schema"
908 );
909 assert!(
910 !probe_slots.is_empty(),
911 "typed enum marker should include variant discriminator slot"
912 );
913 assert_eq!(
914 probe_slots[0].as_i64() as u16,
915 resumed_variant_id,
916 "resume marker variant id should be Snapshot::Resumed"
917 );
918
919 let executor_resume = BytecodeExecutor::new();
922 let resumed_result = executor_resume
923 .resume_snapshot(&mut engine, vm_snapshot, bytecode)
924 .expect("resume should succeed");
925
926 assert_eq!(
927 resumed_result.wire_value.as_number(),
928 Some(42.0),
929 "resume should take Snapshot::Resumed arm"
930 );
931 }
932
933 #[test]
934 fn snapshot_resumed_variant_matches_without_resume_flow() {
935 let source = r#"
936from std::core::snapshot use { Snapshot }
937
938let marker = Snapshot::Resumed
939match marker {
940 Snapshot::Hash(id) => 0,
941 Snapshot::Resumed => 1
942}
943"#;
944
945 let mut engine = ShapeEngine::new().expect("engine");
946 engine.load_stdlib().expect("load stdlib");
947 let executor = BytecodeExecutor::new();
948 let result = engine.execute(&executor, source).expect("execute");
949 assert_eq!(
950 result.value.as_number(),
951 Some(1.0),
952 "Snapshot::Resumed pattern should match direct enum constructor value"
953 );
954 }
955
956 #[test]
957 fn snapshot_resume_direct_vm_from_snapshot_with_marker() {
958 let source = r#"
959from std::core::snapshot use { Snapshot }
960
961function checkpointed(x) {
962 let snap = snapshot()
963 match snap {
964 Snapshot::Hash(id) => id,
965 Snapshot::Resumed => x + 1
966 }
967}
968
969checkpointed(41)
970"#;
971
972 let temp = tempfile::tempdir().expect("tempdir");
973 let store = SnapshotStore::new(temp.path()).expect("snapshot store");
974
975 let mut engine = ShapeEngine::new().expect("engine");
976 engine.load_stdlib().expect("load stdlib");
977 engine.enable_snapshot_store(store.clone());
978
979 let executor = BytecodeExecutor::new();
980 let _ = engine.execute(&executor, source).expect("first execute");
981
982 let snapshot_id = engine
983 .last_snapshot()
984 .cloned()
985 .expect("snapshot id should be recorded");
986 let (_semantic, _context, vm_hash, bytecode_hash) = engine
987 .load_snapshot(&snapshot_id)
988 .expect("load snapshot metadata");
989 let vm_hash = vm_hash.expect("vm hash");
990 let bytecode_hash = bytecode_hash.expect("bytecode hash");
991 let vm_snapshot: VmSnapshot = store.get_struct(&vm_hash).expect("vm snapshot");
992 let bytecode: BytecodeProgram = store.get_struct(&bytecode_hash).expect("bytecode");
993
994 let mut vm = VirtualMachine::from_snapshot(bytecode, &vm_snapshot, &store).expect("vm");
995 let resumed = vm
996 .create_typed_enum_nb("Snapshot", "Resumed", vec![])
997 .expect("typed resumed marker");
998 vm.push_vw(resumed).expect("push marker");
999
1000 let result = vm.execute_with_suspend(None).expect("vm execute");
1001 let value = match result {
1002 crate::VMExecutionResult::Completed(v) => v,
1003 crate::VMExecutionResult::Suspended { .. } => panic!("unexpected suspension"),
1004 };
1005 assert_eq!(
1006 value.as_i64(),
1007 Some(42),
1008 "direct VM resume should return 42"
1009 );
1010 }
1011}