foundry_evm_traces/backtrace/
source_map.rs1use alloy_primitives::Bytes;
4use foundry_compilers::{ProjectCompileOutput, artifacts::sourcemap::SourceMap};
5use foundry_evm_core::ic::IcPcMap;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
10pub struct SourceData {
11 pub source_map: SourceMap,
13 pub bytecode: Bytes,
15}
16
17pub struct PcSourceMapper<'a> {
19 ic_pc_map: IcPcMap,
21 source_data: SourceData,
23 sources: &'a [(PathBuf, String)],
25 line_offsets: Vec<Vec<usize>>,
27}
28
29impl<'a> PcSourceMapper<'a> {
30 pub fn new(source_data: SourceData, sources: &'a [(PathBuf, String)]) -> Self {
32 let ic_pc_map = IcPcMap::new(source_data.bytecode.as_ref());
34
35 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 pub fn map_pc(&self, pc: usize) -> Option<SourceLocation> {
44 let ic = self.find_instruction_counter(pc)?;
46
47 let element = self.source_data.source_map.get(ic)?;
49
50 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 let (file_path, content) = &self.sources[source_idx];
60
61 let offset = element.offset() as usize;
63
64 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 fn find_instruction_counter(&self, pc: usize) -> Option<usize> {
90 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 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 let line = line_offsets.binary_search(&offset).unwrap_or_else(|i| i.saturating_sub(1));
114
115 let line_start = if line == 0 { 0 } else { line_offsets[line - 1] + 1 };
117 let column = offset.saturating_sub(line_start);
118
119 Some((line + 1, column + 1))
121 }
122}
123#[derive(Debug, Clone)]
125pub struct SourceLocation {
126 pub file: PathBuf,
127 pub line: usize,
128 pub column: usize,
129 pub length: usize,
130 pub offset: usize,
133}
134
135fn 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
142pub 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 let max_source_id = build_ctx.source_id_to_path.keys().max().map_or(0, |id| *id) as usize;
153
154 let mut sources = vec![(PathBuf::new(), String::new()); max_source_id + 1];
156
157 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 if source_content.contains('\r') {
167 source_content = source_content.replace("\r\n", "\n");
168 }
169
170 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}