Skip to main content

shape_vm/executor/vm_impl/
program.rs

1use super::super::*;
2
3impl VirtualMachine {
4    /// Load a program into the VM
5    pub fn load_program(&mut self, program: BytecodeProgram) {
6        // Content-addressed bytecode is the canonical runtime format.
7        // Do not silently fall back to the flat instruction stream if linking fails.
8        if let Some(ref ca_program) = program.content_addressed {
9            let linked = crate::linker::link(ca_program).unwrap_or_else(|e| {
10                panic!(
11                    "content-addressed linker failed ({} function blobs): {}",
12                    ca_program.function_store.len(),
13                    e
14                )
15            });
16            self.load_linked_program(linked);
17            return;
18        }
19
20        self.program = program;
21        if shape_runtime::type_schema::builtin_schemas::resolve_builtin_schema_ids(
22            &self.program.type_schema_registry,
23        )
24        .is_none()
25        {
26            // Programs built manually in tests may omit builtin schemas.
27            // Merge the static stdlib registry (includes builtin fixed schemas)
28            // without synthesizing any dynamic runtime schemas.
29            let (stdlib_registry, _) =
30                shape_runtime::type_schema::TypeSchemaRegistry::with_stdlib_types_and_builtin_ids();
31            self.program.type_schema_registry.merge(stdlib_registry);
32        }
33        self.builtin_schemas =
34            shape_runtime::type_schema::builtin_schemas::resolve_builtin_schema_ids(
35                &self.program.type_schema_registry,
36            )
37            .expect(
38                "compiled program is missing builtin schemas (__AnyError, __TraceFrame, ...); \
39             schema registry must include static builtin schemas",
40            );
41        // Reserve schema IDs above the compiled program registry on the
42        // ambient per-Runtime registry. Since B1.7 the ambient registry
43        // is always available — scopeless callers share a process-wide
44        // default — so no legacy-counter fallback is needed.
45        let max_program_id = self
46            .program
47            .type_schema_registry
48            .max_schema_id()
49            .unwrap_or(0);
50        shape_runtime::type_schema::current_registry().ensure_next_id_above(max_program_id);
51        self.rebuild_function_name_index();
52        self.populate_content_addressed_metadata();
53        self.program_entry_ip = 0;
54        self.module_init_done = false;
55        self.feedback_vectors
56            .resize_with(self.program.functions.len(), || None);
57        self.reset();
58
59        // Bytecode verification: ensure trusted opcodes have valid FrameDescriptors.
60        //
61        // R8 W7 G.5 (v0.3 divergence-elimination, ADR-006 §2.7.14 SURFACE):
62        // V2 typed-opcode verifier failures here are warning-only — the
63        // bytecode interpreter handles missing FrameDescriptor opcodes fine
64        // (smoke s2's `Vec.map::i64_*` closure runs cleanly through the VM
65        // even with verifier warnings). The JIT path is the actual
66        // divergence surface: native code emitted for unverified opcodes
67        // skipped the runtime string-key check in `as_string_key` and
68        // returned garbage where the VM cleanly errored (audit
69        // `docs/cluster-audits/v0.3-r8w6-hashmap-key-kind-audit.md` §4 —
70        // `set::from_array([1,2,3])` ec=0 with `{"Integer": -1407...}`
71        // garbage vs VM ec=1 "HashMap key must be a string"). The fatal-
72        // surface lives in `JITExecutor::execute_with_jit` (Option B per
73        // audit §5): the JIT compile step refuses unverified bytecode and
74        // the existing `[jit-fallback]` path routes the program through
75        // the interpreter, which agrees with the VM error surface. Full V2
76        // type soundness for every JIT-emitted opcode is v0.4 follow-up.
77        #[cfg(debug_assertions)]
78        {
79            if let Err(errors) = crate::bytecode::verifier::verify_trusted_opcodes(&self.program) {
80                eprintln!(
81                    "Bytecode verification warning: {} violation(s) found",
82                    errors.len()
83                );
84                for e in &errors {
85                    eprintln!("  - {}", e);
86                }
87            }
88            if let Err(errors) =
89                crate::bytecode::verifier::verify_v2_typed_opcodes(&self.program)
90            {
91                eprintln!(
92                    "V2 bytecode verification warning: {} violation(s) found",
93                    errors.len()
94                );
95                for e in &errors {
96                    eprintln!("  - {}", e);
97                }
98            }
99        }
100
101        #[cfg(not(debug_assertions))]
102        {
103            if let Err(errors) = crate::bytecode::verifier::verify_trusted_opcodes(&self.program) {
104                eprintln!(
105                    "Bytecode verification failed: {} violation(s)",
106                    errors.len()
107                );
108                for e in &errors {
109                    eprintln!("  - {}", e);
110                }
111            }
112            if let Err(errors) =
113                crate::bytecode::verifier::verify_v2_typed_opcodes(&self.program)
114            {
115                eprintln!(
116                    "V2 bytecode verification failed: {} violation(s)",
117                    errors.len()
118                );
119                for e in &errors {
120                    eprintln!("  - {}", e);
121                }
122            }
123        }
124    }
125
126    /// Load a `LinkedProgram` into the VM, extracting content-addressed metadata
127    /// directly from the linked function table.
128    ///
129    /// This converts the `LinkedProgram` into the flat `BytecodeProgram` layout that
130    /// the executor expects, then populates `function_hashes` and `function_entry_points`
131    /// from the linked function metadata.
132    pub fn load_linked_program(&mut self, linked: crate::bytecode::LinkedProgram) {
133        let entry_function_id = linked
134            .hash_to_id
135            .get(&linked.entry)
136            .copied()
137            .or_else(|| linked.functions.iter().position(|f| f.name == "__main__"))
138            .unwrap_or(0);
139        let entry_ip = linked
140            .functions
141            .get(entry_function_id)
142            .map(|f| f.entry_point)
143            .unwrap_or(0);
144
145        // Extract hash metadata before converting
146        let hashes: Vec<Option<FunctionHash>> = linked
147            .functions
148            .iter()
149            .map(|lf| {
150                if lf.blob_hash == FunctionHash::ZERO {
151                    None
152                } else {
153                    Some(lf.blob_hash)
154                }
155            })
156            .collect();
157        let entry_points: Vec<usize> = linked.functions.iter().map(|lf| lf.entry_point).collect();
158
159        // Convert LinkedProgram functions to BytecodeProgram functions
160        let functions: Vec<crate::bytecode::Function> = linked
161            .functions
162            .iter()
163            .map(|lf| crate::bytecode::Function {
164                name: lf.name.clone(),
165                arity: lf.arity,
166                param_names: lf.param_names.clone(),
167                locals_count: lf.locals_count,
168                entry_point: lf.entry_point,
169                body_length: lf.body_length,
170                is_closure: lf.is_closure,
171                captures_count: lf.captures_count,
172                is_async: lf.is_async,
173                ref_params: lf.ref_params.clone(),
174                ref_mutates: lf.ref_mutates.clone(),
175                mutable_captures: lf.mutable_captures.clone(),
176                frame_descriptor: lf.frame_descriptor.clone(),
177                osr_entry_points: Vec::new(),
178                mir_data: None,
179            })
180            .collect();
181
182        let program = BytecodeProgram {
183            instructions: linked.instructions,
184            constants: linked.constants,
185            strings: linked.strings,
186            functions,
187            debug_info: linked.debug_info,
188            data_schema: linked.data_schema,
189            module_binding_names: linked.module_binding_names,
190            top_level_locals_count: linked.top_level_locals_count,
191            top_level_local_storage_hints: linked.top_level_local_storage_hints,
192            type_schema_registry: linked.type_schema_registry,
193            module_binding_storage_hints: linked.module_binding_storage_hints,
194            function_local_storage_hints: linked.function_local_storage_hints,
195            trait_method_symbols: linked.trait_method_symbols,
196            foreign_functions: linked.foreign_functions,
197            native_struct_layouts: linked.native_struct_layouts,
198            function_blob_hashes: entry_points
199                .iter()
200                .enumerate()
201                .map(|(idx, _)| hashes.get(idx).copied().flatten())
202                .collect(),
203            // Closure spec §14.6 (H6.5): propagate the linker's
204            // per-function `ClosureLayout` side-table so the VM
205            // `op_make_closure` producer can emit `HeapValue::ClosureRaw`.
206            closure_function_layouts: linked.closure_function_layouts.clone(),
207            // E+5.5 Unit C step 2: propagate the typed top-level frame so
208            // `vm.execute()` synthesises a tagged ValueWord at the host
209            // boundary per the program's declared return kind.
210            top_level_frame: linked.top_level_frame,
211            // ADR-006 §2.7.24 Q25.C: propagate the linker's trait-object
212            // vtable registry so `op_box_trait_object` can resolve
213            // `(concrete_type, trait)` → `Arc<VTable>` at runtime.
214            trait_vtables: linked.trait_vtables.clone(),
215            ..BytecodeProgram::default()
216        };
217
218        // Load the program normally (handles schema resolution, function name index, etc.)
219        self.load_program(program);
220
221        // Override the content-addressed metadata with the linked data
222        // (load_program calls populate_content_addressed_metadata which won't find
223        // content_addressed since we didn't set it — override here)
224        self.function_hashes = hashes;
225        self.function_hash_raw = self
226            .function_hashes
227            .iter()
228            .map(|opt| opt.map(|fh| fh.0))
229            .collect();
230        self.function_id_by_hash.clear();
231        for (idx, maybe_hash) in self.function_hashes.iter().enumerate() {
232            if let Some(hash) = maybe_hash {
233                self.function_id_by_hash.entry(*hash).or_insert(idx as u16);
234            }
235        }
236        self.function_entry_points = entry_points;
237        self.program_entry_ip = entry_ip;
238        self.reset();
239    }
240
241    /// Hot-patch a single function in the loaded program with a new blob.
242    ///
243    /// The new blob's instructions, constants, and strings replace the existing
244    /// function's bytecode in-place. The function's metadata (arity, param names,
245    /// locals count, etc.) is also updated. The content hash is recorded so that
246    /// in-flight frames referencing the old hash remain valid (they execute from
247    /// their saved IP which is now stale, but callers that resolve by function ID
248    /// will pick up the new code on the next call).
249    ///
250    /// Returns `Ok(old_hash)` on success (the previous content hash, if any),
251    /// or `Err(msg)` if the function ID is out of range.
252    pub fn patch_function(
253        &mut self,
254        fn_id: u16,
255        new_blob: FunctionBlob,
256    ) -> Result<Option<FunctionHash>, String> {
257        let idx = fn_id as usize;
258
259        if idx >= self.program.functions.len() {
260            return Err(format!(
261                "patch_function: fn_id {} out of range (program has {} functions)",
262                fn_id,
263                self.program.functions.len()
264            ));
265        }
266
267        // Capture the old hash before overwriting.
268        let old_hash = self.function_hashes.get(idx).copied().flatten();
269
270        let func = &mut self.program.functions[idx];
271        let old_entry = func.entry_point;
272
273        // Compute instruction splice range: from this function's entry point
274        // to the next function's entry point (or end of instructions).
275        let next_entry = self
276            .program
277            .functions
278            .get(idx + 1)
279            .map(|f| f.entry_point)
280            .unwrap_or(self.program.instructions.len());
281
282        let old_len = next_entry - old_entry;
283        let new_len = new_blob.instructions.len();
284
285        // Splice instructions.
286        self.program.instructions.splice(
287            old_entry..old_entry + old_len,
288            new_blob.instructions.iter().cloned(),
289        );
290
291        // If the new function has a different instruction count, shift all
292        // subsequent function entry points.
293        if new_len != old_len {
294            let delta = new_len as isize - old_len as isize;
295            for subsequent in self.program.functions.iter_mut().skip(idx + 1) {
296                subsequent.entry_point = (subsequent.entry_point as isize + delta) as usize;
297            }
298            // Also update function_entry_points mirror.
299            for ep in self.function_entry_points.iter_mut().skip(idx + 1) {
300                *ep = (*ep as isize + delta) as usize;
301            }
302        }
303
304        // Append new constants and strings to the program pools.
305        // The blob's Operand indices reference its local pools, so we need to
306        // remap them to the global pool offsets.
307        let const_offset = self.program.constants.len();
308        let string_offset = self.program.strings.len();
309        self.program
310            .constants
311            .extend(new_blob.constants.iter().cloned());
312        self.program
313            .strings
314            .extend(new_blob.strings.iter().cloned());
315
316        // Remap operands in the spliced instructions to use global pool offsets.
317        let instr_slice = &mut self.program.instructions[old_entry..old_entry + new_len];
318        for instr in instr_slice.iter_mut() {
319            remap_operand(&mut instr.operand, const_offset, string_offset);
320        }
321
322        // Update function metadata.
323        let func = &mut self.program.functions[idx];
324        func.name = new_blob.name;
325        func.arity = new_blob.arity;
326        func.param_names = new_blob.param_names;
327        func.locals_count = new_blob.locals_count;
328        func.is_closure = new_blob.is_closure;
329        func.captures_count = new_blob.captures_count;
330        func.is_async = new_blob.is_async;
331        func.ref_params = new_blob.ref_params;
332        func.ref_mutates = new_blob.ref_mutates;
333        func.mutable_captures = new_blob.mutable_captures;
334
335        // Update content hash metadata.
336        let new_hash = new_blob.content_hash;
337        if idx < self.function_hashes.len() {
338            self.function_hashes[idx] = Some(new_hash);
339        }
340        if idx < self.function_hash_raw.len() {
341            self.function_hash_raw[idx] = Some(new_hash.0);
342        }
343        self.function_id_by_hash.entry(new_hash).or_insert(fn_id);
344
345        // Update function_entry_points for this function.
346        if idx < self.function_entry_points.len() {
347            self.function_entry_points[idx] = old_entry;
348        }
349
350        // Rebuild function name index so UFCS dispatch picks up renames.
351        self.rebuild_function_name_index();
352
353        Ok(old_hash)
354    }
355
356    /// Load a content-addressed `Program` with permission checking.
357    ///
358    /// Links the program, checks that `total_required_permissions` is a subset of
359    /// `granted`, and loads normally if the check passes. Returns an error listing
360    /// the missing permissions if the check fails.
361    pub fn load_program_with_permissions(
362        &mut self,
363        program: crate::bytecode::Program,
364        granted: &shape_abi_v1::PermissionSet,
365    ) -> Result<(), PermissionError> {
366        let linked =
367            crate::linker::link(&program).map_err(|e| PermissionError::LinkError(e.to_string()))?;
368        if !linked.total_required_permissions.is_subset(granted) {
369            let missing = linked.total_required_permissions.difference(granted);
370            return Err(PermissionError::InsufficientPermissions {
371                required: linked.total_required_permissions.clone(),
372                granted: granted.clone(),
373                missing,
374            });
375        }
376        self.load_linked_program(linked);
377        Ok(())
378    }
379
380    /// Load a `LinkedProgram` with permission checking.
381    ///
382    /// Checks that `total_required_permissions` is a subset of `granted`, then
383    /// loads normally. Returns an error listing the missing permissions if the
384    /// check fails.
385    pub fn load_linked_program_with_permissions(
386        &mut self,
387        linked: crate::bytecode::LinkedProgram,
388        granted: &shape_abi_v1::PermissionSet,
389    ) -> Result<(), PermissionError> {
390        if !linked.total_required_permissions.is_subset(granted) {
391            let missing = linked.total_required_permissions.difference(granted);
392            return Err(PermissionError::InsufficientPermissions {
393                required: linked.total_required_permissions.clone(),
394                granted: granted.clone(),
395                missing,
396            });
397        }
398        self.load_linked_program(linked);
399        Ok(())
400    }
401
402    /// Populate `function_hashes` and `function_entry_points` from the loaded program.
403    ///
404    /// If the program was compiled with content-addressed metadata (`content_addressed`
405    /// is `Some`), we extract blob hashes by matching function names/entry points.
406    /// Otherwise both vectors remain empty and `CallFrame::blob_hash` will be `None`.
407    pub(crate) fn populate_content_addressed_metadata(&mut self) {
408        let func_count = self.program.functions.len();
409        self.function_entry_points = self
410            .program
411            .functions
412            .iter()
413            .map(|f| f.entry_point)
414            .collect();
415
416        if self.program.function_blob_hashes.len() == func_count {
417            self.function_hashes = self.program.function_blob_hashes.clone();
418        } else if let Some(ref ca_program) = self.program.content_addressed {
419            // Build a lookup from function name -> blob hash from the Program's function_store
420            let mut name_to_hash: HashMap<String, FunctionHash> =
421                HashMap::with_capacity(ca_program.function_store.len());
422            for (hash, blob) in &ca_program.function_store {
423                name_to_hash.insert(blob.name.clone(), *hash);
424            }
425
426            self.function_hashes = Vec::with_capacity(func_count);
427            for func in &self.program.functions {
428                self.function_hashes
429                    .push(name_to_hash.get(&func.name).copied());
430            }
431        } else {
432            self.function_hashes = vec![None; func_count];
433        }
434
435        // Build the raw byte mirror for ModuleContext.
436        self.function_hash_raw = self
437            .function_hashes
438            .iter()
439            .map(|opt| opt.map(|fh| fh.0))
440            .collect();
441        self.function_id_by_hash.clear();
442        for (idx, maybe_hash) in self.function_hashes.iter().enumerate() {
443            if let Some(hash) = maybe_hash {
444                self.function_id_by_hash.entry(*hash).or_insert(idx as u16);
445            }
446        }
447    }
448
449    /// Build the function name → index map for runtime UFCS dispatch.
450    /// Called after program load or merge to enable type-scoped method resolution
451    /// (e.g., "DbTable::filter" looked up when calling .filter() on an Object with __type "DbTable").
452    pub(crate) fn rebuild_function_name_index(&mut self) {
453        self.function_name_index.clear();
454        for (i, func) in self.program.functions.iter().enumerate() {
455            self.function_name_index.insert(func.name.clone(), i as u16);
456        }
457    }
458
459    /// Reset VM state
460    /// Get a snapshot of all module binding values.
461    ///
462    /// Phase-1b-vm Wave-ε E-vm-impl-tail: the legacy `Vec<ValueWord>`
463    /// return type referenced the deleted runtime carrier (CLAUDE.md
464    /// "Forbidden Patterns"). The signature is flipped to the kinded
465    /// carrier `Vec<shape_value::KindedSlot>` per ADR-006 §2.7 / Q7.
466    /// The §2.7.8 / Q10 parallel-kind track is now live on
467    /// `module_binding_kinds`, and `module_binding_read_owned_kinded`
468    /// returns `KindedSlot` shares per binding — the kinded read
469    /// backbone is in place. The body remains a `todo!()` until the
470    /// Phase-2c snapshot revival lands the host-API surface (§2.7.4) —
471    /// at that point the body is one map+collect over
472    /// `(0..self.module_bindings_len()).map(|i|
473    /// self.module_binding_read_owned_kinded(i)).collect()`.
474    pub fn module_bindings_snapshot(&self) -> Vec<shape_value::KindedSlot> {
475        todo!(
476            "phase-2c — see ADR-006 §2.7.4: module_bindings_snapshot \
477             body deferred to the Phase-2c snapshot revival. The §2.7.8 \
478             parallel-kind track is live, so the body is one map+collect \
479             over `module_binding_read_owned_kinded` per index."
480        );
481    }
482
483    /// Reset VM execution state for trampoline use.
484    /// Clears stack, call frames, error state, and exception handlers but
485    /// preserves the loaded program, module bindings, module_fn_table,
486    /// and registered extensions.
487    pub fn reset_for_trampoline(&mut self) {
488        // WB2.6 Phase 3: release each live slot's owning share before
489        // clearing to NONE_BITS. The retain-on-read contract (WB2.1–WB2.5)
490        // guarantees no caller aliases these slots without its own retain.
491        // Kind-aware drop walks the parallel `kinds` track in lockstep
492        // with `stack` (ADR-006 §2.7.7).
493        for i in 0..self.sp {
494            super::stack::drop_with_kind(self.stack[i], self.kinds[i]);
495            self.stack[i] = Self::NONE_BITS;
496            self.kinds[i] = shape_value::NativeKind::Bool;
497        }
498        self.sp = 0;
499        self.ip = 0;
500        self.call_stack.clear();
501        self.loop_stack.clear();
502        self.timeframe_stack.clear();
503        self.exception_handlers.clear();
504        self.instruction_count = 0;
505        self.last_error_line = None;
506        self.last_error_file = None;
507        self.last_uncaught_exception = None;
508        self.module_init_done = true; // Skip re-init on next call
509    }
510
511    pub fn reset(&mut self) {
512        self.ip = self.program_entry_ip;
513        // WB2.6 Phase 3: release each live slot's owning share.
514        // Kind-aware drop walks the parallel `kinds` track in lockstep
515        // with `stack` (ADR-006 §2.7.7).
516        for i in 0..self.sp {
517            super::stack::drop_with_kind(self.stack[i], self.kinds[i]);
518            self.stack[i] = Self::NONE_BITS;
519            self.kinds[i] = shape_value::NativeKind::Bool;
520        }
521        // Advance sp past top-level locals so expression evaluation
522        // doesn't overlap with local variable storage in register windows.
523        let tl = self.program.top_level_locals_count as usize;
524        self.sp = tl;
525        self.call_stack.clear();
526        self.loop_stack.clear();
527        self.timeframe_stack.clear();
528        self.exception_handlers.clear();
529        self.instruction_count = 0;
530        self.last_error_line = None;
531        self.last_error_file = None;
532        self.last_uncaught_exception = None;
533    }
534
535    /// Reset stack only (for reusing compiled program across iterations)
536    /// Keeps program, module_bindings, and GC state intact - only clears execution state
537    pub fn reset_stack(&mut self) {
538        self.ip = self.program_entry_ip;
539        // WB2.6 Phase 3: release each live slot's owning share.
540        // Kind-aware drop walks the parallel `kinds` track in lockstep
541        // with `stack` (ADR-006 §2.7.7).
542        for i in 0..self.sp {
543            super::stack::drop_with_kind(self.stack[i], self.kinds[i]);
544            self.stack[i] = Self::NONE_BITS;
545            self.kinds[i] = shape_value::NativeKind::Bool;
546        }
547        let tl = self.program.top_level_locals_count as usize;
548        self.sp = tl;
549        self.call_stack.clear();
550        self.loop_stack.clear();
551        self.timeframe_stack.clear();
552        self.exception_handlers.clear();
553        self.last_error_line = None;
554        self.last_error_file = None;
555        self.last_uncaught_exception = None;
556    }
557
558    /// Minimal reset for hot loops - only clears essential state
559    /// Use this when you know the function doesn't create GC objects or use exceptions
560    #[inline]
561    pub fn reset_minimal(&mut self) {
562        self.ip = self.program_entry_ip;
563        // WB2.6 Phase 3: release each live slot's owning share.
564        // Kind-aware drop walks the parallel `kinds` track in lockstep
565        // with `stack` (ADR-006 §2.7.7).
566        for i in 0..self.sp {
567            super::stack::drop_with_kind(self.stack[i], self.kinds[i]);
568            self.stack[i] = Self::NONE_BITS;
569            self.kinds[i] = shape_value::NativeKind::Bool;
570        }
571        let tl = self.program.top_level_locals_count as usize;
572        self.sp = tl;
573        self.call_stack.clear();
574        self.last_error_line = None;
575        self.last_error_file = None;
576        self.last_uncaught_exception = None;
577    }
578
579    /// Push a value onto the stack (public, for testing and host integration).
580    ///
581    /// Phase-1b-vm Wave-ε E-vm-impl-tail: the legacy `ValueWord`
582    /// parameter type referenced the deleted runtime carrier (CLAUDE.md
583    /// "Forbidden Patterns"). The signature is flipped to the kinded
584    /// carrier `shape_value::KindedSlot` per ADR-006 §2.7 / Q7. The
585    /// kinded direct-write path on the VM is `push_kinded(bits, kind)`;
586    /// host-integration / test helpers that still build values via the
587    /// legacy carrier need a Phase-2c host-API rebuild (see ADR-006
588    /// §2.7.4 — output-adapter cluster). The body is preserved as a
589    /// `todo!()` so callers fail loudly rather than silently dropping
590    /// the value.
591    pub fn push_value(&mut self, _value: shape_value::KindedSlot) {
592        todo!(
593            "phase-2c — see ADR-006 §2.7.4: push_value(KindedSlot) is a \
594             host-boundary helper; the in-VM surface uses \
595             push_kinded(bits, kind) sourced directly from the producer."
596        );
597    }
598}