Skip to main content

cove_ir/bytecode/
disasm.rs

1//! Reading encoded instructions back as text.
2//!
3//! # There is one printer, and it is [`crate::print`]
4//!
5//! ADR 0041 decides this rather than leaving it open, and the reason is the
6//! 1:1 encoding. `crate::print` is already a disassembler for
7//! [`Inst`](crate::Inst) — one instruction per line, one fact per line, no
8//! alignment that shifts when an unrelated line grows, written so that a test
9//! which pins a lowering can diff it — and [`decode`] is lossless. So a
10//! disassembly is `decode` and then that printer, and it **cannot drift from
11//! the IR's own rendering**, because it is the IR's own rendering.
12//!
13//! A second renderer would be a second thing to keep in step, for no reader's
14//! benefit: the two would print the same instruction, and the day they
15//! disagreed the disagreement would be the bug rather than the report of one.
16//! It would also cost the property that makes this worth having — that
17//! *lowered IR* and *executable bytecode* are two views of one index, so a
18//! difference between the two panes of a debugger is a difference in the
19//! encoding and never in the prose.
20//!
21//! # What this module is, then
22//!
23//! The part `print` cannot do: the bytes. [`listing`] adds the program
24//! counter, the byte offset `pc << 4`, and the raw sixteen bytes in front of
25//! the text — issue #245's debugger row — and [`one`] is the text alone.
26//!
27//! Neither can panic on any sixteen bytes at all. A run that does not decode
28//! prints as its bytes and says so, because a disassembler is what somebody
29//! reaches for when the bytes are *wrong*.
30
31use std::fmt::Write as _;
32
33use crate::inst::Pc;
34use crate::program::{FunctionId, Program};
35
36use super::decode::decode;
37use super::EncodedInst;
38
39/// One encoded instruction as [`crate::print::one`] renders it.
40///
41/// Bytes that do not decode print as `<the reason>`, with the reason
42/// [`Malformed`](super::Malformed) gives — which is the one thing the readable
43/// printer has no way to say, because no `Inst` is malformed.
44pub fn one(program: &Program, id: FunctionId, code: EncodedInst, pc: Pc) -> String {
45    let function = program.function(id);
46    match decode(code, pc) {
47        Ok(inst) => crate::print::one(program, function, &inst),
48        Err(why) => format!("<{why}>"),
49    }
50}
51
52/// One encoded instruction's sixteen bytes, in hex, low byte first.
53///
54/// The order the bytes are stored in rather than the order a number reads in,
55/// because what a reader of this is checking is a byte offset.
56pub fn bytes(code: EncodedInst) -> String {
57    code.bytes()
58        .iter()
59        .map(|byte| format!("{byte:02x}"))
60        .collect()
61}
62
63/// A whole run of encoded instructions, one to a line.
64///
65/// `pc`, the byte offset, the raw sixteen bytes, then the text. The first
66/// three are what a readable listing has no reason to carry and a bytecode
67/// view exists for; the fourth is [`one`], so the right-hand column of this
68/// and the body of [`crate::print::function`] are the same characters.
69pub fn listing(program: &Program, id: FunctionId, code: &[EncodedInst]) -> String {
70    let mut out = String::new();
71    for (pc, held) in code.iter().enumerate() {
72        let _ = writeln!(
73            out,
74            "{pc:>4}  +{:<6} {}  {}",
75            EncodedInst::offset_of(pc as Pc),
76            bytes(*held),
77            one(program, id, *held, pc as Pc)
78        );
79    }
80    out
81}
82
83#[cfg(test)]
84mod tests {
85    use std::sync::Arc;
86
87    use cove_diag::{FileId, Span};
88
89    use super::*;
90    use crate::bytecode::encode::encode_function;
91    use crate::inst::{ArithOp, Inst, Num};
92    use crate::layout::{Layout, LayoutId};
93    use crate::program::Function;
94    use crate::repr::{RefMap, Repr};
95
96    const INT: LayoutId = LayoutId(0);
97
98    fn held() -> Program {
99        let reprs = vec![Repr::Int, Repr::Int];
100        let code = vec![
101            Inst::Int { dst: 0, value: 7 },
102            Inst::Arith {
103                num: Num::Int,
104                op: ArithOp::Add,
105                dst: 0,
106                a: 0,
107                b: 1,
108            },
109            Inst::Jump { to: 3 },
110            Inst::Return { src: 0 },
111        ];
112        let span = Span::new(FileId(0), 0, 0);
113        Program {
114            functions: vec![Function {
115                module: Arc::from("m"),
116                name: Arc::from("f"),
117                params: Vec::new(),
118                spans: vec![span; code.len()],
119                refs: RefMap::of(&reprs),
120                reprs,
121                returns: INT,
122                captures: Vec::new(),
123                code,
124                locals: Vec::new(),
125                inlined: Vec::new(),
126                span,
127                is_async: false,
128                stub: false,
129            }],
130            layouts: vec![Layout::word("Int", Repr::Int)],
131            str_layout: INT,
132            boxed_layout: INT,
133            ..Program::default()
134        }
135    }
136
137    /// The disassembly of an encoding *is* the readable listing of the
138    /// instruction it encodes, character for character. That is what one
139    /// printer buys, and it is the property a second renderer would cost.
140    #[test]
141    fn the_disassembly_of_an_encoding_is_the_readable_listing_of_it() {
142        let program = held();
143        let id = FunctionId(0);
144        let code = encode_function(program.function(id)).expect("it encodes");
145        let read: Vec<String> = code
146            .iter()
147            .enumerate()
148            .map(|(pc, held)| one(&program, id, *held, pc as Pc))
149            .collect();
150        let written: Vec<String> = program
151            .function(id)
152            .code
153            .iter()
154            .map(|inst| crate::print::one(&program, program.function(id), inst))
155            .collect();
156        assert_eq!(read, written);
157        assert_eq!(read[0], "int s0:int 7");
158        // A relative displacement is decoded back to the absolute target the
159        // readable IR names, so the two panes agree about where a jump goes.
160        assert_eq!(read[2], "jump 3");
161    }
162
163    /// The bytecode row: the pc, the byte offset `pc << 4`, the raw sixteen
164    /// bytes, and the text. Issue #245's debugger view, and the only thing
165    /// here the readable listing has no reason to carry.
166    #[test]
167    fn a_listing_shows_the_pc_the_byte_offset_and_the_raw_bytes() {
168        let program = held();
169        let id = FunctionId(0);
170        let code = encode_function(program.function(id)).expect("it encodes");
171        let text = listing(&program, id, &code);
172        let lines: Vec<&str> = text.lines().collect();
173        assert_eq!(lines.len(), 4);
174        assert!(lines[0].starts_with("   0  +0     "), "{:?}", lines[0]);
175        assert!(lines[0].ends_with("int s0:int 7"), "{:?}", lines[0]);
176        assert!(lines[1].contains("+16"), "{:?}", lines[1]);
177        assert!(lines[3].contains("+48"), "{:?}", lines[3]);
178        assert_eq!(bytes(code[0]).len(), 32);
179        assert!(bytes(code[0]).starts_with("02"), "{}", bytes(code[0]));
180    }
181
182    /// A disassembler is what somebody reaches for when the bytes are wrong,
183    /// so bytes that decode to nothing print as the reason rather than
184    /// stopping the listing.
185    #[test]
186    fn bytes_that_decode_to_nothing_say_so_rather_than_panicking() {
187        let program = held();
188        let id = FunctionId(0);
189        let bad = EncodedInst::from_bytes([255u8; EncodedInst::BYTES]);
190        assert_eq!(
191            one(&program, id, bad, 0),
192            "<flags is 255, and it is reserved and must be zero>"
193        );
194        let mut held = [0u8; EncodedInst::BYTES];
195        held[0] = 255;
196        let unknown = EncodedInst::from_bytes(held);
197        assert_eq!(
198            one(&program, id, unknown, 0),
199            "<opcode 255 names no operation>"
200        );
201        assert!(listing(&program, id, &[bad, unknown]).contains("names no operation"));
202    }
203}