Skip to main content

foundry_evm_traces/backtrace/
source_map.rs

1//! Source map decoding and PC mapping utilities.
2
3use alloy_primitives::Bytes;
4use foundry_compilers::{ProjectCompileOutput, artifacts::sourcemap::SourceMap};
5use foundry_evm_core::ic::IcPcMap;
6use std::path::{Path, PathBuf};
7
8/// Source data for a single contract.
9#[derive(Debug, Clone)]
10pub struct SourceData {
11    /// Runtime source map for the contract
12    pub source_map: SourceMap,
13    /// Deployed bytecode for accurate PC mapping
14    pub bytecode: Bytes,
15}
16
17/// Maps program counters to source locations.
18pub struct PcSourceMapper<'a> {
19    /// Mapping from instruction counter to program counter.
20    ic_pc_map: IcPcMap,
21    /// Source data consists of the source_map and the deployed bytecode
22    source_data: SourceData,
23    /// Source files i.e source path and content (indexed by source_id)
24    sources: &'a [(PathBuf, String)],
25    /// Cached line offset mappings for each source file.
26    line_offsets: Vec<Vec<usize>>,
27}
28
29impl<'a> PcSourceMapper<'a> {
30    /// Creates a new PC to source mapper.
31    pub fn new(source_data: SourceData, sources: &'a [(PathBuf, String)]) -> Self {
32        // Build instruction counter to program counter mapping
33        let ic_pc_map = IcPcMap::new(source_data.bytecode.as_ref());
34
35        // Pre-calculate line offsets for each source file
36        let line_offsets =
37            sources.iter().map(|(_, content)| compute_line_offsets(content)).collect();
38
39        Self { ic_pc_map, source_data, sources, line_offsets }
40    }
41
42    /// Maps a program counter to source location.
43    pub fn map_pc(&self, pc: usize) -> Option<SourceLocation> {
44        // Find the instruction counter for this PC
45        let ic = self.find_instruction_counter(pc)?;
46
47        // Get the source element for this instruction
48        let element = self.source_data.source_map.get(ic)?;
49
50        // Get the source file index - returns None if index is -1
51        let source_idx_opt = element.index();
52
53        let source_idx = source_idx_opt? as usize;
54        if source_idx >= self.sources.len() {
55            return None;
56        }
57
58        // Get the source file info
59        let (file_path, content) = &self.sources[source_idx];
60
61        // Convert byte offset to line and column
62        let offset = element.offset() as usize;
63
64        // Check if offset is valid for this source file
65        if offset >= content.len() {
66            return None;
67        }
68
69        let line_offsets = self.line_offsets.get(source_idx)?;
70        let (line, column) = offset_to_line_column(line_offsets, offset);
71
72        trace!(
73            file = ?file_path,
74            line = line,
75            column = column,
76            offset = offset,
77            "Mapped PC to source location"
78        );
79
80        Some(SourceLocation {
81            file: file_path.clone(),
82            line,
83            column,
84            length: element.length() as usize,
85            offset,
86        })
87    }
88
89    /// Finds the instruction counter for a given program counter.
90    fn find_instruction_counter(&self, pc: usize) -> Option<usize> {
91        // The IcPcMap maps IC -> PC, we need the reverse
92        // We find the highest IC that has a PC <= our target PC
93        let mut best_ic = None;
94        let mut best_pc = 0;
95
96        for (ic, mapped_pc) in self.ic_pc_map.iter() {
97            let mapped_pc = *mapped_pc as usize;
98            if mapped_pc <= pc && mapped_pc >= best_pc {
99                best_pc = mapped_pc;
100                best_ic = Some(*ic as usize);
101            }
102        }
103
104        best_ic
105    }
106}
107/// Represents a location in source code.
108#[derive(Debug, Clone)]
109pub struct SourceLocation {
110    pub file: PathBuf,
111    pub line: usize,
112    pub column: usize,
113    pub length: usize,
114    /// Byte offset in the source file
115    /// This specifically useful when one source file contains multiple contracts / libraries.
116    pub offset: usize,
117}
118
119/// Computes the byte offset where each line starts in source content.
120fn compute_line_offsets(content: &str) -> Vec<usize> {
121    let mut offsets = vec![0];
122    offsets.extend(memchr::memchr_iter(b'\n', content.as_bytes()).map(|offset| offset + 1));
123    offsets
124}
125
126/// Converts a byte offset to 1-indexed line and column numbers.
127fn offset_to_line_column(line_offsets: &[usize], offset: usize) -> (usize, usize) {
128    let line = line_offsets.partition_point(|&line_start| line_start <= offset) - 1;
129    let column = offset - line_offsets[line];
130    (line + 1, column + 1)
131}
132
133/// Loads sources for a specific ArtifactId.build_id
134pub fn load_build_sources(
135    build_id: &str,
136    output: &ProjectCompileOutput,
137    root: &Path,
138) -> Option<Vec<(PathBuf, String)>> {
139    let build_ctx = output.builds().find(|(bid, _)| *bid == build_id).map(|(_, ctx)| ctx)?;
140
141    // Determine the size needed for sources vector
142    // Highest source_id
143    let max_source_id = build_ctx.source_id_to_path.keys().max().map_or(0, |id| *id) as usize;
144
145    // Vec of source path and it's content
146    let mut sources = vec![(PathBuf::new(), String::new()); max_source_id + 1];
147
148    // Populate sources at their correct indices
149    for (source_id, source_path) in &build_ctx.source_id_to_path {
150        let idx = *source_id as usize;
151
152        let full_path =
153            if source_path.is_absolute() { source_path.clone() } else { root.join(source_path) };
154        let mut source_content = foundry_common::fs::read_to_string(&full_path).unwrap_or_default();
155
156        // Normalize line endings for windows
157        if source_content.contains('\r') {
158            source_content = source_content.replace("\r\n", "\n");
159        }
160
161        // Convert path to relative PathBuf
162        let path_buf = source_path.strip_prefix(root).unwrap_or(source_path).to_path_buf();
163
164        sources[idx] = (path_buf, source_content);
165    }
166
167    Some(sources)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{compute_line_offsets, offset_to_line_column};
173
174    fn line_column(content: &str, offset: usize) -> (usize, usize) {
175        offset_to_line_column(&compute_line_offsets(content), offset)
176    }
177
178    #[test]
179    fn maps_byte_offsets_to_line_and_column() {
180        assert_eq!(line_column("abc", 0), (1, 1));
181        assert_eq!(line_column("abc", 2), (1, 3));
182
183        assert_eq!(line_column("abc\ndef", 3), (1, 4));
184        assert_eq!(line_column("abc\ndef", 4), (2, 1));
185        assert_eq!(line_column("abc\ndef", 6), (2, 3));
186
187        assert_eq!(line_column("a\nbc\ndef", 0), (1, 1));
188        assert_eq!(line_column("a\nbc\ndef", 1), (1, 2));
189        assert_eq!(line_column("a\nbc\ndef", 2), (2, 1));
190        assert_eq!(line_column("a\nbc\ndef", 4), (2, 3));
191        assert_eq!(line_column("a\nbc\ndef", 5), (3, 1));
192        assert_eq!(line_column("a\nbc\ndef", 7), (3, 3));
193    }
194}