use fusevm::Chunk;
pub fn disassemble(bytes: &[u8]) -> Result<Vec<String>, String> {
let chunk: Chunk = bincode::deserialize(bytes).map_err(|e| e.to_string())?;
Ok(render(&chunk, 0))
}
fn render(c: &Chunk, depth: usize) -> Vec<String> {
let ind = " ".repeat(depth);
let mut out = Vec::new();
out.push(format!("{ind}source: {}", c.source));
out.push(format!(
"{ind}{} ops · {} constants · {} names · {} sub-chunks",
c.ops.len(),
c.constants.len(),
c.names.len(),
c.sub_chunks.len()
));
if !c.names.is_empty() {
out.push(format!("{ind}names:"));
for (i, n) in c.names.iter().enumerate() {
out.push(format!("{ind} [{i}] {n}"));
}
}
if !c.constants.is_empty() {
out.push(format!("{ind}constants:"));
for (i, v) in c.constants.iter().enumerate() {
out.push(format!("{ind} [{i}] {v:?}"));
}
}
out.push(format!("{ind}ops:"));
for (i, op) in c.ops.iter().enumerate() {
let line = c.lines.get(i).copied().unwrap_or(0);
out.push(format!("{ind} {i:>5} L{line:<5} {op:?}"));
}
for (idx, sub) in c.sub_chunks.iter().enumerate() {
out.push(String::new());
out.push(format!("{ind}── sub_chunk[{idx}] ──"));
out.extend(render(sub, depth + 1));
}
out
}
#[cfg(test)]
mod tests {
use super::disassemble;
use fusevm::{Chunk, Op, Value};
fn chunk() -> Chunk {
let mut inner = Chunk::new();
inner.source = "inner.sh".into();
inner.ops = vec![Op::Nop];
inner.lines = vec![7];
let mut c = Chunk::new();
c.source = "outer.sh".into();
c.names = vec!["PATH".into(), "greet".into()];
c.constants = vec![Value::Int(42), Value::Str(std::sync::Arc::new("hi".into()))];
c.ops = vec![Op::LoadConst(0), Op::LoadInt(9), Op::Nop];
c.lines = vec![1, 2, 3];
c.sub_chunks = vec![inner];
c
}
#[test]
fn a_listing_names_every_pool_and_numbers_every_op() {
let bytes = bincode::serialize(&chunk()).unwrap();
let lines = disassemble(&bytes).expect("a bare chunk decodes");
let text = lines.join("\n");
assert!(text.contains("source: outer.sh"), "{text}");
assert!(
text.contains("3 ops · 2 constants · 2 names · 1 sub-chunks"),
"the header counts each pool: {text}"
);
assert!(
text.contains("[0] PATH") && text.contains("[1] greet"),
"{text}"
);
assert!(text.contains("[0] Int(42)"), "{text}");
assert!(text.contains("0 L1 LoadConst(0)"), "{text}");
assert!(text.contains("2 L3 Nop"), "{text}");
assert!(text.contains("── sub_chunk[0] ──"), "{text}");
assert!(text.contains(" source: inner.sh"), "indented: {text}");
assert!(text.contains(" L7"), "and keeps its own lines: {text}");
}
#[test]
fn anything_that_is_not_a_chunk_is_refused() {
assert!(disassemble(b"").is_err(), "nothing is not a chunk");
assert!(disassemble(b"not bincode at all").is_err());
let bytes = bincode::serialize(&chunk()).unwrap();
assert!(
disassemble(&bytes[..bytes.len() / 2]).is_err(),
"half a chunk is not a chunk"
);
assert!(!disassemble(b"x").unwrap_err().is_empty());
}
}