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