Skip to main content

vihaco_cpu/
component.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4use crate::RuntimeInstruction;
5use crate::StepOutcome;
6use crate::Word;
7use crate::data::CPU;
8use crate::word::*;
9use eyre::Result;
10use vihaco::Effects;
11use vihaco::{dispatch, frame::Frame, traits::*};
12
13impl Reset for CPU {
14    fn reset(&mut self) {
15        self.frames.clear();
16        self.heap.clear();
17        self.stack.clear();
18        self.span = (0, 0, 0);
19        self.pending_pc = None;
20        self.current_pc = 0;
21        self.return_values.clear();
22    }
23}
24
25impl CPU {
26    #[inline(always)]
27    fn execute_generated(
28        &mut self,
29        inst: &RuntimeInstruction,
30        msg: CPUMessage,
31    ) -> eyre::Result<Effects<StepOutcome>> {
32        use RuntimeInstruction::*;
33
34        self.clear_pending_pc();
35        match (inst, msg) {
36            (Print, CPUMessage::Print(text)) => {
37                self.stack_pop()?;
38                drop(text);
39                return Ok(Effects::one(StepOutcome::Continue));
40            }
41            (Print, _) => return Err(eyre::eyre!("Print requires CPUMessage::Print")),
42            (_, CPUMessage::Print(_)) => {
43                return Err(eyre::eyre!(
44                    "CPUMessage::Print is only valid for Print instruction"
45                ));
46            }
47            (Call(arity, target), CPUMessage::FunctionInfo { local_count, .. }) => {
48                return self.op_call(*arity, *target, local_count).map(Effects::one);
49            }
50            (
51                IndirectCall,
52                CPUMessage::FunctionInfo {
53                    arity,
54                    start_address,
55                    local_count,
56                },
57            ) => {
58                return self
59                    .op_indirect_call(arity, start_address, local_count)
60                    .map(Effects::one);
61            }
62            (_, CPUMessage::FunctionInfo { .. }) => {
63                return Err(eyre::eyre!(
64                    "CPUMessage::FunctionInfo is only valid for call instructions"
65                ));
66            }
67            (_, CPUMessage::None) => {}
68        }
69
70        let outcome = match inst {
71            Span(file, start, end) => self.op_span(*file, *start, *end),
72            Label(_) | FunctionStart | FunctionEnd => Ok(StepOutcome::Continue),
73            Breakpoint => Ok(StepOutcome::Breakpoint),
74            Branch(target) => self.op_branch(*target),
75            ConditionalBranch(true_target, false_target) => {
76                self.op_conditional_branch(*true_target, *false_target)
77            }
78            Return(keep) => self.op_return(*keep),
79            Call(..) | IndirectCall => Err(eyre::eyre!("call requires CPUMessage::FunctionInfo")),
80            Halt => Ok(StepOutcome::Halt),
81            Print => Err(eyre::eyre!(
82                "Print must be handled via execute with CPUMessage::Print"
83            )),
84            LoadI32(addr) => self.op_load(*addr),
85            LoadI64(addr) => self.op_load(*addr),
86            LoadU32(addr) => self.op_load(*addr),
87            LoadU64(addr) => self.op_load(*addr),
88            LoadF32(addr) => self.op_load(*addr),
89            LoadF64(addr) => self.op_load(*addr),
90            LoadBool(addr) => self.op_load(*addr),
91            StoreI32(addr) => self.op_store(*addr),
92            StoreI64(addr) => self.op_store(*addr),
93            StoreU32(addr) => self.op_store(*addr),
94            StoreU64(addr) => self.op_store(*addr),
95            StoreF32(addr) => self.op_store(*addr),
96            StoreF64(addr) => self.op_store(*addr),
97            StoreBool(addr) => self.op_store(*addr),
98            Dup => self.op_dup(),
99            HeapAlloc(n_elements) => self.op_heap_alloc(*n_elements),
100            GetItem => self.op_get_item(),
101            HeapDealloc => self.op_heap_dealloc(),
102            ConstI32(v) | ConstI64(v) | ConstU32(v) | ConstU64(v) | ConstF32(v) | ConstF64(v)
103            | ConstBool(v) | ConstString(v) | ConstFunctionRef(v) | ConstHeapRef(v) => {
104                self.op_const(*v)
105            }
106            AddI32 => self.add_i32(),
107            AddI64 => self.add_i64(),
108            AddU32 => self.add_u32(),
109            AddU64 => self.add_u64(),
110            AddF32 => self.add_f32(),
111            AddF64 => self.add_f64(),
112            SubI32 => self.sub_i32(),
113            SubI64 => self.sub_i64(),
114            SubU32 => self.sub_u32(),
115            SubU64 => self.sub_u64(),
116            SubF32 => self.sub_f32(),
117            SubF64 => self.sub_f64(),
118            MulI32 => self.mul_i32(),
119            MulI64 => self.mul_i64(),
120            MulU32 => self.mul_u32(),
121            MulU64 => self.mul_u64(),
122            MulF32 => self.mul_f32(),
123            MulF64 => self.mul_f64(),
124            DivI32 => self.div_i32(),
125            DivI64 => self.div_i64(),
126            DivU32 => self.div_u32(),
127            DivU64 => self.div_u64(),
128            DivF32 => self.div_f32(),
129            DivF64 => self.div_f64(),
130            RemI32 => self.rem_i32(),
131            RemI64 => self.rem_i64(),
132            RemU32 => self.rem_u32(),
133            RemU64 => self.rem_u64(),
134            RemF32 => self.rem_f32(),
135            RemF64 => self.rem_f64(),
136            NegI32 => self.neg_i32(),
137            NegI64 => self.neg_i64(),
138            NegF32 => self.neg_f32(),
139            NegF64 => self.neg_f64(),
140            ShlI32 => self.shl_i32(),
141            ShlI64 => self.shl_i64(),
142            ShlU32 => self.shl_u32(),
143            ShlU64 => self.shl_u64(),
144            ShrI32 => self.shr_i32(),
145            ShrI64 => self.shr_i64(),
146            ShrU32 => self.shr_u32(),
147            ShrU64 => self.shr_u64(),
148            RolI32 => self.rol_i32(),
149            RolI64 => self.rol_i64(),
150            RolU32 => self.rol_u32(),
151            RolU64 => self.rol_u64(),
152            RorI32 => self.ror_i32(),
153            RorI64 => self.ror_i64(),
154            RorU32 => self.ror_u32(),
155            RorU64 => self.ror_u64(),
156            BitAndI32 => self.bitand_i32(),
157            BitAndI64 => self.bitand_i64(),
158            BitAndU32 => self.bitand_u32(),
159            BitAndU64 => self.bitand_u64(),
160            BitOrI32 => self.bitor_i32(),
161            BitOrI64 => self.bitor_i64(),
162            BitOrU32 => self.bitor_u32(),
163            BitOrU64 => self.bitor_u64(),
164            BitXorI32 => self.bitxor_i32(),
165            BitXorI64 => self.bitxor_i64(),
166            BitXorU32 => self.bitxor_u32(),
167            BitXorU64 => self.bitxor_u64(),
168            Not => self.op_not(),
169            And => self.op_and(),
170            Or => self.op_or(),
171            Xor => self.op_xor(),
172            EqI32 => self.eq_i32(),
173            EqI64 => self.eq_i64(),
174            EqU32 => self.eq_u32(),
175            EqU64 => self.eq_u64(),
176            EqF32 => self.eq_f32(),
177            EqF64 => self.eq_f64(),
178            NeI32 => self.ne_i32(),
179            NeI64 => self.ne_i64(),
180            NeU32 => self.ne_u32(),
181            NeU64 => self.ne_u64(),
182            NeF32 => self.ne_f32(),
183            NeF64 => self.ne_f64(),
184            LtI32 => self.lt_i32(),
185            LtI64 => self.lt_i64(),
186            LtU32 => self.lt_u32(),
187            LtU64 => self.lt_u64(),
188            LtF32 => self.lt_f32(),
189            LtF64 => self.lt_f64(),
190            GtI32 => self.gt_i32(),
191            GtI64 => self.gt_i64(),
192            GtU32 => self.gt_u32(),
193            GtU64 => self.gt_u64(),
194            GtF32 => self.gt_f32(),
195            GtF64 => self.gt_f64(),
196            LeI32 => self.le_i32(),
197            LeI64 => self.le_i64(),
198            LeU32 => self.le_u32(),
199            LeU64 => self.le_u64(),
200            LeF32 => self.le_f32(),
201            LeF64 => self.le_f64(),
202            GeI32 => self.ge_i32(),
203            GeI64 => self.ge_i64(),
204            GeU32 => self.ge_u32(),
205            GeU64 => self.ge_u64(),
206            GeF32 => self.ge_f32(),
207            GeF64 => self.ge_f64(),
208        }?;
209        Ok(Effects::one(outcome))
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, vihaco::Message)]
214pub enum CPUMessage {
215    None,
216    /// Target metadata selected by the composite for the executing CPU device.
217    FunctionInfo {
218        arity: u32,
219        start_address: u32,
220        local_count: u32,
221    },
222    Print(String),
223}
224
225#[dispatch(instruction = RuntimeInstruction, message = CPUMessage, effect = StepOutcome)]
226impl CPU {
227    fn execute(
228        &mut self,
229        inst: &RuntimeInstruction,
230        msg: CPUMessage,
231    ) -> eyre::Result<Effects<StepOutcome>> {
232        self.execute_generated(inst, msg)
233    }
234}
235
236impl CPU {
237    pub fn op_span(&mut self, file: u32, start: u32, end: u32) -> eyre::Result<StepOutcome> {
238        self.span = (file, start, end);
239        Ok(StepOutcome::Continue)
240    }
241
242    pub fn op_branch(&mut self, target: u32) -> eyre::Result<StepOutcome> {
243        self.set_pending_pc(target);
244        Ok(StepOutcome::Continue)
245    }
246
247    pub fn op_conditional_branch(
248        &mut self,
249        true_target: u32,
250        false_target: u32,
251    ) -> eyre::Result<StepOutcome> {
252        let cond = self.stack_pop()?;
253        match canonical_bool(cond)? {
254            true => {
255                self.set_pending_pc(true_target);
256                Ok(StepOutcome::Continue)
257            }
258            false => {
259                self.set_pending_pc(false_target);
260                Ok(StepOutcome::Continue)
261            }
262        }
263    }
264
265    pub fn op_return(&mut self, keep: u32) -> eyre::Result<StepOutcome> {
266        let frame = *self.get_frame()?;
267        let available = self
268            .stack
269            .len()
270            .checked_sub(frame.operands_index())
271            .ok_or_else(|| eyre::eyre!("frame locals out of bounds"))?;
272        if available < keep as usize {
273            return Err(eyre::eyre!("not enough values to return"));
274        }
275
276        self.pop_frame()?;
277        // Collect return values before truncating
278        let top = self.stack.len() - keep as usize;
279        let return_values: Vec<Word> = self.stack[top..].to_vec();
280        self.stack.drain(frame.base..top);
281
282        if self.get_frame().is_err() {
283            // No more frames - program is returning
284            self.set_return_values(return_values);
285            Ok(StepOutcome::Return)
286        } else {
287            self.set_pending_pc(frame.ret_pc);
288            Ok(StepOutcome::Continue)
289        }
290    }
291
292    /// Set up an invocation from arguments already on the operand stack.
293    ///
294    /// Also used for program entry: push the entry arguments before calling this
295    /// method, then begin execution at the returned pending PC. `local_count`
296    /// includes parameters and comes from the composite's function metadata.
297    ///
298    /// # Errors
299    /// Returns an error for insufficient operand arguments, a local count
300    /// smaller than the arity, or an overflowing frame size or return address.
301    pub fn enter_function(
302        &mut self,
303        arity: u32,
304        target: u32,
305        local_count: u32,
306        function: Option<usize>,
307    ) -> eyre::Result<StepOutcome> {
308        self.require_operands(arity as usize)?;
309        self.ensure_local_count_is_at_least_arity(arity, local_count)?;
310        let base = self.stack.len() - arity as usize;
311        let end = base
312            .checked_add(local_count as usize)
313            .ok_or_else(|| eyre::eyre!("frame size overflow"))?;
314        let ret_pc = if self.frames.is_empty() {
315            0
316        } else {
317            self.current_pc
318                .checked_add(1)
319                .ok_or_else(|| eyre::eyre!("return address overflow"))?
320        };
321        self.stack.resize(end, 0);
322        self.push_frame(Frame {
323            base,
324            local_count: local_count as usize,
325            span: self.span,
326            function,
327            ret_pc,
328        });
329        self.set_pending_pc(target);
330        Ok(StepOutcome::Continue)
331    }
332
333    pub fn op_call(
334        &mut self,
335        arity: u32,
336        target: u32,
337        local_count: u32,
338    ) -> eyre::Result<StepOutcome> {
339        self.enter_function(arity, target, local_count, None)
340    }
341
342    pub fn op_indirect_call(
343        &mut self,
344        arity: u32,
345        target: u32,
346        local_count: u32,
347    ) -> eyre::Result<StepOutcome> {
348        // Only the function reference is an operand; metadata comes from the message.
349        let required = (arity as usize)
350            .checked_add(1)
351            .ok_or_else(|| eyre::eyre!("argument count overflow"))?;
352        self.require_operands(required)?;
353        let function = decode_function_ref(*self.stack_top()?);
354        self.stack_pop()?;
355        self.enter_function(arity, target, local_count, Some(function as usize))
356    }
357
358    fn op_load(&mut self, addr: u32) -> eyre::Result<StepOutcome> {
359        // addr should be local to frame.
360        let value = self.get_local(addr as usize)?;
361        self.stack_push(*value);
362        Ok(StepOutcome::Continue)
363    }
364
365    pub fn op_store(&mut self, addr: u32) -> Result<StepOutcome> {
366        let address = self.local_address(addr as usize)?;
367        let value: Word = self.stack_pop()?;
368        *self
369            .stack
370            .get_mut(address)
371            .ok_or_else(|| eyre::eyre!("local index out of bounds"))? = value;
372        Ok(StepOutcome::Continue)
373    }
374
375    pub fn op_dup(&mut self) -> Result<StepOutcome> {
376        let v = *self.stack_top()?;
377        self.stack.push(v);
378        Ok(StepOutcome::Continue)
379    }
380
381    pub fn op_heap_alloc(&mut self, n_elements: u32) -> Result<StepOutcome> {
382        let n: usize = n_elements as usize;
383        self.require_operands(n)?;
384        let start = self.stack.len() - n;
385        let values: Box<[Word]> = self.stack.drain(start..).collect();
386        let heap_id = self.push_heap_object(values);
387        self.stack_push(encode_heap_ref(heap_id));
388        Ok(StepOutcome::Continue)
389    }
390
391    pub fn op_get_item(&mut self) -> Result<StepOutcome> {
392        let index = Self::heap_index(self.stack_pop()?)?;
393        let heap_id = decode_heap_ref(self.stack_pop()?);
394        let value = *self
395            .heap_object(heap_id)?
396            .get(index)
397            .ok_or_else(|| eyre::eyre!("heap index {} out of bounds", index))?;
398        self.stack_push(value);
399        Ok(StepOutcome::Continue)
400    }
401
402    pub fn op_heap_dealloc(&mut self) -> Result<StepOutcome> {
403        let id = decode_heap_ref(self.stack_pop()?);
404        self.dealloc_heap_object(id)?;
405        Ok(StepOutcome::Continue)
406    }
407
408    pub fn op_const(&mut self, v: Word) -> Result<StepOutcome> {
409        self.stack.push(v);
410        Ok(StepOutcome::Continue)
411    }
412
413    fn heap_index(value: Word) -> Result<usize> {
414        match decode_i64(value) {
415            index if index >= 0 => usize::try_from(index)
416                .map_err(|_| eyre::eyre!("heap index {} does not fit in usize", index)),
417            index => Err(eyre::eyre!(
418                "heap index must be non-negative, got {}",
419                index
420            )),
421        }
422    }
423}
424
425#[cfg(test)]
426#[allow(clippy::items_after_test_module)]
427mod tests {
428    use super::*;
429    use vihaco::{Effects, GeneratedComponent, frame::Frame, traits::StackMemory};
430    use vihaco_parser::Ident;
431
432    trait ExecuteInstruction {
433        fn execute_instruction(&mut self, instruction: RuntimeInstruction) -> Result<StepOutcome>;
434    }
435
436    impl ExecuteInstruction for CPU {
437        fn execute_instruction(&mut self, instruction: RuntimeInstruction) -> Result<StepOutcome> {
438            vihaco::expect_exactly_one_effect(GeneratedComponent::execute_generated(
439                self,
440                &instruction,
441                CPUMessage::None,
442            )?)
443        }
444    }
445
446    #[test]
447    fn cpu_generated_component_executes_instruction_without_message() {
448        let mut cpu = CPU::default();
449
450        GeneratedComponent::execute_generated(
451            &mut cpu,
452            &RuntimeInstruction::ConstI64(encode_i64(7)),
453            CPUMessage::None,
454        )
455        .unwrap();
456
457        assert_eq!(cpu.stack(), &vec![encode_i64(7)]);
458    }
459
460    #[test]
461    fn execute_instruction_applies_control_flow_without_action() {
462        let mut cpu = CPU::default();
463
464        let branch = cpu
465            .execute_instruction(RuntimeInstruction::Branch(9))
466            .unwrap();
467        assert_eq!(branch, StepOutcome::Continue);
468        assert_eq!(cpu.take_pending_pc(), Some(9));
469
470        let halt = cpu.execute_instruction(RuntimeInstruction::Halt).unwrap();
471        assert_eq!(halt, StepOutcome::Halt);
472        assert_eq!(cpu.take_pending_pc(), None);
473    }
474
475    #[test]
476    fn op_return_stores_terminal_values_in_runtime_state() {
477        let mut cpu = CPU::default();
478        cpu.push_frame(Frame {
479            base: 0,
480            local_count: 0,
481            span: (0, 0, 0),
482            function: None,
483            ret_pc: 0,
484        });
485        cpu.stack_push(encode_i64(7));
486
487        let outcome = cpu
488            .execute_instruction(RuntimeInstruction::Return(1))
489            .unwrap();
490
491        assert_eq!(outcome, StepOutcome::Return);
492        assert_eq!(cpu.return_values(), &[encode_i64(7)]);
493    }
494
495    #[test]
496    fn op_return_restores_callers_pc() {
497        let mut cpu = CPU {
498            current_pc: 10,
499            ..Default::default()
500        };
501        // Outer ("main") frame so the inner Return takes the Continue branch.
502        cpu.push_frame(Frame {
503            base: 0,
504            local_count: 0,
505            span: (0, 0, 0),
506            function: None,
507            ret_pc: 0,
508        });
509
510        // Caller would be executing `call 0, 100` at some PC; op_call sets
511        // pending_pc to the callee target.
512        GeneratedComponent::execute_generated(
513            &mut cpu,
514            &RuntimeInstruction::Call(0, 100),
515            CPUMessage::FunctionInfo {
516                arity: 0,
517                start_address: 100,
518                local_count: 0,
519            },
520        )
521        .unwrap();
522        assert_eq!(cpu.take_pending_pc(), Some(100));
523        assert_eq!(cpu.frames[1].ret_pc, 11);
524
525        // Callee returns immediately. pending_pc should be restored to the
526        // instruction after the call.
527        let outcome = cpu
528            .execute_instruction(RuntimeInstruction::Return(0))
529            .unwrap();
530        assert_eq!(outcome, StepOutcome::Continue);
531        assert_eq!(cpu.take_pending_pc(), Some(11),);
532    }
533
534    #[test]
535    fn op_indirect_call_records_return_pc_after_call_site() {
536        let mut cpu = CPU {
537            current_pc: 10,
538            ..Default::default()
539        };
540        cpu.push_frame(Frame {
541            base: 0,
542            local_count: 0,
543            span: (0, 0, 0),
544            function: None,
545            ret_pc: 0,
546        });
547
548        // The composite supplies arity/address/count; only FunctionRef is on the stack.
549        cpu.stack_push(encode_function_ref(7));
550        GeneratedComponent::execute_generated(
551            &mut cpu,
552            &RuntimeInstruction::IndirectCall,
553            CPUMessage::FunctionInfo {
554                arity: 0,
555                start_address: 100,
556                local_count: 0,
557            },
558        )
559        .unwrap();
560        assert_eq!(cpu.take_pending_pc(), Some(100));
561        assert_eq!(cpu.frames[1].ret_pc, 11);
562
563        let outcome = cpu
564            .execute_instruction(RuntimeInstruction::Return(0))
565            .unwrap();
566        assert_eq!(outcome, StepOutcome::Continue);
567        assert_eq!(cpu.take_pending_pc(), Some(11));
568    }
569
570    #[test]
571    fn op_return_keeps_bottom_of_frame_when_callee_leaves_scratch() {
572        let mut cpu = CPU::default();
573        // Outer frame so Return takes the Continue branch.
574        cpu.push_frame(Frame {
575            base: 0,
576            local_count: 0,
577            span: (0, 0, 0),
578            function: None,
579            ret_pc: 0,
580        });
581
582        // Simulate a callee frame holding [scratch_a, scratch_b, return_val]
583        // where only `return_val` (the top) should survive `ret 1`.
584        cpu.push_frame(Frame {
585            base: 0,
586            local_count: 0,
587            span: (0, 0, 0),
588            function: None,
589            ret_pc: 0,
590        });
591        cpu.stack_push(encode_i64(111)); // scratch — bottom of callee frame
592        cpu.stack_push(encode_i64(222)); // scratch — middle
593        cpu.stack_push(encode_i64(999)); // intended return value — top
594
595        let outcome = cpu
596            .execute_instruction(RuntimeInstruction::Return(1))
597            .unwrap();
598        assert_eq!(outcome, StepOutcome::Continue);
599
600        assert_eq!(cpu.stack(), &vec![encode_i64(999)],);
601    }
602
603    #[test]
604    fn op_heap_alloc_preserves_natural_push_order_and_returns_heap_ref() {
605        let mut cpu = CPU::default();
606        cpu.stack_push(encode_i64(10));
607        cpu.stack_push(encode_i64(20));
608        cpu.stack_push(encode_i64(30));
609
610        let outcome = cpu
611            .execute_instruction(RuntimeInstruction::HeapAlloc(3))
612            .unwrap();
613
614        assert_eq!(outcome, StepOutcome::Continue);
615        assert_eq!(cpu.stack(), &vec![encode_heap_ref(0)]);
616        assert_eq!(
617            cpu.heap.get(0).unwrap(),
618            &[encode_i64(10), encode_i64(20), encode_i64(30)]
619        );
620    }
621
622    #[test]
623    fn op_heap_alloc_supports_empty_heap_objects() {
624        let mut cpu = CPU::default();
625
626        let outcome = cpu
627            .execute_instruction(RuntimeInstruction::HeapAlloc(0))
628            .unwrap();
629
630        assert_eq!(outcome, StepOutcome::Continue);
631        assert_eq!(cpu.stack(), &vec![encode_heap_ref(0)]);
632        assert_eq!(cpu.heap.get(0).unwrap(), &[] as &[Word]);
633    }
634
635    #[test]
636    fn op_get_item_reads_heap_value() {
637        let mut cpu = CPU::default();
638        cpu.stack_push(encode_i64(10));
639        cpu.stack_push(encode_i64(20));
640        cpu.stack_push(encode_i64(30));
641        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(3))
642            .unwrap();
643        cpu.stack_push(encode_u32(1));
644
645        let outcome = cpu
646            .execute_instruction(RuntimeInstruction::GetItem)
647            .unwrap();
648
649        assert_eq!(outcome, StepOutcome::Continue);
650        assert_eq!(cpu.stack(), &vec![encode_i64(20)]);
651    }
652
653    #[test]
654    fn op_get_item_rejects_non_heap_refs() {
655        let mut cpu = CPU::default();
656        cpu.stack_push(encode_i64(7));
657        cpu.stack_push(encode_u32(0));
658
659        let err = cpu
660            .execute_instruction(RuntimeInstruction::GetItem)
661            .unwrap_err();
662
663        assert!(err.to_string().contains("heap"));
664    }
665
666    #[test]
667    fn op_get_item_rejects_invalid_heap_ids() {
668        let mut cpu = CPU::default();
669        cpu.stack_push(encode_heap_ref(99));
670        cpu.stack_push(encode_u32(0));
671
672        let err = cpu
673            .execute_instruction(RuntimeInstruction::GetItem)
674            .unwrap_err();
675
676        assert!(err.to_string().contains("heap"));
677    }
678
679    #[test]
680    fn op_get_item_rejects_out_of_bounds_indices() {
681        let mut cpu = CPU::default();
682        cpu.stack_push(encode_i64(10));
683        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
684            .unwrap();
685        cpu.stack_push(encode_u32(3));
686
687        let err = cpu
688            .execute_instruction(RuntimeInstruction::GetItem)
689            .unwrap_err();
690
691        assert!(err.to_string().contains("index"));
692    }
693
694    #[test]
695    fn reset_clears_heap_allocations() {
696        let mut cpu = CPU::default();
697        cpu.stack_push(encode_i64(10));
698        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
699            .unwrap();
700
701        cpu.reset();
702
703        assert!(cpu.heap.is_empty());
704        assert!(cpu.stack().is_empty());
705    }
706
707    #[test]
708    fn execute_generated_dispatches_instruction_without_message() {
709        let mut cpu = CPU::default();
710        cpu.push_frame(Frame {
711            base: 0,
712            local_count: 0,
713            span: (0, 0, 0),
714            function: None,
715            ret_pc: 0,
716        });
717
718        let outcome = GeneratedComponent::execute_generated(
719            &mut cpu,
720            &RuntimeInstruction::ConstI64(encode_i64(99)),
721            CPUMessage::None,
722        )
723        .unwrap();
724
725        assert_eq!(outcome, Effects::one(StepOutcome::Continue));
726        assert_eq!(cpu.stack(), &vec![encode_i64(99)]);
727    }
728
729    #[test]
730    fn execute_generated_function_info_is_only_accepted_for_calls() {
731        let mut cpu = CPU::default();
732        cpu.push_frame(Frame {
733            base: 0,
734            local_count: 0,
735            span: (0, 0, 0),
736            function: None,
737            ret_pc: 0,
738        });
739
740        let outcome = GeneratedComponent::execute_generated(
741            &mut cpu,
742            &RuntimeInstruction::Label(Ident("label".to_owned())),
743            CPUMessage::FunctionInfo {
744                arity: 2,
745                start_address: 42,
746                local_count: 2,
747            },
748        )
749        .unwrap_err();
750
751        assert!(outcome.to_string().contains("only valid for call"));
752        assert!(cpu.stack().is_empty());
753    }
754
755    #[test]
756    fn execute_generated_print_returns_control_effect_and_pops_stack() {
757        let mut cpu = CPU::default();
758        cpu.push_frame(Frame {
759            base: 0,
760            local_count: 0,
761            span: (0, 0, 0),
762            function: None,
763            ret_pc: 0,
764        });
765        cpu.stack_push(encode_i64(42));
766
767        let outcome = GeneratedComponent::execute_generated(
768            &mut cpu,
769            &RuntimeInstruction::Print,
770            CPUMessage::Print("hello".into()),
771        )
772        .unwrap();
773
774        assert_eq!(outcome, Effects::one(StepOutcome::Continue));
775        assert!(cpu.stack().is_empty());
776    }
777
778    #[test]
779    fn execute_generated_print_rejects_wrong_message() {
780        let mut cpu = CPU::default();
781        cpu.push_frame(Frame {
782            base: 0,
783            local_count: 0,
784            span: (0, 0, 0),
785            function: None,
786            ret_pc: 0,
787        });
788        cpu.stack_push(encode_i64(42));
789
790        let err = GeneratedComponent::execute_generated(
791            &mut cpu,
792            &RuntimeInstruction::Print,
793            CPUMessage::None,
794        )
795        .unwrap_err();
796
797        assert!(err.to_string().contains("Print requires"));
798    }
799
800    #[test]
801    fn op_heap_dealloc_marks_slot_dead() {
802        let mut cpu = CPU::default();
803        cpu.stack_push(encode_i64(42));
804        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
805            .unwrap();
806        cpu.stack_push(encode_heap_ref(0));
807
808        cpu.execute_instruction(RuntimeInstruction::HeapDealloc)
809            .unwrap();
810
811        assert!(
812            cpu.heap
813                .get(0)
814                .unwrap_err()
815                .to_string()
816                .contains("deallocated")
817        );
818    }
819
820    #[test]
821    fn op_heap_dealloc_slot_is_reused_on_next_alloc() {
822        let mut cpu = CPU::default();
823        cpu.stack_push(encode_i64(1));
824        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
825            .unwrap();
826        cpu.execute_instruction(RuntimeInstruction::HeapDealloc)
827            .unwrap();
828
829        cpu.stack_push(encode_i64(2));
830        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
831            .unwrap();
832
833        assert_eq!(cpu.stack(), &vec![encode_heap_ref(0)]);
834        assert_eq!(cpu.heap.get(0).unwrap(), &[encode_i64(2)]);
835    }
836
837    #[test]
838    fn op_heap_dealloc_rejects_double_free() {
839        let mut cpu = CPU::default();
840        cpu.stack_push(encode_i64(1));
841        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
842            .unwrap();
843        cpu.stack_push(encode_heap_ref(0));
844        cpu.execute_instruction(RuntimeInstruction::HeapDealloc)
845            .unwrap();
846
847        cpu.stack_push(encode_heap_ref(0));
848        let err = cpu
849            .execute_instruction(RuntimeInstruction::HeapDealloc)
850            .unwrap_err();
851
852        assert!(err.to_string().contains("double-free"));
853    }
854
855    #[test]
856    fn op_heap_dealloc_rejects_invalid_id() {
857        let mut cpu = CPU::default();
858        cpu.stack_push(encode_heap_ref(99));
859
860        let err = cpu
861            .execute_instruction(RuntimeInstruction::HeapDealloc)
862            .unwrap_err();
863
864        assert!(err.to_string().contains("invalid heap object id"));
865    }
866
867    #[test]
868    fn reset_clears_free_list() {
869        let mut cpu = CPU::default();
870        cpu.stack_push(encode_i64(1));
871        cpu.execute_instruction(RuntimeInstruction::HeapAlloc(1))
872            .unwrap();
873        cpu.stack_push(encode_heap_ref(0));
874        cpu.execute_instruction(RuntimeInstruction::HeapDealloc)
875            .unwrap();
876
877        cpu.reset();
878
879        assert!(cpu.heap.is_empty());
880    }
881
882    #[test]
883    fn typed_word_arithmetic_canonicalizes_narrow_results() {
884        let mut cpu = CPU::default();
885        cpu.stack_push(encode_i32(i32::MAX));
886        cpu.stack_push(encode_i32(1));
887        cpu.execute_instruction(RuntimeInstruction::AddI32).unwrap();
888        assert_eq!(cpu.stack_pop().unwrap(), encode_i32(i32::MIN));
889
890        cpu.stack_push(encode_u32(u32::MAX));
891        cpu.stack_push(encode_u32(1));
892        cpu.execute_instruction(RuntimeInstruction::AddU32).unwrap();
893        assert_eq!(cpu.stack_pop().unwrap(), 0);
894
895        cpu.stack_push(encode_f32(1.5));
896        cpu.stack_push(encode_f32(2.0));
897        cpu.execute_instruction(RuntimeInstruction::MulF32).unwrap();
898        assert_eq!(decode_f32(cpu.stack_pop().unwrap()), 3.0);
899    }
900
901    #[test]
902    fn integer_division_and_remainder_report_errors() {
903        let mut cpu = CPU::default();
904        cpu.stack_push(encode_i64(7));
905        cpu.stack_push(encode_i64(0));
906        assert!(cpu.execute_instruction(RuntimeInstruction::DivI64).is_err());
907
908        cpu.stack_push(encode_u32(7));
909        cpu.stack_push(encode_u32(0));
910        assert!(cpu.execute_instruction(RuntimeInstruction::RemU32).is_err());
911    }
912
913    #[test]
914    fn boolean_words_must_be_canonical() {
915        let mut cpu = CPU::default();
916        cpu.stack_push(2u64);
917        assert!(cpu.execute_instruction(RuntimeInstruction::Not).is_err());
918
919        cpu.stack_push(2u64);
920        assert!(
921            cpu.execute_instruction(RuntimeInstruction::ConditionalBranch(1, 2))
922                .is_err()
923        );
924    }
925}
926
927fn canonical_bool(value: Word) -> Result<bool> {
928    match value {
929        0 => Ok(false),
930        1 => Ok(true),
931        other => Err(eyre::eyre!("invalid boolean word {}", other)),
932    }
933}
934
935macro_rules! int_wrapping {
936    ($($name:ident {
937        decode: $decode:ident,
938        encode: $encode:ident,
939        operation: $op:ident
940    });+ $(;)?) => {$ (
941        #[inline(always)]
942        fn $name(&mut self) -> Result<StepOutcome> {
943            let rhs = $decode(self.stack_pop()?);
944            let lhs = $decode(self.stack_pop()?);
945            self.stack_push($encode(lhs.$op(rhs)));
946            Ok(StepOutcome::Continue)
947        }
948    )+ };
949}
950
951macro_rules! int_checked {
952    ($($name:ident {
953        decode: $decode:ident,
954        encode: $encode:ident,
955        operation: $op:ident,
956        error: $message:literal
957    });+ $(;)?) => {$ (
958        #[inline(always)]
959        fn $name(&mut self) -> Result<StepOutcome> {
960            let rhs = $decode(self.stack_pop()?);
961            let lhs = $decode(self.stack_pop()?);
962            let value = lhs.$op(rhs).ok_or_else(|| eyre::eyre!($message))?;
963            self.stack_push($encode(value));
964            Ok(StepOutcome::Continue)
965        }
966    )+ };
967}
968
969macro_rules! float_binary {
970    ($($name:ident {
971        decode: $decode:ident,
972        encode: $encode:ident,
973        operator: $op:tt
974    });+ $(;)?) => {$ (
975        #[inline(always)]
976        fn $name(&mut self) -> Result<StepOutcome> {
977            let rhs = $decode(self.stack_pop()?);
978            let lhs = $decode(self.stack_pop()?);
979            self.stack_push($encode(lhs $op rhs));
980            Ok(StepOutcome::Continue)
981        }
982    )+ };
983}
984
985macro_rules! shift {
986    ($($name:ident {
987        decode: $decode:ident,
988        encode: $encode:ident,
989        operation: $op:ident,
990        count_mask: $mask:expr
991    });+ $(;)?) => {$ (
992        #[inline(always)]
993        fn $name(&mut self) -> Result<StepOutcome> {
994            let rhs = decode_u32(self.stack_pop()?);
995            let lhs = $decode(self.stack_pop()?);
996            self.stack_push($encode(lhs.$op(rhs & $mask)));
997            Ok(StepOutcome::Continue)
998        }
999    )+ };
1000}
1001
1002macro_rules! rotate {
1003    ($($name:ident {
1004        decode: $decode:ident,
1005        encode: $encode:ident,
1006        operation: $op:ident
1007    });+ $(;)?) => {$ (
1008        #[inline(always)]
1009        fn $name(&mut self) -> Result<StepOutcome> {
1010            let rhs = decode_u32(self.stack_pop()?);
1011            let lhs = $decode(self.stack_pop()?);
1012            self.stack_push($encode(lhs.$op(rhs)));
1013            Ok(StepOutcome::Continue)
1014        }
1015    )+ };
1016}
1017
1018macro_rules! bitwise {
1019    ($($name:ident {
1020        decode: $decode:ident,
1021        encode: $encode:ident,
1022        operator: $op:tt
1023    });+ $(;)?) => {$ (
1024        #[inline(always)]
1025        fn $name(&mut self) -> Result<StepOutcome> {
1026            let rhs = $decode(self.stack_pop()?);
1027            let lhs = $decode(self.stack_pop()?);
1028            self.stack_push($encode(lhs $op rhs));
1029            Ok(StepOutcome::Continue)
1030        }
1031    )+ };
1032}
1033
1034macro_rules! compare {
1035    ($($name:ident {
1036        decode: $decode:ident,
1037        operator: $op:tt
1038    });+ $(;)?) => {$ (
1039        #[inline(always)]
1040        fn $name(&mut self) -> Result<StepOutcome> {
1041            let rhs = $decode(self.stack_pop()?);
1042            let lhs = $decode(self.stack_pop()?);
1043            self.stack_push(encode_bool(lhs $op rhs));
1044            Ok(StepOutcome::Continue)
1045        }
1046    )+ };
1047}
1048
1049impl CPU {
1050    int_wrapping! {
1051        add_i32 { decode: decode_i32, encode: encode_i32, operation: wrapping_add };
1052        add_i64 { decode: decode_i64, encode: encode_i64, operation: wrapping_add };
1053        add_u32 { decode: decode_u32, encode: encode_u32, operation: wrapping_add };
1054        add_u64 { decode: decode_u64, encode: encode_u64, operation: wrapping_add };
1055        sub_i32 { decode: decode_i32, encode: encode_i32, operation: wrapping_sub };
1056        sub_i64 { decode: decode_i64, encode: encode_i64, operation: wrapping_sub };
1057        sub_u32 { decode: decode_u32, encode: encode_u32, operation: wrapping_sub };
1058        sub_u64 { decode: decode_u64, encode: encode_u64, operation: wrapping_sub };
1059        mul_i32 { decode: decode_i32, encode: encode_i32, operation: wrapping_mul };
1060        mul_i64 { decode: decode_i64, encode: encode_i64, operation: wrapping_mul };
1061        mul_u32 { decode: decode_u32, encode: encode_u32, operation: wrapping_mul };
1062        mul_u64 { decode: decode_u64, encode: encode_u64, operation: wrapping_mul };
1063    }
1064    int_checked! {
1065        div_i32 { decode: decode_i32, encode: encode_i32, operation: checked_div, error: "integer division error" };
1066        div_i64 { decode: decode_i64, encode: encode_i64, operation: checked_div, error: "integer division error" };
1067        div_u32 { decode: decode_u32, encode: encode_u32, operation: checked_div, error: "integer division error" };
1068        div_u64 { decode: decode_u64, encode: encode_u64, operation: checked_div, error: "integer division error" };
1069        rem_i32 { decode: decode_i32, encode: encode_i32, operation: checked_rem, error: "integer remainder error" };
1070        rem_i64 { decode: decode_i64, encode: encode_i64, operation: checked_rem, error: "integer remainder error" };
1071        rem_u32 { decode: decode_u32, encode: encode_u32, operation: checked_rem, error: "integer remainder error" };
1072        rem_u64 { decode: decode_u64, encode: encode_u64, operation: checked_rem, error: "integer remainder error" };
1073    }
1074    float_binary! {
1075        add_f32 { decode: decode_f32, encode: encode_f32, operator: + };
1076        add_f64 { decode: decode_f64, encode: encode_f64, operator: + };
1077        sub_f32 { decode: decode_f32, encode: encode_f32, operator: - };
1078        sub_f64 { decode: decode_f64, encode: encode_f64, operator: - };
1079        mul_f32 { decode: decode_f32, encode: encode_f32, operator: * };
1080        mul_f64 { decode: decode_f64, encode: encode_f64, operator: * };
1081        div_f32 { decode: decode_f32, encode: encode_f32, operator: / };
1082        div_f64 { decode: decode_f64, encode: encode_f64, operator: / };
1083        rem_f32 { decode: decode_f32, encode: encode_f32, operator: % };
1084        rem_f64 { decode: decode_f64, encode: encode_f64, operator: % };
1085    }
1086
1087    #[inline(always)]
1088    fn neg_i32(&mut self) -> Result<StepOutcome> {
1089        let value = decode_i32(self.stack_pop()?).wrapping_neg();
1090        self.stack_push(encode_i32(value));
1091        Ok(StepOutcome::Continue)
1092    }
1093    #[inline(always)]
1094    fn neg_i64(&mut self) -> Result<StepOutcome> {
1095        let value = decode_i64(self.stack_pop()?).wrapping_neg();
1096        self.stack_push(encode_i64(value));
1097        Ok(StepOutcome::Continue)
1098    }
1099    #[inline(always)]
1100    fn neg_f32(&mut self) -> Result<StepOutcome> {
1101        let value = -decode_f32(self.stack_pop()?);
1102        self.stack_push(encode_f32(value));
1103        Ok(StepOutcome::Continue)
1104    }
1105    #[inline(always)]
1106    fn neg_f64(&mut self) -> Result<StepOutcome> {
1107        let value = -decode_f64(self.stack_pop()?);
1108        self.stack_push(encode_f64(value));
1109        Ok(StepOutcome::Continue)
1110    }
1111
1112    shift! {
1113        shl_i32 { decode: decode_i32, encode: encode_i32, operation: wrapping_shl, count_mask: 31 };
1114        shl_i64 { decode: decode_i64, encode: encode_i64, operation: wrapping_shl, count_mask: 63 };
1115        shl_u32 { decode: decode_u32, encode: encode_u32, operation: wrapping_shl, count_mask: 31 };
1116        shl_u64 { decode: decode_u64, encode: encode_u64, operation: wrapping_shl, count_mask: 63 };
1117        shr_i32 { decode: decode_i32, encode: encode_i32, operation: wrapping_shr, count_mask: 31 };
1118        shr_i64 { decode: decode_i64, encode: encode_i64, operation: wrapping_shr, count_mask: 63 };
1119        shr_u32 { decode: decode_u32, encode: encode_u32, operation: wrapping_shr, count_mask: 31 };
1120        shr_u64 { decode: decode_u64, encode: encode_u64, operation: wrapping_shr, count_mask: 63 };
1121    }
1122    rotate! {
1123        rol_i32 { decode: decode_i32, encode: encode_i32, operation: rotate_left };
1124        rol_i64 { decode: decode_i64, encode: encode_i64, operation: rotate_left };
1125        rol_u32 { decode: decode_u32, encode: encode_u32, operation: rotate_left };
1126        rol_u64 { decode: decode_u64, encode: encode_u64, operation: rotate_left };
1127        ror_i32 { decode: decode_i32, encode: encode_i32, operation: rotate_right };
1128        ror_i64 { decode: decode_i64, encode: encode_i64, operation: rotate_right };
1129        ror_u32 { decode: decode_u32, encode: encode_u32, operation: rotate_right };
1130        ror_u64 { decode: decode_u64, encode: encode_u64, operation: rotate_right };
1131    }
1132    bitwise! {
1133        bitand_i32 { decode: decode_i32, encode: encode_i32, operator: & };
1134        bitand_i64 { decode: decode_i64, encode: encode_i64, operator: & };
1135        bitand_u32 { decode: decode_u32, encode: encode_u32, operator: & };
1136        bitand_u64 { decode: decode_u64, encode: encode_u64, operator: & };
1137        bitor_i32 { decode: decode_i32, encode: encode_i32, operator: | };
1138        bitor_i64 { decode: decode_i64, encode: encode_i64, operator: | };
1139        bitor_u32 { decode: decode_u32, encode: encode_u32, operator: | };
1140        bitor_u64 { decode: decode_u64, encode: encode_u64, operator: | };
1141        bitxor_i32 { decode: decode_i32, encode: encode_i32, operator: ^ };
1142        bitxor_i64 { decode: decode_i64, encode: encode_i64, operator: ^ };
1143        bitxor_u32 { decode: decode_u32, encode: encode_u32, operator: ^ };
1144        bitxor_u64 { decode: decode_u64, encode: encode_u64, operator: ^ };
1145    }
1146    compare! {
1147        eq_i32 { decode: decode_i32, operator: == };
1148        eq_i64 { decode: decode_i64, operator: == };
1149        eq_u32 { decode: decode_u32, operator: == };
1150        eq_u64 { decode: decode_u64, operator: == };
1151        eq_f32 { decode: decode_f32, operator: == };
1152        eq_f64 { decode: decode_f64, operator: == };
1153        ne_i32 { decode: decode_i32, operator: != };
1154        ne_i64 { decode: decode_i64, operator: != };
1155        ne_u32 { decode: decode_u32, operator: != };
1156        ne_u64 { decode: decode_u64, operator: != };
1157        ne_f32 { decode: decode_f32, operator: != };
1158        ne_f64 { decode: decode_f64, operator: != };
1159        lt_i32 { decode: decode_i32, operator: < };
1160        lt_i64 { decode: decode_i64, operator: < };
1161        lt_u32 { decode: decode_u32, operator: < };
1162        lt_u64 { decode: decode_u64, operator: < };
1163        lt_f32 { decode: decode_f32, operator: < };
1164        lt_f64 { decode: decode_f64, operator: < };
1165        gt_i32 { decode: decode_i32, operator: > };
1166        gt_i64 { decode: decode_i64, operator: > };
1167        gt_u32 { decode: decode_u32, operator: > };
1168        gt_u64 { decode: decode_u64, operator: > };
1169        gt_f32 { decode: decode_f32, operator: > };
1170        gt_f64 { decode: decode_f64, operator: > };
1171        le_i32 { decode: decode_i32, operator: <= };
1172        le_i64 { decode: decode_i64, operator: <= };
1173        le_u32 { decode: decode_u32, operator: <= };
1174        le_u64 { decode: decode_u64, operator: <= };
1175        le_f32 { decode: decode_f32, operator: <= };
1176        le_f64 { decode: decode_f64, operator: <= };
1177        ge_i32 { decode: decode_i32, operator: >= };
1178        ge_i64 { decode: decode_i64, operator: >= };
1179        ge_u32 { decode: decode_u32, operator: >= };
1180        ge_u64 { decode: decode_u64, operator: >= };
1181        ge_f32 { decode: decode_f32, operator: >= };
1182        ge_f64 { decode: decode_f64, operator: >= };
1183    }
1184
1185    fn op_not(&mut self) -> Result<StepOutcome> {
1186        let value = !canonical_bool(self.stack_pop()?)?;
1187        self.stack_push(encode_bool(value));
1188        Ok(StepOutcome::Continue)
1189    }
1190    fn op_and(&mut self) -> Result<StepOutcome> {
1191        let rhs = canonical_bool(self.stack_pop()?)?;
1192        let lhs = canonical_bool(self.stack_pop()?)?;
1193        self.stack_push(encode_bool(lhs && rhs));
1194        Ok(StepOutcome::Continue)
1195    }
1196    fn op_or(&mut self) -> Result<StepOutcome> {
1197        let rhs = canonical_bool(self.stack_pop()?)?;
1198        let lhs = canonical_bool(self.stack_pop()?)?;
1199        self.stack_push(encode_bool(lhs || rhs));
1200        Ok(StepOutcome::Continue)
1201    }
1202    fn op_xor(&mut self) -> Result<StepOutcome> {
1203        let rhs = canonical_bool(self.stack_pop()?)?;
1204        let lhs = canonical_bool(self.stack_pop()?)?;
1205        self.stack_push(encode_bool(lhs ^ rhs));
1206        Ok(StepOutcome::Continue)
1207    }
1208}