Skip to content

Commit 77aa0c8

Browse files
pioheizerosnacks
authored andcommitted
Add debug file dump (foundry-rs#7375)
* Refactored debugger to extract TUI abstraction. Added option to dump debugger context to file as json. * Update crates/forge/bin/cmd/test/mod.rs Co-authored-by: zerosnacks <[email protected]> * Update crates/script/src/lib.rs Co-authored-by: zerosnacks <[email protected]> * Cleanup code. * Added test. * Reformat code. * Reformat code. --------- Co-authored-by: zerosnacks <[email protected]>
1 parent a4eb58b commit 77aa0c8

File tree

14 files changed

+355
-69
lines changed

14 files changed

+355
-69
lines changed

crates/cli/src/utils/cmd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ pub async fn handle_traces(
410410
.decoder(&decoder)
411411
.sources(sources)
412412
.build();
413-
debugger.try_run()?;
413+
debugger.try_run_tui()?;
414414
} else {
415415
print_traces(&mut result, &decoder, verbose).await?;
416416
}

crates/debugger/src/tui/builder.rs renamed to crates/debugger/src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! TUI debugger builder.
1+
//! Debugger builder.
22
33
use crate::{node::flatten_call_trace, DebugNode, Debugger};
44
use alloy_primitives::{map::AddressHashMap, Address};

crates/debugger/src/context.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::DebugNode;
2+
use alloy_primitives::map::AddressHashMap;
3+
use foundry_common::evm::Breakpoints;
4+
use foundry_evm_traces::debug::ContractSources;
5+
6+
pub struct DebuggerContext {
7+
pub debug_arena: Vec<DebugNode>,
8+
pub identified_contracts: AddressHashMap<String>,
9+
/// Source map of contract sources
10+
pub contracts_sources: ContractSources,
11+
pub breakpoints: Breakpoints,
12+
}

crates/debugger/src/debugger.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Debugger implementation.
2+
3+
use crate::{
4+
context::DebuggerContext, tui::TUI, DebugNode, DebuggerBuilder, ExitReason, FileDumper,
5+
};
6+
use alloy_primitives::map::AddressHashMap;
7+
use eyre::Result;
8+
use foundry_common::evm::Breakpoints;
9+
use foundry_evm_traces::debug::ContractSources;
10+
use std::path::PathBuf;
11+
12+
pub struct Debugger {
13+
context: DebuggerContext,
14+
}
15+
16+
impl Debugger {
17+
/// Creates a new debugger builder.
18+
#[inline]
19+
pub fn builder() -> DebuggerBuilder {
20+
DebuggerBuilder::new()
21+
}
22+
23+
/// Creates a new debugger.
24+
pub fn new(
25+
debug_arena: Vec<DebugNode>,
26+
identified_contracts: AddressHashMap<String>,
27+
contracts_sources: ContractSources,
28+
breakpoints: Breakpoints,
29+
) -> Self {
30+
Self {
31+
context: DebuggerContext {
32+
debug_arena,
33+
identified_contracts,
34+
contracts_sources,
35+
breakpoints,
36+
},
37+
}
38+
}
39+
40+
/// Starts the debugger TUI. Terminates the current process on failure or user exit.
41+
pub fn run_tui_exit(mut self) -> ! {
42+
let code = match self.try_run_tui() {
43+
Ok(ExitReason::CharExit) => 0,
44+
Err(e) => {
45+
println!("{e}");
46+
1
47+
}
48+
};
49+
std::process::exit(code)
50+
}
51+
52+
/// Starts the debugger TUI.
53+
pub fn try_run_tui(&mut self) -> Result<ExitReason> {
54+
eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
55+
56+
let mut tui = TUI::new(&mut self.context);
57+
tui.try_run()
58+
}
59+
60+
/// Dumps debugger data to file.
61+
pub fn dump_to_file(&mut self, path: &PathBuf) -> Result<()> {
62+
eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
63+
64+
let mut file_dumper = FileDumper::new(path, &mut self.context);
65+
file_dumper.run()
66+
}
67+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! The file dumper implementation
2+
3+
use crate::{context::DebuggerContext, DebugNode};
4+
use alloy_primitives::Address;
5+
use eyre::Result;
6+
use foundry_common::fs::write_json_file;
7+
use foundry_compilers::{
8+
artifacts::sourcemap::{Jump, SourceElement},
9+
multi::MultiCompilerLanguage,
10+
};
11+
use foundry_evm_traces::debug::{ArtifactData, ContractSources, SourceData};
12+
use serde::Serialize;
13+
use std::{collections::HashMap, ops::Deref, path::PathBuf};
14+
15+
/// The file dumper
16+
pub struct FileDumper<'a> {
17+
path: &'a PathBuf,
18+
debugger_context: &'a mut DebuggerContext,
19+
}
20+
21+
impl<'a> FileDumper<'a> {
22+
pub fn new(path: &'a PathBuf, debugger_context: &'a mut DebuggerContext) -> Self {
23+
Self { path, debugger_context }
24+
}
25+
26+
pub fn run(&mut self) -> Result<()> {
27+
let data = DebuggerDump::from(self.debugger_context);
28+
write_json_file(self.path, &data).unwrap();
29+
Ok(())
30+
}
31+
}
32+
33+
impl DebuggerDump {
34+
fn from(debugger_context: &DebuggerContext) -> Self {
35+
Self {
36+
contracts: ContractsDump::new(debugger_context),
37+
debug_arena: debugger_context.debug_arena.clone(),
38+
}
39+
}
40+
}
41+
42+
#[derive(Serialize)]
43+
struct DebuggerDump {
44+
contracts: ContractsDump,
45+
debug_arena: Vec<DebugNode>,
46+
}
47+
48+
#[derive(Serialize)]
49+
pub struct SourceElementDump {
50+
offset: u32,
51+
length: u32,
52+
index: i32,
53+
jump: u32,
54+
modifier_depth: u32,
55+
}
56+
57+
#[derive(Serialize)]
58+
struct ContractsDump {
59+
// Map of call address to contract name
60+
identified_contracts: HashMap<Address, String>,
61+
sources: ContractsSourcesDump,
62+
}
63+
64+
#[derive(Serialize)]
65+
struct ContractsSourcesDump {
66+
sources_by_id: HashMap<String, HashMap<u32, SourceDataDump>>,
67+
artifacts_by_name: HashMap<String, Vec<ArtifactDataDump>>,
68+
}
69+
70+
#[derive(Serialize)]
71+
struct SourceDataDump {
72+
source: String,
73+
language: MultiCompilerLanguage,
74+
path: PathBuf,
75+
}
76+
77+
#[derive(Serialize)]
78+
struct ArtifactDataDump {
79+
pub source_map: Option<Vec<SourceElementDump>>,
80+
pub source_map_runtime: Option<Vec<SourceElementDump>>,
81+
pub pc_ic_map: Option<HashMap<usize, usize>>,
82+
pub pc_ic_map_runtime: Option<HashMap<usize, usize>>,
83+
pub build_id: String,
84+
pub file_id: u32,
85+
}
86+
87+
impl ContractsDump {
88+
pub fn new(debugger_context: &DebuggerContext) -> Self {
89+
Self {
90+
identified_contracts: debugger_context
91+
.identified_contracts
92+
.iter()
93+
.map(|(k, v)| (*k, v.clone()))
94+
.collect(),
95+
sources: ContractsSourcesDump::new(&debugger_context.contracts_sources),
96+
}
97+
}
98+
}
99+
100+
impl ContractsSourcesDump {
101+
pub fn new(contracts_sources: &ContractSources) -> Self {
102+
Self {
103+
sources_by_id: contracts_sources
104+
.sources_by_id
105+
.iter()
106+
.map(|(name, inner_map)| {
107+
(
108+
name.clone(),
109+
inner_map
110+
.iter()
111+
.map(|(id, source_data)| (*id, SourceDataDump::new(source_data)))
112+
.collect(),
113+
)
114+
})
115+
.collect(),
116+
artifacts_by_name: contracts_sources
117+
.artifacts_by_name
118+
.iter()
119+
.map(|(name, data)| {
120+
(name.clone(), data.iter().map(ArtifactDataDump::new).collect())
121+
})
122+
.collect(),
123+
}
124+
}
125+
}
126+
127+
impl SourceDataDump {
128+
pub fn new(v: &SourceData) -> Self {
129+
Self { source: v.source.deref().clone(), language: v.language, path: v.path.clone() }
130+
}
131+
}
132+
133+
impl SourceElementDump {
134+
pub fn new(v: &SourceElement) -> Self {
135+
Self {
136+
offset: v.offset(),
137+
length: v.length(),
138+
index: v.index_i32(),
139+
jump: match v.jump() {
140+
Jump::In => 0,
141+
Jump::Out => 1,
142+
Jump::Regular => 2,
143+
},
144+
modifier_depth: v.modifier_depth(),
145+
}
146+
}
147+
}
148+
149+
impl ArtifactDataDump {
150+
pub fn new(v: &ArtifactData) -> Self {
151+
Self {
152+
source_map: v
153+
.source_map
154+
.clone()
155+
.map(|source_map| source_map.iter().map(SourceElementDump::new).collect()),
156+
source_map_runtime: v
157+
.source_map_runtime
158+
.clone()
159+
.map(|source_map| source_map.iter().map(SourceElementDump::new).collect()),
160+
pc_ic_map: v.pc_ic_map.clone().map(|v| v.inner.iter().map(|(k, v)| (*k, *v)).collect()),
161+
pc_ic_map_runtime: v
162+
.pc_ic_map_runtime
163+
.clone()
164+
.map(|v| v.inner.iter().map(|(k, v)| (*k, *v)).collect()),
165+
build_id: v.build_id.clone(),
166+
file_id: v.file_id,
167+
}
168+
}
169+
}

crates/debugger/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! # foundry-debugger
22
//!
3-
//! Interactive Solidity TUI debugger.
3+
//! Interactive Solidity TUI debugger and debugger data file dumper
44
55
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
66
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
@@ -10,8 +10,16 @@ extern crate tracing;
1010

1111
mod op;
1212

13+
mod builder;
14+
mod context;
15+
mod debugger;
16+
mod file_dumper;
1317
mod tui;
14-
pub use tui::{Debugger, DebuggerBuilder, ExitReason};
1518

1619
mod node;
1720
pub use node::DebugNode;
21+
22+
pub use builder::DebuggerBuilder;
23+
pub use debugger::Debugger;
24+
pub use file_dumper::FileDumper;
25+
pub use tui::{ExitReason, TUI};

crates/debugger/src/tui/context.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Debugger context and event handler implementation.
22
3-
use crate::{DebugNode, Debugger, ExitReason};
3+
use crate::{context::DebuggerContext, DebugNode, ExitReason};
44
use alloy_primitives::{hex, Address};
55
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
66
use foundry_evm_core::buffer::BufferKind;
@@ -16,8 +16,8 @@ pub(crate) struct DrawMemory {
1616
pub(crate) current_stack_startline: usize,
1717
}
1818

19-
pub(crate) struct DebuggerContext<'a> {
20-
pub(crate) debugger: &'a mut Debugger,
19+
pub(crate) struct TUIContext<'a> {
20+
pub(crate) debugger_context: &'a mut DebuggerContext,
2121

2222
/// Buffer for keys prior to execution, i.e. '10' + 'k' => move up 10 operations.
2323
pub(crate) key_buffer: String,
@@ -35,10 +35,10 @@ pub(crate) struct DebuggerContext<'a> {
3535
pub(crate) active_buffer: BufferKind,
3636
}
3737

38-
impl<'a> DebuggerContext<'a> {
39-
pub(crate) fn new(debugger: &'a mut Debugger) -> Self {
40-
DebuggerContext {
41-
debugger,
38+
impl<'a> TUIContext<'a> {
39+
pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
40+
TUIContext {
41+
debugger_context,
4242

4343
key_buffer: String::with_capacity(64),
4444
current_step: 0,
@@ -58,7 +58,7 @@ impl<'a> DebuggerContext<'a> {
5858
}
5959

6060
pub(crate) fn debug_arena(&self) -> &[DebugNode] {
61-
&self.debugger.debug_arena
61+
&self.debugger_context.debug_arena
6262
}
6363

6464
pub(crate) fn debug_call(&self) -> &DebugNode {
@@ -87,7 +87,8 @@ impl<'a> DebuggerContext<'a> {
8787

8888
fn gen_opcode_list(&mut self) {
8989
self.opcode_list.clear();
90-
let debug_steps = &self.debugger.debug_arena[self.draw_memory.inner_call_index].steps;
90+
let debug_steps =
91+
&self.debugger_context.debug_arena[self.draw_memory.inner_call_index].steps;
9192
for step in debug_steps {
9293
self.opcode_list.push(pretty_opcode(step));
9394
}
@@ -109,7 +110,7 @@ impl<'a> DebuggerContext<'a> {
109110
}
110111
}
111112

112-
impl DebuggerContext<'_> {
113+
impl TUIContext<'_> {
113114
pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
114115
let ret = match event {
115116
Event::Key(event) => self.handle_key_event(event),
@@ -259,7 +260,7 @@ impl DebuggerContext<'_> {
259260
fn handle_breakpoint(&mut self, c: char) {
260261
// Find the location of the called breakpoint in the whole debug arena (at this address with
261262
// this pc)
262-
if let Some((caller, pc)) = self.debugger.breakpoints.get(&c) {
263+
if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) {
263264
for (i, node) in self.debug_arena().iter().enumerate() {
264265
if node.address == *caller {
265266
if let Some(step) = node.steps.iter().position(|step| step.pc == *pc) {

crates/debugger/src/tui/draw.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! TUI draw implementation.
22
3-
use super::context::DebuggerContext;
3+
use super::context::TUIContext;
44
use crate::op::OpcodeParam;
55
use foundry_compilers::artifacts::sourcemap::SourceElement;
66
use foundry_evm_core::buffer::{get_buffer_accesses, BufferKind};
@@ -15,7 +15,7 @@ use ratatui::{
1515
use revm_inspectors::tracing::types::CallKind;
1616
use std::{collections::VecDeque, fmt::Write, io};
1717

18-
impl DebuggerContext<'_> {
18+
impl TUIContext<'_> {
1919
/// Draws the TUI layout and subcomponents to the given terminal.
2020
pub(crate) fn draw(&self, terminal: &mut super::DebuggerTerminal) -> io::Result<()> {
2121
terminal.draw(|f| self.draw_layout(f)).map(drop)
@@ -343,11 +343,11 @@ impl DebuggerContext<'_> {
343343
/// Returns source map, source code and source name of the current line.
344344
fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
345345
let address = self.address();
346-
let Some(contract_name) = self.debugger.identified_contracts.get(address) else {
346+
let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
347347
return Err(format!("Unknown contract at address {address}"));
348348
};
349349

350-
self.debugger
350+
self.debugger_context
351351
.contracts_sources
352352
.find_source_mapping(
353353
contract_name,

0 commit comments

Comments
 (0)