Skip to main content

javm/
ecall.rs

1//! Ecall dispatch.
2//!
3//! The `Vm` impls `javm_exec::EcallHandler`. The interpreter / JIT
4//! invokes `handle` for every PVM `ecall` (opcode 3, no immediate)
5//! and `ecalli imm` (opcode 10, u32 immediate). The handler decodes
6//! the operation from the immediate and routes to the appropriate
7//! sub-dispatcher.
8//!
9//! ## ecalli opcode encoding (Stage 3 baseline)
10//!
11//! The `imm` value of an `ecalli` instruction is partitioned by range:
12//!
13//! ```text
14//!   0          REPLY (kernel-shorthand for "return to caller")
15//!                — driven by run_instance / Interpreter::Halt translation.
16//!   1..=15     MGMT operations (this module).
17//!   16..=63    Kernel-known host calls (this module; sub-stages 3.8 ↑).
18//!   64..       Chain-user host calls (out of scope; reserved).
19//! ```
20//!
21//! MGMT operand encoding (register-based, simple flat addressing —
22//! single-step root-cnode slot indices):
23//!
24//! ```text
25//!   Common: φ[7] = src_slot_idx (u8 in low byte)
26//!           φ[8] = dst_slot_idx (u8 in low byte)
27//!   MGMT_COPY        (op=1)   src→dst
28//!   MGMT_MOVE        (op=2)   src→dst
29//!   MGMT_DROP        (op=3)   src
30//!   MGMT_CNODE_SWAP  (op=4)   a=φ[7], b=φ[8]
31//!   MGMT_CNODE_MINT  (op=5)   dst=φ[7], size_log=φ[8] (u8)
32//! ```
33//!
34//! Host call operand encoding (Stage 3.8+):
35//!
36//! ```text
37//!   HOST_YIELD       (op=16)  φ[7] = marker_slot_idx (u8)
38//! ```
39//!
40//! After the move to the `javm_cap::Cap` cache model, ecalls
41//! operate on `CapHashOrRef` targets in the running root cnode and
42//! cross-reference into the caller-supplied `CacheDirectory` for kind
43//! dispatch. `Vm::drive_and_translate` installs a short-lived
44//! `CachedEcallHandler` for interpreter runs, so cache-touching host
45//! calls can read/write cap content without storing the cache borrow
46//! in the long-lived `Vm`.
47
48use javm_cap::{
49    Blake2b256, CacheDirectory, Cap, CapHashOrRef, DataCap, DataContent, Hash, SlotIdx, TypeCap,
50};
51use javm_exec::{EcallHandler, EcallKind, EcallResult, ExitReason, Memory, Regs};
52
53use crate::callstack::Entry;
54use crate::error::VmError;
55use crate::kernel_assist::KernelAssist;
56use crate::vm::Vm;
57
58/// MGMT opcode space (in the `ecalli` immediate).
59pub mod mgmt_op {
60    pub const COPY: u32 = 1;
61    pub const MOVE: u32 = 2;
62    pub const DROP: u32 = 3;
63    pub const CNODE_SWAP: u32 = 4;
64    pub const CNODE_MINT: u32 = 5;
65    /// Inclusive upper bound of the MGMT range.
66    pub const MAX: u32 = 15;
67}
68
69/// Kernel-known host call opcode space (in the `ecalli` immediate),
70/// 16..=63. Stage 3.8 lands `HOST_YIELD`; subsequent sub-stages fill
71/// the rest.
72pub mod host_op {
73    pub const HOST_YIELD: u32 = 16;
74    /// `set_image(image_slot=φ[7])` — extend the active Instance's
75    /// `image_hash_chain` with the Image at `image_slot`.
76    pub const SET_IMAGE: u32 = 17;
77    /// `host_derive_spawn(image_slot=φ[7], dst_slot=φ[8])`.
78    pub const DERIVE_SPAWN: u32 = 18;
79    /// `host_make_image(...)` — Stage 3.9 stub.
80    pub const MAKE_IMAGE: u32 = 19;
81    /// `host_same_type(slot_a=φ[7], slot_b=φ[8])`.
82    pub const HOST_SAME_TYPE: u32 = 20;
83    /// `host_type_of(src_slot=φ[7], dst_slot=φ[8])` — Stage 4 (needs
84    /// cache write).
85    pub const HOST_TYPE_OF: u32 = 21;
86    /// `host_read_data_cap` — Stage 4 (needs cache read into mem).
87    pub const HOST_READ_DATA_CAP: u32 = 22;
88    /// `host_mint_data_cap` — Stage 4 (needs cache write).
89    pub const HOST_MINT_DATA_CAP: u32 = 23;
90    /// `host_open` — Stage 4 (needs cache read + slot write).
91    pub const HOST_OPEN: u32 = 24;
92    /// `host_save` — Stage 4 (needs cache write + slot write).
93    pub const HOST_SAVE: u32 = 25;
94    /// `host_call(instance_slot=φ[7], endpoint_idx=φ[8])` — push a
95    /// child `InstanceEntry` from the `Cap::Instance` at
96    /// `instance_slot`, move the caller's `slot[0]` into the child's
97    /// `slot[0]`. The interpreter exits with
98    /// `ExitReason::HostCall(HOST_CALL)`; the `drive_and_translate`
99    /// loop re-enters `Interpreter::run` on the new top frame. On
100    /// child HALT the loop pops the child, reflects its `slot[0]`
101    /// back into the caller's `slot[0]`, and resumes the caller.
102    pub const HOST_CALL: u32 = 26;
103    /// Inclusive upper bound of the kernel-known host call range.
104    pub const MAX: u32 = 63;
105}
106
107impl<K: KernelAssist> EcallHandler for Vm<K> {
108    fn handle(&mut self, kind: EcallKind, regs: &mut Regs, mem: &mut dyn Memory) -> EcallResult {
109        match kind {
110            EcallKind::Ecalli(op) => self.dispatch_ecalli(op, regs, mem, None),
111            EcallKind::Ecall => self.dispatch_ecall(regs, mem, None),
112        }
113    }
114}
115
116pub(crate) struct CachedEcallHandler<'a, K: KernelAssist> {
117    pub(crate) vm: &'a mut Vm<K>,
118    pub(crate) cache: &'a mut CacheDirectory,
119}
120
121impl<K: KernelAssist> EcallHandler for CachedEcallHandler<'_, K> {
122    fn handle(&mut self, kind: EcallKind, regs: &mut Regs, mem: &mut dyn Memory) -> EcallResult {
123        match kind {
124            EcallKind::Ecalli(op) => self
125                .vm
126                .dispatch_ecalli(op, regs, mem, Some(&mut *self.cache)),
127            EcallKind::Ecall => self.vm.dispatch_ecall(regs, mem, Some(&mut *self.cache)),
128        }
129    }
130}
131
132impl<K: KernelAssist> Vm<K> {
133    fn dispatch_ecalli(
134        &mut self,
135        op: u32,
136        regs: &mut Regs,
137        mem: &mut dyn Memory,
138        cache: Option<&mut CacheDirectory>,
139    ) -> EcallResult {
140        match op {
141            0 => {
142                // REPLY is handled by the CALL/HALT driver.
143                EcallResult::Exit(ExitReason::Halt)
144            }
145            o if o <= mgmt_op::MAX => match self.dispatch_mgmt(o, regs, cache) {
146                Ok(()) => EcallResult::Continue,
147                Err(_) => EcallResult::Exit(ExitReason::Trap),
148            },
149            o if o <= host_op::MAX => self.dispatch_host_call(o, regs, mem, cache),
150            _ => {
151                // Chain-user host calls (64+) land later. Continue
152                // silently for now so prologue-like ecalls (used by
153                // javm-transpiler's blob format) don't fault.
154                EcallResult::Continue
155            }
156        }
157    }
158
159    /// Dispatch a kernel-known host call (op-codes 16..=63).
160    fn dispatch_host_call(
161        &mut self,
162        op: u32,
163        regs: &mut Regs,
164        mem: &mut dyn Memory,
165        cache: Option<&mut CacheDirectory>,
166    ) -> EcallResult {
167        fn trap_on_err<T>(r: Result<T, VmError>, ok: impl FnOnce(T) -> EcallResult) -> EcallResult {
168            match r {
169                Ok(v) => ok(v),
170                Err(_) => EcallResult::Exit(ExitReason::Trap),
171            }
172        }
173        match op {
174            host_op::HOST_YIELD => trap_on_err(self.dispatch_host_yield(regs), |r| r),
175            host_op::SET_IMAGE => match cache {
176                Some(cache) => trap_on_err(self.dispatch_set_image_cached(regs, cache), |()| {
177                    EcallResult::Exit(ExitReason::HostCall(host_op::SET_IMAGE))
178                }),
179                None => trap_on_err(self.dispatch_set_image(regs), |()| EcallResult::Continue),
180            },
181            host_op::DERIVE_SPAWN => match cache {
182                Some(cache) => trap_on_err(self.dispatch_derive_spawn_cached(regs, cache), |()| {
183                    EcallResult::Continue
184                }),
185                None => trap_on_err(self.dispatch_derive_spawn(regs), |()| EcallResult::Continue),
186            },
187            host_op::MAKE_IMAGE => {
188                // Stage 3.9 stub.
189                EcallResult::Exit(ExitReason::Trap)
190            }
191            host_op::HOST_SAME_TYPE => trap_on_err(self.dispatch_host_same_type(regs), |()| {
192                EcallResult::Continue
193            }),
194            host_op::HOST_TYPE_OF => match cache {
195                Some(cache) => trap_on_err(self.dispatch_host_type_of(regs, cache), |()| {
196                    EcallResult::Continue
197                }),
198                None => EcallResult::Exit(ExitReason::Trap),
199            },
200            host_op::HOST_READ_DATA_CAP => match cache {
201                Some(cache) => {
202                    trap_on_err(self.dispatch_host_read_data_cap(regs, mem, cache), |()| {
203                        EcallResult::Continue
204                    })
205                }
206                None => EcallResult::Exit(ExitReason::Trap),
207            },
208            host_op::HOST_MINT_DATA_CAP => match cache {
209                Some(cache) => {
210                    trap_on_err(self.dispatch_host_mint_data_cap(regs, mem, cache), |()| {
211                        EcallResult::Continue
212                    })
213                }
214                None => EcallResult::Exit(ExitReason::Trap),
215            },
216            host_op::HOST_OPEN => match cache {
217                Some(cache) => trap_on_err(self.dispatch_host_open(regs, cache), |()| {
218                    EcallResult::Continue
219                }),
220                None => EcallResult::Exit(ExitReason::Trap),
221            },
222            host_op::HOST_SAVE => match cache {
223                Some(cache) => trap_on_err(self.dispatch_host_save(regs, cache), |()| {
224                    EcallResult::Continue
225                }),
226                None => EcallResult::Exit(ExitReason::Trap),
227            },
228            host_op::HOST_CALL => match cache {
229                Some(cache) => trap_on_err(self.dispatch_host_call_cached(regs, cache), |()| {
230                    EcallResult::Exit(ExitReason::HostCall(host_op::HOST_CALL))
231                }),
232                None => EcallResult::Exit(ExitReason::Trap),
233            },
234            _ => {
235                // Chain-user host calls (64+) land later. Continue
236                // silently for now.
237                EcallResult::Continue
238            }
239        }
240    }
241
242    /// `host_yield(marker_slot=φ[7])`.
243    fn dispatch_host_yield(&mut self, regs: &mut Regs) -> Result<EcallResult, VmError> {
244        let marker_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
245
246        // 1. Read marker's hash from the running Instance's cnode.
247        //    In the new model the slot stores a `CapHashOrRef`; we
248        //    key yield routing on the Hash form (the marker template
249        //    image_hash). Ref-form markers are not catchable in V1.
250        let marker_hash = {
251            let running = self
252                .stack
253                .running_instance()
254                .ok_or(VmError::CallStackEmpty)?;
255            match running.root_cnode.get(marker_slot) {
256                Some(CapHashOrRef::Hash(h)) => h,
257                Some(CapHashOrRef::Ref(_)) => {
258                    return Err(VmError::SlotKindMismatch(marker_slot.get()));
259                }
260                None => return Err(VmError::SlotEmpty(marker_slot.get())),
261            }
262        };
263
264        // 2. Walk the stack top→bottom (skip the top — that's the
265        //    yielder). Find first InstanceEntry whose declared
266        //    yield_marker_slot holds a YieldCatcher catching this
267        //    marker.
268        let stack_len = self.stack.entries().len();
269        let mut target_pos: Option<usize> = None;
270        for pos in (0..stack_len.saturating_sub(1)).rev() {
271            let ie = match &self.stack.entries()[pos] {
272                Entry::Instance(ie) => ie.as_ref(),
273                Entry::Reference(_) => continue,
274            };
275            let Some(catcher_slot) = ie.yield_marker_slot else {
276                continue;
277            };
278            let catcher_hash = match ie.root_cnode.get(catcher_slot) {
279                Some(CapHashOrRef::Hash(h)) => h,
280                _ => continue,
281            };
282            let markers = self.kernel_assist.yield_catcher_markers(catcher_hash);
283            if markers.contains(&marker_hash) {
284                target_pos = Some(pos);
285                break;
286            }
287        }
288
289        let pos = target_pos.ok_or(VmError::UnhandledMarker)?;
290
291        // 3. Push the ReferenceEntry. The yielder transitions to
292        //    Waiting; the new reference becomes Running.
293        self.stack.push_reference(pos)?;
294
295        Ok(EcallResult::Exit(ExitReason::HostCall(host_op::HOST_YIELD)))
296    }
297
298    /// `set_image(image_slot=φ[7])`.
299    ///
300    /// Reads the Cap::Image hash at the named slot; chain_extends the
301    /// running instance's `image_hash_chain` with the slot's hash.
302    /// The new model resolves Image bytes from the cache rather than
303    /// the kernel-assist registry — but since the cache isn't
304    /// reachable from inside `Interpreter::run` in V1, set_image
305    /// updates only the chain hash; bytecode swap is deferred.
306    fn dispatch_set_image(&mut self, regs: &mut Regs) -> Result<(), VmError> {
307        let image_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
308
309        // 1. Resolve the Cap::Image hash from the slot.
310        let new_image_hash = {
311            let running = self
312                .stack
313                .running_instance()
314                .ok_or(VmError::CallStackEmpty)?;
315            match running.root_cnode.get(image_slot) {
316                Some(CapHashOrRef::Hash(h)) => h,
317                Some(CapHashOrRef::Ref(_)) => {
318                    return Err(VmError::SlotKindMismatch(image_slot.get()));
319                }
320                None => return Err(VmError::SlotEmpty(image_slot.get())),
321            }
322        };
323
324        // 2. Extend the chain hash.
325        let extended_chain = {
326            let running = self
327                .stack
328                .running_instance()
329                .ok_or(VmError::CallStackEmpty)?;
330            Blake2b256::hash_pair(&running.image_hash_chain, &new_image_hash)
331        };
332
333        // 3. Install the chain extension. Bytecode swap deferred —
334        //    Stage 4 wires the cache borrow.
335        let running = self
336            .stack
337            .running_instance_mut()
338            .ok_or(VmError::CallStackEmpty)?;
339        running.image_hash_chain = extended_chain;
340        running.image_hash = new_image_hash;
341        Ok(())
342    }
343
344    fn dispatch_set_image_cached(
345        &mut self,
346        regs: &mut Regs,
347        cache: &CacheDirectory,
348    ) -> Result<(), VmError> {
349        let image_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
350        let (new_image_hash, extended_chain) = {
351            let running = self
352                .stack
353                .running_instance()
354                .ok_or(VmError::CallStackEmpty)?;
355            let new_image_hash = match running.root_cnode.get(image_slot) {
356                Some(CapHashOrRef::Hash(h)) => h,
357                Some(CapHashOrRef::Ref(_)) => {
358                    return Err(VmError::SlotKindMismatch(image_slot.get()));
359                }
360                None => return Err(VmError::SlotEmpty(image_slot.get())),
361            };
362            (
363                new_image_hash,
364                Blake2b256::hash_pair(&running.image_hash_chain, &new_image_hash),
365            )
366        };
367
368        let img = match &*cache
369            .get(CapHashOrRef::Hash(new_image_hash))
370            .ok_or(VmError::ImageNotFound)?
371        {
372            Cap::Image(i) => i.clone(),
373            _ => return Err(VmError::ImageNotFound),
374        };
375        let unpacked_bitmask = javm_exec::unpack_bitmask(img.bitmask.as_slice(), img.code.len());
376        let program = self.image_cache.get_or_decode(
377            new_image_hash,
378            img.code.as_slice().to_vec(),
379            unpacked_bitmask,
380            img.jump_table.as_slice().to_vec(),
381        )?;
382        let pinned_slots: Vec<SlotIdx> = img.pinned.iter().map(|e| e.slot).collect();
383
384        let running = self
385            .stack
386            .running_instance_mut()
387            .ok_or(VmError::CallStackEmpty)?;
388        running.image_hash_chain = extended_chain;
389        running.image_hash = new_image_hash;
390        running.program = program;
391        running.pinned_slots = pinned_slots;
392        running.yield_marker_slot = img.yield_marker_slot;
393        Ok(())
394    }
395
396    /// `host_derive_spawn(image_slot=φ[7], dst_slot=φ[8])` — uncached
397    /// fallback that only records the extended chain hash. CacheDirectory-less
398    /// callers (no `dispatch_host_call_cached` borrow) can't publish a
399    /// real `Cap::Instance`, so this writes the chain-hash placeholder
400    /// for back-compat with pre-Stage-4 fixtures.
401    fn dispatch_derive_spawn(&mut self, regs: &mut Regs) -> Result<(), VmError> {
402        let image_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
403        let dst_slot = SlotIdx((regs.gpr[8] & 0xFF) as u32);
404
405        // 1. Read Image hash at image_slot.
406        let new_image_hash = {
407            let running = self
408                .stack
409                .running_instance()
410                .ok_or(VmError::CallStackEmpty)?;
411            match running.root_cnode.get(image_slot) {
412                Some(CapHashOrRef::Hash(h)) => h,
413                Some(CapHashOrRef::Ref(_)) => {
414                    return Err(VmError::SlotKindMismatch(image_slot.get()));
415                }
416                None => return Err(VmError::SlotEmpty(image_slot.get())),
417            }
418        };
419
420        // 2. Derive the child chain hash.
421        let extended = {
422            let running = self
423                .stack
424                .running_instance()
425                .ok_or(VmError::CallStackEmpty)?;
426            Blake2b256::hash_pair(&running.image_hash_chain, &new_image_hash)
427        };
428
429        // 3. Place at dst_slot (rejects pinned).
430        let running = self
431            .stack
432            .running_instance_mut()
433            .ok_or(VmError::CallStackEmpty)?;
434        if running.pinned_slots.binary_search(&dst_slot).is_ok() {
435            return Err(javm_cap::OpError::SlotPinned(dst_slot.get()).into());
436        }
437        running
438            .root_cnode
439            .set(dst_slot, Some(CapHashOrRef::Hash(extended)))?;
440        Ok(())
441    }
442
443    /// `host_derive_spawn(image_slot=φ[7], cnode_slot=φ[8],
444    /// dst_slot=φ[9])` — full spec form.
445    ///
446    /// Builds a fresh child `Cap::Instance`:
447    /// 1. Read the `Cap::Image` hash at `image_slot` and the prepared
448    ///    `Cap::CNode` hash at `cnode_slot`.
449    /// 2. `child.image_hash_chain = blake2b(parent.chain ||
450    ///    hash(image))`.
451    /// 3. Build the child's root cnode = prepared cnode + the
452    ///    spawned image's pinned slots overlaid on top. Publish.
453    /// 4. Publish a fresh `Cap::Instance` referencing the image and
454    ///    new root cnode with default initial state (mem_size from
455    ///    image mappings, no overlays, zeroed regs/pc/gas).
456    /// 5. Consume (clear) the prepared cnode slot in the caller's
457    ///    cnode — spec MOVE semantics.
458    /// 6. Write `Hash(new_instance_hash)` to `dst_slot`.
459    fn dispatch_derive_spawn_cached(
460        &mut self,
461        regs: &mut Regs,
462        cache: &mut CacheDirectory,
463    ) -> Result<(), VmError> {
464        let image_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
465        let cnode_slot = SlotIdx((regs.gpr[8] & 0xFF) as u32);
466        let dst_slot = SlotIdx((regs.gpr[9] & 0xFF) as u32);
467
468        // 1. Resolve image_hash, cnode_hash, parent chain.
469        let (image_hash, cnode_hash, parent_chain) = {
470            let running = self
471                .stack
472                .running_instance()
473                .ok_or(VmError::CallStackEmpty)?;
474            let img_h = match running.root_cnode.get(image_slot) {
475                Some(CapHashOrRef::Hash(h)) => h,
476                Some(CapHashOrRef::Ref(_)) => {
477                    return Err(VmError::SlotKindMismatch(image_slot.get()));
478                }
479                None => return Err(VmError::SlotEmpty(image_slot.get())),
480            };
481            let cn_h = match running.root_cnode.get(cnode_slot) {
482                Some(CapHashOrRef::Hash(h)) => h,
483                Some(CapHashOrRef::Ref(_)) => {
484                    return Err(VmError::SlotKindMismatch(cnode_slot.get()));
485                }
486                None => return Err(VmError::SlotEmpty(cnode_slot.get())),
487            };
488            (img_h, cn_h, running.image_hash_chain)
489        };
490
491        // 2. Child chain hash.
492        let child_chain = Blake2b256::hash_pair(&parent_chain, &image_hash);
493
494        // 3. Build child cnode = prepared cnode + image's pinned +
495        //    initial slots overlaid. Spec strictly says initial is
496        //    ignored for parented instances; V1 simplification: also
497        //    apply initial when the prepared slot is empty, so the
498        //    parent doesn't have to mint stack/heap/rw_data caps
499        //    by hand on every spawn. A future spec-strict mode can
500        //    skip the initial overlay.
501        let img_cap = match &*cache
502            .get(CapHashOrRef::Hash(image_hash))
503            .ok_or(VmError::ImageNotFound)?
504        {
505            Cap::Image(i) => i.clone(),
506            _ => return Err(VmError::ImageNotFound),
507        };
508        let mut child_cn = match &*cache
509            .get(CapHashOrRef::Hash(cnode_hash))
510            .ok_or(VmError::Invariant("derive_spawn: prepared cnode missing"))?
511        {
512            Cap::CNode(c) => c.clone(),
513            _ => {
514                return Err(VmError::Invariant(
515                    "derive_spawn: cnode_slot does not hold Cap::CNode",
516                ));
517            }
518        };
519        for e in img_cap.pinned.iter() {
520            child_cn.set(e.slot, Some(CapHashOrRef::Hash(e.cap_hash)))?;
521        }
522        for e in img_cap.initial.iter() {
523            if child_cn.get(e.slot).is_none() {
524                child_cn.set(e.slot, Some(CapHashOrRef::Hash(e.cap_hash)))?;
525            }
526        }
527        let new_cnode_hash = cache.put_cap(&Cap::CNode(child_cn))?;
528
529        // 4. Build the child's `rw_overlays` by walking the image's
530        //    memory mappings and resolving each source slot to a
531        //    `Cap::Data` in the (post-overlay) cnode. Each mapping
532        //    becomes one (start, bytes) overlay; the build_entry side
533        //    lays them into RW memory at CALL time. The base RW
534        //    region is sized to cover the max(start+size) span.
535        let new_cnode_cap = cache
536            .get(CapHashOrRef::Hash(new_cnode_hash))
537            .ok_or(VmError::Invariant("derive_spawn: new cnode missing"))?;
538        let new_cnode = match &*new_cnode_cap {
539            Cap::CNode(c) => c.clone(),
540            _ => return Err(VmError::Invariant("derive_spawn: cnode hash misroutes")),
541        };
542        let mut overlay_bufs: Vec<(u32, Vec<u8>)> = Vec::new();
543        let mut mem_size: u32 = 0;
544        for m in img_cap.mappings.iter() {
545            let end = (m.start + m.size) as u32;
546            if end > mem_size {
547                mem_size = end;
548            }
549            if m.source_path_len == 0 {
550                continue;
551            }
552            // V1: only single-step source paths are exercised.
553            let src_slot = m.source_path[0];
554            let target = match new_cnode.get(src_slot) {
555                Some(t) => t,
556                None => continue,
557            };
558            let data_arc = cache.get(target);
559            let bytes_vec = match data_arc.as_deref() {
560                Some(Cap::Data(d)) => match &d.content {
561                    javm_cap::DataContent::Inline(v) => v.as_slice().to_vec(),
562                    javm_cap::DataContent::Paged { .. } => continue,
563                },
564                _ => continue,
565            };
566            if !bytes_vec.is_empty() {
567                overlay_bufs.push((m.start as u32, bytes_vec));
568            }
569        }
570        let overlay_slices: Vec<(u32, &[u8])> = overlay_bufs
571            .iter()
572            .map(|(s, b)| (*s, b.as_slice()))
573            .collect();
574
575        let inst_cap = Cap::instance_with_overlays(
576            child_chain,
577            image_hash,
578            new_cnode_hash,
579            &overlay_slices,
580            mem_size,
581            [0u64; javm_cap::NUM_REGS],
582            0,
583            0,
584        );
585        let new_instance_hash = cache.put_cap(&inst_cap)?;
586
587        // 5+6. Consume prepared cnode, write child instance hash to
588        //      dst (rejects pinned).
589        let running = self
590            .stack
591            .running_instance_mut()
592            .ok_or(VmError::CallStackEmpty)?;
593        if running.pinned_slots.binary_search(&dst_slot).is_ok() {
594            return Err(javm_cap::OpError::SlotPinned(dst_slot.get()).into());
595        }
596        running.root_cnode.take(cnode_slot)?;
597        running
598            .root_cnode
599            .set(dst_slot, Some(CapHashOrRef::Hash(new_instance_hash)))?;
600        Ok(())
601    }
602
603    /// `host_same_type(slot_a=φ[7], slot_b=φ[8])`.
604    ///
605    /// Compares the slot targets' Hash bytes (which encode
606    /// image_hash_chain identity in V1). Result 1 if same, 0
607    /// otherwise, into φ[7].
608    fn dispatch_host_same_type(&mut self, regs: &mut Regs) -> Result<(), VmError> {
609        let a = SlotIdx((regs.gpr[7] & 0xFF) as u32);
610        let b = SlotIdx((regs.gpr[8] & 0xFF) as u32);
611        let running = self
612            .stack
613            .running_instance()
614            .ok_or(VmError::CallStackEmpty)?;
615        let ha = match running.root_cnode.get(a) {
616            Some(CapHashOrRef::Hash(h)) => h,
617            _ => return Err(VmError::SlotEmpty(a.get())),
618        };
619        let hb = match running.root_cnode.get(b) {
620            Some(CapHashOrRef::Hash(h)) => h,
621            _ => return Err(VmError::SlotEmpty(b.get())),
622        };
623        regs.gpr[7] = if ha == hb { 1 } else { 0 };
624        Ok(())
625    }
626
627    fn dispatch_host_type_of(
628        &mut self,
629        regs: &mut Regs,
630        cache: &mut CacheDirectory,
631    ) -> Result<(), VmError> {
632        let src = SlotIdx((regs.gpr[7] & 0xFF) as u32);
633        let dst = SlotIdx((regs.gpr[8] & 0xFF) as u32);
634        let target = self
635            .stack
636            .running_instance()
637            .ok_or(VmError::CallStackEmpty)?
638            .root_cnode
639            .get(src)
640            .ok_or(VmError::SlotEmpty(src.get()))?;
641        let image_hash_chain = match &*cache.get(target).ok_or(VmError::InstanceNotFound)? {
642            Cap::Instance(i) => i.image_hash_chain,
643            _ => return Err(VmError::InstanceNotFound),
644        };
645        let cap = Cap::Type(TypeCap { image_hash_chain });
646        let h = javm_cap::cap_hash(&cap);
647        cache.put_cap_with_hash(h, &cap)?;
648
649        let running = self
650            .stack
651            .running_instance_mut()
652            .ok_or(VmError::CallStackEmpty)?;
653        if running.pinned_slots.binary_search(&dst).is_ok() {
654            return Err(javm_cap::OpError::SlotPinned(dst.get()).into());
655        }
656        running.root_cnode.set(dst, Some(CapHashOrRef::Hash(h)))?;
657        Ok(())
658    }
659
660    fn dispatch_host_read_data_cap(
661        &mut self,
662        regs: &mut Regs,
663        mem: &mut dyn Memory,
664        cache: &CacheDirectory,
665    ) -> Result<(), VmError> {
666        let src = SlotIdx((regs.gpr[7] & 0xFF) as u32);
667        let dst_offset = regs.gpr[8] as u32;
668        let len = regs.gpr[9] as usize;
669        let target = self
670            .stack
671            .running_instance()
672            .ok_or(VmError::CallStackEmpty)?
673            .root_cnode
674            .get(src)
675            .ok_or(VmError::SlotEmpty(src.get()))?;
676        let data_arc = cache
677            .get(target)
678            .ok_or(VmError::Invariant("data cap missing"))?;
679        let data = match &*data_arc {
680            Cap::Data(d) => d,
681            _ => return Err(VmError::Invariant("slot does not hold Cap::Data")),
682        };
683        let bytes = data_cap_prefix(data, len);
684        mem.write(dst_offset, &bytes)
685            .map_err(|_| VmError::Invariant("host_read_data_cap memory write failed"))?;
686        regs.gpr[7] = bytes.len() as u64;
687        Ok(())
688    }
689
690    fn dispatch_host_mint_data_cap(
691        &mut self,
692        regs: &mut Regs,
693        mem: &mut dyn Memory,
694        cache: &mut CacheDirectory,
695    ) -> Result<(), VmError> {
696        let src_offset = regs.gpr[7] as u32;
697        let len = regs.gpr[8] as usize;
698        let quota_id = regs.gpr[9];
699        let dst = SlotIdx((regs.gpr[10] & 0xFF) as u32);
700        let bytes = mem
701            .read(src_offset, len)
702            .map_err(|_| VmError::Invariant("host_mint_data_cap memory read failed"))?;
703        // DataCap content is page-multiple by construction: pad the
704        // caller's bytes up to the next 4 KiB boundary with zeros.
705        // Quota is debited by the padded length — the kernel owns
706        // a full page-aligned allocation regardless of caller's slice
707        // length, so callers pay for what they store.
708        let mut inline = javm_cap::data::alloc_page_aligned_zeroed(bytes.len());
709        inline[..bytes.len()].copy_from_slice(&bytes);
710        let debit = inline.len() as u64;
711        let quota = self.kernel_assist.storage_quota_get(quota_id);
712        if quota < debit {
713            return Err(VmError::Invariant("storage quota exhausted"));
714        }
715        self.kernel_assist
716            .storage_quota_set(quota_id, quota - debit);
717        let cap = Cap::Data(DataCap {
718            content: DataContent::Inline(inline),
719        });
720        let h = javm_cap::cap_hash(&cap);
721        cache.put_cap_with_hash(h, &cap)?;
722
723        let running = self
724            .stack
725            .running_instance_mut()
726            .ok_or(VmError::CallStackEmpty)?;
727        if running.pinned_slots.binary_search(&dst).is_ok() {
728            return Err(javm_cap::OpError::SlotPinned(dst.get()).into());
729        }
730        running.root_cnode.set(dst, Some(CapHashOrRef::Hash(h)))?;
731        Ok(())
732    }
733
734    fn dispatch_host_open(
735        &mut self,
736        regs: &mut Regs,
737        cache: &mut CacheDirectory,
738    ) -> Result<(), VmError> {
739        let file_id = regs.gpr[7];
740        let dst = SlotIdx((regs.gpr[8] & 0xFF) as u32);
741        let data_ref = self
742            .kernel_assist
743            .host_open(file_id)
744            .ok_or(VmError::Invariant("unknown file id"))?;
745        match &*cache
746            .get(data_ref.clone())
747            .ok_or(VmError::Invariant("file data missing"))?
748        {
749            Cap::Data(_) => {}
750            _ => return Err(VmError::Invariant("file target is not Cap::Data")),
751        }
752        let h = cache.settle(data_ref)?;
753
754        let running = self
755            .stack
756            .running_instance_mut()
757            .ok_or(VmError::CallStackEmpty)?;
758        if running.pinned_slots.binary_search(&dst).is_ok() {
759            return Err(javm_cap::OpError::SlotPinned(dst.get()).into());
760        }
761        running.root_cnode.set(dst, Some(CapHashOrRef::Hash(h)))?;
762        Ok(())
763    }
764
765    fn dispatch_host_save(
766        &mut self,
767        regs: &mut Regs,
768        cache: &CacheDirectory,
769    ) -> Result<(), VmError> {
770        let src = SlotIdx((regs.gpr[7] & 0xFF) as u32);
771        let quota_id = regs.gpr[8];
772        let target = self
773            .stack
774            .running_instance()
775            .ok_or(VmError::CallStackEmpty)?
776            .root_cnode
777            .get(src)
778            .ok_or(VmError::SlotEmpty(src.get()))?;
779        let size = match &*cache
780            .get(target.clone())
781            .ok_or(VmError::Invariant("host_save data missing"))?
782        {
783            Cap::Data(d) => d.content_len(),
784            _ => return Err(VmError::Invariant("host_save source is not Cap::Data")),
785        };
786        let file_id = self
787            .kernel_assist
788            .host_save(target, quota_id, size)
789            .ok_or(VmError::Invariant("host_save failed"))?;
790        regs.gpr[7] = file_id;
791        Ok(())
792    }
793
794    /// `host_call(instance_slot=φ[7], endpoint_idx=φ[8])`.
795    ///
796    /// Resolve the `Cap::Instance` at `instance_slot` in the running
797    /// cnode, build a child `InstanceEntry` via [`Vm::build_entry`],
798    /// move the caller's `slot[0]` into the child's `slot[0]` (CALL
799    /// scratchpad), and push the child. The dispatcher returns; the
800    /// caller (in `dispatch_host_call`) wraps the result as
801    /// `EcallResult::Exit(ExitReason::HostCall(HOST_CALL))` so the
802    /// `drive_and_translate` loop re-enters `Interpreter::run` on the
803    /// new top frame.
804    ///
805    /// Child gas: V1 threads the parent's live `GasCounter` through to
806    /// the child (shared pool). The child's `entry.gas` field is left
807    /// as the build_entry placeholder; the live counter is owned by
808    /// `drive_and_translate`.
809    fn dispatch_host_call_cached(
810        &mut self,
811        regs: &mut Regs,
812        cache: &mut CacheDirectory,
813    ) -> Result<(), VmError> {
814        let inst_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
815        let endpoint_idx = (regs.gpr[8] & 0xFF) as u8;
816
817        // 1. Resolve target Cap::Instance from caller's cnode.
818        let target_ref = self
819            .stack
820            .running_instance()
821            .ok_or(VmError::CallStackEmpty)?
822            .root_cnode
823            .get(inst_slot)
824            .ok_or(VmError::SlotEmpty(inst_slot.get()))?;
825        let target_arc = cache.get(target_ref.clone());
826        match target_arc.as_deref() {
827            Some(Cap::Instance(_)) => {}
828            _ => return Err(VmError::InstanceNotFound),
829        }
830
831        // 2. Move caller's slot[0] (CALL scratchpad). Empty is fine.
832        let scratch = self
833            .stack
834            .running_instance_mut()
835            .ok_or(VmError::CallStackEmpty)?
836            .root_cnode
837            .take(SlotIdx(0))?;
838
839        // 3. Build the child entry + its initial regs/mem. Gas budget
840        //    is irrelevant — V1 shares the parent's live counter, so
841        //    this is a throwaway.
842        let (mut child, child_mem, child_regs, _child_gas, _) =
843            self.build_entry(cache, target_ref, endpoint_idx, [0u64; 4], 0)?;
844
845        // 4. Plant slot[0] in the child's cnode.
846        if let Some(cap) = scratch {
847            child.root_cnode.set(SlotIdx(0), Some(cap))?;
848        }
849
850        // 5. Stash child's initial regs/mem in the entry so the
851        //    `drive_and_translate` loop can pick them up on the next
852        //    iteration. Gas stays threaded via the loop's live
853        //    counter; the entry.gas placeholder isn't read.
854        child.regs = child_regs;
855        child.mem = child_mem;
856
857        // 6. Push. push_instance flips the parent's status
858        //    Running→Waiting; the child becomes Running.
859        self.stack.push_instance(child)?;
860        Ok(())
861    }
862}
863
864fn data_cap_prefix(data: &DataCap, len: usize) -> Vec<u8> {
865    let actual_len = len.min(data.content_len() as usize);
866    let mut out = vec![0u8; actual_len];
867    match &data.content {
868        DataContent::Inline(bytes) => {
869            let copy_len = actual_len.min(bytes.len());
870            out[..copy_len].copy_from_slice(&bytes[..copy_len]);
871        }
872        DataContent::Paged { page_size, pages } => {
873            let page_size = *page_size as usize;
874            for (page_idx, page) in pages.iter().enumerate() {
875                let start = page_idx * page_size;
876                if start >= actual_len {
877                    break;
878                }
879                let end = (start + page_size).min(actual_len);
880                if let javm_cap::page::PageSlot::Loaded(page_ref) = page {
881                    let page_bytes = &page_ref.bytes;
882                    out[start..end].copy_from_slice(&page_bytes[..end - start]);
883                }
884            }
885        }
886    }
887    out
888}
889
890impl<K: KernelAssist> Vm<K> {
891    /// Plain `ecall` (opcode 3, no immediate). Spec §4 reads φ[11]
892    /// (mgmt_op) and φ[12] (subject|object) for the management
893    /// dispatch. Stage 3 routes the same way as `ecalli imm`, treating
894    /// φ[11] as the op.
895    fn dispatch_ecall(
896        &mut self,
897        regs: &mut Regs,
898        mem: &mut dyn Memory,
899        cache: Option<&mut CacheDirectory>,
900    ) -> EcallResult {
901        let op = regs.gpr[11] as u32;
902        self.dispatch_ecalli(op, regs, mem, cache)
903    }
904
905    fn dispatch_mgmt(
906        &mut self,
907        op: u32,
908        regs: &mut Regs,
909        cache: Option<&mut CacheDirectory>,
910    ) -> Result<(), VmError> {
911        let a = SlotIdx((regs.gpr[7] & 0xFF) as u32);
912        let b = SlotIdx((regs.gpr[8] & 0xFF) as u32);
913        match op {
914            mgmt_op::COPY => self.mgmt_copy(a, b),
915            mgmt_op::MOVE => self.mgmt_move(a, b),
916            mgmt_op::DROP => self.mgmt_drop(a),
917            mgmt_op::CNODE_SWAP => self.mgmt_cnode_swap(a, b),
918            mgmt_op::CNODE_MINT => {
919                let size_log = (regs.gpr[8] & 0xFF) as u8;
920                self.mgmt_cnode_mint(a, size_log, cache)
921            }
922            _ => Err(VmError::Invariant("unknown MGMT op")),
923        }
924    }
925
926    fn mgmt_copy(&mut self, a: SlotIdx, b: SlotIdx) -> Result<(), VmError> {
927        let running = self
928            .stack
929            .running_instance_mut()
930            .ok_or(VmError::CallStackEmpty)?;
931        if running.pinned_slots.binary_search(&b).is_ok() {
932            return Err(javm_cap::OpError::SlotPinned(b.get()).into());
933        }
934        let src = running
935            .root_cnode
936            .get(a)
937            .ok_or(javm_cap::OpError::SourceEmpty)?;
938        running.root_cnode.set(b, Some(src))?;
939        Ok(())
940    }
941
942    fn mgmt_move(&mut self, a: SlotIdx, b: SlotIdx) -> Result<(), VmError> {
943        let running = self
944            .stack
945            .running_instance_mut()
946            .ok_or(VmError::CallStackEmpty)?;
947        if running.pinned_slots.binary_search(&a).is_ok() {
948            return Err(javm_cap::OpError::SlotPinned(a.get()).into());
949        }
950        if running.pinned_slots.binary_search(&b).is_ok() {
951            return Err(javm_cap::OpError::SlotPinned(b.get()).into());
952        }
953        let src = running
954            .root_cnode
955            .take(a)?
956            .ok_or(javm_cap::OpError::SourceEmpty)?;
957        running.root_cnode.set(b, Some(src))?;
958        Ok(())
959    }
960
961    fn mgmt_drop(&mut self, a: SlotIdx) -> Result<(), VmError> {
962        let running = self
963            .stack
964            .running_instance_mut()
965            .ok_or(VmError::CallStackEmpty)?;
966        if running.pinned_slots.binary_search(&a).is_ok() {
967            return Err(javm_cap::OpError::SlotPinned(a.get()).into());
968        }
969        running.root_cnode.take(a)?;
970        Ok(())
971    }
972
973    fn mgmt_cnode_swap(&mut self, a: SlotIdx, b: SlotIdx) -> Result<(), VmError> {
974        let running = self
975            .stack
976            .running_instance_mut()
977            .ok_or(VmError::CallStackEmpty)?;
978        if running.pinned_slots.binary_search(&a).is_ok() {
979            return Err(javm_cap::OpError::SlotPinned(a.get()).into());
980        }
981        if running.pinned_slots.binary_search(&b).is_ok() {
982            return Err(javm_cap::OpError::SlotPinned(b.get()).into());
983        }
984        let av = running.root_cnode.take(a)?;
985        let bv = running.root_cnode.take(b)?;
986        if let Some(t) = bv {
987            running.root_cnode.set(a, Some(t))?;
988        }
989        if let Some(t) = av {
990            running.root_cnode.set(b, Some(t))?;
991        }
992        Ok(())
993    }
994
995    fn mgmt_cnode_mint(
996        &mut self,
997        dst: SlotIdx,
998        size_log: u8,
999        cache: Option<&mut CacheDirectory>,
1000    ) -> Result<(), VmError> {
1001        let cap = Cap::CNode(javm_cap::CNodeCap::new(size_log)?);
1002        let cap_hash = javm_cap::cap_hash(&cap);
1003        let h = match cache {
1004            Some(cache) => {
1005                cache.put_cap_with_hash(cap_hash, &cap)?;
1006                cap_hash
1007            }
1008            None => cap_hash,
1009        };
1010        let running = self
1011            .stack
1012            .running_instance_mut()
1013            .ok_or(VmError::CallStackEmpty)?;
1014        if running.pinned_slots.binary_search(&dst).is_ok() {
1015            return Err(javm_cap::OpError::SlotPinned(dst.get()).into());
1016        }
1017        running.root_cnode.set(dst, Some(CapHashOrRef::Hash(h)))?;
1018        Ok(())
1019    }
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024    use super::*;
1025    use crate::callstack::{EntryStatus, InstanceEntry};
1026    use crate::kernel_assist::InProcessKernelAssist;
1027    use javm_cap::image::Image;
1028    use javm_cap::{CNodeCap, NUM_REGS};
1029    use javm_exec::{Access, GasCounter, Mem, PAGE_SIZE, PvmProgram, Regs};
1030    use std::sync::Arc;
1031
1032    fn fixture_vm() -> Vm<InProcessKernelAssist> {
1033        let mut vm = Vm::new(InProcessKernelAssist::new());
1034        let mut cnode = CNodeCap::new(4).unwrap();
1035        // Seed slot 2 with a Hash-form target (treated as an Image hash).
1036        cnode
1037            .set(SlotIdx(2), Some(CapHashOrRef::Hash([0xAA; 32])))
1038            .unwrap();
1039        let prog = Arc::new(PvmProgram::new(vec![0u8], vec![1u8], vec![], 25).unwrap());
1040        let entry = InstanceEntry {
1041            instance_ref: CapHashOrRef::Hash([1u8; 32]),
1042            image_hash_chain: [1u8; 32],
1043            image_hash: [2u8; 32],
1044            program: prog,
1045            root_cnode: cnode,
1046            yield_marker_slot: None,
1047            pinned_slots: Vec::new(),
1048            regs: Regs::new(),
1049            mem: Mem::new(),
1050            gas: GasCounter::new(1000),
1051            status: EntryStatus::Waiting,
1052        };
1053        vm.stack.push_instance(entry).unwrap();
1054        vm
1055    }
1056
1057    fn handle_cached(
1058        vm: &mut Vm<InProcessKernelAssist>,
1059        cache: &mut CacheDirectory,
1060        op: u32,
1061        regs: &mut Regs,
1062        mem: &mut Mem,
1063    ) -> EcallResult {
1064        let mut handler = CachedEcallHandler { vm, cache };
1065        handler.handle(EcallKind::Ecalli(op), regs, mem)
1066    }
1067
1068    fn publish_data_inline(cache: &mut CacheDirectory, bytes: &[u8]) -> javm_cap::CapHash {
1069        cache.put_cap(&Cap::data_inline(bytes)).unwrap()
1070    }
1071
1072    #[test]
1073    fn mgmt_copy_dispatch_via_ecalli() {
1074        let mut vm = fixture_vm();
1075        let mut regs = Regs::new();
1076        regs.gpr[7] = 2; // src
1077        regs.gpr[8] = 7; // dst
1078        let mut mem = Mem::new();
1079        let r = vm.handle(EcallKind::Ecalli(mgmt_op::COPY), &mut regs, &mut mem);
1080        assert!(matches!(r, EcallResult::Continue));
1081        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1082        assert!(cnode.get(SlotIdx(2)).is_some());
1083        assert!(cnode.get(SlotIdx(7)).is_some());
1084    }
1085
1086    #[test]
1087    fn mgmt_move_dispatch_via_ecalli() {
1088        let mut vm = fixture_vm();
1089        let mut regs = Regs::new();
1090        regs.gpr[7] = 2;
1091        regs.gpr[8] = 9;
1092        let mut mem = Mem::new();
1093        let r = vm.handle(EcallKind::Ecalli(mgmt_op::MOVE), &mut regs, &mut mem);
1094        assert!(matches!(r, EcallResult::Continue));
1095        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1096        assert!(cnode.get(SlotIdx(2)).is_none()); // source moved out
1097        assert!(cnode.get(SlotIdx(9)).is_some()); // dst now holds it
1098    }
1099
1100    #[test]
1101    fn mgmt_drop_dispatch_via_ecalli() {
1102        let mut vm = fixture_vm();
1103        let mut regs = Regs::new();
1104        regs.gpr[7] = 2;
1105        let mut mem = Mem::new();
1106        let r = vm.handle(EcallKind::Ecalli(mgmt_op::DROP), &mut regs, &mut mem);
1107        assert!(matches!(r, EcallResult::Continue));
1108        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1109        assert!(cnode.get(SlotIdx(2)).is_none());
1110    }
1111
1112    #[test]
1113    fn mgmt_cnode_mint_places_hash_at_dst() {
1114        let mut vm = fixture_vm();
1115        let mut regs = Regs::new();
1116        regs.gpr[7] = 5; // dst
1117        regs.gpr[8] = 3; // size_log = 8 slots
1118        let mut mem = Mem::new();
1119        let r = vm.handle(EcallKind::Ecalli(mgmt_op::CNODE_MINT), &mut regs, &mut mem);
1120        assert!(matches!(r, EcallResult::Continue));
1121        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1122        assert!(matches!(cnode.get(SlotIdx(5)), Some(CapHashOrRef::Hash(_))));
1123    }
1124
1125    #[test]
1126    fn mgmt_cnode_mint_publishes_cnode_when_cache_threaded() {
1127        let mut vm = fixture_vm();
1128        let mut cache = CacheDirectory::new();
1129        let mut regs = Regs::new();
1130        regs.gpr[7] = 5;
1131        regs.gpr[8] = 3;
1132        let mut mem = Mem::new();
1133        let r = handle_cached(
1134            &mut vm,
1135            &mut cache,
1136            mgmt_op::CNODE_MINT,
1137            &mut regs,
1138            &mut mem,
1139        );
1140        assert!(matches!(r, EcallResult::Continue));
1141        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1142        let target = cnode.get(SlotIdx(5)).unwrap();
1143        assert!(matches!(cache.get(target).as_deref(), Some(Cap::CNode(_))));
1144    }
1145
1146    #[test]
1147    fn mgmt_cnode_swap_swaps_slots() {
1148        let mut vm = fixture_vm();
1149        vm.stack
1150            .running_instance_mut()
1151            .unwrap()
1152            .root_cnode
1153            .set(SlotIdx(3), Some(CapHashOrRef::Hash([0xBB; 32])))
1154            .unwrap();
1155        let mut regs = Regs::new();
1156        regs.gpr[7] = 2;
1157        regs.gpr[8] = 3;
1158        let mut mem = Mem::new();
1159        let r = vm.handle(EcallKind::Ecalli(mgmt_op::CNODE_SWAP), &mut regs, &mut mem);
1160        assert!(matches!(r, EcallResult::Continue));
1161        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1162        let s2 = cnode.get(SlotIdx(2)).unwrap();
1163        let s3 = cnode.get(SlotIdx(3)).unwrap();
1164        assert_eq!(s2, CapHashOrRef::Hash([0xBB; 32]));
1165        assert_eq!(s3, CapHashOrRef::Hash([0xAA; 32]));
1166    }
1167
1168    #[test]
1169    fn mgmt_op_on_empty_stack_traps() {
1170        let mut vm: Vm<InProcessKernelAssist> = Vm::new(InProcessKernelAssist::new());
1171        let mut regs = Regs::new();
1172        let mut mem = Mem::new();
1173        let r = vm.handle(EcallKind::Ecalli(mgmt_op::DROP), &mut regs, &mut mem);
1174        assert!(matches!(r, EcallResult::Exit(ExitReason::Trap)));
1175    }
1176
1177    #[test]
1178    fn ecalli_reply_zero_exits_halt() {
1179        let mut vm = fixture_vm();
1180        let mut regs = Regs::new();
1181        let mut mem = Mem::new();
1182        let r = vm.handle(EcallKind::Ecalli(0), &mut regs, &mut mem);
1183        assert!(matches!(r, EcallResult::Exit(ExitReason::Halt)));
1184    }
1185
1186    #[test]
1187    fn plain_ecall_reads_op_from_phi11() {
1188        let mut vm = fixture_vm();
1189        let mut regs = Regs::new();
1190        regs.gpr[11] = mgmt_op::DROP as u64;
1191        regs.gpr[7] = 2;
1192        let mut mem = Mem::new();
1193        let r = vm.handle(EcallKind::Ecall, &mut regs, &mut mem);
1194        assert!(matches!(r, EcallResult::Continue));
1195        assert!(
1196            vm.stack
1197                .running_instance()
1198                .unwrap()
1199                .root_cnode
1200                .get(SlotIdx(2))
1201                .is_none()
1202        );
1203    }
1204
1205    #[test]
1206    fn unknown_op_continues_silently() {
1207        let mut vm = fixture_vm();
1208        let mut regs = Regs::new();
1209        let mut mem = Mem::new();
1210        let r = vm.handle(EcallKind::Ecalli(999), &mut regs, &mut mem);
1211        assert!(matches!(r, EcallResult::Continue));
1212    }
1213
1214    #[test]
1215    fn set_image_extends_chain_hash() {
1216        let mut vm = fixture_vm();
1217        let original_chain = vm.stack.running_instance().unwrap().image_hash_chain;
1218        let mut regs = Regs::new();
1219        regs.gpr[7] = 2;
1220        let mut mem = Mem::new();
1221        let r = vm.handle(EcallKind::Ecalli(host_op::SET_IMAGE), &mut regs, &mut mem);
1222        assert!(matches!(r, EcallResult::Continue));
1223        let new_chain = vm.stack.running_instance().unwrap().image_hash_chain;
1224        assert_ne!(original_chain, new_chain);
1225    }
1226
1227    #[test]
1228    fn set_image_reloads_program_from_cache() {
1229        let mut vm = fixture_vm();
1230        let mut cache = CacheDirectory::new();
1231        let mut img = Image::empty();
1232        img.code = vec![10u8, 0];
1233        img.packed_bitmask = vec![0b01u8];
1234        let image_hash = cache
1235            .put_cap(&Cap::image_with_slots(&img, &[], &[]).unwrap())
1236            .unwrap();
1237        vm.stack
1238            .running_instance_mut()
1239            .unwrap()
1240            .root_cnode
1241            .set(SlotIdx(3), Some(CapHashOrRef::Hash(image_hash)))
1242            .unwrap();
1243
1244        let mut regs = Regs::new();
1245        regs.gpr[7] = 3;
1246        let mut mem = Mem::new();
1247        let r = handle_cached(&mut vm, &mut cache, host_op::SET_IMAGE, &mut regs, &mut mem);
1248        assert!(
1249            matches!(r, EcallResult::Exit(ExitReason::HostCall(op)) if op == host_op::SET_IMAGE)
1250        );
1251        let running = vm.stack.running_instance().unwrap();
1252        assert_eq!(running.image_hash, image_hash);
1253        assert_eq!(running.program.code, vec![10u8, 0]);
1254    }
1255
1256    #[test]
1257    fn derive_spawn_mints_extended_chain_target() {
1258        let mut vm = fixture_vm();
1259        let parent_chain = vm.stack.running_instance().unwrap().image_hash_chain;
1260        let mut regs = Regs::new();
1261        regs.gpr[7] = 2;
1262        regs.gpr[8] = 5;
1263        let mut mem = Mem::new();
1264        let r = vm.handle(
1265            EcallKind::Ecalli(host_op::DERIVE_SPAWN),
1266            &mut regs,
1267            &mut mem,
1268        );
1269        assert!(matches!(r, EcallResult::Continue));
1270        let cnode = &vm.stack.running_instance().unwrap().root_cnode;
1271        let child = cnode.get(SlotIdx(5)).unwrap();
1272        match child {
1273            CapHashOrRef::Hash(h) => assert_ne!(h, parent_chain),
1274            _ => panic!("expected Hash form at dst"),
1275        }
1276    }
1277
1278    #[test]
1279    fn host_same_type_compares_chain() {
1280        let mut vm = fixture_vm();
1281        vm.stack
1282            .running_instance_mut()
1283            .unwrap()
1284            .root_cnode
1285            .set(SlotIdx(3), Some(CapHashOrRef::Hash([0xAA; 32])))
1286            .unwrap();
1287        vm.stack
1288            .running_instance_mut()
1289            .unwrap()
1290            .root_cnode
1291            .set(SlotIdx(4), Some(CapHashOrRef::Hash([0x99; 32])))
1292            .unwrap();
1293        // slot 2 already has [0xAA;32].
1294        let mut regs = Regs::new();
1295        regs.gpr[7] = 2;
1296        regs.gpr[8] = 3;
1297        let mut mem = Mem::new();
1298        let r = vm.handle(
1299            EcallKind::Ecalli(host_op::HOST_SAME_TYPE),
1300            &mut regs,
1301            &mut mem,
1302        );
1303        assert!(matches!(r, EcallResult::Continue));
1304        assert_eq!(regs.gpr[7], 1);
1305
1306        regs.gpr[7] = 2;
1307        regs.gpr[8] = 4;
1308        let r = vm.handle(
1309            EcallKind::Ecalli(host_op::HOST_SAME_TYPE),
1310            &mut regs,
1311            &mut mem,
1312        );
1313        assert!(matches!(r, EcallResult::Continue));
1314        assert_eq!(regs.gpr[7], 0);
1315    }
1316
1317    #[test]
1318    fn host_type_of_publishes_type_cap() {
1319        let mut vm = fixture_vm();
1320        let mut cache = CacheDirectory::new();
1321        // Instance references image + cnode by hash; both must be in
1322        // the cache for `put_cap` to accept the Instance.
1323        let image_hash = cache
1324            .put_cap(&Cap::image_with_slots(&Image::empty(), &[], &[]).unwrap())
1325            .unwrap();
1326        let cnode_hash = cache.put_cap(&Cap::empty_cnode(0).unwrap()).unwrap();
1327        let inst_hash = cache
1328            .put_cap(&Cap::instance_with_overlays(
1329                [0x42; 32],
1330                image_hash,
1331                cnode_hash,
1332                &[],
1333                0,
1334                [0u64; NUM_REGS],
1335                0,
1336                0,
1337            ))
1338            .unwrap();
1339        vm.stack
1340            .running_instance_mut()
1341            .unwrap()
1342            .root_cnode
1343            .set(SlotIdx(3), Some(CapHashOrRef::Hash(inst_hash)))
1344            .unwrap();
1345
1346        let mut regs = Regs::new();
1347        regs.gpr[7] = 3;
1348        regs.gpr[8] = 6;
1349        let mut mem = Mem::new();
1350        let r = handle_cached(
1351            &mut vm,
1352            &mut cache,
1353            host_op::HOST_TYPE_OF,
1354            &mut regs,
1355            &mut mem,
1356        );
1357        assert!(matches!(r, EcallResult::Continue));
1358        let target = vm
1359            .stack
1360            .running_instance()
1361            .unwrap()
1362            .root_cnode
1363            .get(SlotIdx(6))
1364            .unwrap();
1365        assert!(matches!(
1366            cache.get(target).as_deref(),
1367            Some(Cap::Type(TypeCap {
1368                image_hash_chain
1369            })) if *image_hash_chain == [0x42; 32]
1370        ));
1371    }
1372
1373    #[test]
1374    fn host_read_data_cap_copies_bytes_from_cache() {
1375        // After the page-aligned DataCap refactor, the cap'\''s content
1376        // is always page-multiple. `host_read_data_cap` copies up to
1377        // `len` bytes (capped at `content_len()`), so callers asking
1378        // for fewer bytes than a page get exactly that many — with
1379        // the meaningful prefix at the start and trailing zero-pad.
1380        let mut vm = fixture_vm();
1381        let mut cache = CacheDirectory::new();
1382        let data_hash = publish_data_inline(&mut cache, b"hello");
1383        vm.stack
1384            .running_instance_mut()
1385            .unwrap()
1386            .root_cnode
1387            .set(SlotIdx(3), Some(CapHashOrRef::Hash(data_hash)))
1388            .unwrap();
1389        let mut mem = Mem::new();
1390        mem.map_region(0, PAGE_SIZE as u64, Access::ReadWrite, None)
1391            .unwrap();
1392        let mut regs = Regs::new();
1393        regs.gpr[7] = 3;
1394        regs.gpr[8] = 16;
1395        regs.gpr[9] = 8;
1396
1397        let r = handle_cached(
1398            &mut vm,
1399            &mut cache,
1400            host_op::HOST_READ_DATA_CAP,
1401            &mut regs,
1402            &mut mem,
1403        );
1404        assert!(matches!(r, EcallResult::Continue));
1405        // Asked for 8 bytes; the cap has 4096 bytes available so we
1406        // get all 8: 5 meaningful "hello" plus 3 trailing zeros.
1407        assert_eq!(regs.gpr[7], 8);
1408        assert_eq!(mem.read(16, 8).unwrap(), b"hello\0\0\0");
1409    }
1410
1411    #[test]
1412    fn host_mint_data_cap_publishes_page_padded_bytes() {
1413        // After the page-aligned DataCap refactor, mint pads the
1414        // caller's bytes up to the next 4 KiB boundary and debits
1415        // quota by the padded length (1 page = 4096 bytes).
1416        let mut vm = fixture_vm();
1417        let mut cache = CacheDirectory::new();
1418        vm.kernel_assist.storage_quota_set(0, 8192);
1419        let mut mem = Mem::new();
1420        mem.map_region(0, PAGE_SIZE as u64, Access::ReadWrite, None)
1421            .unwrap();
1422        mem.write(32, b"abc\0\0").unwrap();
1423        let mut regs = Regs::new();
1424        regs.gpr[7] = 32;
1425        regs.gpr[8] = 5;
1426        regs.gpr[9] = 0;
1427        regs.gpr[10] = 6;
1428
1429        let r = handle_cached(
1430            &mut vm,
1431            &mut cache,
1432            host_op::HOST_MINT_DATA_CAP,
1433            &mut regs,
1434            &mut mem,
1435        );
1436        assert!(matches!(r, EcallResult::Continue));
1437        // Debit = one page (4096), starting from 8192 leaves 4096.
1438        assert_eq!(vm.kernel_assist.storage_quota_get(0), 4096);
1439        let target = vm
1440            .stack
1441            .running_instance()
1442            .unwrap()
1443            .root_cnode
1444            .get(SlotIdx(6))
1445            .unwrap();
1446        let target_arc = cache.get(target).unwrap();
1447        match &*target_arc {
1448            Cap::Data(d) => {
1449                assert_eq!(d.content_len(), javm_cap::PAGE_SIZE as u64);
1450                // First 5 bytes echo what we wrote, including the
1451                // two trailing zeros — no stripping.
1452                assert_eq!(data_cap_prefix(d, 5), b"abc\0\0");
1453            }
1454            _ => panic!("expected Data cap"),
1455        }
1456    }
1457
1458    #[test]
1459    fn host_open_places_registered_file_data_in_slot() {
1460        let mut vm = fixture_vm();
1461        let mut cache = CacheDirectory::new();
1462        let data_hash = publish_data_inline(&mut cache, b"file");
1463        vm.kernel_assist
1464            .register_file(9, CapHashOrRef::Hash(data_hash));
1465        let mut regs = Regs::new();
1466        regs.gpr[7] = 9;
1467        regs.gpr[8] = 6;
1468        let mut mem = Mem::new();
1469        let r = handle_cached(&mut vm, &mut cache, host_op::HOST_OPEN, &mut regs, &mut mem);
1470        assert!(matches!(r, EcallResult::Continue));
1471        let target = vm
1472            .stack
1473            .running_instance()
1474            .unwrap()
1475            .root_cnode
1476            .get(SlotIdx(6))
1477            .unwrap();
1478        assert!(matches!(cache.get(target).as_deref(), Some(Cap::Data(_))));
1479    }
1480
1481    #[test]
1482    fn host_save_debits_actual_data_size_and_returns_file_id() {
1483        // Page-aligned DataCap: `host_save` debits by the full
1484        // page-multiple content length (4 KiB for the padded "stored"
1485        // cap). Quota seeded with enough headroom for one save.
1486        let mut vm = fixture_vm();
1487        let mut cache = CacheDirectory::new();
1488        let data_hash = publish_data_inline(&mut cache, b"stored");
1489        vm.kernel_assist.storage_quota_set(0, 8192);
1490        vm.stack
1491            .running_instance_mut()
1492            .unwrap()
1493            .root_cnode
1494            .set(SlotIdx(3), Some(CapHashOrRef::Hash(data_hash)))
1495            .unwrap();
1496        let mut regs = Regs::new();
1497        regs.gpr[7] = 3;
1498        regs.gpr[8] = 0;
1499        let mut mem = Mem::new();
1500        let r = handle_cached(&mut vm, &mut cache, host_op::HOST_SAVE, &mut regs, &mut mem);
1501        assert!(matches!(r, EcallResult::Continue));
1502        assert_eq!(regs.gpr[7], 1);
1503        // Debit one page (4096) from initial 8192 → 4096 remaining.
1504        assert_eq!(vm.kernel_assist.storage_quota_get(0), 4096);
1505        assert_eq!(
1506            vm.kernel_assist.host_open(1),
1507            Some(CapHashOrRef::Hash(data_hash))
1508        );
1509    }
1510
1511    #[test]
1512    fn make_image_stubbed_traps() {
1513        let mut vm = fixture_vm();
1514        let mut regs = Regs::new();
1515        let mut mem = Mem::new();
1516        let r = vm.handle(EcallKind::Ecalli(host_op::MAKE_IMAGE), &mut regs, &mut mem);
1517        assert!(matches!(r, EcallResult::Exit(ExitReason::Trap)));
1518    }
1519
1520    #[test]
1521    fn cache_dependent_host_calls_without_cache_still_trap() {
1522        // Direct `Vm` handler calls don't carry a cache borrow, so
1523        // cache-dependent host calls still trap outside invoke_cached.
1524        for op in [
1525            host_op::HOST_TYPE_OF,
1526            host_op::HOST_READ_DATA_CAP,
1527            host_op::HOST_MINT_DATA_CAP,
1528            host_op::HOST_OPEN,
1529            host_op::HOST_SAVE,
1530            host_op::HOST_CALL,
1531        ] {
1532            let mut vm = fixture_vm();
1533            let mut regs = Regs::new();
1534            let mut mem = Mem::new();
1535            let r = vm.handle(EcallKind::Ecalli(op), &mut regs, &mut mem);
1536            assert!(
1537                matches!(r, EcallResult::Exit(ExitReason::Trap)),
1538                "op {} should trap (cache-dependent host call)",
1539                op
1540            );
1541        }
1542    }
1543
1544    /// `derive_spawn_cached` publishes a fresh `Cap::Instance` whose
1545    /// `image_hash_chain` extends the parent's, references the
1546    /// caller-prepared CNode (with the image's pinned slots overlaid
1547    /// on top), and lands by hash in the dst slot. Consumes the
1548    /// prepared-cnode slot.
1549    #[test]
1550    fn derive_spawn_cached_publishes_child_instance() {
1551        let mut vm = fixture_vm();
1552        let mut cache = CacheDirectory::new();
1553
1554        // Publish a tiny child image with no pinned/initial slots.
1555        let mut child_img = javm_cap::image::Image::empty();
1556        child_img.code = vec![10u8, 0]; // ecalli 0
1557        child_img.packed_bitmask = vec![0b01u8];
1558        let image_hash = cache
1559            .put_cap(&Cap::image_with_slots(&child_img, &[], &[]).unwrap())
1560            .unwrap();
1561
1562        // Publish an empty prepared cnode.
1563        let prep_cnode_hash = cache.put_cap(&Cap::empty_cnode(4).unwrap()).unwrap();
1564
1565        // Put both into the running instance's cnode at known slots.
1566        let parent_chain = [0xC1; 32];
1567        {
1568            let running = vm.stack.running_instance_mut().unwrap();
1569            running.image_hash_chain = parent_chain;
1570            running
1571                .root_cnode
1572                .set(SlotIdx(3), Some(CapHashOrRef::Hash(image_hash)))
1573                .unwrap();
1574            running
1575                .root_cnode
1576                .set(SlotIdx(4), Some(CapHashOrRef::Hash(prep_cnode_hash)))
1577                .unwrap();
1578        }
1579
1580        let mut regs = Regs::new();
1581        regs.gpr[7] = 3; // image slot
1582        regs.gpr[8] = 4; // prepared cnode slot
1583        regs.gpr[9] = 7; // dst
1584        let mut mem = Mem::new();
1585        let mut handler = CachedEcallHandler {
1586            vm: &mut vm,
1587            cache: &mut cache,
1588        };
1589        let r = handler.handle(
1590            EcallKind::Ecalli(host_op::DERIVE_SPAWN),
1591            &mut regs,
1592            &mut mem,
1593        );
1594        assert!(matches!(r, EcallResult::Continue), "got {:?}", r);
1595
1596        // dst slot now holds Hash(new_instance_hash).
1597        let new_target = vm
1598            .stack
1599            .running_instance()
1600            .unwrap()
1601            .root_cnode
1602            .get(SlotIdx(7))
1603            .expect("dst slot populated");
1604        let new_instance_hash = match new_target {
1605            CapHashOrRef::Hash(h) => h,
1606            _ => panic!("expected Hash target"),
1607        };
1608
1609        // The published Cap::Instance has the extended chain.
1610        let cap = cache.get(new_target).expect("instance in cache");
1611        let inst = match &*cap {
1612            Cap::Instance(i) => i,
1613            _ => panic!("expected Cap::Instance"),
1614        };
1615        let expected_chain = Blake2b256::hash_pair(&parent_chain, &image_hash);
1616        assert_eq!(inst.image_hash_chain, expected_chain);
1617        assert_eq!(inst.image_hash, image_hash);
1618        assert!(matches!(inst.root_cnode, CapHashOrRef::Hash(_)));
1619
1620        // The prepared cnode slot is now empty (MOVE semantics).
1621        assert!(
1622            vm.stack
1623                .running_instance()
1624                .unwrap()
1625                .root_cnode
1626                .get(SlotIdx(4))
1627                .is_none()
1628        );
1629
1630        // Hash hygiene: the new instance hash actually matches what
1631        // cap_hash computes on the published cap.
1632        assert_eq!(new_instance_hash, javm_cap::cap_hash(&cap));
1633    }
1634
1635    /// `dispatch_host_call_cached` pushes a child entry on top of
1636    /// the running instance and moves caller's slot[0] into the
1637    /// child's slot[0].
1638    #[test]
1639    fn host_call_cached_pushes_child_and_moves_slot0() {
1640        let mut vm = fixture_vm();
1641        let mut cache = CacheDirectory::new();
1642
1643        // Publish a no-op image (one Halt instruction) + empty cnode
1644        // + Cap::Instance referencing them.
1645        let mut child_img = javm_cap::image::Image::empty();
1646        child_img.code = vec![10u8, 0];
1647        child_img.packed_bitmask = vec![0b01u8];
1648        let image_hash = cache
1649            .put_cap(&Cap::image_with_slots(&child_img, &[], &[]).unwrap())
1650            .unwrap();
1651        let cnode_hash = cache.put_cap(&Cap::empty_cnode(4).unwrap()).unwrap();
1652        let child_instance_hash = cache
1653            .put_cap(&Cap::instance_with_overlays(
1654                [0xCC; 32],
1655                image_hash,
1656                cnode_hash,
1657                &[],
1658                0,
1659                [0u64; javm_cap::NUM_REGS],
1660                0,
1661                0,
1662            ))
1663            .unwrap();
1664
1665        // Wire the parent: slot 9 → Cap::Instance(child); slot 0 →
1666        // some marker the child should see in its slot 0.
1667        let marker_hash = [0xAB; 32];
1668        {
1669            let running = vm.stack.running_instance_mut().unwrap();
1670            running
1671                .root_cnode
1672                .set(SlotIdx(9), Some(CapHashOrRef::Hash(child_instance_hash)))
1673                .unwrap();
1674            running
1675                .root_cnode
1676                .set(SlotIdx(0), Some(CapHashOrRef::Hash(marker_hash)))
1677                .unwrap();
1678        }
1679
1680        let mut regs = Regs::new();
1681        regs.gpr[7] = 9; // instance_slot
1682        regs.gpr[8] = 0; // endpoint_idx (the only endpoint, default)
1683        let mut mem = Mem::new();
1684        let mut handler = CachedEcallHandler {
1685            vm: &mut vm,
1686            cache: &mut cache,
1687        };
1688        let r = handler.handle(EcallKind::Ecalli(host_op::HOST_CALL), &mut regs, &mut mem);
1689        assert!(
1690            matches!(r, EcallResult::Exit(ExitReason::HostCall(op)) if op == host_op::HOST_CALL),
1691            "got {:?}",
1692            r,
1693        );
1694
1695        // Stack grew by 1; child is Running.
1696        assert_eq!(vm.stack.len(), 2);
1697        assert_eq!(vm.stack.entries()[0].status(), EntryStatus::Waiting);
1698        assert_eq!(vm.stack.entries()[1].status(), EntryStatus::Running);
1699        let child = vm.stack.running_instance().unwrap();
1700        // Child's image identity matches what we published.
1701        assert_eq!(child.image_hash, image_hash);
1702        // The scratchpad moved into child's slot[0].
1703        assert_eq!(
1704            child.root_cnode.get(SlotIdx(0)),
1705            Some(CapHashOrRef::Hash(marker_hash))
1706        );
1707        // Parent's slot[0] was emptied (MOVE).
1708        let parent = match &vm.stack.entries()[0] {
1709            Entry::Instance(e) => e.as_ref(),
1710            _ => panic!("entry 0 not Instance"),
1711        };
1712        assert!(parent.root_cnode.get(SlotIdx(0)).is_none());
1713    }
1714}