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 &mut 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 let mut root_program = program.clone();
400 crate::module_resolution::annotate_program_native_abi_package_key(
401 &mut root_program,
402 self.root_package_key.as_deref(),
403 );
404
405 let mut merged_program = imported_program;
406 merged_program.items.extend(root_program.items);
407 let mut stdlib_names = crate::module_resolution::prepend_prelude_items(&mut merged_program);
408 stdlib_names.extend(self.append_imported_module_items(&mut merged_program)?);
409
410 let mut compiler = BytecodeCompiler::new();
411 compiler.stdlib_function_names = stdlib_names;
412 compiler.register_known_bindings(&known_bindings);
413
414 if !self.extensions.is_empty() {
415 compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
416 }
417
418 if let Ok(cwd) = std::env::current_dir() {
419 compiler.set_source_dir(cwd);
420 }
421
422 compiler.native_resolution_context = self.native_resolution_context.clone();
423
424 let bytecode = if let Some(source) = &source_for_compilation {
425 compiler.compile_with_source(&merged_program, source)?
426 } else {
427 compiler.compile(&merged_program)?
428 };
429
430 if let (Some(cache), Some(source)) = (&self.bytecode_cache, &source_for_compilation) {
432 let _ = cache.put(source, &bytecode);
433 }
434
435 Ok(bytecode)
436 }
437
438 pub fn compile_program_for_inspection(
443 &mut self,
444 engine: &mut ShapeEngine,
445 program: &Program,
446 ) -> Result<BytecodeProgram> {
447 self.compile_program_impl(engine, program)
448 }
449
450 pub fn recompile_and_resume(
457 &mut self,
458 engine: &mut ShapeEngine,
459 mut vm_snapshot: shape_runtime::snapshot::VmSnapshot,
460 old_bytecode: BytecodeProgram,
461 program: &Program,
462 ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
463 use crate::bytecode::{BuiltinFunction, OpCode, Operand};
464
465 let new_bytecode = self.compile_program_impl(engine, program)?;
466
467 let old_snapshot_ips: Vec<usize> = old_bytecode
469 .instructions
470 .iter()
471 .enumerate()
472 .filter(|(_, instr)| {
473 instr.opcode == OpCode::BuiltinCall
474 && matches!(
475 &instr.operand,
476 Some(Operand::Builtin(BuiltinFunction::Snapshot))
477 )
478 })
479 .map(|(i, _)| i)
480 .collect();
481
482 let new_snapshot_ips: Vec<usize> = new_bytecode
484 .instructions
485 .iter()
486 .enumerate()
487 .filter(|(_, instr)| {
488 instr.opcode == OpCode::BuiltinCall
489 && matches!(
490 &instr.operand,
491 Some(Operand::Builtin(BuiltinFunction::Snapshot))
492 )
493 })
494 .map(|(i, _)| i)
495 .collect();
496
497 let old_snapshot_idx = old_snapshot_ips
500 .iter()
501 .position(|&ip| ip + 1 == vm_snapshot.ip)
502 .ok_or_else(|| shape_runtime::error::ShapeError::RuntimeError {
503 message: format!(
504 "Could not find snapshot() call in original bytecode at IP {} \
505 (snapshot calls found at: {:?})",
506 vm_snapshot.ip, old_snapshot_ips
507 ),
508 location: None,
509 })?;
510
511 let &new_snapshot_ip = new_snapshot_ips.get(old_snapshot_idx).ok_or_else(|| {
513 shape_runtime::error::ShapeError::RuntimeError {
514 message: format!(
515 "Recompiled source has {} snapshot() call(s) but resuming from \
516 snapshot #{} (0-indexed)",
517 new_snapshot_ips.len(),
518 old_snapshot_idx
519 ),
520 location: None,
521 }
522 })?;
523
524 if !vm_snapshot.call_stack.is_empty() {
527 return Err(shape_runtime::error::ShapeError::RuntimeError {
528 message: "Recompile-and-resume is only supported when snapshot() is called \
529 at the top level (call stack is non-empty)"
530 .to_string(),
531 location: None,
532 });
533 }
534
535 vm_snapshot.ip = new_snapshot_ip + 1;
537
538 eprintln!(
539 "Remapped snapshot IP: {} -> {} (snapshot #{})",
540 old_snapshot_ips[old_snapshot_idx] + 1,
541 vm_snapshot.ip,
542 old_snapshot_idx
543 );
544
545 self.resume_snapshot(engine, vm_snapshot, new_bytecode)
546 }
547}
548
549impl shape_runtime::engine::ExpressionEvaluator for BytecodeExecutor {
550 fn eval_statements(
551 &self,
552 stmts: &[shape_ast::Statement],
553 ctx: &mut ExecutionContext,
554 ) -> Result<ValueWord> {
555 let items: Vec<shape_ast::Item> = stmts
557 .iter()
558 .map(|s| shape_ast::Item::Statement(s.clone(), shape_ast::Span::DUMMY))
559 .collect();
560 let mut program = Program {
561 items,
562 docs: shape_ast::ast::ProgramDocs::default(),
563 };
564 crate::module_resolution::annotate_program_native_abi_package_key(
565 &mut program,
566 self.root_package_key.as_deref(),
567 );
568
569 let stdlib_names = crate::module_resolution::prepend_prelude_items(&mut program);
571
572 let mut compiler = BytecodeCompiler::new();
574 compiler.stdlib_function_names = stdlib_names;
575 compiler.native_resolution_context = self.native_resolution_context.clone();
576 let bytecode = compiler.compile(&program)?;
577
578 let module_binding_names = bytecode.module_binding_names.clone();
579 let mut vm = VirtualMachine::new(VMConfig::default());
580 vm.load_program(bytecode);
581 for ext in &self.extensions {
583 vm.register_extension(ext.clone());
584 }
585 vm.populate_module_objects();
586
587 for (idx, name) in module_binding_names.iter().enumerate() {
589 if name.is_empty() {
590 continue;
591 }
592 if let Ok(Some(value)) = ctx.get_variable(name) {
593 let is_closure = value
594 .as_heap_ref()
595 .is_some_and(|h| matches!(h, HeapValue::Closure { .. }));
596 if !is_closure {
597 vm.set_module_binding(idx, value);
598 }
599 }
600 }
601
602 let result_nb =
603 vm.execute(Some(ctx))
604 .map_err(|e| shape_runtime::error::ShapeError::RuntimeError {
605 message: e.to_string(),
606 location: None,
607 })?;
608
609 Self::save_module_bindings_to_context(&vm, ctx, &module_binding_names);
611
612 Ok(result_nb.clone())
613 }
614
615 fn eval_expr(&self, expr: &shape_ast::Expr, ctx: &mut ExecutionContext) -> Result<ValueWord> {
616 let stmt = shape_ast::Statement::Expression(expr.clone(), shape_ast::Span::DUMMY);
618 self.eval_statements(&[stmt], ctx)
619 }
620}
621
622impl ProgramExecutor for BytecodeExecutor {
623 fn execute_program(
624 &mut self,
625 engine: &mut ShapeEngine,
626 program: &Program,
627 ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
628 let source_for_compilation = engine.current_source().map(|s| s.to_string());
630
631 let (mut vm, module_binding_names, bytecode_for_snapshot) = {
633 let runtime = engine.get_runtime_mut();
634
635 let known_bindings: Vec<String> = if let Some(ctx) = runtime.persistent_context() {
637 ctx.root_scope_binding_names()
638 } else {
639 Vec::new()
640 };
641
642 Self::extract_and_store_format_hints(program, runtime.persistent_context_mut());
645
646 let module_binding_registry = runtime.module_binding_registry();
648 let imported_program = Self::create_program_from_imports(&module_binding_registry)?;
649 let mut root_program = program.clone();
650 crate::module_resolution::annotate_program_native_abi_package_key(
651 &mut root_program,
652 self.root_package_key.as_deref(),
653 );
654
655 let mut merged_program = imported_program;
657 merged_program.items.extend(root_program.items);
658 let mut stdlib_names =
659 crate::module_resolution::prepend_prelude_items(&mut merged_program);
660 stdlib_names.extend(self.append_imported_module_items(&mut merged_program)?);
661
662 let mut compiler = BytecodeCompiler::new();
664 compiler.stdlib_function_names = stdlib_names;
665 compiler.register_known_bindings(&known_bindings);
666
667 if !self.extensions.is_empty() {
669 compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
670 }
671
672 if let Ok(cwd) = std::env::current_dir() {
674 compiler.set_source_dir(cwd);
675 }
676
677 compiler.native_resolution_context = self.native_resolution_context.clone();
678
679 let bytecode = if let Some(source) = &source_for_compilation {
681 compiler.compile_with_source(&merged_program, source)?
682 } else {
683 compiler.compile(&merged_program)?
684 };
685
686 let module_binding_names = bytecode.module_binding_names.clone();
688
689 let mut vm = VirtualMachine::new(VMConfig::default());
691 vm.set_interrupt(self.interrupt.clone());
692 let bytecode_for_snapshot = bytecode.clone();
693 vm.load_program(bytecode);
694 for ext in &self.extensions {
695 vm.register_extension(ext.clone());
696 }
697 vm.populate_module_objects();
698
699 vm.foreign_fn_handles.clear();
701
702 if !vm.program.foreign_functions.is_empty() {
704 let entries = vm.program.foreign_functions.clone();
705 let mut handles = Vec::with_capacity(entries.len());
706 let mut native_library_cache: std::collections::HashMap<
707 String,
708 std::sync::Arc<libloading::Library>,
709 > = std::collections::HashMap::new();
710 let runtime_ctx = runtime.persistent_context();
711
712 for (idx, entry) in entries.iter().enumerate() {
713 if let Some(native_spec) = &entry.native_abi {
714 let linked = crate::executor::native_abi::link_native_function(
715 native_spec,
716 &vm.program.native_struct_layouts,
717 &mut native_library_cache,
718 )
719 .map_err(|e| {
720 shape_runtime::error::ShapeError::RuntimeError {
721 message: format!(
722 "Failed to link native function '{}': {}",
723 entry.name, e
724 ),
725 location: None,
726 }
727 })?;
728
729 vm.program.foreign_functions[idx].dynamic_errors = false;
731 handles.push(Some(ForeignFunctionHandle::Native(std::sync::Arc::new(
732 linked,
733 ))));
734 continue;
735 }
736
737 let Some(ctx) = runtime_ctx.as_ref() else {
738 return Err(shape_runtime::error::ShapeError::RuntimeError {
739 message: format!(
740 "No runtime context available to link foreign function '{}'",
741 entry.name
742 ),
743 location: None,
744 });
745 };
746
747 if let Some(lang_runtime) = ctx.get_language_runtime(&entry.language) {
748 vm.program.foreign_functions[idx].dynamic_errors =
751 lang_runtime.has_dynamic_errors();
752
753 let compiled = lang_runtime.compile(
754 &entry.name,
755 &entry.body_text,
756 &entry.param_names,
757 &entry.param_types,
758 entry.return_type.as_deref(),
759 entry.is_async,
760 )?;
761 handles.push(Some(ForeignFunctionHandle::Runtime {
762 runtime: lang_runtime,
763 compiled,
764 }));
765 } else {
766 return Err(shape_runtime::error::ShapeError::RuntimeError {
767 message: format!(
768 "No language runtime registered for '{}'. \
769 Install the {} extension to use `fn {} ...` blocks.",
770 entry.language, entry.language, entry.language
771 ),
772 location: None,
773 });
774 }
775 }
776 vm.foreign_fn_handles = handles;
777 }
778
779 let module_binding_registry = runtime.module_binding_registry();
780 let mut ctx = runtime.persistent_context_mut();
781
782 if let Some(ctx) = ctx.as_mut() {
784 Self::load_module_bindings_from_context(
785 &mut vm,
786 ctx,
787 &module_binding_registry,
788 &module_binding_names,
789 );
790 }
791
792 (vm, module_binding_names, bytecode_for_snapshot)
793 }; let result = self.run_vm_loop(
797 &mut vm,
798 engine,
799 &module_binding_names,
800 &bytecode_for_snapshot,
801 None,
802 )?;
803
804 let (wire_value, type_info, content_json, content_html, content_terminal) =
806 Self::finalize_result(&vm, engine, &module_binding_names, &result);
807
808 Ok(shape_runtime::engine::ProgramExecutorResult {
809 wire_value,
810 type_info,
811 execution_type: ExecutionType::Script,
812 content_json,
813 content_html,
814 content_terminal,
815 })
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use crate::bytecode::OpCode;
823 use crate::bytecode::Operand;
824 use crate::executor::VirtualMachine;
825 use shape_runtime::snapshot::{SnapshotStore, VmSnapshot};
826
827 #[test]
828 fn snapshot_resume_keeps_snapshot_enum_matching_after_bytecode_roundtrip() {
829 let source = r#"
830from std::core::snapshot use { Snapshot }
831
832function checkpointed(x) {
833 let snap = snapshot()
834 match snap {
835 Snapshot::Hash(id) => id,
836 Snapshot::Resumed => x + 1
837 }
838}
839
840checkpointed(41)
841"#;
842
843 let temp = tempfile::tempdir().expect("tempdir");
844 let store = SnapshotStore::new(temp.path()).expect("snapshot store");
845
846 let mut engine = ShapeEngine::new().expect("engine");
847 engine.load_stdlib().expect("load stdlib");
848 engine.enable_snapshot_store(store.clone());
849
850 let mut executor_first = BytecodeExecutor::new();
851 let first_result = engine
852 .execute(&mut executor_first, source)
853 .expect("first execute should succeed");
854 assert!(
855 first_result.value.as_str().is_some(),
856 "first run should return snapshot hash string from Snapshot::Hash arm, got {:?}",
857 first_result.value
858 );
859
860 let snapshot_id = engine
861 .last_snapshot()
862 .cloned()
863 .expect("snapshot id should be recorded");
864 let (semantic, context, vm_hash, bytecode_hash) = engine
865 .load_snapshot(&snapshot_id)
866 .expect("load snapshot metadata");
867 engine
868 .apply_snapshot(semantic, context)
869 .expect("apply snapshot context");
870
871 let vm_hash = vm_hash.expect("vm hash should be present");
872 let bytecode_hash = bytecode_hash.expect("bytecode hash should be present");
873 let vm_snapshot: VmSnapshot = store.get_struct(&vm_hash).expect("deserialize vm snapshot");
874 let bytecode: BytecodeProgram = store
875 .get_struct(&bytecode_hash)
876 .expect("deserialize bytecode");
877 let resume_ip = vm_snapshot.ip;
878 assert!(
879 resume_ip < bytecode.instructions.len(),
880 "snapshot resume ip should be within instruction stream"
881 );
882 assert_eq!(
883 bytecode.instructions[resume_ip].opcode,
884 OpCode::StoreLocal,
885 "snapshot resume ip should point to StoreLocal consuming snapshot() value"
886 );
887
888 let snapshot_schema = bytecode
889 .type_schema_registry
890 .get("Snapshot")
891 .expect("bytecode should contain Snapshot schema");
892 let snapshot_schema_id = snapshot_schema.id as u16;
893 let snapshot_by_id = bytecode
894 .type_schema_registry
895 .get_by_id(snapshot_schema.id)
896 .expect("Snapshot schema id should resolve");
897 assert_eq!(
898 snapshot_by_id.name, "Snapshot",
899 "schema id mapping should resolve back to Snapshot"
900 );
901 let resumed_variant_id = snapshot_schema
902 .get_enum_info()
903 .and_then(|info| info.variant_id("Resumed"))
904 .expect("Snapshot::Resumed variant should exist");
905
906 let typed_field_type_ids: Vec<u16> = bytecode
907 .instructions
908 .iter()
909 .filter_map(|instruction| match instruction.operand {
910 Some(Operand::TypedField {
911 type_id, field_idx, ..
912 }) if field_idx == 0 => Some(type_id),
913 _ => None,
914 })
915 .collect();
916 assert!(
917 typed_field_type_ids.contains(&snapshot_schema_id),
918 "match bytecode should reference Snapshot schema id {} (found typed field ids {:?})",
919 snapshot_schema_id,
920 typed_field_type_ids
921 );
922
923 let vm_probe = VirtualMachine::from_snapshot(bytecode.clone(), &vm_snapshot, &store)
924 .expect("vm probe");
925 let resumed_probe = vm_probe
926 .create_typed_enum_nb("Snapshot", "Resumed", vec![])
927 .expect("create typed Snapshot::Resumed");
928 let (probe_schema_id, probe_slots, _) = resumed_probe
929 .as_typed_object()
930 .expect("resumed marker should be typed object");
931 assert_eq!(
932 probe_schema_id as u16, snapshot_schema_id,
933 "resume marker schema should match compiled Snapshot schema"
934 );
935 assert!(
936 !probe_slots.is_empty(),
937 "typed enum marker should include variant discriminator slot"
938 );
939 assert_eq!(
940 probe_slots[0].as_i64() as u16,
941 resumed_variant_id,
942 "resume marker variant id should be Snapshot::Resumed"
943 );
944
945 let executor_resume = BytecodeExecutor::new();
948 let resumed_result = executor_resume
949 .resume_snapshot(&mut engine, vm_snapshot, bytecode)
950 .expect("resume should succeed");
951
952 assert_eq!(
953 resumed_result.wire_value.as_number(),
954 Some(42.0),
955 "resume should take Snapshot::Resumed arm"
956 );
957 }
958
959 #[test]
960 fn snapshot_resumed_variant_matches_without_resume_flow() {
961 let source = r#"
962from std::core::snapshot use { Snapshot }
963
964let marker = Snapshot::Resumed
965match marker {
966 Snapshot::Hash(id) => 0,
967 Snapshot::Resumed => 1
968}
969"#;
970
971 let mut engine = ShapeEngine::new().expect("engine");
972 engine.load_stdlib().expect("load stdlib");
973 let mut executor = BytecodeExecutor::new();
974 let result = engine.execute(&mut executor, source).expect("execute");
975 assert_eq!(
976 result.value.as_number(),
977 Some(1.0),
978 "Snapshot::Resumed pattern should match direct enum constructor value"
979 );
980 }
981
982 #[test]
983 fn stdlib_json_value_methods_can_use_internal_json_builtins() {
984 let source = r#"
985use std::core::json_value
986
987let value = Json::Null
988value.is_null()
989"#;
990
991 let mut engine = ShapeEngine::new().expect("engine");
992 engine.load_stdlib().expect("load stdlib");
993 let mut executor = BytecodeExecutor::new();
994 let result = engine
995 .execute(&mut executor, source)
996 .expect("json_value module should execute successfully");
997 assert_eq!(result.value.as_bool(), Some(true));
998 }
999
1000 #[test]
1001 fn snapshot_resume_direct_vm_from_snapshot_with_marker() {
1002 let source = r#"
1003from std::core::snapshot use { Snapshot }
1004
1005function checkpointed(x) {
1006 let snap = snapshot()
1007 match snap {
1008 Snapshot::Hash(id) => id,
1009 Snapshot::Resumed => x + 1
1010 }
1011}
1012
1013checkpointed(41)
1014"#;
1015
1016 let temp = tempfile::tempdir().expect("tempdir");
1017 let store = SnapshotStore::new(temp.path()).expect("snapshot store");
1018
1019 let mut engine = ShapeEngine::new().expect("engine");
1020 engine.load_stdlib().expect("load stdlib");
1021 engine.enable_snapshot_store(store.clone());
1022
1023 let mut executor = BytecodeExecutor::new();
1024 let _ = engine
1025 .execute(&mut executor, source)
1026 .expect("first execute");
1027
1028 let snapshot_id = engine
1029 .last_snapshot()
1030 .cloned()
1031 .expect("snapshot id should be recorded");
1032 let (_semantic, _context, vm_hash, bytecode_hash) = engine
1033 .load_snapshot(&snapshot_id)
1034 .expect("load snapshot metadata");
1035 let vm_hash = vm_hash.expect("vm hash");
1036 let bytecode_hash = bytecode_hash.expect("bytecode hash");
1037 let vm_snapshot: VmSnapshot = store.get_struct(&vm_hash).expect("vm snapshot");
1038 let bytecode: BytecodeProgram = store.get_struct(&bytecode_hash).expect("bytecode");
1039
1040 let mut vm = VirtualMachine::from_snapshot(bytecode, &vm_snapshot, &store).expect("vm");
1041 let resumed = vm
1042 .create_typed_enum_nb("Snapshot", "Resumed", vec![])
1043 .expect("typed resumed marker");
1044 vm.push_vw(resumed).expect("push marker");
1045
1046 let result = vm.execute_with_suspend(None).expect("vm execute");
1047 let value = match result {
1048 crate::VMExecutionResult::Completed(v) => v,
1049 crate::VMExecutionResult::Suspended { .. } => panic!("unexpected suspension"),
1050 };
1051 assert_eq!(
1052 value.as_i64(),
1053 Some(42),
1054 "direct VM resume should return 42"
1055 );
1056 }
1057}