Skip to main content

mailang_bytecode/
disasm.rs

1//! Human-readable dumps of compiled bytecode (`mailang disasm`).
2//!
3//! Two forms can be printed: the stack instruction stream the compiler emits
4//! (what `--vm=stack` runs) and the three-address register lowering the default
5//! VM runs (`--vm=register` names it explicitly). Operands are decoded exactly
6//! like the VMs decode them — constant-pool indices with their value, global
7//! slots with their name, jump targets as instruction indices, local slots as
8//! register numbers — so a dump can be read without counting bytes.
9//!
10//! This is the view that the Phase N work needed and did not have: `match`
11//! delivering its value through the wrong protocol and the register backend's
12//! dataflow heights are both visible in a few adjacent lines of a dump.
13
14use crate::register::{parameter_slots, RegOp, StackToRegister};
15use crate::{Bytecode, Chunk, Instruction, Opcode, Value};
16use alloc::format;
17use alloc::string::{String, ToString};
18use core::fmt::Write;
19
20/// Stack bytecode as the compiler emitted it. `only` limits the dump to one
21/// chunk index (all chunks when `None`).
22pub fn disassemble_stack(bytecode: &Bytecode, only: Option<usize>) -> String {
23    let mut out = String::new();
24    for (i, ch) in bytecode.chunks.iter().enumerate() {
25        if only.is_some_and(|o| o != i) {
26            continue;
27        }
28        let _ = writeln!(
29            out,
30            "== chunk {} ({}) == {} instructions, {} constants, params {}",
31            i,
32            ch.name,
33            ch.instructions.len(),
34            ch.constants.len(),
35            parameter_slots(bytecode, i),
36        );
37        for (ip, ins) in ch.instructions.iter().enumerate() {
38            let _ = writeln!(
39                out,
40                "{:5}  {:<14} {:<10} {}",
41                ip,
42                opcode_name(ins.opcode),
43                operand_text(ins.operand),
44                stack_note(bytecode, ch, ins),
45            );
46        }
47        out.push('\n');
48    }
49    out
50}
51
52/// Register (three-address) IR for the same bytecode, lowered exactly like
53/// `RegisterVm::new` lowers it (same `parameter_slots` starting height).
54pub fn disassemble_register(bytecode: &Bytecode, only: Option<usize>) -> String {
55    let mut out = String::new();
56    for (i, ch) in bytecode.chunks.iter().enumerate() {
57        if only.is_some_and(|o| o != i) {
58            continue;
59        }
60        let slots = parameter_slots(bytecode, i);
61        let reg = StackToRegister::new(&ch.name, &ch.instructions, &ch.constants)
62            .with_param_slots(slots)
63            .translate();
64        let _ = writeln!(
65            out,
66            "== chunk {} ({}) == {} regops, {} regs, {} param slots (registers 0..{})",
67            i,
68            reg.name,
69            reg.code.len(),
70            reg.n_regs,
71            reg.param_slots,
72            reg.param_slots,
73        );
74        for (rip, op) in reg.code.iter().enumerate() {
75            let _ = writeln!(
76                out,
77                "{:5}  {:<46} {}",
78                rip,
79                format!("{:?}", op),
80                reg_note(&reg.constants, op),
81            );
82        }
83        out.push('\n');
84    }
85    out
86}
87
88fn opcode_name(op: Opcode) -> String {
89    format!("{:?}", op)
90}
91
92fn operand_text(operand: Option<u32>) -> String {
93    match operand {
94        Some(o) => format!("#{}", o),
95        None => String::new(),
96    }
97}
98
99/// Decoded meaning of a stack instruction's operand.
100fn stack_note(bytecode: &Bytecode, ch: &Chunk, ins: &Instruction) -> String {
101    let k = ins.operand.unwrap_or(0);
102    match ins.opcode {
103        Opcode::Push => match ch.constants.get(k as usize) {
104            Some(v) => format!("; {}", value_text(v)),
105            None => String::new(),
106        },
107        Opcode::GetProperty | Opcode::SetProperty => match ch.constants.get(k as usize) {
108            Some(Value::Str(name)) => format!("; \"{}\"", name),
109            Some(v) => format!("; {}", value_text(v)),
110            None => String::new(),
111        },
112        Opcode::LoadGlobal | Opcode::StoreGlobal => match bytecode.global_names.get(k as usize) {
113            Some(name) => format!("; global {}", name),
114            None => String::new(),
115        },
116        Opcode::Jump
117        | Opcode::JumpIfFalse
118        | Opcode::JumpIfTrue
119        | Opcode::TryBegin
120        | Opcode::TryEnd => format!("; -> {}", k),
121        Opcode::LoadLocal | Opcode::StoreLocal => format!("; local r{}", k),
122        Opcode::Slide => format!("; keep top, drop {} below", k),
123        Opcode::LoadUpvalue | Opcode::StoreUpvalue => format!("; upvalue {}", k),
124        Opcode::Call | Opcode::TailCall | Opcode::BuildArray | Opcode::BuildMap => {
125            format!("; {} operands", k)
126        }
127        Opcode::CallDirect => {
128            // Packed as (argc << 16) | const index of the callee name.
129            let argc = k >> 16;
130            let callee = k & 0xFFFF;
131            match ch.constants.get(callee as usize) {
132                Some(Value::Str(name)) => format!("; {}({} args, direct)", name, argc),
133                _ => format!("; {} args, direct", argc),
134            }
135        }
136        _ => String::new(),
137    }
138}
139
140/// Decoded meaning of a register instruction.
141fn reg_note(consts: &[Value], op: &RegOp) -> String {
142    match op {
143        RegOp::LoadConst { k, .. } => match consts.get(*k as usize) {
144            Some(v) => format!("; {}", value_text(v)),
145            None => String::new(),
146        },
147        RegOp::LoadGlobal { slot, .. } | RegOp::StoreGlobal { slot, .. } => {
148            format!("; global #{}", slot)
149        }
150        RegOp::GetProp { name, .. } | RegOp::SetProp { name, .. } | RegOp::Invoke { name, .. } => {
151            match consts.get(*name as usize) {
152                Some(Value::Str(s)) => format!("; \"{}\"", s),
153                _ => String::new(),
154            }
155        }
156        RegOp::Jump { target } => format!("; -> {}", target),
157        RegOp::JumpIfFalse { target, .. } | RegOp::JumpIfTrue { target, .. } => {
158            format!("; -> {}", target)
159        }
160        RegOp::CreateClass { k, .. } => match consts.get(*k as usize) {
161            Some(v) => format!("; {}", value_text(v)),
162            None => String::new(),
163        },
164        _ => String::new(),
165    }
166}
167
168/// Short rendering of a value. Heap nodes print their shape, not their
169/// contents, so a self-referential constant cannot recurse forever.
170fn value_text(v: &Value) -> String {
171    match v {
172        Value::Null => "null".to_string(),
173        Value::Bool(b) => b.to_string(),
174        Value::Int(n) => n.to_string(),
175        Value::Float(f) => f.to_string(),
176        Value::Str(s) => format!("\"{}\"", s),
177        Value::Char(c) => format!("'{}'", c),
178        Value::Array(a) => format!("<array len={}>", a.borrow().len()),
179        Value::Tuple(t) => format!("<tuple len={}>", t.borrow().len()),
180        Value::Map(m) => format!("<map len={}>", m.borrow().len()),
181        Value::Function(f) => format!("<fn {}>", f.name),
182        Value::Closure(_) => "<closure>".to_string(),
183        Value::Class(c) => format!("<class {}>", c.name),
184        Value::Instance { class_index, .. } => format!("<instance of class {}>", class_index),
185        Value::Ok(_) => "Ok(..)".to_string(),
186        Value::Err(_) => "Err(..)".to_string(),
187        Value::Some(_) => "Some(..)".to_string(),
188        Value::Builtin { name, .. } => format!("<builtin {}>", name),
189    }
190}