Skip to main content

foundry_evm_traces/debug/
sources.rs

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