Skip to main content

logicaffeine_compile/vm/
disasm.rs

1//! Bytecode disassembler — renders a [`CompiledProgram`]'s `Op` stream as readable
2//! text for the debugger (status line, bytecode tape) and any future
3//! `largo --disasm`. Common ops get a clean infix form (constants resolved,
4//! registers as `R{n}`, jump targets inline); the long tail falls back to the
5//! `Op` `Debug` form, so a newly added op is never a panic — just a plainer line.
6
7use super::instruction::{CompiledProgram, ConstIdx, Constant, Op, Reg};
8
9/// One disassembled instruction.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct DisasmLine {
12    pub pc: usize,
13    pub text: String,
14    /// Absolute jump/branch targets this op can transfer control to (for the
15    /// bytecode tape's arrows). Empty for straight-line ops.
16    pub targets: Vec<usize>,
17}
18
19/// Disassemble every instruction in `prog.code` (Main first, then the function
20/// bodies, which share the one `code` vector).
21pub fn disassemble(prog: &CompiledProgram) -> Vec<DisasmLine> {
22    prog.code
23        .iter()
24        .enumerate()
25        .map(|(pc, op)| DisasmLine { pc, text: format_op(op, prog), targets: op_targets(op) })
26        .collect()
27}
28
29/// Render one instruction. Registers print as `R{n}`; constants and global names
30/// are resolved against `prog`.
31pub fn format_op(op: &Op, prog: &CompiledProgram) -> String {
32    let r = |reg: Reg| format!("R{reg}");
33    let k = |idx: ConstIdx| format_const(prog.constants.get(idx as usize));
34    let g = |idx: u16| {
35        prog.globals
36            .get(idx as usize)
37            .cloned()
38            .unwrap_or_else(|| format!("global#{idx}"))
39    };
40    match *op {
41        Op::LoadConst { dst, idx } => format!("LoadConst {} = {}", r(dst), k(idx)),
42        Op::Move { dst, src } => format!("Move {} = {}", r(dst), r(src)),
43        Op::EnsureOwned { reg } => format!("EnsureOwned {}", r(reg)),
44        Op::Add { dst, lhs, rhs } => format!("Add {} = {} + {}", r(dst), r(lhs), r(rhs)),
45        Op::AddAssign { dst, src } => format!("AddAssign {} += {}", r(dst), r(src)),
46        Op::Sub { dst, lhs, rhs } => format!("Sub {} = {} - {}", r(dst), r(lhs), r(rhs)),
47        Op::Mul { dst, lhs, rhs } => format!("Mul {} = {} * {}", r(dst), r(lhs), r(rhs)),
48        Op::Div { dst, lhs, rhs } => format!("Div {} = {} / {}", r(dst), r(lhs), r(rhs)),
49        Op::FloorDiv { dst, lhs, rhs } => format!("FloorDiv {} = {} // {}", r(dst), r(lhs), r(rhs)),
50        Op::Mod { dst, lhs, rhs } => format!("Mod {} = {} % {}", r(dst), r(lhs), r(rhs)),
51        Op::Lt { dst, lhs, rhs } => format!("Lt {} = {} < {}", r(dst), r(lhs), r(rhs)),
52        Op::Gt { dst, lhs, rhs } => format!("Gt {} = {} > {}", r(dst), r(lhs), r(rhs)),
53        Op::LtEq { dst, lhs, rhs } => format!("LtEq {} = {} <= {}", r(dst), r(lhs), r(rhs)),
54        Op::GtEq { dst, lhs, rhs } => format!("GtEq {} = {} >= {}", r(dst), r(lhs), r(rhs)),
55        Op::Eq { dst, lhs, rhs } => format!("Eq {} = {} == {}", r(dst), r(lhs), r(rhs)),
56        Op::NotEq { dst, lhs, rhs } => format!("NotEq {} = {} != {}", r(dst), r(lhs), r(rhs)),
57        Op::Not { dst, src } => format!("Not {} = !{}", r(dst), r(src)),
58        Op::Concat { dst, lhs, rhs } => format!("Concat {} = {} ++ {}", r(dst), r(lhs), r(rhs)),
59        Op::Jump { target } => format!("Jump -> {target}"),
60        Op::JumpIfFalse { cond, target } => format!("JumpIfFalse {} -> {target}", r(cond)),
61        Op::JumpIfTrue { cond, target } => format!("JumpIfTrue {} -> {target}", r(cond)),
62        Op::Call { dst, func, args_start, arg_count } => {
63            format!("Call {} = fn#{func}({}..+{arg_count})", r(dst), r(args_start))
64        }
65        Op::Return { src } => format!("Return {}", r(src)),
66        Op::ReturnNothing => "ReturnNothing".to_string(),
67        Op::GlobalGet { dst, idx } => format!("GlobalGet {} = {}", r(dst), g(idx)),
68        Op::GlobalSet { idx, src } => format!("GlobalSet {} = {}", g(idx), r(src)),
69        Op::NewEmptyList { dst } => format!("NewEmptyList {}", r(dst)),
70        Op::NewList { dst, start, count } => format!("NewList {} = [{}..+{count}]", r(dst), r(start)),
71        Op::NewRange { dst, start, end } => format!("NewRange {} = {}..={}", r(dst), r(start), r(end)),
72        Op::ListPush { list, value } => format!("ListPush {} <- {}", r(list), r(value)),
73        Op::ListPop { list, dst } => format!("ListPop {} = {}.pop()", r(dst), r(list)),
74        Op::Index { dst, collection, index } => format!("Index {} = {}[{}]", r(dst), r(collection), r(index)),
75        Op::SetIndex { collection, index, value } => {
76            format!("SetIndex {}[{}] = {}", r(collection), r(index), r(value))
77        }
78        Op::Length { dst, collection } => format!("Length {} = len {}", r(dst), r(collection)),
79        Op::Contains { dst, collection, value } => {
80            format!("Contains {} = {} contains {}", r(dst), r(collection), r(value))
81        }
82        Op::IterPrepare { iterable } => format!("IterPrepare {}", r(iterable)),
83        Op::IterNext { dst, exit } => format!("IterNext {} (exhausted -> {exit})", r(dst)),
84        Op::IterPop => "IterPop".to_string(),
85        Op::Show { src } => format!("Show {}", r(src)),
86        Op::FailWith { msg } => format!("FailWith {}", k(msg)),
87        Op::Halt => "Halt".to_string(),
88        // The long tail (CRDT, structs, temporal, concurrency, magic-div, …) is
89        // rendered by its derived Debug — correct, just less pretty.
90        other => format!("{other:?}"),
91    }
92}
93
94/// Absolute control-transfer targets of an op (for tape arrows / reachability).
95pub fn op_targets(op: &Op) -> Vec<usize> {
96    match *op {
97        Op::Jump { target } => vec![target],
98        Op::JumpIfFalse { target, .. }
99        | Op::JumpIfTrue { target, .. } => vec![target],
100        Op::IterNext { exit, .. } => vec![exit],
101        _ => Vec::new(),
102    }
103}
104
105/// The registers an op reads (its sources) and the one it writes (its destination).
106/// Drives the debugger's datapath animation — which register cells light up, flow into
107/// the engine, and receive the result — and the plain-English narration. Empty for the
108/// long-tail ops (no animation/narration), which fall back to the disassembly text.
109#[derive(Clone, Debug, Default, PartialEq, Eq)]
110pub struct OpIo {
111    pub writes: Option<Reg>,
112    pub reads: Vec<Reg>,
113}
114
115/// Compute the [`OpIo`] of a bytecode op.
116pub fn op_io(op: &Op) -> OpIo {
117    let rw = |dst: Reg, reads: Vec<Reg>| OpIo { writes: Some(dst), reads };
118    let ro = |reads: Vec<Reg>| OpIo { writes: None, reads };
119    match *op {
120        Op::LoadConst { dst, .. } => rw(dst, vec![]),
121        Op::Move { dst, src } => rw(dst, vec![src]),
122        Op::EnsureOwned { reg } => rw(reg, vec![reg]),
123        Op::Add { dst, lhs, rhs }
124        | Op::Sub { dst, lhs, rhs }
125        | Op::Mul { dst, lhs, rhs }
126        | Op::Div { dst, lhs, rhs }
127        | Op::ExactDiv { dst, lhs, rhs }
128        | Op::FloorDiv { dst, lhs, rhs }
129        | Op::Mod { dst, lhs, rhs }
130        | Op::Lt { dst, lhs, rhs }
131        | Op::Gt { dst, lhs, rhs }
132        | Op::LtEq { dst, lhs, rhs }
133        | Op::GtEq { dst, lhs, rhs }
134        | Op::Eq { dst, lhs, rhs }
135        | Op::NotEq { dst, lhs, rhs }
136        | Op::Concat { dst, lhs, rhs }
137        | Op::Pow { dst, lhs, rhs }
138        | Op::BitXor { dst, lhs, rhs }
139        | Op::BitAnd { dst, lhs, rhs }
140        | Op::BitOr { dst, lhs, rhs }
141        | Op::Shl { dst, lhs, rhs }
142        | Op::Shr { dst, lhs, rhs } => rw(dst, vec![lhs, rhs]),
143        Op::AddAssign { dst, src } => rw(dst, vec![dst, src]),
144        Op::Not { dst, src } => rw(dst, vec![src]),
145        Op::Show { src } => ro(vec![src]),
146        Op::Return { src } => ro(vec![src]),
147        Op::JumpIfFalse { cond, .. } | Op::JumpIfTrue { cond, .. } => {
148            ro(vec![cond])
149        }
150        Op::Index { dst, collection, index } | Op::IndexUnchecked { dst, collection, index } => {
151            rw(dst, vec![collection, index])
152        }
153        Op::SetIndex { collection, index, value } | Op::SetIndexUnchecked { collection, index, value } => {
154            rw(collection, vec![index, value])
155        }
156        Op::Length { dst, collection } => rw(dst, vec![collection]),
157        Op::Contains { dst, collection, value } => rw(dst, vec![collection, value]),
158        Op::ListPush { list, value } => rw(list, vec![value]),
159        _ => OpIo::default(),
160    }
161}
162
163/// The constant at `idx`, formatted for narration (e.g. `6`, `"hi"`, `nothing`).
164pub fn format_constant(prog: &CompiledProgram, idx: ConstIdx) -> String {
165    format_const(prog.constants.get(idx as usize))
166}
167
168fn format_const(c: Option<&Constant>) -> String {
169    match c {
170        Some(Constant::Int(n)) => n.to_string(),
171        Some(Constant::Float(f)) => format!("{f}"),
172        Some(Constant::Bool(b)) => b.to_string(),
173        Some(Constant::Text(s)) => format!("{s:?}"),
174        Some(Constant::Char(ch)) => format!("'{ch}'"),
175        Some(Constant::Nothing) => "nothing".to_string(),
176        Some(other) => format!("{other:?}"),
177        None => "?".to_string(),
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    fn prog() -> CompiledProgram {
186        CompiledProgram {
187            constants: vec![Constant::Int(6), Constant::Int(7), Constant::Text("hi".into())],
188            code: vec![
189                Op::LoadConst { dst: 0, idx: 0 },
190                Op::LoadConst { dst: 1, idx: 1 },
191                Op::Mul { dst: 2, lhs: 0, rhs: 1 },
192                Op::JumpIfFalse { cond: 2, target: 7 },
193                Op::Show { src: 2 },
194                Op::Jump { target: 7 },
195                Op::LoadConst { dst: 3, idx: 2 },
196                Op::Halt,
197            ],
198            ..Default::default()
199        }
200    }
201
202    #[test]
203    fn renders_common_ops_with_resolved_operands() {
204        let d = disassemble(&prog());
205        assert_eq!(d[0].text, "LoadConst R0 = 6");
206        assert_eq!(d[1].text, "LoadConst R1 = 7");
207        assert_eq!(d[2].text, "Mul R2 = R0 * R1");
208        assert_eq!(d[3].text, "JumpIfFalse R2 -> 7");
209        assert_eq!(d[4].text, "Show R2");
210        assert_eq!(d[5].text, "Jump -> 7");
211        assert_eq!(d[6].text, "LoadConst R3 = \"hi\"");
212        assert_eq!(d[7].text, "Halt");
213    }
214
215    #[test]
216    fn extracts_jump_targets() {
217        let d = disassemble(&prog());
218        assert_eq!(d[3].targets, vec![7]); // JumpIfFalse
219        assert_eq!(d[5].targets, vec![7]); // Jump
220        assert!(d[2].targets.is_empty()); // straight-line Mul
221    }
222
223    #[test]
224    fn op_io_identifies_reads_and_writes() {
225        assert_eq!(op_io(&Op::Add { dst: 2, lhs: 0, rhs: 1 }), OpIo { writes: Some(2), reads: vec![0, 1] });
226        assert_eq!(op_io(&Op::LoadConst { dst: 0, idx: 0 }), OpIo { writes: Some(0), reads: vec![] });
227        assert_eq!(op_io(&Op::Show { src: 3 }), OpIo { writes: None, reads: vec![3] });
228        assert_eq!(op_io(&Op::Move { dst: 1, src: 0 }), OpIo { writes: Some(1), reads: vec![0] });
229    }
230
231    #[test]
232    fn pc_indices_are_sequential() {
233        let d = disassemble(&prog());
234        for (i, line) in d.iter().enumerate() {
235            assert_eq!(line.pc, i);
236        }
237    }
238}