Skip to main content

foundry_debugger/
debugger.rs

1//! Debugger implementation.
2
3use crate::{DebugNode, DebuggerBuilder, ExitReason, tui::TUI};
4use alloy_primitives::map::AddressHashMap;
5use clap::ValueEnum;
6use eyre::Result;
7use foundry_common::slot_identifier::SlotIdentifier;
8use foundry_evm_core::Breakpoints;
9use foundry_evm_traces::debug::ContractSources;
10use std::path::Path;
11
12/// Debugger TUI layout selection.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
14pub enum DebuggerLayout {
15    /// Select horizontal or vertical layout from the terminal size.
16    #[default]
17    Auto,
18    /// Force the two-column debugger layout.
19    Horizontal,
20    /// Force the single-column debugger layout.
21    Vertical,
22}
23
24impl DebuggerLayout {
25    pub(crate) const fn next(self) -> Self {
26        match self {
27            Self::Auto | Self::Vertical => Self::Horizontal,
28            Self::Horizontal => Self::Vertical,
29        }
30    }
31
32    pub(crate) const fn as_str(self) -> &'static str {
33        match self {
34            Self::Auto => "auto",
35            Self::Horizontal => "horizontal",
36            Self::Vertical => "vertical",
37        }
38    }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub struct DebuggerStats {
43    /// Sum of root-call gas used across every trace arena passed to the debugger.
44    pub session_trace_gas_used: u64,
45    /// Number of subcalls in the traces passed to the debugger.
46    pub session_subcalls: usize,
47}
48
49pub struct DebuggerContext {
50    pub debug_arena: Vec<DebugNode>,
51    pub stats: Option<DebuggerStats>,
52    pub identified_contracts: AddressHashMap<String>,
53    pub(crate) slot_identifiers: Option<AddressHashMap<SlotIdentifier>>,
54    /// Source map of contract sources
55    pub contracts_sources: ContractSources,
56    pub breakpoints: Breakpoints,
57    pub layout: DebuggerLayout,
58}
59
60pub struct Debugger {
61    context: DebuggerContext,
62}
63
64impl Debugger {
65    /// Creates a new debugger builder.
66    #[inline]
67    pub fn builder() -> DebuggerBuilder {
68        DebuggerBuilder::new()
69    }
70
71    /// Creates a new debugger.
72    pub const fn new(
73        debug_arena: Vec<DebugNode>,
74        identified_contracts: AddressHashMap<String>,
75        contracts_sources: ContractSources,
76        breakpoints: Breakpoints,
77    ) -> Self {
78        Self {
79            context: DebuggerContext {
80                debug_arena,
81                stats: None,
82                identified_contracts,
83                slot_identifiers: None,
84                contracts_sources,
85                breakpoints,
86                layout: DebuggerLayout::Auto,
87            },
88        }
89    }
90
91    pub(crate) const fn new_with_stats(
92        debug_arena: Vec<DebugNode>,
93        stats: DebuggerStats,
94        identified_contracts: AddressHashMap<String>,
95        slot_identifiers: AddressHashMap<SlotIdentifier>,
96        contracts_sources: ContractSources,
97        breakpoints: Breakpoints,
98        layout: DebuggerLayout,
99    ) -> Self {
100        Self {
101            context: DebuggerContext {
102                debug_arena,
103                stats: Some(stats),
104                identified_contracts,
105                slot_identifiers: Some(slot_identifiers),
106                contracts_sources,
107                breakpoints,
108                layout,
109            },
110        }
111    }
112
113    /// Starts the debugger TUI. Terminates the current process on failure or user exit.
114    pub fn run_tui_exit(mut self) -> ! {
115        let code = match self.try_run_tui() {
116            Ok(ExitReason::CharExit) => 0,
117            Err(e) => {
118                let _ = sh_eprintln!("{e}");
119                1
120            }
121        };
122        std::process::exit(code)
123    }
124
125    /// Starts the debugger TUI.
126    pub fn try_run_tui(&mut self) -> Result<ExitReason> {
127        eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
128
129        let mut tui = TUI::new(&mut self.context);
130        tui.try_run()
131    }
132
133    /// Dumps debugger data to file.
134    pub fn dump_to_file(&mut self, path: &Path) -> Result<()> {
135        eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
136        crate::dump::dump(path, &self.context)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use alloy_primitives::Address;
144    use foundry_common::ContractsByArtifactBuilder;
145    use foundry_compilers::{
146        ArtifactId,
147        artifacts::{CompactContractBytecodeCow, StorageLayout},
148    };
149    use foundry_evm_traces::CallTraceDecoder;
150    use std::borrow::Cow;
151
152    fn artifact_id(name: &str, profile: &str) -> ArtifactId {
153        ArtifactId {
154            path: format!("out/{profile}/{name}.json").into(),
155            name: name.to_string(),
156            source: format!("src/{name}.sol").into(),
157            version: "0.8.30".parse().unwrap(),
158            build_id: profile.to_string(),
159            profile: profile.to_string(),
160        }
161    }
162
163    #[test]
164    fn builder_skips_ambiguous_storage_layout_matches() {
165        let ids = [
166            artifact_id("Ambiguous", "default"),
167            artifact_id("Ambiguous", "optimized"),
168            artifact_id("Unique", "default"),
169        ];
170        let known_contracts = ContractsByArtifactBuilder::new(ids.iter().cloned().map(|id| {
171            (
172                id,
173                CompactContractBytecodeCow {
174                    abi: Some(Cow::Owned(Default::default())),
175                    ..Default::default()
176                },
177            )
178        }))
179        .with_storage_layouts(ids.iter().cloned().map(|id| (id, StorageLayout::default())))
180        .build();
181        let ambiguous_address = Address::repeat_byte(0x11);
182        let unique_address = Address::repeat_byte(0x22);
183        let mut decoder = CallTraceDecoder::default();
184        decoder.contracts.insert(ambiguous_address, ids[0].identifier());
185        decoder.contracts.insert(unique_address, ids[2].identifier());
186
187        let debugger =
188            Debugger::builder().decoder(&decoder).known_contracts(&known_contracts).build();
189        let slot_identifiers = debugger.context.slot_identifiers.as_ref().unwrap();
190
191        assert!(!slot_identifiers.contains_key(&ambiguous_address));
192        assert!(slot_identifiers.contains_key(&unique_address));
193    }
194}