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//!
7//! W12-host-boundary (ADR-006 §2.7.4 / §2.7.5): the program-completion
8//! host boundary now flows the VM's `KindedSlot` completion value
9//! through the kind-threaded `wire_conversion::slot_to_envelope` /
10//! `slot_to_wire` / `slot_extract_content` helpers. The deleted
11//! `nb_to_wire` / `nb_to_envelope` / `nb_extract_content` /
12//! `synthesize_value_word_from_raw` ValueWord-shape host-API surface
13//! does not return; the kinded helpers take `(bits, kind)` directly per
14//! ADR-006 §2.7.5 and the slot's kind is sourced from `KindedSlot::kind`
15//! (compiler-proven via `BytecodeProgram::top_level_frame.return_kind`).
16//!
17//! Snapshot resume / `eval_statements` remain Phase-2c stubs — the
18//! suspend/resume marker rebuild (kinded `Snapshot::Resumed`
19//! constructor + push) and the REPL-binding round-trip
20//! (`save_module_bindings_to_context` / `load_module_bindings_from_context`)
21//! are independent host-boundary workstreams.
22
23use std::sync::Arc;
24
25use crate::bytecode::BytecodeProgram;
26use crate::compiler::BytecodeCompiler;
27use crate::configuration::BytecodeExecutor;
28use crate::executor::{ForeignFunctionHandle, VMConfig, VirtualMachine};
29
30use shape_ast::Program;
31use shape_runtime::context::ExecutionContext;
32use shape_runtime::engine::{ExecutionType, ProgramExecutor, ShapeEngine};
33use shape_runtime::error::Result;
34use shape_runtime::wire_conversion;
35use shape_value::KindedSlot;
36
37impl BytecodeExecutor {
38    /// Compile a program to bytecode without executing it.
39    ///
40    /// This performs the same compilation pipeline as `execute_program`
41    /// (merging core stdlib, extensions, virtual modules) but stops
42    /// before creating a VM or executing. Compilation does not depend on
43    /// the deleted `ValueWord` carrier — it returns `BytecodeProgram`
44    /// directly.
45    pub(crate) fn compile_program_impl(
46        &mut self,
47        engine: &mut ShapeEngine,
48        program: &Program,
49    ) -> Result<BytecodeProgram> {
50        let source_for_compilation = engine.current_source().map(|s| s.to_string());
51
52        // Check bytecode cache before expensive compilation
53        if let (Some(cache), Some(source)) = (&self.bytecode_cache, &source_for_compilation) {
54            if let Some(cached) = cache.get(source) {
55                return Ok(cached);
56            }
57        }
58
59        // Install this engine's runtime-scoped TypeSchemaRegistry as the
60        // ambient handle for the duration of compilation.
61        let _schema_scope = engine.runtime.enter_schema_scope();
62
63        // REPL cross-cell persistence (WS-11): the persisted user type
64        // schemas (struct / enum) carrying their session-stable
65        // `SchemaId`s, and a `schema_id -> name` reverse map for
66        // resolving the type name of a persisted `TypedObject` binding.
67        let persistent_schemas: Vec<shape_runtime::type_schema::TypeSchema> =
68            engine.repl_user_schemas().values().cloned().collect();
69        let schema_id_to_name: std::collections::HashMap<u32, String> = persistent_schemas
70            .iter()
71            .map(|s| (s.id, s.name.clone()))
72            .collect();
73
74        let runtime = engine.get_runtime_mut();
75
76        let known_bindings: Vec<String> = if let Some(ctx) = runtime.persistent_context() {
77            ctx.root_scope_binding_names()
78        } else {
79            Vec::new()
80        };
81
82        // REPL cross-cell persistence (WS-11): derive a compiler-facing
83        // type name for each persisted binding from the value stored in
84        // the context. Without this the next cell's `a + b` (where `a`,
85        // `b` were `let`-bound in earlier cells) falls into the
86        // strict-typing `unknown + unknown` reject path. The kind is read
87        // off the persisted `KindedSlot` — no fabrication, the producer
88        // stamped it (ADR-006 §2.7.5).
89        let known_binding_types: Vec<(String, String)> =
90            if let Some(ctx) = runtime.persistent_context() {
91                known_bindings
92                    .iter()
93                    .filter_map(|name| {
94                        let value = ctx.get_variable(name).ok().flatten()?;
95                        let type_name = binding_type_name_for_kind(
96                            value.kind(),
97                            value.raw(),
98                            &schema_id_to_name,
99                        )?;
100                        Some((name.clone(), type_name))
101                    })
102                    .collect()
103            } else {
104                Vec::new()
105            };
106
107        let mut root_program = program.clone();
108        crate::module_resolution::annotate_program_native_abi_package_key(
109            &mut root_program,
110            self.root_package_key.as_deref(),
111        );
112
113        let mut loader = self.module_loader.take().unwrap_or_else(
114            shape_runtime::module_loader::ModuleLoader::new,
115        );
116        let (graph, stdlib_names, prelude_imports) =
117            crate::module_resolution::build_graph_and_stdlib_names(
118                &root_program,
119                &mut loader,
120                &self.extensions,
121            )?;
122        self.module_loader = Some(loader);
123
124        let mut compiler = BytecodeCompiler::new();
125        compiler.stdlib_function_names = stdlib_names;
126
127        // REPL cross-cell persistence (WS-11): seed the compiler's
128        // schema registry with user `type` / `enum` schemas from prior
129        // cells so each keeps a stable `SchemaId` for the whole session
130        // — a `TypedObject` persisted across cells carries the id
131        // stamped at construction, and `GetFieldTyped` resolves it
132        // against this cell's program registry. Seeding before
133        // `register_known_bindings` (which can itself touch schemas via
134        // `register_extension_module_schema`) keeps the user ids stable.
135        if !persistent_schemas.is_empty() {
136            compiler.seed_persistent_schemas(&persistent_schemas);
137        }
138
139        compiler.register_known_bindings(&known_bindings);
140        for (name, type_name) in &known_binding_types {
141            compiler.register_known_binding_type(name, type_name);
142        }
143
144        if !self.extensions.is_empty() {
145            compiler.extension_registry = Some(Arc::new(self.extensions.clone()));
146        }
147
148        if let Ok(cwd) = std::env::current_dir() {
149            compiler.set_source_dir(cwd);
150        }
151
152        compiler.native_resolution_context = self.native_resolution_context.clone();
153
154        if let Some(source) = &source_for_compilation {
155            compiler.set_source(source);
156        }
157
158        let bytecode =
159            compiler.compile_with_graph_and_prelude(&root_program, graph, &prelude_imports)?;
160
161        // Store in bytecode cache (best-effort, ignore errors)
162        if let (Some(cache), Some(source)) = (&self.bytecode_cache, &source_for_compilation) {
163            let _ = cache.put(source, &bytecode);
164        }
165
166        Ok(bytecode)
167    }
168
169    /// Compile a program with the same pipeline as execution, but do not run it.
170    pub fn compile_program_for_inspection(
171        &mut self,
172        engine: &mut ShapeEngine,
173        program: &Program,
174    ) -> Result<BytecodeProgram> {
175        self.compile_program_impl(engine, program)
176    }
177
178    /// Resume execution from a snapshot — Phase-2c stub.
179    ///
180    /// The legacy body built a `Snapshot::Resumed` marker via the deleted
181    /// `create_typed_enum_nb` returning a `ValueWord`, pushed it via the
182    /// deleted raw-bits stack push, then ran the suspend/resume loop —
183    /// every step of which depended on `ValueWord` / `EnumValue` /
184    /// `nb_to_wire`. Phase-2c (ADR-006 §2.7.4) rebuilds the marker as a
185    /// kinded `Arc<TypedObjectStorage>` payload + parallel-kind track,
186    /// pushed via `push_kinded(bits, NativeKind::Ptr(HeapKind::TypedObject))`.
187    pub fn resume_snapshot(
188        &self,
189        _engine: &mut ShapeEngine,
190        _vm_snapshot: shape_runtime::snapshot::VmSnapshot,
191        _bytecode: BytecodeProgram,
192    ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
193        Err(shape_runtime::error::ShapeError::RuntimeError {
194            message: "resume_snapshot: snapshot rebuild depends on the deleted \
195                      ValueWord carrier and the deleted `create_typed_enum_nb` / \
196                      `nb_to_wire` host-API surface — Phase-2c, see ADR-006 §2.7.4."
197                .to_string(),
198            location: None,
199        })
200    }
201
202    /// Recompile source and resume from a snapshot — Phase-2c stub.
203    ///
204    /// Same surface as `resume_snapshot`: the snapshot-to-host marker
205    /// hop depends on the deleted `ValueWord` carrier (ADR-006 §2.7.4).
206    pub fn recompile_and_resume(
207        &mut self,
208        _engine: &mut ShapeEngine,
209        _vm_snapshot: shape_runtime::snapshot::VmSnapshot,
210        _old_bytecode: BytecodeProgram,
211        _program: &Program,
212    ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
213        Err(shape_runtime::error::ShapeError::RuntimeError {
214            message: "recompile_and_resume: snapshot resume depends on the \
215                      deleted ValueWord carrier and the kinded suspend/resume \
216                      marker rebuild is Phase-2c (ADR-006 §2.7.4)."
217                .to_string(),
218            location: None,
219        })
220    }
221}
222
223impl shape_runtime::engine::ExpressionEvaluator for BytecodeExecutor {
224    fn eval_statements(
225        &self,
226        _stmts: &[shape_ast::Statement],
227        _ctx: &mut ExecutionContext,
228    ) -> Result<KindedSlot> {
229        // Phase-2c surface (ADR-006 §2.7.4): the legacy implementation
230        // round-tripped the result through `vm.execute()` (which returned
231        // `ValueWord`) and persisted module bindings via
232        // `save_module_bindings_to_context` (which called the deleted
233        // `synthesize_value_word_from_raw`). The kinded rebuild returns
234        // `KindedSlot` directly from a `vm.execute_kinded()` shape and
235        // persists bindings via per-slot `(bits, NativeKind)` writes —
236        // both Phase-2c.
237        Err(shape_runtime::error::ShapeError::RuntimeError {
238            message: "eval_statements: depends on `vm.execute() -> ValueWord` \
239                      and the deleted `synthesize_value_word_from_raw` \
240                      host-boundary path; the kinded `vm.execute_kinded() \
241                      -> KindedSlot` rebuild is Phase-2c (ADR-006 §2.7.4)."
242                .to_string(),
243            location: None,
244        })
245    }
246
247    fn eval_expr(&self, expr: &shape_ast::Expr, ctx: &mut ExecutionContext) -> Result<KindedSlot> {
248        let stmt = shape_ast::Statement::Expression(expr.clone(), shape_ast::Span::DUMMY);
249        self.eval_statements(&[stmt], ctx)
250    }
251}
252
253/// Derive the compiler-facing type-name string for a persisted REPL
254/// binding from its runtime `NativeKind` (WS-11).
255///
256/// The bytecode compiler's type tracker recognises canonical type names
257/// (`"int"`, `"number"`, `"bool"`, `"string"`, and registered struct /
258/// enum names) via `register_known_binding_type`. Mapping the persisted
259/// `KindedSlot`'s kind to one of those names lets the next cell's
260/// expressions referencing the binding (`a + b`, `p.x`) compile through
261/// the strict-typing path instead of the `unknown` reject.
262///
263/// The kind was stamped by the original producer at compile time
264/// (ADR-006 §2.7.5) — this reads it, it does not fabricate it. Kinds
265/// without a stable surface type name (the no-op `Bool`/`Null` sentinel
266/// for never-written slots, container kinds the compiler resolves
267/// structurally) return `None`; the binding then carries no type info
268/// and falls back to the polymorphic path, which is correct — it is not
269/// a wrong-result, just a missed specialization.
270fn binding_type_name_for_kind(
271    kind: shape_value::NativeKind,
272    bits: u64,
273    schema_id_to_name: &std::collections::HashMap<u32, String>,
274) -> Option<String> {
275    use shape_value::{HeapKind, NativeKind};
276    match kind {
277        NativeKind::Int64 | NativeKind::UInt64 | NativeKind::IntSize | NativeKind::UIntSize => {
278            Some("int".to_string())
279        }
280        NativeKind::Int8
281        | NativeKind::UInt8
282        | NativeKind::Int16
283        | NativeKind::UInt16
284        | NativeKind::Int32
285        | NativeKind::UInt32 => Some("int".to_string()),
286        NativeKind::Float64 | NativeKind::Float32 => Some("number".to_string()),
287        NativeKind::Bool => Some("bool".to_string()),
288        NativeKind::String | NativeKind::StringV2 => Some("string".to_string()),
289        NativeKind::Ptr(HeapKind::TypedObject) => {
290            // The slot bits are a live `*const TypedObjectStorage`
291            // (ADR-006 §2.4 v2-raw carrier). Read its `schema_id` and
292            // resolve the registered struct name via the persisted
293            // user-schema id map so the next cell's `binding.field`
294            // access compiles through the typed-field path.
295            if bits == 0 {
296                return None;
297            }
298            let ptr = bits as *const shape_value::heap_value::TypedObjectStorage;
299            // SAFETY: a `Ptr(HeapKind::TypedObject)`-kinded slot whose
300            // bits are non-zero points at a live `TypedObjectStorage`
301            // (the context's `KindedSlot` holds an owning share for the
302            // duration of this read). `schema_id` is a POD `u64` field.
303            let schema_id = unsafe { (*ptr).schema_id };
304            schema_id_to_name.get(&(schema_id as u32)).cloned()
305        }
306        // Other heap kinds (arrays, maps, options, results, …) are
307        // resolved structurally by the compiler; no flat type name to
308        // register. Bool/Null sentinel and remaining scalar kinds carry
309        // no useful binding-type info.
310        _ => None,
311    }
312}
313
314/// Collect the names introduced by top-level `let` / `var` / `const`
315/// declarations in a program (REPL cross-cell persistence, WS-11).
316///
317/// A top-level binding can appear either as `Item::VariableDecl` or as
318/// `Item::Statement(Statement::VariableDecl(..))` depending on how the
319/// parser bucketed the line; both shapes are walked. Destructuring
320/// patterns contribute every bound identifier.
321fn collect_top_level_binding_names(program: &Program) -> std::collections::HashSet<String> {
322    use shape_ast::ast::{Item, Statement};
323    let mut names = std::collections::HashSet::new();
324    let mut absorb = |decl: &shape_ast::ast::VariableDecl| {
325        for ident in decl.pattern.get_identifiers() {
326            names.insert(ident);
327        }
328    };
329    for item in &program.items {
330        match item {
331            Item::VariableDecl(decl, _) => absorb(decl),
332            Item::Statement(Statement::VariableDecl(decl, _), _) => absorb(decl),
333            _ => {}
334        }
335    }
336    names
337}
338
339impl BytecodeExecutor {
340    /// REPL load-side binding round-trip (WS-11).
341    ///
342    /// For every VM module-binding slot whose name matches a value
343    /// binding live in the persistent `ExecutionContext`'s root scope,
344    /// copy the context's `KindedSlot` into the slot. This is what makes
345    /// a variable defined in a prior cell resolvable from a later one:
346    /// the compiler reserved the slot via `register_known_bindings`, and
347    /// this fills it with the persisted value before execution.
348    ///
349    /// The copy retains an independent strong-count share — the
350    /// context's variable keeps its own ownership; the VM slot owns the
351    /// clone, and `module_binding_write_kinded` releases whatever
352    /// occupied the slot before. No tag synthesis: `KindedSlot` already
353    /// carries the `NativeKind`, so the slot's bits and kind transfer
354    /// directly per ADR-006 §2.7.8 / Q10.
355    fn load_module_bindings_from_context(
356        vm: &mut VirtualMachine,
357        ctx: &ExecutionContext,
358    ) {
359        let names = vm.program.module_binding_names.clone();
360        for (idx, name) in names.iter().enumerate() {
361            if name.is_empty() {
362                continue;
363            }
364            // Only root-scope context variables are user value bindings;
365            // `get_variable` searches inner-to-outer but the REPL context
366            // has a single root scope between cells.
367            let Ok(Some(value)) = ctx.get_variable(name) else {
368                continue;
369            };
370            // `value` is a fresh clone with its own share (KindedSlot's
371            // Clone retains). Hand that share to the binding slot.
372            let bits = value.raw();
373            let kind = value.kind();
374            std::mem::forget(value);
375            vm.module_binding_write_kinded(idx, bits, kind);
376        }
377    }
378
379    /// REPL save-side binding round-trip (WS-11).
380    ///
381    /// After a cell executes, copy every VM module-binding slot whose
382    /// name is a user value binding (`user_binding_names`) back into the
383    /// persistent `ExecutionContext` so the next cell sees it. The name
384    /// filter excludes module-namespace objects and stdlib/prelude
385    /// function bindings — those are not user values and must not leak
386    /// into the context's variable scope.
387    ///
388    /// `module_binding_read_owned_kinded` bumps the strong-count so the
389    /// VM's own slot (dropped with the VM at end of cell) and the
390    /// context's stored copy each hold an independent share.
391    fn save_module_bindings_to_context(
392        vm: &VirtualMachine,
393        ctx: &mut ExecutionContext,
394        user_binding_names: &std::collections::HashSet<String>,
395    ) {
396        let names = vm.program.module_binding_names.clone();
397        for (idx, name) in names.iter().enumerate() {
398            if name.is_empty() || !user_binding_names.contains(name) {
399                continue;
400            }
401            if idx >= vm.module_bindings_len() {
402                continue;
403            }
404            let value = vm.module_binding_read_owned_kinded(idx);
405            // `set_variable` updates an existing variable or creates a
406            // fresh `var`; either way the persisted slot now holds this
407            // cell's final value for `name`.
408            let _ = ctx.set_variable(name, value);
409        }
410    }
411}
412
413impl ProgramExecutor for BytecodeExecutor {
414    fn execute_program(
415        &mut self,
416        engine: &mut ShapeEngine,
417        program: &Program,
418    ) -> Result<shape_runtime::engine::ProgramExecutorResult> {
419        // Phase 1 — compile (does not depend on the deleted ValueWord).
420        let _schema_scope = engine.runtime.enter_schema_scope();
421
422        // REPL cross-cell persistence (WS-11): re-prepend `fn` / `type` /
423        // `enum` / `trait` / `impl` / type-alias / annotation definitions
424        // from prior cells so the bytecode compiler resolves names
425        // declared in earlier lines. `execute_program` owns the only
426        // cross-cell-stable handle (`ShapeEngine`) and is the single
427        // path every executor caller routes through (real `execute_repl`,
428        // notebook, test helpers), so the injection lives here rather
429        // than in any one entry point.
430        let augmented_program: Program;
431        let compile_target: &Program = if engine.has_repl_definitions() {
432            let priors = engine.repl_definitions();
433            let mut items = Vec::with_capacity(priors.len() + program.items.len());
434            items.extend(priors.iter().cloned());
435            items.extend(program.items.iter().cloned());
436            augmented_program = Program {
437                items,
438                docs: program.docs.clone(),
439            };
440            &augmented_program
441        } else {
442            program
443        };
444
445        let bytecode = self.compile_program_impl(engine, compile_target)?;
446
447        // Build a VM and prime extensions / foreign-function links.
448        // These steps don't reach into the deleted ValueWord carrier
449        // themselves; the host-boundary persistence + completion-value
450        // synthesis is what's deferred to Phase-2c.
451        let mut vm = VirtualMachine::new(VMConfig::default());
452        vm.set_interrupt(self.interrupt.clone());
453        vm.load_program(bytecode);
454        for ext in &self.extensions {
455            vm.register_extension(ext.clone());
456        }
457        // populate_module_objects is itself a Phase-2c stub (see
458        // vm_impl/modules.rs) — calling it is a no-op until the kinded
459        // module-binding cell-storage rebuild lands per ADR-006 §2.7.8 / Q10.
460        vm.populate_module_objects();
461        vm.foreign_fn_handles.clear();
462        if !vm.program.foreign_functions.is_empty() {
463            let entries = vm.program.foreign_functions.clone();
464            let mut handles: Vec<Option<ForeignFunctionHandle>> = Vec::with_capacity(entries.len());
465            let mut native_library_cache: std::collections::HashMap<
466                String,
467                std::sync::Arc<libloading::Library>,
468            > = std::collections::HashMap::new();
469            for (idx, entry) in entries.iter().enumerate() {
470                if let Some(native_spec) = &entry.native_abi {
471                    let linked = crate::executor::native_abi::link_native_function(
472                        native_spec,
473                        &vm.program.native_struct_layouts,
474                        &mut native_library_cache,
475                    )
476                    .map_err(|e| {
477                        shape_runtime::error::ShapeError::RuntimeError {
478                            message: format!(
479                                "Failed to link native function '{}': {}",
480                                entry.name, e
481                            ),
482                            location: None,
483                        }
484                    })?;
485                    vm.program.foreign_functions[idx].dynamic_errors = false;
486                    handles.push(Some(ForeignFunctionHandle::Native(std::sync::Arc::new(
487                        linked,
488                    ))));
489                    continue;
490                }
491                handles.push(None);
492            }
493            vm.foreign_fn_handles = handles;
494        }
495
496        // Phase 2 — execute. `vm.execute(ctx)` returns
497        // `Result<KindedSlot, VMError>` (dispatch.rs:25). The slot's
498        // kind is sourced from `BytecodeProgram::top_level_frame.
499        // return_kind` for typed-producer programs and from the
500        // §2.7.7 stack parallel-kind track when the producer pushed a
501        // post-resolution kind directly. No tag-bit decode, no
502        // ValueWord round-trip.
503        // REPL cross-cell persistence (WS-11): the set of names the user
504        // can reference as value bindings across cells — every name
505        // already live in the persistent context's root scope plus every
506        // top-level `let`/`var` declared in this cell. Module-namespace
507        // objects, stdlib functions, and prelude builtins are never in
508        // this set (they are not `VariableDecl`s and the prior cell never
509        // `set_variable`d them), so the round-trip touches user variables
510        // only.
511        let repl_persistence = engine.repl_persistence();
512        let user_binding_names: std::collections::HashSet<String> = if repl_persistence {
513            let mut names = collect_top_level_binding_names(program);
514            if let Some(ctx) = engine.runtime.persistent_context() {
515                names.extend(ctx.root_scope_binding_names());
516            }
517            names
518        } else {
519            std::collections::HashSet::new()
520        };
521
522        let runtime = engine.get_runtime_mut();
523        let mut owned_ctx_fallback;
524        let ctx_borrow: &mut ExecutionContext = match runtime.persistent_context_mut() {
525            Some(ctx) => ctx,
526            None => {
527                // Programs without a persistent ExecutionContext (the
528                // non-REPL `shape run` path) still need a live context
529                // for stdlib I/O dispatch + wire-conversion lookups.
530                // An empty context exposes no host data but satisfies
531                // the borrow.
532                owned_ctx_fallback = ExecutionContext::new_empty();
533                &mut owned_ctx_fallback
534            }
535        };
536
537        // REPL load-side: copy every persisted value binding from the
538        // context into its VM module-binding slot before execution, so a
539        // reference to a variable defined in a prior cell resolves.
540        if repl_persistence {
541            Self::load_module_bindings_from_context(&mut vm, ctx_borrow);
542        }
543
544        let completion: KindedSlot = vm.execute(Some(ctx_borrow)).map_err(|e| {
545            shape_runtime::error::ShapeError::RuntimeError {
546                message: e.to_string(),
547                location: None,
548            }
549        })?;
550
551        // REPL save-side: copy this cell's value bindings back into the
552        // context so the next cell can reference them.
553        if repl_persistence {
554            Self::save_module_bindings_to_context(&vm, ctx_borrow, &user_binding_names);
555        }
556
557        // Phase 3 — host-boundary projection. Pull `(bits, kind)` off
558        // the `KindedSlot` once and feed the kinded
559        // `wire_conversion::slot_*` helpers (ADR-006 §2.7.5). The
560        // KindedSlot owns the strong-count share for the duration of
561        // this scope; the helpers read by-pointer and do not consume
562        // the share.
563        let bits = completion.raw();
564        let kind = completion.kind();
565
566        let envelope = wire_conversion::slot_to_envelope(bits, kind, "", ctx_borrow);
567        let (content_json, content_html, content_terminal) =
568            wire_conversion::slot_extract_content(bits, kind);
569
570        // The `ctx_borrow` reborrow of `engine.runtime` ends at its last
571        // use above (NLL), freeing `engine` for the persistence
572        // bookkeeping below.
573        //
574        // REPL cross-cell persistence (WS-11): the cell executed cleanly,
575        // so fold its definition items into the accumulator for the next
576        // cell. Harvest from the ORIGINAL `program` (not the augmented
577        // one) — prior definitions are already in the accumulator and
578        // re-absorbing them would be redundant (the identity-dedup makes
579        // it harmless either way, but harvesting the cell's own items is
580        // the precise intent).
581        if repl_persistence {
582            // Record each user `type` / `enum` schema under its
583            // first-assigned id. `remember_repl_user_schema` is
584            // first-write-wins, so a type compiled in an earlier cell
585            // keeps that cell's id — exactly the id every already
586            // persisted instance of the type carries. Harvest from the
587            // cell's own definition items; types declared in this cell
588            // for the first time are captured here, and re-injected
589            // prior types resolve to their seeded (already-recorded)
590            // schema so the `or_insert` is a no-op.
591            for type_name in ShapeEngine::repl_user_type_names(program) {
592                if let Some(schema) = vm.program.type_schema_registry.get(&type_name) {
593                    engine.remember_repl_user_schema(schema.clone());
594                }
595            }
596            engine.absorb_repl_cell_definitions(program);
597        }
598
599        Ok(shape_runtime::engine::ProgramExecutorResult {
600            wire_value: envelope.value,
601            type_info: Some(envelope.type_info),
602            execution_type: ExecutionType::Script,
603            content_json,
604            content_html,
605            content_terminal,
606        })
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    // The snapshot-resume integration tests (snapshot_resume_keeps_…,
613    // snapshot_resumed_variant_matches_without_resume_flow,
614    // stdlib_json_value_methods_can_use_internal_json_builtins,
615    // snapshot_resume_direct_vm_from_snapshot_with_marker) all asserted
616    // on `WireValue::as_number()` / `as_str()` / `as_bool()` round-trips
617    // through the deleted ValueWord host boundary, plus called the
618    // deleted `vm.create_typed_enum_nb` / `synthesize_value_word_from_raw`
619    // helpers directly. They land in the Phase-2c snapshot rebuild
620    // session along with their host-API counterparts (ADR-006 §2.7.4).
621    //
622    // No tests are kept in this module for the duration of the surface;
623    // the integration coverage lives in
624    // `crates/shape-vm/src/lib_tests_parts/` once the kinded host-API
625    // returns.
626}