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, column) = self.offset_to_line_column(source_idx, offset)?;
70
71        trace!(
72            file = ?file_path,
73            line = line,
74            column = column,
75            offset = offset,
76            "Mapped PC to source location"
77        );
78
79        Some(SourceLocation {
80            file: file_path.clone(),
81            line,
82            column,
83            length: element.length() as usize,
84            offset,
85        })
86    }
87
88    /// Finds the instruction counter for a given program counter.
89    fn find_instruction_counter(&self, pc: usize) -> Option<usize> {
90        // The IcPcMap maps IC -> PC, we need the reverse
91        // We find the highest IC that has a PC <= our target PC
92        let mut best_ic = None;
93        let mut best_pc = 0;
94
95        for (ic, mapped_pc) in &self.ic_pc_map.inner {
96            let mapped_pc = *mapped_pc as usize;
97            if mapped_pc <= pc && mapped_pc >= best_pc {
98                best_pc = mapped_pc;
99                best_ic = Some(*ic as usize);
100            }
101        }
102
103        best_ic
104    }
105
106    /// Converts a byte offset to line and column numbers.
107    ///
108    /// Returned lines and column numbers are 1-indexed.
109    fn offset_to_line_column(&self, source_idx: usize, offset: usize) -> Option<(usize, usize)> {
110        let line_offsets = self.line_offsets.get(source_idx)?;
111
112        // Find the line containing this offset
113        let line = line_offsets.binary_search(&offset).unwrap_or_else(|i| i.saturating_sub(1));
114
115        // Calculate column within the line
116        let line_start = if line == 0 { 0 } else { line_offsets[line - 1] + 1 };
117        let column = offset.saturating_sub(line_start);
118
119        // Lines and columns are 1-indexed
120        Some((line + 1, column + 1))
121    }
122}
123/// Represents a location in source code.
124#[derive(Debug, Clone)]
125pub struct SourceLocation {
126    pub file: PathBuf,
127    pub line: usize,
128    pub column: usize,
129    pub length: usize,
130    /// Byte offset in the source file
131    /// This specifically useful when one source file contains multiple contracts / libraries.
132    pub offset: usize,
133}
134
135/// Computes line offset positions in source content.
136fn compute_line_offsets(content: &str) -> Vec<usize> {
137    let mut offsets = vec![0];
138    offsets.extend(memchr::memchr_iter(b'\n', content.as_bytes()));
139    offsets
140}
141
142/// Loads sources for a specific ArtifactId.build_id
143pub fn load_build_sources(
144    build_id: &str,
145    output: &ProjectCompileOutput,
146    root: &Path,
147) -> Option<Vec<(PathBuf, String)>> {
148    let build_ctx = output.builds().find(|(bid, _)| *bid == build_id).map(|(_, ctx)| ctx)?;
149
150    // Determine the size needed for sources vector
151    // Highest source_id
152    let max_source_id = build_ctx.source_id_to_path.keys().max().map_or(0, |id| *id) as usize;
153
154    // Vec of source path and it's content
155    let mut sources = vec![(PathBuf::new(), String::new()); max_source_id + 1];
156
157    // Populate sources at their correct indices
158    for (source_id, source_path) in &build_ctx.source_id_to_path {
159        let idx = *source_id as usize;
160
161        let full_path =
162            if source_path.is_absolute() { source_path.clone() } else { root.join(source_path) };
163        let mut source_content = foundry_common::fs::read_to_string(&full_path).unwrap_or_default();
164
165        // Normalize line endings for windows
166        if source_content.contains('\r') {
167            source_content = source_content.replace("\r\n", "\n");
168        }
169
170        // Convert path to relative PathBuf
171        let path_buf = source_path.strip_prefix(root).unwrap_or(source_path).to_path_buf();
172
173        sources[idx] = (path_buf, source_content);
174    }
175
176    Some(sources)
177}