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}