Skip to main content

shape_vm/
execution.rs

1//! Program compilation and execution logic.
2//!
3//! Contains the VM execution loop, module_binding variable synchronization,
4//! snapshot resume, compilation pipeline, and trait implementations
5//! for `ProgramExecutor` and `ExpressionEvaluator`.
6
7use 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    /// Load variables from ExecutionContext and ModuleBindingRegistry into VM module_bindings
28    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            // Check ModuleBindingRegistry first
40            if let Some(value) = module_binding_registry.read().unwrap().get_by_name(name) {
41                // Skip functions - they're already compiled into the bytecode
42                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            // Fall back to ExecutionContext
53            if let Ok(Some(value)) = ctx.get_variable(name) {
54                // Skip functions - they're already compiled
55                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    /// Save VM module_bindings back to ExecutionContext
67    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                // Use set_variable which creates or updates the variable
80                // Note: This preserves existing format hints since set_variable
81                // only updates the value, not the metadata
82                let _ = ctx.set_variable(name, value);
83            }
84        }
85    }
86
87    /// Extract format hints from AST and store in ExecutionContext
88    ///
89    /// Format hints are now handled via the meta system with type aliases.
90    /// This function is kept for API compatibility but is a no-op.
91    fn extract_and_store_format_hints(_program: &Program, _ctx: Option<&mut ExecutionContext>) {
92        // No-op: legacy @ format hints removed, use type aliases with meta instead
93    }
94
95    /// Shared execution loop for both execute_program and resume_snapshot.
96    ///
97    /// Runs the VM in a suspend/resume loop, handling snapshot suspension,
98    /// Ctrl+C interrupts, and errors. If `initial_push` is Some, pushes
99    /// that value onto the VM stack before the first execution cycle.
100    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    /// Finalize execution: save module_bindings back to context and convert result to wire format.
262    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    /// Resume execution from a snapshot
300    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        // Reconstruct VM from snapshot
314        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        // Register extensions and built-in module_bindings
324        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        // Build the Snapshot::Resumed marker to push before first execution cycle
333        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    /// Compile a program to bytecode without executing it.
364    ///
365    /// This performs the same compilation pipeline as `execute_program`
366    /// (merging core stdlib, extensions, virtual modules) but stops
367    /// before creating a VM or executing.
368    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        // Check bytecode cache before expensive compilation
376        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        // Store in bytecode cache (best-effort, ignore errors)
423        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    /// Compile a program with the same pipeline as execution, but do not run it.
431    ///
432    /// The returned bytecode includes tooling artifacts such as
433    /// `expanded_function_defs` for comptime inspection.
434    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    /// Recompile source and resume from a snapshot.
443    ///
444    /// Compiles the new program to bytecode, finds the snapshot() call
445    /// position in both old and new bytecodes, adjusts the VM snapshot's
446    /// instruction pointer, then resumes execution from the snapshot point
447    /// using the new bytecode.
448    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        // Find snapshot() call positions (BuiltinCall with Snapshot operand) in old bytecode
460        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        // Same for new bytecode
475        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        // VmSnapshot.ip points to the instruction AFTER the BuiltinCall(Snapshot).
490        // Find which snapshot() in the old bytecode corresponds to the saved IP.
491        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        // Map to the corresponding snapshot() in the new bytecode (by ordinal)
504        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        // Check for non-empty call stack — recompile mode can only adjust the
517        // top-level IP; return addresses inside function frames would be stale.
518        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        // Adjust the snapshot's IP to point after the snapshot() call in new bytecode
528        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        // Wrap statements as a program
548        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        // Inject prelude and resolve imports
555        crate::module_resolution::prepend_prelude_items(&mut program);
556
557        // Compile and execute
558        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        // Register extensions before built-in module_bindings so extensions are also available
565        for ext in &self.extensions {
566            vm.register_extension(ext.clone());
567        }
568        vm.populate_module_objects();
569
570        // Load variables from context
571        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        // Save back modified module_bindings
593        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        // Wrap expression as an expression statement
600        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        // Capture source text before getting runtime reference (for error messages)
612        let source_for_compilation = engine.current_source().map(|s| s.to_string());
613
614        // Phase 1: Compile and prepare bytecode (borrows runtime, then drops it)
615        let (mut vm, module_binding_names, bytecode_for_snapshot) = {
616            let runtime = engine.get_runtime_mut();
617
618            // Get known module_binding variables from previous REPL sessions
619            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            // Extract format hints from variable declarations BEFORE compilation
626            // This preserves metadata that bytecode doesn't carry
627            Self::extract_and_store_format_hints(program, runtime.persistent_context_mut());
628
629            // Extract imported functions from ModuleBindingRegistry and add them to the program
630            let module_binding_registry = runtime.module_binding_registry();
631            let imported_program = Self::create_program_from_imports(&module_binding_registry)?;
632
633            // Merge imported functions into the main program
634            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            // Compile AST to Bytecode with knowledge of existing module_bindings
640            let mut compiler = BytecodeCompiler::new();
641            compiler.register_known_bindings(&known_bindings);
642
643            // Wire extension registry into compiler for comptime execution
644            if !self.extensions.is_empty() {
645                compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
646            }
647
648            // Set source directory for compile-time schema validation in data-source calls
649            if let Ok(cwd) = std::env::current_dir() {
650                compiler.set_source_dir(cwd);
651            }
652
653            // Use compile_with_source if source text is available for better error messages
654            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            // Save the module_binding names for syncing (includes both new and existing)
661            let module_binding_names = bytecode.module_binding_names.clone();
662
663            // Execute Bytecode
664            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            // Drop stale links from previous runs before relinking this program's foreign table.
674            vm.foreign_fn_handles.clear();
675
676            // Link foreign functions: compile foreign function bodies via language runtime extensions
677            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                        // Native ABI path is static by contract.
704                        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                        // Override the compile-time default with the actual
723                        // runtime's error model now that we have the extension.
724                        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            // Load existing variables from context and module_binding registry into VM before execution
757            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        }; // runtime borrow ends here
768
769        // Phase 2: Execute bytecode (re-borrows runtime for ctx)
770        let result = self.run_vm_loop(
771            &mut vm,
772            engine,
773            &module_binding_names,
774            &bytecode_for_snapshot,
775            None,
776        )?;
777
778        // Phase 3: Save VM module_bindings back to context after execution
779        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        // Intentionally use a fresh executor to mimic a new process / session
920        // where stdlib schema IDs may differ.
921        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}