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        &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        // 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        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        // Store in bytecode cache (best-effort, ignore errors)
431        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    /// Compile a program with the same pipeline as execution, but do not run it.
439    ///
440    /// The returned bytecode includes tooling artifacts such as
441    /// `expanded_function_defs` for comptime inspection.
442    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    /// Recompile source and resume from a snapshot.
451    ///
452    /// Compiles the new program to bytecode, finds the snapshot() call
453    /// position in both old and new bytecodes, adjusts the VM snapshot's
454    /// instruction pointer, then resumes execution from the snapshot point
455    /// using the new bytecode.
456    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        // Find snapshot() call positions (BuiltinCall with Snapshot operand) in old bytecode
468        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        // Same for new bytecode
483        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        // VmSnapshot.ip points to the instruction AFTER the BuiltinCall(Snapshot).
498        // Find which snapshot() in the old bytecode corresponds to the saved IP.
499        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        // Map to the corresponding snapshot() in the new bytecode (by ordinal)
512        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        // Check for non-empty call stack — recompile mode can only adjust the
525        // top-level IP; return addresses inside function frames would be stale.
526        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        // Adjust the snapshot's IP to point after the snapshot() call in new bytecode
536        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        // Wrap statements as a program
556        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        // Inject prelude and resolve imports
570        let stdlib_names = crate::module_resolution::prepend_prelude_items(&mut program);
571
572        // Compile and execute
573        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        // Register extensions before built-in module_bindings so extensions are also available
582        for ext in &self.extensions {
583            vm.register_extension(ext.clone());
584        }
585        vm.populate_module_objects();
586
587        // Load variables from context
588        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        // Save back modified module_bindings
610        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        // Wrap expression as an expression statement
617        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        // Capture source text before getting runtime reference (for error messages)
629        let source_for_compilation = engine.current_source().map(|s| s.to_string());
630
631        // Phase 1: Compile and prepare bytecode (borrows runtime, then drops it)
632        let (mut vm, module_binding_names, bytecode_for_snapshot) = {
633            let runtime = engine.get_runtime_mut();
634
635            // Get known module_binding variables from previous REPL sessions
636            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            // Extract format hints from variable declarations BEFORE compilation
643            // This preserves metadata that bytecode doesn't carry
644            Self::extract_and_store_format_hints(program, runtime.persistent_context_mut());
645
646            // Extract imported functions from ModuleBindingRegistry and add them to the program
647            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            // Merge imported functions into the main program
656            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            // Compile AST to Bytecode with knowledge of existing module_bindings
663            let mut compiler = BytecodeCompiler::new();
664            compiler.stdlib_function_names = stdlib_names;
665            compiler.register_known_bindings(&known_bindings);
666
667            // Wire extension registry into compiler for comptime execution
668            if !self.extensions.is_empty() {
669                compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
670            }
671
672            // Set source directory for compile-time schema validation in data-source calls
673            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            // Use compile_with_source if source text is available for better error messages
680            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            // Save the module_binding names for syncing (includes both new and existing)
687            let module_binding_names = bytecode.module_binding_names.clone();
688
689            // Execute Bytecode
690            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            // Drop stale links from previous runs before relinking this program's foreign table.
700            vm.foreign_fn_handles.clear();
701
702            // Link foreign functions: compile foreign function bodies via language runtime extensions
703            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                        // Native ABI path is static by contract.
730                        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                        // Override the compile-time default with the actual
749                        // runtime's error model now that we have the extension.
750                        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            // Load existing variables from context and module_binding registry into VM before execution
783            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        }; // runtime borrow ends here
794
795        // Phase 2: Execute bytecode (re-borrows runtime for ctx)
796        let result = self.run_vm_loop(
797            &mut vm,
798            engine,
799            &module_binding_names,
800            &bytecode_for_snapshot,
801            None,
802        )?;
803
804        // Phase 3: Save VM module_bindings back to context after execution
805        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        // Intentionally use a fresh executor to mimic a new process / session
946        // where stdlib schema IDs may differ.
947        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}