koto_bytecode/
chunk.rs

1use crate::InstructionReader;
2use koto_memory::Ptr;
3use koto_parser::{ConstantPool, KString, Span};
4use std::fmt::{self, Write};
5
6/// Debug information for a Koto program
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct DebugInfo {
9    source_map: Vec<(u32, Span)>,
10    /// The source of the program that the debug info was derived from
11    pub source: String,
12}
13
14impl DebugInfo {
15    /// Adds a span to the source map for a given ip
16    ///
17    /// Instructions with matching spans share the same entry, so if the span matches the
18    /// previously pushed span then this is a no-op.
19    pub fn push(&mut self, ip: u32, span: Span) {
20        if let Some(entry) = self.source_map.last() {
21            if entry.1 == span {
22                // Don't add entries with matching spans, a search is performed in
23                // get_source_span which will find the correct span
24                // for intermediate ips.
25                return;
26            }
27        }
28        self.source_map.push((ip, span));
29    }
30
31    /// Returns a source span for a given instruction pointer
32    pub fn get_source_span(&self, ip: u32) -> Option<Span> {
33        // Find the last entry with an ip less than or equal to the input
34        // an upper_bound would nice here, but this isn't currently a performance sensitive function
35        // so a scan through the entries will do.
36        let mut result = None;
37        for entry in self.source_map.iter() {
38            if entry.0 <= ip {
39                result = Some(entry.1);
40            } else {
41                break;
42            }
43        }
44        result
45    }
46}
47
48/// A compiled chunk of bytecode, along with its associated constants and metadata
49#[derive(Clone, Default, PartialEq)]
50pub struct Chunk {
51    /// The bytes representing the chunk's bytecode
52    pub bytes: Vec<u8>,
53    /// The constant data associated with the chunk's bytecode
54    pub constants: ConstantPool,
55    /// The path of the program's source file
56    pub path: Option<KString>,
57    /// Debug information associated with the chunk's bytecode
58    pub debug_info: DebugInfo,
59}
60
61impl Chunk {
62    /// Returns a [String] displaying the instructions contained in the compiled [Chunk]
63    pub fn bytes_as_string(chunk: &Chunk) -> String {
64        let mut iter = chunk.bytes.iter();
65        let mut result = String::new();
66
67        'outer: loop {
68            for i in 1..=16 {
69                match iter.next() {
70                    Some(byte) => write!(result, "{byte:02x}").ok(),
71                    None => break 'outer,
72                };
73
74                if i < 16 {
75                    result += " ";
76
77                    if i % 4 == 0 {
78                        result += " ";
79                    }
80                }
81            }
82            result += "\n";
83        }
84
85        result
86    }
87
88    /// Returns a [String] displaying the annotated instructions contained in the compiled [Chunk]
89    pub fn instructions_as_string(chunk: Ptr<Chunk>, source_lines: &[&str]) -> String {
90        let ip_width = 5 + chunk.bytes.len().ilog10() as usize;
91
92        let mut result = String::new();
93        let mut reader = InstructionReader::new(chunk);
94        let mut ip = reader.ip;
95        let mut span: Option<Span> = None;
96        let mut first = true;
97
98        while let Some(instruction) = reader.next() {
99            let instruction_span = reader
100                .chunk
101                .debug_info
102                .get_source_span(ip as u32)
103                .expect("Missing source span");
104
105            let print_source_lines = if let Some(span) = span {
106                instruction_span.start.line != span.start.line
107            } else {
108                true
109            };
110
111            if print_source_lines && !source_lines.is_empty() {
112                if !first {
113                    result += "\n";
114                }
115                first = false;
116
117                let line = instruction_span
118                    .start
119                    .line
120                    .clamp(0, source_lines.len() as u32 - 1) as usize;
121                writeln!(result, "|{}| {}", line + 1, source_lines[line]).ok();
122                span = Some(instruction_span);
123            }
124
125            for (i, line) in format!("{instruction:?}").lines().enumerate() {
126                if i == 0 {
127                    writeln!(result, "{ip:<ip_width$}{line}").ok();
128                } else {
129                    writeln!(result, "{:ip_width$}{line}", "").ok();
130                }
131            }
132
133            ip = reader.ip;
134        }
135
136        result
137    }
138}
139
140impl fmt::Debug for Chunk {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
142        write!(f, "Chunk ({self:p})")
143    }
144}