Skip to content

Commit 940393c

Browse files
committed
Refactored debugger to extract TUI abstraction. Added option to dump debugger context to file as json.
1 parent b2f9346 commit 940393c

File tree

17 files changed

+367
-97
lines changed

17 files changed

+367
-97
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/utils/cmd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ pub async fn handle_traces(
414414
.decoder(&decoder)
415415
.sources(sources)
416416
.build();
417-
debugger.try_run()?;
417+
debugger.try_run_tui()?;
418418
} else {
419419
print_traces(&mut result, &decoder).await?;
420420
}

crates/common/src/compile.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ impl ProjectCompiler {
277277
pub struct ContractSources {
278278
/// Map over artifacts' contract names -> vector of file IDs
279279
pub ids_by_name: HashMap<String, Vec<u32>>,
280-
/// Map over file_id -> (source code, contract)
281-
pub sources_by_id: HashMap<u32, (String, ContractBytecodeSome)>,
280+
/// Map over file_id -> (source code, contract, source path)
281+
pub sources_by_id: HashMap<u32, (String, ContractBytecodeSome, Option<PathBuf>)>,
282282
}
283283

284284
impl ContractSources {
@@ -291,7 +291,7 @@ impl ContractSources {
291291
for (id, artifact) in output.artifact_ids() {
292292
if let Some(file_id) = artifact.id {
293293
let abs_path = root.join(&id.source);
294-
let source_code = std::fs::read_to_string(abs_path).wrap_err_with(|| {
294+
let source_code = std::fs::read_to_string(abs_path.clone()).wrap_err_with(|| {
295295
format!("failed to read artifact source file for `{}`", id.identifier())
296296
})?;
297297
let compact = CompactContractBytecode {
@@ -300,7 +300,7 @@ impl ContractSources {
300300
deployed_bytecode: artifact.deployed_bytecode.clone(),
301301
};
302302
let contract = compact_to_contract(compact)?;
303-
sources.insert(&id, file_id, source_code, contract);
303+
sources.insert(&id, file_id, source_code, contract, Some(abs_path));
304304
} else {
305305
warn!(id = id.identifier(), "source not found");
306306
}
@@ -315,28 +315,29 @@ impl ContractSources {
315315
file_id: u32,
316316
source: String,
317317
bytecode: ContractBytecodeSome,
318+
source_path: Option<PathBuf>
318319
) {
319320
self.ids_by_name.entry(artifact_id.name.clone()).or_default().push(file_id);
320-
self.sources_by_id.insert(file_id, (source, bytecode));
321+
self.sources_by_id.insert(file_id, (source, bytecode, source_path));
321322
}
322323

323324
/// Returns the source for a contract by file ID.
324-
pub fn get(&self, id: u32) -> Option<&(String, ContractBytecodeSome)> {
325+
pub fn get(&self, id: u32) -> Option<&(String, ContractBytecodeSome, Option<PathBuf>)> {
325326
self.sources_by_id.get(&id)
326327
}
327328

328329
/// Returns all sources for a contract by name.
329330
pub fn get_sources(
330331
&self,
331332
name: &str,
332-
) -> Option<impl Iterator<Item = (u32, &(String, ContractBytecodeSome))>> {
333+
) -> Option<impl Iterator<Item = (u32, &(String, ContractBytecodeSome, Option<PathBuf>))>> {
333334
self.ids_by_name
334335
.get(name)
335336
.map(|ids| ids.iter().filter_map(|id| Some((*id, self.sources_by_id.get(id)?))))
336337
}
337338

338339
/// Returns all (name, source) pairs.
339-
pub fn entries(&self) -> impl Iterator<Item = (String, &(String, ContractBytecodeSome))> {
340+
pub fn entries(&self) -> impl Iterator<Item = (String, &(String, ContractBytecodeSome, Option<PathBuf>))> {
340341
self.ids_by_name.iter().flat_map(|(name, ids)| {
341342
ids.iter().filter_map(|id| self.sources_by_id.get(id).map(|s| (name.clone(), s)))
342343
})

crates/debugger/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ eyre.workspace = true
2323
ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] }
2424
revm.workspace = true
2525
tracing.workspace = true
26+
serde.workspace = true

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::Debugger;
44
use alloy_primitives::Address;

crates/debugger/src/context.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use alloy_primitives::Address;
2+
use foundry_common::compile::ContractSources;
3+
use foundry_common::evm::Breakpoints;
4+
use foundry_evm_core::debug::DebugNodeFlat;
5+
use foundry_evm_core::utils::PcIcMap;
6+
use std::collections::{BTreeMap, HashMap};
7+
8+
pub struct DebuggerContext {
9+
pub debug_arena: Vec<DebugNodeFlat>,
10+
pub identified_contracts: HashMap<Address, String>,
11+
/// Source map of contract sources
12+
pub contracts_sources: ContractSources,
13+
/// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code)
14+
pub pc_ic_maps: BTreeMap<String, (PcIcMap, PcIcMap)>,
15+
pub breakpoints: Breakpoints,
16+
}

crates/debugger/src/debugger.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Debugger implementation.
2+
3+
use alloy_primitives::Address;
4+
use eyre::Result;
5+
use foundry_common::{compile::ContractSources, evm::Breakpoints};
6+
use foundry_evm_core::{debug::DebugNodeFlat, utils::PcIcMap};
7+
use revm::primitives::SpecId;
8+
use std::collections::HashMap;
9+
use std::path::PathBuf;
10+
11+
use crate::context::DebuggerContext;
12+
use crate::tui::TUI;
13+
use crate::{DebuggerBuilder, ExitReason, FileDumper};
14+
15+
pub struct Debugger {
16+
context: DebuggerContext,
17+
}
18+
19+
impl Debugger {
20+
/// Creates a new debugger builder.
21+
#[inline]
22+
pub fn builder() -> DebuggerBuilder {
23+
DebuggerBuilder::new()
24+
}
25+
26+
/// Creates a new debugger.
27+
pub fn new(
28+
debug_arena: Vec<DebugNodeFlat>,
29+
identified_contracts: HashMap<Address, String>,
30+
contracts_sources: ContractSources,
31+
breakpoints: Breakpoints,
32+
) -> Self {
33+
let pc_ic_maps = contracts_sources
34+
.entries()
35+
.filter_map(|(contract_name, (_, contract, _))| {
36+
Some((
37+
contract_name.clone(),
38+
(
39+
PcIcMap::new(SpecId::LATEST, contract.bytecode.bytes()?),
40+
PcIcMap::new(SpecId::LATEST, contract.deployed_bytecode.bytes()?),
41+
),
42+
))
43+
})
44+
.collect();
45+
Self {
46+
context: DebuggerContext {
47+
debug_arena,
48+
identified_contracts,
49+
contracts_sources,
50+
pc_ic_maps,
51+
breakpoints,
52+
},
53+
}
54+
}
55+
56+
/// Starts the debugger TUI. Terminates the current process on failure or user exit.
57+
pub fn run_tui_exit(mut self) -> ! {
58+
let code = match self.try_run_tui() {
59+
Ok(ExitReason::CharExit) => 0,
60+
Err(e) => {
61+
println!("{e}");
62+
1
63+
}
64+
};
65+
std::process::exit(code)
66+
}
67+
68+
/// Starts the debugger TUI.
69+
pub fn try_run_tui(&mut self) -> Result<ExitReason> {
70+
eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
71+
72+
let mut tui = TUI::new(&mut self.context);
73+
tui.try_run()
74+
}
75+
76+
/// Dumps debugger data to file.
77+
pub fn dump_to_file(&mut self, path: &PathBuf) -> Result<()> {
78+
eyre::ensure!(!self.context.debug_arena.is_empty(), "debug arena is empty");
79+
80+
let mut file_dumper = FileDumper::new(path, &mut self.context);
81+
file_dumper.run()
82+
}
83+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//! The file dumper implementation
2+
3+
use alloy_primitives::{Address, Bytes, U256};
4+
use serde::Serialize;
5+
use std::collections::HashMap;
6+
use std::path::PathBuf;
7+
use std::option::Option;
8+
9+
use crate::context::DebuggerContext;
10+
use eyre::Result;
11+
use foundry_common::compile::ContractSources;
12+
use foundry_common::fs::write_json_file;
13+
use foundry_compilers::artifacts::ContractBytecodeSome;
14+
use foundry_evm_core::debug::{DebugNodeFlat, DebugStep, Instruction};
15+
use foundry_evm_core::utils::PcIcMap;
16+
use revm_inspectors::tracing::types::CallKind;
17+
18+
/// The file dumper
19+
pub struct FileDumper<'a> {
20+
path: &'a PathBuf,
21+
debugger_context: &'a mut DebuggerContext,
22+
}
23+
24+
impl<'a> FileDumper<'a> {
25+
pub fn new(path: &'a PathBuf, debugger_context: &'a mut DebuggerContext) -> Self {
26+
Self { path, debugger_context }
27+
}
28+
29+
pub fn run(&mut self) -> Result<()> {
30+
let data = DebuggerDump::from(self.debugger_context);
31+
write_json_file(self.path, &data).unwrap();
32+
Ok(())
33+
}
34+
}
35+
36+
impl DebuggerDump {
37+
fn from(debugger_context: &DebuggerContext) -> DebuggerDump {
38+
Self {
39+
contracts: to_contracts_dump(debugger_context),
40+
executions: to_executions_dump(debugger_context),
41+
}
42+
}
43+
}
44+
45+
#[derive(Serialize)]
46+
struct DebuggerDump {
47+
contracts: ContractsDump,
48+
executions: ExecutionsDump,
49+
}
50+
51+
#[derive(Serialize)]
52+
struct ExecutionsDump {
53+
calls: Vec<CallDump>,
54+
// Map of contract name to PcIcMapDump
55+
pc_ic_maps: HashMap<String, PcIcMapDump>,
56+
}
57+
58+
#[derive(Serialize)]
59+
struct CallDump {
60+
address: Address,
61+
kind: CallKind,
62+
steps: Vec<StepDump>,
63+
}
64+
65+
#[derive(Serialize)]
66+
struct StepDump {
67+
/// Stack *prior* to running the associated opcode
68+
stack: Vec<U256>,
69+
/// Memory *prior* to running the associated opcode
70+
memory: Bytes,
71+
/// Calldata *prior* to running the associated opcode
72+
calldata: Bytes,
73+
/// Returndata *prior* to running the associated opcode
74+
returndata: Bytes,
75+
/// Opcode to be executed
76+
instruction: Instruction,
77+
/// Optional bytes that are being pushed onto the stack
78+
push_bytes: Option<Bytes>,
79+
/// The program counter at this step.
80+
pc: usize,
81+
/// Cumulative gas usage
82+
total_gas_used: u64,
83+
}
84+
85+
#[derive(Serialize)]
86+
struct PcIcMapDump {
87+
deploy_code_map: HashMap<usize, usize>,
88+
runtime_code_map: HashMap<usize, usize>,
89+
}
90+
91+
#[derive(Serialize)]
92+
struct ContractsDump {
93+
// Map of call address to contract name
94+
identified_calls: HashMap<Address, String>,
95+
sources: ContractsSourcesDump,
96+
}
97+
98+
#[derive(Serialize)]
99+
struct ContractsSourcesDump {
100+
ids_by_name: HashMap<String, Vec<u32>>,
101+
sources_by_id: HashMap<u32, ContractSourceDetailsDump>,
102+
}
103+
104+
#[derive(Serialize)]
105+
struct ContractSourceDetailsDump {
106+
source_code: String,
107+
contract_bytecode: ContractBytecodeSome,
108+
source_path: Option<PathBuf>,
109+
}
110+
111+
fn to_executions_dump(debugger_context: &DebuggerContext) -> ExecutionsDump {
112+
ExecutionsDump {
113+
calls: debugger_context.debug_arena.iter().map(|call| to_call_dump(call)).collect(),
114+
pc_ic_maps: debugger_context.pc_ic_maps.iter().map(|(k, v)| (k.clone(), to_pc_ic_map_dump(&v))).collect(),
115+
}
116+
}
117+
118+
fn to_call_dump(call: &DebugNodeFlat) -> CallDump {
119+
CallDump {
120+
address: call.address,
121+
kind: call.kind,
122+
steps: call.steps.iter().map(|step| to_step_dump(step.clone())).collect(),
123+
}
124+
}
125+
126+
fn to_step_dump(step: DebugStep) -> StepDump {
127+
StepDump {
128+
stack: step.stack,
129+
memory: step.memory,
130+
calldata: step.calldata,
131+
returndata: step.returndata,
132+
instruction: step.instruction,
133+
push_bytes: step.push_bytes.map(|v| Bytes::from(v)),
134+
pc: step.pc,
135+
total_gas_used: step.total_gas_used,
136+
}
137+
}
138+
139+
fn to_pc_ic_map_dump(pc_ic_map: &(PcIcMap, PcIcMap)) -> PcIcMapDump {
140+
let mut deploy_code_map = HashMap::new();
141+
142+
for (k, v) in pc_ic_map.0.inner.iter() {
143+
deploy_code_map.insert(*k, *v);
144+
}
145+
146+
let mut runtime_code_map = HashMap::new();
147+
for (k, v) in pc_ic_map.1.inner.iter() {
148+
runtime_code_map.insert(*k, *v);
149+
}
150+
151+
PcIcMapDump { deploy_code_map, runtime_code_map }
152+
}
153+
154+
fn to_contracts_dump(debugger_context: &DebuggerContext) -> ContractsDump {
155+
ContractsDump {
156+
identified_calls: debugger_context.identified_contracts.clone(),
157+
sources: to_contracts_sources_dump(&debugger_context.contracts_sources),
158+
}
159+
}
160+
161+
fn to_contracts_sources_dump(contracts_sources: &ContractSources) -> ContractsSourcesDump {
162+
ContractsSourcesDump {
163+
ids_by_name: contracts_sources.ids_by_name.clone(),
164+
sources_by_id: contracts_sources
165+
.sources_by_id
166+
.iter()
167+
.map(|(id, (source_code, contract_bytecode, source_path))| {
168+
(
169+
*id,
170+
ContractSourceDetailsDump {
171+
source_code: source_code.clone(),
172+
contract_bytecode: contract_bytecode.clone(),
173+
source_path: source_path.clone(),
174+
},
175+
)
176+
})
177+
.collect(),
178+
}
179+
}

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
#![warn(unused_crate_dependencies, unreachable_pub)]
66

@@ -9,5 +9,13 @@ extern crate tracing;
99

1010
mod op;
1111

12+
mod builder;
13+
mod context;
14+
mod debugger;
15+
mod file_dumper;
1216
mod tui;
13-
pub use tui::{Debugger, DebuggerBuilder, ExitReason};
17+
18+
pub use builder::DebuggerBuilder;
19+
pub use debugger::Debugger;
20+
pub use file_dumper::FileDumper;
21+
pub use tui::{ExitReason, TUI};

0 commit comments

Comments
 (0)