Skip to main content

foundry_evm_traces/debug/
sources.rs

1use eyre::{Context, Result};
2use foundry_common::{
3    compact_to_contract, external_compiler::is_external_artifact, is_deploy_helper_path,
4    strip_bytecode_placeholders,
5};
6use foundry_compilers::{
7    Artifact, ProjectCompileOutput,
8    artifacts::{
9        Bytecode, ContractBytecodeSome, Libraries, Source,
10        sourcemap::{SourceElement, SourceMap},
11    },
12    multi::MultiCompilerLanguage,
13};
14use foundry_evm_core::ic::PcIcMap;
15use foundry_linking::Linker;
16use rayon::prelude::*;
17use solar::{ast, interface::SpannedOption};
18use std::{
19    collections::{BTreeMap, HashMap, HashSet},
20    fmt::Write,
21    ops::Range,
22    path::{Path, PathBuf},
23    sync::Arc,
24};
25
26#[derive(Clone, Debug)]
27pub struct SourceData {
28    pub source: Arc<String>,
29    pub language: MultiCompilerLanguage,
30    pub path: PathBuf,
31    /// Maps contract name to (start, end) of the contract definition in the source code.
32    /// This is useful for determining which contract contains given function definition.
33    pub contract_definitions: Vec<(String, Range<usize>)>,
34    /// Solidity function scopes and variable declarations available to the debugger.
35    pub debug_scopes: Vec<DebugSourceScope>,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct DebugSourceScope {
40    pub contract_name: String,
41    pub function_name: String,
42    pub range: Range<usize>,
43    pub body_range: Range<usize>,
44    pub parameters_src: String,
45    pub returns_src: Option<String>,
46    pub parameters: Vec<DebugVariable>,
47    pub returns: Vec<DebugVariable>,
48    pub locals: Vec<DebugVariable>,
49}
50
51impl DebugSourceScope {
52    pub fn visible_locals(&self, offset: usize) -> impl Iterator<Item = &DebugVariable> {
53        self.locals.iter().filter(move |local| {
54            local.declaration.end <= offset
55                && offset >= local.scope.start
56                && offset <= local.scope.end
57        })
58    }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct DebugVariable {
63    pub name: Option<String>,
64    pub declaration: Range<usize>,
65    pub scope: Range<usize>,
66}
67
68impl SourceData {
69    pub fn new(
70        output: &ProjectCompileOutput,
71        source: Arc<String>,
72        language: MultiCompilerLanguage,
73        path: PathBuf,
74        root: &Path,
75    ) -> Self {
76        let mut contract_definitions = Vec::new();
77        let mut debug_scopes = Vec::new();
78
79        match language {
80            MultiCompilerLanguage::Vyper(_) => {
81                // Vyper contracts have the same name as the file name.
82                if let Some(name) = path.file_stem().map(|s| s.to_string_lossy().to_string()) {
83                    contract_definitions.push((name, 0..source.len()));
84                }
85            }
86            MultiCompilerLanguage::Solc(_) => {
87                let r = output.parser().solc().compiler().enter(|compiler| -> Option<()> {
88                    let (_, source) = compiler.gcx().get_ast_source(root.join(&path))?;
89                    let source_map = compiler.sess().source_map();
90                    for item in source.ast.as_ref()?.items.iter() {
91                        if let solar::ast::ItemKind::Contract(contract) = &item.kind {
92                            let Some(contract_range) = span_to_range(source_map, item.span) else {
93                                continue;
94                            };
95                            contract_definitions
96                                .push((contract.name.to_string(), contract_range.clone()));
97                            collect_contract_debug_scopes(
98                                source_map,
99                                contract,
100                                contract_range,
101                                &mut debug_scopes,
102                            );
103                        }
104                    }
105                    Some(())
106                });
107                if r.is_none() {
108                    warn!("failed to parse contract definitions for {}", path.display());
109                }
110            }
111        }
112
113        Self { source, language, path, contract_definitions, debug_scopes }
114    }
115
116    /// Finds name of contract that contains given loc.
117    pub fn find_contract_name(&self, start: usize, end: usize) -> Option<&str> {
118        self.contract_definitions
119            .iter()
120            .find(|(_, r)| start >= r.start && end <= r.end)
121            .map(|(name, _)| name.as_str())
122    }
123
124    /// Finds the innermost Solidity function scope containing the given byte range.
125    pub fn find_debug_scope(&self, start: usize, end: usize) -> Option<&DebugSourceScope> {
126        self.debug_scopes
127            .iter()
128            .filter(|scope| start >= scope.range.start && end <= scope.range.end)
129            .min_by_key(|scope| scope.range.end.saturating_sub(scope.range.start))
130    }
131}
132
133fn collect_contract_debug_scopes(
134    source_map: &solar::interface::source_map::SourceMap,
135    contract: &ast::ItemContract<'_>,
136    contract_range: Range<usize>,
137    out: &mut Vec<DebugSourceScope>,
138) {
139    let mut scopes = Vec::new();
140    for item in contract.body.iter() {
141        let ast::ItemKind::Function(func) = &item.kind else { continue };
142        if !func.is_implemented() {
143            continue;
144        }
145
146        let Some(function_range) = span_to_range(source_map, item.span) else { continue };
147        let body_range =
148            span_to_range(source_map, func.body_span).unwrap_or_else(|| function_range.clone());
149        let function_name = function_name(func);
150        let parameters_src =
151            source_map.span_to_snippet(func.header.parameters.span).unwrap_or_default();
152        let returns_src = func
153            .header
154            .returns
155            .as_ref()
156            .and_then(|returns| source_map.span_to_snippet(returns.span).ok());
157
158        let mut locals = Vec::new();
159        if let Some(body) = &func.body {
160            collect_block_locals(source_map, body, body_range.clone(), &mut locals);
161        }
162
163        scopes.push(DebugSourceScope {
164            contract_name: contract.name.to_string(),
165            function_name,
166            range: function_range.clone(),
167            body_range,
168            parameters_src,
169            returns_src,
170            parameters: variables_from_list(
171                source_map,
172                &func.header.parameters,
173                function_range.clone(),
174            ),
175            returns: func
176                .header
177                .returns
178                .as_ref()
179                .map(|returns| variables_from_list(source_map, returns, function_range.clone()))
180                .unwrap_or_default(),
181            locals,
182        });
183    }
184
185    // Keep inherited/inline source-map lookups deterministic when multiple ranges contain a PC.
186    scopes.sort_by_key(|scope| {
187        (
188            scope.range.start,
189            scope.range.end.saturating_sub(scope.range.start),
190            scope.contract_name.clone(),
191            scope.function_name.clone(),
192        )
193    });
194
195    // Do not let a malformed nested item extend past the contract it was collected from.
196    scopes.retain(|scope| {
197        scope.range.start >= contract_range.start && scope.range.end <= contract_range.end
198    });
199    out.extend(scopes);
200}
201
202fn function_name(func: &ast::ItemFunction<'_>) -> String {
203    match func.kind {
204        ast::FunctionKind::Constructor => "constructor".to_string(),
205        ast::FunctionKind::Fallback => "fallback".to_string(),
206        ast::FunctionKind::Receive => "receive".to_string(),
207        ast::FunctionKind::Modifier => {
208            func.header.name.as_ref().map(|n| n.as_str()).unwrap_or("modifier").to_string()
209        }
210        ast::FunctionKind::Function => {
211            func.header.name.as_ref().map(|n| n.as_str()).unwrap_or("function").to_string()
212        }
213    }
214}
215
216fn variables_from_list(
217    source_map: &solar::interface::source_map::SourceMap,
218    vars: &[ast::VariableDefinition<'_>],
219    scope: Range<usize>,
220) -> Vec<DebugVariable> {
221    vars.iter().filter_map(|var| variable_from_definition(source_map, var, scope.clone())).collect()
222}
223
224fn collect_block_locals(
225    source_map: &solar::interface::source_map::SourceMap,
226    block: &ast::Block<'_>,
227    fallback_scope: Range<usize>,
228    out: &mut Vec<DebugVariable>,
229) {
230    let scope = span_to_range(source_map, block.span).unwrap_or(fallback_scope);
231    for stmt in block.stmts.iter() {
232        collect_stmt_locals(source_map, stmt, scope.clone(), out);
233    }
234}
235
236fn collect_stmt_locals(
237    source_map: &solar::interface::source_map::SourceMap,
238    stmt: &ast::Stmt<'_>,
239    scope: Range<usize>,
240    out: &mut Vec<DebugVariable>,
241) {
242    match &stmt.kind {
243        ast::StmtKind::DeclSingle(var) => {
244            if let Some(var) = variable_from_definition(source_map, var, scope) {
245                out.push(var);
246            }
247        }
248        ast::StmtKind::DeclMulti(vars, _) => {
249            for var in vars.iter() {
250                let SpannedOption::Some(var) = var else { continue };
251                if let Some(var) = variable_from_definition(source_map, var, scope.clone()) {
252                    out.push(var);
253                }
254            }
255        }
256        ast::StmtKind::Block(block) | ast::StmtKind::UncheckedBlock(block) => {
257            collect_block_locals(source_map, block, scope, out);
258        }
259        ast::StmtKind::If(_, then_stmt, else_stmt) => {
260            let then_scope =
261                span_to_range(source_map, then_stmt.span).unwrap_or_else(|| scope.clone());
262            collect_stmt_locals(source_map, then_stmt, then_scope, out);
263            if let Some(else_stmt) = else_stmt {
264                let else_scope =
265                    span_to_range(source_map, else_stmt.span).unwrap_or_else(|| scope.clone());
266                collect_stmt_locals(source_map, else_stmt, else_scope, out);
267            }
268        }
269        ast::StmtKind::For { init, body, .. } => {
270            let for_scope = span_to_range(source_map, stmt.span).unwrap_or_else(|| scope.clone());
271            if let Some(init) = init {
272                collect_stmt_locals(source_map, init, for_scope.clone(), out);
273            }
274            collect_stmt_locals(source_map, body, for_scope, out);
275        }
276        ast::StmtKind::While(_, body) | ast::StmtKind::DoWhile(body, _) => {
277            let stmt_scope = span_to_range(source_map, stmt.span).unwrap_or(scope);
278            collect_stmt_locals(source_map, body, stmt_scope, out);
279        }
280        ast::StmtKind::Try(try_stmt) => {
281            for clause in try_stmt.clauses.iter() {
282                let clause_scope =
283                    span_to_range(source_map, clause.span).unwrap_or_else(|| scope.clone());
284                for arg in clause.args.iter() {
285                    if let Some(arg) =
286                        variable_from_definition(source_map, arg, clause_scope.clone())
287                    {
288                        out.push(arg);
289                    }
290                }
291                collect_block_locals(source_map, &clause.block, clause_scope, out);
292            }
293        }
294        ast::StmtKind::Assembly(_)
295        | ast::StmtKind::Break
296        | ast::StmtKind::Continue
297        | ast::StmtKind::Emit(..)
298        | ast::StmtKind::Expr(_)
299        | ast::StmtKind::Return(_)
300        | ast::StmtKind::Revert(..)
301        | ast::StmtKind::Placeholder => {}
302    }
303}
304
305fn variable_from_definition(
306    source_map: &solar::interface::source_map::SourceMap,
307    var: &ast::VariableDefinition<'_>,
308    scope: Range<usize>,
309) -> Option<DebugVariable> {
310    Some(DebugVariable {
311        name: var.name.map(|name| name.to_string()),
312        declaration: span_to_range(source_map, var.span)?,
313        scope,
314    })
315}
316
317fn span_to_range(
318    source_map: &solar::interface::source_map::SourceMap,
319    span: solar::interface::Span,
320) -> Option<Range<usize>> {
321    source_map.span_to_range(span).ok()
322}
323
324#[derive(Clone, Debug)]
325pub struct ArtifactData {
326    pub source_map: Option<SourceMap>,
327    pub source_map_runtime: Option<SourceMap>,
328    pub pc_ic_map: Option<PcIcMap>,
329    pub pc_ic_map_runtime: Option<PcIcMap>,
330    pub build_id: String,
331    pub file_id: u32,
332}
333
334impl ArtifactData {
335    fn new(bytecode: ContractBytecodeSome, build_id: String, file_id: u32) -> Result<Self> {
336        let parse = |b: &Bytecode, name: &str| {
337            // Only parse source map if it's not empty.
338            let source_map = if b.source_map.as_ref().is_none_or(|s| s.is_empty()) {
339                Ok(None)
340            } else {
341                b.source_map().transpose().wrap_err_with(|| {
342                    format!("failed to parse {name} source map of file {file_id} in {build_id}")
343                })
344            };
345
346            // Only parse bytecode if it's not empty, stripping placeholders if necessary.
347            let pc_ic_map = if let Some(bytes) = strip_bytecode_placeholders(&b.object) {
348                (!bytes.is_empty()).then(|| PcIcMap::new(bytes.as_ref()))
349            } else {
350                None
351            };
352
353            source_map.map(|source_map| (source_map, pc_ic_map))
354        };
355        let (source_map, pc_ic_map) = parse(&bytecode.bytecode, "creation")?;
356        let (source_map_runtime, pc_ic_map_runtime) = bytecode
357            .deployed_bytecode
358            .bytecode
359            .map(|b| parse(&b, "runtime"))
360            .unwrap_or_else(|| Ok((None, None)))?;
361
362        Ok(Self { source_map, source_map_runtime, pc_ic_map, pc_ic_map_runtime, build_id, file_id })
363    }
364}
365
366/// Container with artifacts data useful for identifying individual execution steps.
367#[derive(Clone, Debug, Default)]
368pub struct ContractSources {
369    /// Map over build_id -> file_id -> (source code, language)
370    pub sources_by_id: HashMap<String, HashMap<u32, Arc<SourceData>>>,
371    /// Map over contract name -> Vec<(bytecode, build_id, file_id)>
372    pub artifacts_by_name: HashMap<String, Vec<ArtifactData>>,
373}
374
375impl ContractSources {
376    /// Collects the contract sources and artifacts from the project compile output.
377    pub fn from_project_output(
378        output: &ProjectCompileOutput,
379        root: &Path,
380        libraries: Option<&Libraries>,
381    ) -> Result<Self> {
382        let mut sources = Self::default();
383        sources.insert(output, root, libraries)?;
384        Ok(sources)
385    }
386
387    pub fn insert(
388        &mut self,
389        output: &ProjectCompileOutput,
390        root: &Path,
391        libraries: Option<&Libraries>,
392    ) -> Result<()> {
393        let link_data = libraries.map(|libraries| {
394            let linker = Linker::new(root, output.artifact_ids().collect());
395            (linker, libraries)
396        });
397
398        let artifacts: Vec<_> = output
399            .artifact_ids()
400            .filter(|(id, _)| !is_external_artifact(&id.build_id))
401            .collect::<Vec<_>>()
402            .par_iter()
403            .map(|(id, artifact)| {
404                let mut new_artifact = None;
405                if let Some(file_id) = artifact.id {
406                    let artifact = if let Some((linker, libraries)) = link_data.as_ref() {
407                        linker.link(id, libraries)?
408                    } else {
409                        artifact.get_contract_bytecode()
410                    };
411                    let bytecode = compact_to_contract(artifact.into_contract_bytecode())?;
412
413                    new_artifact = Some((
414                        id.name.clone(),
415                        ArtifactData::new(bytecode, id.build_id.clone(), file_id)?,
416                    ));
417                } else {
418                    warn!(id = id.identifier(), "source not found");
419                };
420
421                Ok(new_artifact)
422            })
423            .collect::<Result<Vec<_>>>()?;
424
425        for (name, artifact) in artifacts.into_iter().flatten() {
426            self.artifacts_by_name.entry(name).or_default().push(artifact);
427        }
428
429        // Not all source files produce artifacts, so we are populating sources by using build
430        // infos.
431        let mut files: BTreeMap<PathBuf, Arc<SourceData>> = BTreeMap::new();
432        let mut removed_files = HashSet::new();
433        for (build_id, build) in output.builds() {
434            for (source_id, path) in &build.source_id_to_path {
435                if !path.exists() {
436                    // Preprocessor deploy helpers are compiled from memory and never exist on disk.
437                    if !is_deploy_helper_path(path) {
438                        removed_files.insert(path);
439                    }
440                    continue;
441                }
442
443                let source_data = match files.entry(path.clone()) {
444                    std::collections::btree_map::Entry::Vacant(entry) => {
445                        let source = Source::read(path).wrap_err_with(|| {
446                            format!("failed to read artifact source file for `{}`", path.display())
447                        })?;
448                        let stripped = path.strip_prefix(root).unwrap_or(path).to_path_buf();
449                        let source_data = Arc::new(SourceData::new(
450                            output,
451                            source.content,
452                            build.language,
453                            stripped,
454                            root,
455                        ));
456                        entry.insert(source_data.clone());
457                        source_data
458                    }
459                    std::collections::btree_map::Entry::Occupied(entry) => entry.get().clone(),
460                };
461                self.sources_by_id
462                    .entry(build_id.clone())
463                    .or_default()
464                    .insert(*source_id, source_data);
465            }
466        }
467
468        if !removed_files.is_empty() {
469            let mut warning = "Detected artifacts built from source files that no longer exist. \
470                Run `forge clean` to make sure builds are in sync with project files."
471                .to_string();
472            for file in removed_files {
473                write!(warning, "\n - {}", file.display())?;
474            }
475            let _ = sh_warn!("{}", warning);
476        }
477
478        Ok(())
479    }
480
481    /// Merges given contract sources.
482    pub fn merge(&mut self, sources: Self) {
483        self.sources_by_id.extend(sources.sources_by_id);
484        for (name, artifacts) in sources.artifacts_by_name {
485            self.artifacts_by_name.entry(name).or_default().extend(artifacts);
486        }
487    }
488
489    /// Returns all sources for a contract by name.
490    pub fn get_sources(
491        &self,
492        name: &str,
493    ) -> Option<impl Iterator<Item = (&ArtifactData, &SourceData)>> {
494        self.artifacts_by_name.get(name).map(|artifacts| {
495            artifacts.iter().filter_map(|artifact| {
496                let source =
497                    self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
498                Some((artifact, source.as_ref()))
499            })
500        })
501    }
502
503    /// Returns all (name, bytecode, source) sets.
504    pub fn entries(&self) -> impl Iterator<Item = (&str, &ArtifactData, &SourceData)> {
505        self.artifacts_by_name.iter().flat_map(|(name, artifacts)| {
506            artifacts.iter().filter_map(|artifact| {
507                let source =
508                    self.sources_by_id.get(artifact.build_id.as_str())?.get(&artifact.file_id)?;
509                Some((name.as_str(), artifact, source.as_ref()))
510            })
511        })
512    }
513
514    pub fn find_source_mapping(
515        &self,
516        contract_name: &str,
517        pc: u32,
518        init_code: bool,
519    ) -> Option<(SourceElement, &SourceData)> {
520        self.get_sources(contract_name)?.find_map(|(artifact, source)| {
521            let source_map = if init_code {
522                artifact.source_map.as_ref()
523            } else {
524                artifact.source_map_runtime.as_ref()
525            }?;
526
527            // Solc indexes source maps by instruction counter, but Vyper indexes by program
528            // counter.
529            let source_element = if matches!(source.language, MultiCompilerLanguage::Solc(_)) {
530                let pc_ic_map = if init_code {
531                    artifact.pc_ic_map.as_ref()
532                } else {
533                    artifact.pc_ic_map_runtime.as_ref()
534                }?;
535                let ic = pc_ic_map.get(pc)?;
536
537                source_map.get(ic as usize)
538            } else {
539                source_map.get(pc as usize)
540            }?;
541            // if the source element has an index, find the sourcemap for that index
542            source_element
543                .index()
544                // if index matches current file_id, return current source code
545                .and_then(|index| {
546                    (index == artifact.file_id).then(|| (source_element.clone(), source))
547                })
548                .or_else(|| {
549                    // otherwise find the source code for the element's index
550                    self.sources_by_id
551                        .get(&artifact.build_id)?
552                        .get(&source_element.index()?)
553                        .map(|source| (source_element.clone(), source.as_ref()))
554                })
555        })
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn variable(name: &str, declaration: Range<usize>, scope: Range<usize>) -> DebugVariable {
564        DebugVariable { name: Some(name.to_string()), declaration, scope }
565    }
566
567    fn scope(locals: Vec<DebugVariable>) -> DebugSourceScope {
568        DebugSourceScope {
569            contract_name: "DebugMe".to_string(),
570            function_name: "foo".to_string(),
571            range: 0..100,
572            body_range: 10..90,
573            parameters_src: String::new(),
574            returns_src: None,
575            parameters: Vec::new(),
576            returns: Vec::new(),
577            locals,
578        }
579    }
580
581    #[test]
582    fn visible_locals_require_declaration_and_scope() {
583        let scope = scope(vec![
584            variable("before", 10..15, 10..90),
585            variable("after", 70..75, 10..90),
586            variable("nested", 20..25, 20..40),
587        ]);
588
589        let names = |offset| {
590            scope
591                .visible_locals(offset)
592                .map(|variable| variable.name.as_deref().unwrap())
593                .collect::<Vec<_>>()
594        };
595
596        assert_eq!(names(14), Vec::<&str>::new());
597        assert_eq!(names(30), ["before", "nested"]);
598        assert_eq!(names(50), ["before"]);
599        assert_eq!(names(80), ["before", "after"]);
600    }
601}