From bacacceb97da35ccd03b955aab2a6642ccfedd37 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:51:59 +0100 Subject: [PATCH] refactor(debugger): rewrite draw code (#6522) * chore: enable unreachable_pub * chore: use Debugger::builder() * test: add a simple debuggable test * refactor: use let-else to reduce indentation * fix: debugger panic handler * test: update debugger test * fix: solc artifact absolute path * refactor: src_text * refactor: add a wrapper for source lines * feat: minimum terminal size * refactor: rest of the draw functions * chore: explain panic hook * chore: remove whitespace hack * chore: clippy --- .cargo/config.toml | 15 +- Cargo.lock | 1 + crates/cli/src/utils/cmd.rs | 4 +- crates/debugger/Cargo.toml | 3 +- crates/debugger/src/lib.rs | 2 +- crates/debugger/src/op.rs | 8 +- crates/debugger/src/tui/draw.rs | 1109 ++++++++++++------------- crates/debugger/src/tui/mod.rs | 46 +- crates/forge/bin/cmd/script/build.rs | 9 +- crates/forge/bin/cmd/script/cmd.rs | 4 +- crates/forge/bin/cmd/test/mod.rs | 7 +- crates/forge/tests/cli/debug.rs | 94 +++ crates/forge/tests/cli/main.rs | 1 + crates/forge/tests/it/cheats.rs | 2 +- crates/forge/tests/it/config.rs | 4 +- crates/forge/tests/it/core.rs | 2 +- crates/forge/tests/it/fork.rs | 2 +- crates/forge/tests/it/fs.rs | 2 +- crates/forge/tests/it/fuzz.rs | 2 +- crates/forge/tests/it/inline.rs | 2 + crates/forge/tests/it/invariant.rs | 2 +- crates/forge/tests/it/spec.rs | 2 + crates/forge/tests/it/test_helpers.rs | 2 + 23 files changed, 711 insertions(+), 614 deletions(-) create mode 100644 crates/forge/tests/cli/debug.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 45bca53ae5eb..76cf725f9e2e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,16 +1,11 @@ [alias] cheats = "test -p foundry-cheatcodes-spec --features schema tests::" +test-debugger = "test -p forge --test cli manual_debug_setup -- --include-ignored --nocapture" +# Increase the stack size to 10MB for Windows targets, which is in line with Linux +# (whereas default for Windows is 1MB). [target.x86_64-pc-windows-msvc] -rustflags = [ - # Increases the stack size to 10MB, which is - # in line with Linux (whereas default for Windows is 1MB) - "-Clink-arg=/STACK:10000000", -] +rustflags = ["-Clink-arg=/STACK:10000000"] [target.i686-pc-windows-msvc] -rustflags = [ - # Increases the stack size to 10MB, which is - # in line with Linux (whereas default for Windows is 1MB) - "-Clink-arg=/STACK:10000000", -] +rustflags = ["-Clink-arg=/STACK:10000000"] diff --git a/Cargo.lock b/Cargo.lock index 8e89b6a51bc5..10914dff0b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2839,6 +2839,7 @@ dependencies = [ "crossterm", "eyre", "foundry-common", + "foundry-compilers", "foundry-evm-core", "foundry-evm-traces", "ratatui", diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 910e137fce2a..e0e11d21a51f 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -10,7 +10,7 @@ use foundry_compilers::{ Artifact, ProjectCompileOutput, }; use foundry_config::{error::ExtractConfigError, figment::Figment, Chain, Config, NamedChain}; -use foundry_debugger::DebuggerBuilder; +use foundry_debugger::Debugger; use foundry_evm::{ debug::DebugArena, executors::{DeployResult, EvmError, ExecutionErr, RawCallResult}, @@ -404,7 +404,7 @@ pub async fn handle_traces( if debug { let sources = etherscan_identifier.get_compiled_contracts().await?; - let mut debugger = DebuggerBuilder::new() + let mut debugger = Debugger::builder() .debug_arena(&result.debug) .decoder(&decoder) .sources(sources) diff --git a/crates/debugger/Cargo.toml b/crates/debugger/Cargo.toml index fa67b59294c8..a350d34b2cb0 100644 --- a/crates/debugger/Cargo.toml +++ b/crates/debugger/Cargo.toml @@ -10,9 +10,10 @@ homepage.workspace = true repository.workspace = true [dependencies] +foundry-common.workspace = true +foundry-compilers.workspace = true foundry-evm-core.workspace = true foundry-evm-traces.workspace = true -foundry-common.workspace = true alloy-primitives.workspace = true diff --git a/crates/debugger/src/lib.rs b/crates/debugger/src/lib.rs index a50a73660fd7..5683cb8a5b7c 100644 --- a/crates/debugger/src/lib.rs +++ b/crates/debugger/src/lib.rs @@ -2,7 +2,7 @@ //! //! Interactive Solidity TUI debugger. -#![warn(unused_crate_dependencies)] +#![warn(unused_crate_dependencies, unreachable_pub)] #[macro_use] extern crate tracing; diff --git a/crates/debugger/src/op.rs b/crates/debugger/src/op.rs index a6755483678a..486fbe09fbeb 100644 --- a/crates/debugger/src/op.rs +++ b/crates/debugger/src/op.rs @@ -1,16 +1,16 @@ /// Named parameter of an EVM opcode. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct OpcodeParam { +pub(crate) struct OpcodeParam { /// The name of the parameter. - pub name: &'static str, + pub(crate) name: &'static str, /// The index of the parameter on the stack. This is relative to the top of the stack. - pub index: usize, + pub(crate) index: usize, } impl OpcodeParam { /// Returns the list of named parameters for the given opcode. #[inline] - pub fn of(op: u8) -> &'static [Self] { + pub(crate) fn of(op: u8) -> &'static [Self] { MAP[op as usize] } } diff --git a/crates/debugger/src/tui/draw.rs b/crates/debugger/src/tui/draw.rs index 50f66a05b0a0..84e56ffe1e12 100644 --- a/crates/debugger/src/tui/draw.rs +++ b/crates/debugger/src/tui/draw.rs @@ -3,6 +3,7 @@ use super::context::DebuggerContext; use crate::op::OpcodeParam; use alloy_primitives::U256; +use foundry_compilers::sourcemap::SourceElement; use foundry_evm_core::{debug::Instruction, utils::CallKind}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -12,7 +13,7 @@ use ratatui::{ widgets::{Block, Borders, Paragraph, Wrap}, }; use revm::interpreter::opcode; -use std::{cmp, collections::VecDeque, io}; +use std::{cmp, collections::VecDeque, fmt::Write, io}; impl DebuggerContext<'_> { /// Draws the TUI layout and subcomponents to the given terminal. @@ -20,440 +21,345 @@ impl DebuggerContext<'_> { terminal.draw(|f| self.draw_layout(f)).map(drop) } + #[inline] fn draw_layout(&self, f: &mut Frame<'_>) { - if f.size().width < 225 { - self.vertical_layout(f); + // We need 100 columns to display a 32 byte word in the memory and stack panes. + let size = f.size(); + let min_width = 100; + let min_height = 16; + if size.width < min_width || size.height < min_height { + self.size_too_small(f, min_width, min_height); + return; + } + + // The horizontal layout draws these panes at 50% width. + let min_column_width_for_horizontal = 200; + if size.width >= min_column_width_for_horizontal { + self.horizontal_layout(f); } else { - self.square_layout(f); + self.vertical_layout(f); } } + fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) { + let mut lines = Vec::with_capacity(4); + + let l1 = "Terminal size too small:"; + lines.push(Line::from(l1)); + + let size = f.size(); + let width_color = if size.width >= min_width { Color::Green } else { Color::Red }; + let height_color = if size.height >= min_height { Color::Green } else { Color::Red }; + let l2 = vec![ + Span::raw("Width = "), + Span::styled(size.width.to_string(), Style::new().fg(width_color)), + Span::raw(" Height = "), + Span::styled(size.height.to_string(), Style::new().fg(height_color)), + ]; + lines.push(Line::from(l2)); + + let l3 = "Needed for current config:"; + lines.push(Line::from(l3)); + let l4 = format!("Width = {min_width} Height = {min_height}"); + lines.push(Line::from(l4)); + + let paragraph = + Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true }); + f.render_widget(paragraph, size) + } + + /// Draws the layout in vertical mode. + /// + /// ```text + /// |-----------------------------| + /// | op | + /// |-----------------------------| + /// | stack | + /// |-----------------------------| + /// | mem | + /// |-----------------------------| + /// | | + /// | src | + /// | | + /// |-----------------------------| + /// ``` fn vertical_layout(&self, f: &mut Frame<'_>) { - let total_size = f.size(); + let area = f.size(); let h_height = if self.show_shortcuts { 4 } else { 0 }; - if let [app, footer] = Layout::default() - .constraints( - [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)].as_ref(), - ) + // NOTE: `Layout::split` always returns a slice of the same length as the number of + // constraints, so the `else` branch is unreachable. + + // Split off footer. + let [app, footer] = Layout::new() + .constraints([Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)]) .direction(Direction::Vertical) - .split(total_size)[..] - { - if let [op_pane, stack_pane, memory_pane, src_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(3, 6), - ] - .as_ref(), - ) - .split(app)[..] - { - if self.show_shortcuts { - Self::draw_footer(f, footer); - } - self.draw_src(f, src_pane); - self.draw_op_list(f, op_pane); - self.draw_stack(f, stack_pane); - self.draw_memory(f, memory_pane); - } else { - panic!("unable to create vertical panes") - } - } else { - panic!("unable to create footer / app") + .split(area)[..] + else { + unreachable!() }; + + // Split the app in 4 vertically to construct all the panes. + let [op_pane, stack_pane, memory_pane, src_pane] = Layout::new() + .direction(Direction::Vertical) + .constraints([ + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(3, 6), + ]) + .split(app)[..] + else { + unreachable!() + }; + + if self.show_shortcuts { + self.draw_footer(f, footer); + } + self.draw_src(f, src_pane); + self.draw_op_list(f, op_pane); + self.draw_stack(f, stack_pane); + self.draw_memory(f, memory_pane); } - fn square_layout(&self, f: &mut Frame<'_>) { - let total_size = f.size(); + /// Draws the layout in horizontal mode. + /// + /// ```text + /// |-----------------|-----------| + /// | op | stack | + /// |-----------------|-----------| + /// | | | + /// | src | mem | + /// | | | + /// |-----------------|-----------| + /// ``` + fn horizontal_layout(&self, f: &mut Frame<'_>) { + let area = f.size(); let h_height = if self.show_shortcuts { 4 } else { 0 }; - // split in 2 vertically - - if let [app, footer] = Layout::default() + // Split off footer. + let [app, footer] = Layout::new() .direction(Direction::Vertical) - .constraints( - [Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)].as_ref(), - ) - .split(total_size)[..] - { - if let [left_pane, right_pane] = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)].as_ref()) - .split(app)[..] - { - // split right pane horizontally to construct stack and memory - if let [op_pane, src_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)].as_ref()) - .split(left_pane)[..] - { - if let [stack_pane, memory_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)].as_ref()) - .split(right_pane)[..] - { - if self.show_shortcuts { - Self::draw_footer(f, footer) - }; - self.draw_src(f, src_pane); - self.draw_op_list(f, op_pane); - self.draw_stack(f, stack_pane); - self.draw_memory(f, memory_pane); - } - } else { - panic!("Couldn't generate horizontal split layout 1:2."); - } - } else { - panic!("Couldn't generate vertical split layout 1:2."); - } - } else { - panic!("Couldn't generate application & footer") - } - } + .constraints([Constraint::Ratio(100 - h_height, 100), Constraint::Ratio(h_height, 100)]) + .split(area)[..] + else { + unreachable!() + }; - fn draw_footer(f: &mut Frame<'_>, area: Rect) { - let block_controls = Block::default(); + // Split app in 2 horizontally. + let [app_left, app_right] = Layout::new() + .direction(Direction::Horizontal) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) + .split(app)[..] + else { + unreachable!() + }; + + // Split left pane in 2 vertically to opcode list and source. + let [op_pane, src_pane] = Layout::new() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)]) + .split(app_left)[..] + else { + unreachable!() + }; - let text_output = vec![Line::from(Span::styled( - "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end", Style::default().add_modifier(Modifier::DIM))), -Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll memory | [']: goto breakpoint | [h] toggle help", Style::default().add_modifier(Modifier::DIM)))]; + // Split right pane horizontally to construct stack and memory. + let [stack_pane, memory_pane] = Layout::new() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)]) + .split(app_right)[..] + else { + unreachable!() + }; - let paragraph = Paragraph::new(text_output) - .block(block_controls) - .alignment(Alignment::Center) - .wrap(Wrap { trim: false }); + if self.show_shortcuts { + self.draw_footer(f, footer); + } + self.draw_src(f, src_pane); + self.draw_op_list(f, op_pane); + self.draw_stack(f, stack_pane); + self.draw_memory(f, memory_pane); + } + fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) { + let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end"; + let l2 = "[t]: stack labels | [m]: memory decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll memory | [']: goto breakpoint | [h] toggle help"; + let dimmed = Style::new().add_modifier(Modifier::DIM); + let lines = + vec![Line::from(Span::styled(l1, dimmed)), Line::from(Span::styled(l2, dimmed))]; + let paragraph = + Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } fn draw_src(&self, f: &mut Frame<'_>, area: Rect) { - let block_source_code = Block::default() - .title(match self.call_kind() { - CallKind::Create | CallKind::Create2 => "Contract creation", - CallKind::Call => "Contract call", - CallKind::StaticCall => "Contract staticcall", - CallKind::CallCode => "Contract callcode", - CallKind::DelegateCall => "Contract delegatecall", - }) - .borders(Borders::ALL); + let text_output = self.src_text(area); + let title = match self.call_kind() { + CallKind::Create | CallKind::Create2 => "Contract creation", + CallKind::Call => "Contract call", + CallKind::StaticCall => "Contract staticcall", + CallKind::CallCode => "Contract callcode", + CallKind::DelegateCall => "Contract delegatecall", + }; + let block = Block::default().title(title).borders(Borders::ALL); + let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false }); + f.render_widget(paragraph, area); + } - let mut text_output: Text = Text::from(""); + fn src_text(&self, area: Rect) -> Text<'_> { + let (source_element, source_code) = match self.src_map() { + Ok(r) => r, + Err(e) => return Text::from(e), + }; - let pc = self.current_step().pc; - if let Some(contract_name) = self.debugger.identified_contracts.get(self.address()) { - if let Some(files_source_code) = self.debugger.contracts_sources.0.get(contract_name) { - let pc_ic_map = self.debugger.pc_ic_maps.get(contract_name); - // find the contract source with the correct source_element's file_id - if let Some((source_element, source_code)) = files_source_code.iter().find_map( - |(file_id, (source_code, contract_source))| { - // grab either the creation source map or runtime sourcemap - if let Some((Ok(source_map), ic)) = - if matches!(self.call_kind(), CallKind::Create | CallKind::Create2) { - contract_source - .bytecode - .source_map() - .zip(pc_ic_map.and_then(|(c, _)| c.get(&pc))) - } else { - contract_source - .deployed_bytecode - .bytecode - .as_ref() - .expect("no bytecode") - .source_map() - .zip(pc_ic_map.and_then(|(_, r)| r.get(&pc))) - } - { - let source_element = source_map[*ic].clone(); - if let Some(index) = source_element.index { - if *file_id == index { - Some((source_element, source_code)) - } else { - None - } - } else { - None - } - } else { - None - } - }, - ) { - // we are handed a vector of SourceElements that give - // us a span of sourcecode that is currently being executed - // This includes an offset and length. This vector is in - // instruction pointer order, meaning the location of - // the instruction - sum(push_bytes[..pc]) - let offset = source_element.offset; - let len = source_element.length; - let max = source_code.len(); - - // split source into before, relevant, and after chunks - // split by line as well to do some formatting stuff - let mut before = source_code[..std::cmp::min(offset, max)] - .split_inclusive('\n') - .collect::>(); - let actual = source_code - [std::cmp::min(offset, max)..std::cmp::min(offset + len, max)] - .split_inclusive('\n') - .map(|s| s.to_string()) - .collect::>(); - let mut after = source_code[std::cmp::min(offset + len, max)..] - .split_inclusive('\n') - .collect::>(); - - let mut line_number = 0; - - let num_lines = before.len() + actual.len() + after.len(); - let height = area.height as usize; - let needed_highlight = actual.len(); - let mid_len = before.len() + actual.len(); - - // adjust what text we show of the source code - let (start_line, end_line) = if needed_highlight > height { - // highlighted section is more lines than we have avail - (before.len(), before.len() + needed_highlight) - } else if height > num_lines { - // we can fit entire source - (0, num_lines) - } else { - let remaining = height - needed_highlight; - let mut above = remaining / 2; - let mut below = remaining / 2; - if below > after.len() { - // unused space below the highlight - above += below - after.len(); - } else if above > before.len() { - // we have unused space above the highlight - below += above - before.len(); - } else { - // no unused space - } + // We are handed a vector of SourceElements that give us a span of sourcecode that is + // currently being executed. This includes an offset and length. + // This vector is in instruction pointer order, meaning the location of the instruction + // minus `sum(push_bytes[..pc])`. + let offset = source_element.offset; + let len = source_element.length; + let max = source_code.len(); - (before.len().saturating_sub(above), mid_len + below) - }; - - let max_line_num = num_lines.to_string().len(); - // We check if there is other text on the same line before the - // highlight starts - if let Some(last) = before.pop() { - if !last.ends_with('\n') { - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default().fg(Color::Gray).bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default().add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::raw(last), - Span::styled( - actual[0].to_string(), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - - actual.iter().skip(1).for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - // this is a hack to add coloring - // because tui does weird trimming - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } else { - before.push(last); - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default().fg(Color::Gray).bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default().add_modifier(Modifier::DIM), - ), - ])); - - line_number += 1; - }); - actual.iter().for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } - } else { - actual.iter().for_each(|s| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } + // Split source into before, relevant, and after chunks, split by line, for formatting. + let actual_start = offset.min(max); + let actual_end = (offset + len).min(max); - // fill in the rest of the line as unhighlighted - if let Some(last) = actual.last() { - if !last.ends_with('\n') { - if let Some(post) = after.pop_front() { - if let Some(last) = text_output.lines.last_mut() { - last.spans.push(Span::raw(post)); - } - } - } - } + let mut before: Vec<_> = source_code[..actual_start].split_inclusive('\n').collect(); + let actual: Vec<_> = source_code[actual_start..actual_end].split_inclusive('\n').collect(); + let mut after: VecDeque<_> = source_code[actual_end..].split_inclusive('\n').collect(); - // add after highlighted text - while mid_len + after.len() > end_line { - after.pop_back(); - } - after.iter().for_each(|line| { - text_output.lines.push(Line::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default().fg(Color::Gray).bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default().add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - } else { - text_output.extend(Text::from("No sourcemap for contract")); - } + let num_lines = before.len() + actual.len() + after.len(); + let height = area.height as usize; + let needed_highlight = actual.len(); + let mid_len = before.len() + actual.len(); + + // adjust what text we show of the source code + let (start_line, end_line) = if needed_highlight > height { + // highlighted section is more lines than we have avail + (before.len(), before.len() + needed_highlight) + } else if height > num_lines { + // we can fit entire source + (0, num_lines) + } else { + let remaining = height - needed_highlight; + let mut above = remaining / 2; + let mut below = remaining / 2; + if below > after.len() { + // unused space below the highlight + above += below - after.len(); + } else if above > before.len() { + // we have unused space above the highlight + below += above - before.len(); + } else { + // no unused space + } + + (before.len().saturating_sub(above), mid_len + below) + }; + + // Unhighlighted line number: gray. + let u_num = Style::new().fg(Color::Gray); + // Unhighlighted text: default, dimmed. + let u_text = Style::new().add_modifier(Modifier::DIM); + // Highlighted line number: cyan. + let h_num = Style::new().fg(Color::Cyan); + // Highlighted text: cyan, bold. + let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD); + + let mut lines = SourceLines::new(decimal_digits(num_lines)); + + // We check if there is other text on the same line before the highlight starts. + if let Some(last) = before.pop() { + let last_has_nl = last.ends_with('\n'); + + if last_has_nl { + before.push(last); + } + for line in &before[start_line..] { + lines.push(u_num, line, u_text); + } + + let first = if !last_has_nl { + lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]); + 1 } else { - text_output - .extend(Text::from(format!("No srcmap index for contract {contract_name}"))); + 0 + }; + + // Skip the first line if it has already been handled above. + for line in &actual[first..] { + lines.push(h_num, line, h_text); } } else { - let address = self.address(); - text_output.extend(Text::from(format!("Unknown contract at address {address}"))); + // No text before the current line. + for line in &actual { + lines.push(h_num, line, h_text); + } } - let paragraph = - Paragraph::new(text_output).block(block_source_code).wrap(Wrap { trim: false }); - f.render_widget(paragraph, area); + // Fill in the rest of the line as unhighlighted. + if let Some(last) = actual.last() { + if !last.ends_with('\n') { + if let Some(post) = after.pop_front() { + if let Some(last) = lines.lines.last_mut() { + last.spans.push(Span::raw(post)); + } + } + } + } + + // Add after highlighted text. + while mid_len + after.len() > end_line { + after.pop_back(); + } + for line in after { + lines.push(u_num, line, u_text); + } + + Text::from(lines.lines) } - /// Draw opcode list into main component - fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) { - let step = self.current_step(); - let block_source_code = Block::default() - .title(format!( - "Address: {} | PC: {} | Gas used in call: {}", - self.address(), - step.pc, - step.total_gas_used, - )) - .borders(Borders::ALL); - let mut text_output: Vec = Vec::new(); - - // Scroll: - // Focused line is line that should always be at the center of the screen. - let display_start; + fn src_map(&self) -> Result<(SourceElement, &str), String> { + let address = self.address(); + let Some(contract_name) = self.debugger.identified_contracts.get(address) else { + return Err(format!("Unknown contract at address {address}")); + }; + let Some(files_source_code) = self.debugger.contracts_sources.0.get(contract_name) else { + return Err(format!("No source map index for contract {contract_name}")); + }; + + let Some((create_map, rt_map)) = self.debugger.pc_ic_maps.get(contract_name) else { + return Err(format!("No PC-IC maps for contract {contract_name}")); + }; + + let is_create = matches!(self.call_kind(), CallKind::Create | CallKind::Create2); + let pc = self.current_step().pc; + let Some((source_element, source_code)) = + files_source_code.iter().find_map(|(file_id, (source_code, contract_source))| { + let bytecode = if is_create { + &contract_source.bytecode + } else { + contract_source.deployed_bytecode.bytecode.as_ref()? + }; + let mut source_map = bytecode.source_map()?.ok()?; + + let pc_ic_map = if is_create { create_map } else { rt_map }; + let ic = pc_ic_map.get(&pc)?; + let source_element = source_map.swap_remove(*ic); + (*file_id == source_element.index?).then_some((source_element, source_code)) + }) + else { + return Err(format!("No source map for contract {contract_name}")); + }; + + Ok((source_element, source_code)) + } + + fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) { let height = area.height as i32; let extra_top_lines = height / 2; - let prev_start = *self.draw_memory.current_startline.borrow(); // Absolute minimum start line let abs_min_start = 0; // Adjust for weird scrolling for max top line @@ -474,61 +380,58 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k std::mem::swap(&mut min_start, &mut max_start); } - if prev_start < min_start { - display_start = min_start; - } else if prev_start > max_start { - display_start = max_start; - } else { - display_start = prev_start; - } + let prev_start = *self.draw_memory.current_startline.borrow(); + let display_start = prev_start.clamp(min_start, max_start); *self.draw_memory.current_startline.borrow_mut() = display_start; - let max_pc_len = - self.debug_steps().iter().fold(0, |max_val, val| val.pc.max(max_val)).to_string().len(); + let max_pc = self.debug_steps().iter().map(|step| step.pc).max().unwrap_or(0); + let max_pc_len = hex_digits(max_pc); - // Define closure that prints one more line of source code - let mut add_new_line = |line_number| { - let is_current_step = line_number == self.current_step; - let bg_color = if is_current_step { Color::DarkGray } else { Color::Reset }; + let debug_steps = self.debug_steps(); + let mut lines = Vec::new(); + let mut add_new_line = |line_number: usize| { + let mut line = String::with_capacity(64); - // Format line number - let line_number_format = if is_current_step { - format!("{:0>max_pc_len$x}|▶", step.pc) - } else if line_number < self.debug_steps().len() { - format!("{:0>max_pc_len$x}| ", step.pc) - } else { - "END CALL".to_string() - }; - - if let Some(op) = self.opcode_list.get(line_number) { - text_output.push(Line::from(Span::styled( - format!("{line_number_format}{op}"), - Style::default().fg(Color::White).bg(bg_color), - ))); + let is_current_step = line_number == self.current_step; + if line_number < self.debug_steps().len() { + let step = &debug_steps[line_number]; + write!(line, "{:0>max_pc_len$x}|", step.pc).unwrap(); + line.push_str(if is_current_step { "▶" } else { " " }); + if let Some(op) = self.opcode_list.get(line_number) { + line.push_str(op); + } } else { - text_output.push(Line::from(Span::styled( - line_number_format, - Style::default().fg(Color::White).bg(bg_color), - ))); + line.push_str("END CALL"); } + + let bg_color = if is_current_step { Color::DarkGray } else { Color::Reset }; + let style = Style::new().fg(Color::White).bg(bg_color); + lines.push(Line::from(Span::styled(line, style))); }; + for number in display_start..self.opcode_list.len() { add_new_line(number); } + // Add one more "phantom" line so we see line where current segment execution ends add_new_line(self.opcode_list.len()); - let paragraph = - Paragraph::new(text_output).block(block_source_code).wrap(Wrap { trim: true }); + + let title = format!( + "Address: {} | PC: {} | Gas used in call: {}", + self.address(), + self.current_step().pc, + self.current_step().total_gas_used, + ); + let block = Block::default().title(title).borders(Borders::ALL); + let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: true }); f.render_widget(paragraph, area); } - /// Draw the stack into the stack pane fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) { let step = self.current_step(); let stack = &step.stack; - let stack_space = - Block::default().title(format!("Stack: {}", stack.len())).borders(Borders::ALL); - let min_len = usize::max(format!("{}", stack.len()).len(), 2); + + let min_len = decimal_digits(stack.len()).max(2); let params = if let Instruction::OpCode(op) = step.instruction { OpcodeParam::of(op) } else { &[] }; @@ -540,120 +443,55 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k .skip(self.draw_memory.current_stack_startline) .map(|(i, stack_item)| { let param = params.iter().find(|param| param.index == i); - let mut words: Vec = (0..32) - .rev() - .map(|i| stack_item.byte(i)) - .map(|byte| { - Span::styled( - format!("{byte:02x} "), - if param.is_some() { - Style::default().fg(Color::Cyan) - } else if byte == 0 { - // this improves compatibility across terminals by not combining - // color with DIM modifier - Style::default().add_modifier(Modifier::DIM) - } else { - Style::default().fg(Color::White) - }, - ) - }) - .collect(); + + let mut spans = Vec::with_capacity(1 + 32 * 2 + 3); + + // Stack index. + spans.push(Span::styled(format!("{i:0min_len$}| "), Style::new().fg(Color::White))); + + // Item hex bytes. + hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| { + if param.is_some() { + Style::new().fg(Color::Cyan) + } else { + Style::new().fg(Color::White) + } + }); if self.stack_labels { if let Some(param) = param { - words.push(Span::raw(format!("| {}", param.name))); - } else { - words.push(Span::raw("| ".to_string())); + spans.push(Span::raw("| ")); + spans.push(Span::raw(param.name)); } } - let mut spans = vec![Span::styled( - format!("{i:0min_len$}| "), - Style::default().fg(Color::White), - )]; - spans.extend(words); spans.push(Span::raw("\n")); Line::from(spans) }) .collect(); - let paragraph = Paragraph::new(text).block(stack_space).wrap(Wrap { trim: true }); + let title = format!("Stack: {}", stack.len()); + let block = Block::default().title(title).borders(Borders::ALL); + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); f.render_widget(paragraph, area); } - /// The memory_access variable stores the index on the stack that indicates the memory - /// offset/size accessed by the given opcode: - /// (read memory offset, read memory size, write memory offset, write memory size) - /// >= 1: the stack index - /// 0: no memory access - /// -1: a fixed size of 32 bytes - /// -2: a fixed size of 1 byte - /// The return value is a tuple about accessed memory region by the given opcode: - /// (read memory offset, read memory size, write memory offset, write memory size) - fn get_memory_access( - op: u8, - stack: &[U256], - ) -> (Option, Option, Option, Option) { - let memory_access = match op { - opcode::KECCAK256 | opcode::RETURN | opcode::REVERT => (1, 2, 0, 0), - opcode::CALLDATACOPY | opcode::CODECOPY | opcode::RETURNDATACOPY => (0, 0, 1, 3), - opcode::EXTCODECOPY => (0, 0, 2, 4), - opcode::MLOAD => (1, -1, 0, 0), - opcode::MSTORE => (0, 0, 1, -1), - opcode::MSTORE8 => (0, 0, 1, -2), - opcode::LOG0 | opcode::LOG1 | opcode::LOG2 | opcode::LOG3 | opcode::LOG4 => { - (1, 2, 0, 0) - } - opcode::CREATE | opcode::CREATE2 => (2, 3, 0, 0), - opcode::CALL | opcode::CALLCODE => (4, 5, 0, 0), - opcode::DELEGATECALL | opcode::STATICCALL => (3, 4, 0, 0), - _ => Default::default(), - }; - - let stack_len = stack.len(); - let get_size = |stack_index| match stack_index { - -2 => Some(1), - -1 => Some(32), - 0 => None, - 1.. => { - if (stack_index as usize) <= stack_len { - Some(stack[stack_len - stack_index as usize].saturating_to()) - } else { - None - } - } - _ => panic!("invalid stack index"), - }; - - let (read_offset, read_size, write_offset, write_size) = ( - get_size(memory_access.0), - get_size(memory_access.1), - get_size(memory_access.2), - get_size(memory_access.3), - ); - (read_offset, read_size, write_offset, write_size) - } - - /// Draw memory in memory pane fn draw_memory(&self, f: &mut Frame<'_>, area: Rect) { let step = self.current_step(); let memory = &step.memory; - let memory_space = Block::default() - .title(format!("Memory (max expansion: {} bytes)", memory.len())) - .borders(Borders::ALL); - let max_i = memory.len() / 32; - let min_len = format!("{:x}", max_i * 32).len(); - - // color memory region based on write/read - let mut offset: Option = None; - let mut size: Option = None; + + let min_len = hex_digits(memory.len()); + + // Color memory region based on read/write. + let mut offset = None; + let mut size = None; let mut color = None; if let Instruction::OpCode(op) = step.instruction { let stack_len = step.stack.len(); if stack_len > 0 { let (read_offset, read_size, write_offset, write_size) = - Self::get_memory_access(op, &step.stack); + get_memory_access(op, &step.stack); if read_offset.is_some() { offset = read_offset; size = read_size; @@ -671,8 +509,7 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k let prev_step = self.current_step - 1; let prev_step = &self.debug_steps()[prev_step]; if let Instruction::OpCode(op) = prev_step.instruction { - let (_, _, write_offset, write_size) = - Self::get_memory_access(op, &prev_step.stack); + let (_, _, write_offset, write_size) = get_memory_access(op, &prev_step.stack); if write_offset.is_some() { offset = write_offset; size = write_size; @@ -688,53 +525,40 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k .chunks(32) .enumerate() .skip(self.draw_memory.current_mem_startline) - .take_while(|(i, _)| i < &end_line) + .take_while(|(i, _)| *i < end_line) .map(|(i, mem_word)| { - let words: Vec = mem_word - .iter() - .enumerate() - .map(|(j, byte)| { - Span::styled( - format!("{byte:02x} "), - if let (Some(offset), Some(size), Some(color)) = (offset, size, color) { - if i * 32 + j >= offset && i * 32 + j < offset + size { - // [offset, offset + size] is the memory region to be colored. - // If a byte at row i and column j in the memory panel - // falls in this region, set the color. - Style::default().fg(color) - } else if *byte == 0 { - Style::default().add_modifier(Modifier::DIM) - } else { - Style::default().fg(Color::White) - } - } else if *byte == 0 { - Style::default().add_modifier(Modifier::DIM) - } else { - Style::default().fg(Color::White) - }, - ) - }) - .collect(); - - let mut spans = vec![Span::styled( + let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1); + + // Memory index. + spans.push(Span::styled( format!("{:0min_len$x}| ", i * 32), - Style::default().fg(Color::White), - )]; - spans.extend(words); + Style::new().fg(Color::White), + )); + + // Word hex bytes. + hex_bytes_spans(mem_word, &mut spans, |j, _| { + let mut byte_color = Color::White; + if let (Some(offset), Some(size), Some(color)) = (offset, size, color) { + let idx = i * 32 + j; + if (offset..offset + size).contains(&idx) { + // [offset, offset + size] is the memory region to be colored. + // If a byte at row i and column j in the memory panel + // falls in this region, set the color. + byte_color = color; + } + } + Style::new().fg(byte_color) + }); if self.mem_utf { - let chars: Vec = mem_word - .chunks(4) - .map(|utf| { - if let Ok(utf_str) = std::str::from_utf8(utf) { - Span::raw(utf_str.replace(char::from(0), ".")) - } else { - Span::raw(".") - } - }) - .collect(); spans.push(Span::raw("|")); - spans.extend(chars); + for utf in mem_word.chunks(4) { + if let Ok(utf_str) = std::str::from_utf8(utf) { + spans.push(Span::raw(utf_str.replace('\0', "."))); + } else { + spans.push(Span::raw(".")); + } + } } spans.push(Span::raw("\n")); @@ -742,7 +566,152 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k Line::from(spans) }) .collect(); - let paragraph = Paragraph::new(text).block(memory_space).wrap(Wrap { trim: true }); + + let title = format!("Memory (max expansion: {} bytes)", memory.len()); + let block = Block::default().title(title).borders(Borders::ALL); + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); f.render_widget(paragraph, area); } } + +/// Wrapper around a list of [`Line`]s that prepends the line number on each new line. +struct SourceLines<'a> { + lines: Vec>, + max_line_num: usize, +} + +impl<'a> SourceLines<'a> { + fn new(max_line_num: usize) -> Self { + Self { lines: Vec::new(), max_line_num } + } + + fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) { + self.push_raw(line_number_style, &[Span::styled(line, line_style)]); + } + + fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) { + let mut line_spans = Vec::with_capacity(4); + + let line_number = + format!("{number: >width$} ", number = self.lines.len() + 1, width = self.max_line_num); + line_spans.push(Span::styled(line_number, line_number_style)); + + // Space between line number and line text. + line_spans.push(Span::raw(" ")); + + line_spans.extend_from_slice(spans); + + self.lines.push(Line::from(line_spans)); + } +} + +/// The memory_access variable stores the index on the stack that indicates the memory +/// offset/size accessed by the given opcode: +/// (read memory offset, read memory size, write memory offset, write memory size) +/// >= 1: the stack index +/// 0: no memory access +/// -1: a fixed size of 32 bytes +/// -2: a fixed size of 1 byte +/// The return value is a tuple about accessed memory region by the given opcode: +/// (read memory offset, read memory size, write memory offset, write memory size) +fn get_memory_access( + op: u8, + stack: &[U256], +) -> (Option, Option, Option, Option) { + let memory_access = match op { + opcode::KECCAK256 | opcode::RETURN | opcode::REVERT => (1, 2, 0, 0), + opcode::CALLDATACOPY | opcode::CODECOPY | opcode::RETURNDATACOPY => (0, 0, 1, 3), + opcode::EXTCODECOPY => (0, 0, 2, 4), + opcode::MLOAD => (1, -1, 0, 0), + opcode::MSTORE => (0, 0, 1, -1), + opcode::MSTORE8 => (0, 0, 1, -2), + opcode::LOG0 | opcode::LOG1 | opcode::LOG2 | opcode::LOG3 | opcode::LOG4 => (1, 2, 0, 0), + opcode::CREATE | opcode::CREATE2 => (2, 3, 0, 0), + opcode::CALL | opcode::CALLCODE => (4, 5, 0, 0), + opcode::DELEGATECALL | opcode::STATICCALL => (3, 4, 0, 0), + _ => Default::default(), + }; + + let stack_len = stack.len(); + let get_size = |stack_index| match stack_index { + -2 => Some(1), + -1 => Some(32), + 0 => None, + 1.. => { + if (stack_index as usize) <= stack_len { + Some(stack[stack_len - stack_index as usize].saturating_to()) + } else { + None + } + } + _ => panic!("invalid stack index"), + }; + + let (read_offset, read_size, write_offset, write_size) = ( + get_size(memory_access.0), + get_size(memory_access.1), + get_size(memory_access.2), + get_size(memory_access.3), + ); + (read_offset, read_size, write_offset, write_size) +} + +fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec>, f: impl Fn(usize, u8) -> Style) { + for (i, &byte) in bytes.iter().enumerate() { + if i > 0 { + spans.push(Span::raw(" ")); + } + spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte))); + } +} + +/// Returns the number of decimal digits in the given number. +/// +/// This is the same as `n.to_string().len()`. +fn decimal_digits(n: usize) -> usize { + n.checked_ilog10().unwrap_or(0) as usize + 1 +} + +/// Returns the number of hexadecimal digits in the given number. +/// +/// This is the same as `format!("{n:x}").len()`. +fn hex_digits(n: usize) -> usize { + n.checked_ilog(16).unwrap_or(0) as usize + 1 +} + +#[cfg(test)] +mod tests { + #[test] + fn decimal_digits() { + assert_eq!(super::decimal_digits(0), 1); + assert_eq!(super::decimal_digits(1), 1); + assert_eq!(super::decimal_digits(2), 1); + assert_eq!(super::decimal_digits(9), 1); + assert_eq!(super::decimal_digits(10), 2); + assert_eq!(super::decimal_digits(11), 2); + assert_eq!(super::decimal_digits(50), 2); + assert_eq!(super::decimal_digits(99), 2); + assert_eq!(super::decimal_digits(100), 3); + assert_eq!(super::decimal_digits(101), 3); + assert_eq!(super::decimal_digits(201), 3); + assert_eq!(super::decimal_digits(999), 3); + assert_eq!(super::decimal_digits(1000), 4); + assert_eq!(super::decimal_digits(1001), 4); + } + + #[test] + fn hex_digits() { + assert_eq!(super::hex_digits(0), 1); + assert_eq!(super::hex_digits(1), 1); + assert_eq!(super::hex_digits(2), 1); + assert_eq!(super::hex_digits(9), 1); + assert_eq!(super::hex_digits(10), 1); + assert_eq!(super::hex_digits(11), 1); + assert_eq!(super::hex_digits(15), 1); + assert_eq!(super::hex_digits(16), 2); + assert_eq!(super::hex_digits(17), 2); + assert_eq!(super::hex_digits(0xff), 2); + assert_eq!(super::hex_digits(0x100), 3); + assert_eq!(super::hex_digits(0x101), 3); + } +} diff --git a/crates/debugger/src/tui/mod.rs b/crates/debugger/src/tui/mod.rs index 01043fc53b54..399c63dfabfa 100644 --- a/crates/debugger/src/tui/mod.rs +++ b/crates/debugger/src/tui/mod.rs @@ -21,7 +21,7 @@ use std::{ collections::{BTreeMap, HashMap}, io, ops::ControlFlow, - sync::mpsc, + sync::{mpsc, Arc}, thread, time::{Duration, Instant}, }; @@ -167,28 +167,56 @@ impl Debugger { } } +type PanicHandler = Box) + 'static + Sync + Send>; + /// Handles terminal state. #[must_use] -struct TerminalGuard<'a, B: Backend + io::Write>(&'a mut Terminal); +struct TerminalGuard<'a, B: Backend + io::Write> { + terminal: &'a mut Terminal, + hook: Option>, +} impl<'a, B: Backend + io::Write> TerminalGuard<'a, B> { fn with(terminal: &'a mut Terminal, mut f: impl FnMut(&mut Terminal) -> T) -> T { - let mut guard = Self(terminal); + let mut guard = Self { terminal, hook: None }; guard.setup(); - f(guard.0) + f(guard.terminal) } fn setup(&mut self) { + let previous = Arc::new(std::panic::take_hook()); + self.hook = Some(previous.clone()); + // We need to restore the terminal state before displaying the panic message. + // TODO: Use `std::panic::update_hook` when it's stable + std::panic::set_hook(Box::new(move |info| { + Self::half_restore(&mut std::io::stdout()); + (previous)(info) + })); + let _ = enable_raw_mode(); - let _ = execute!(*self.0.backend_mut(), EnterAlternateScreen, EnableMouseCapture); - let _ = self.0.hide_cursor(); - let _ = self.0.clear(); + let _ = execute!(*self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture); + let _ = self.terminal.hide_cursor(); + let _ = self.terminal.clear(); } fn restore(&mut self) { + if !std::thread::panicking() { + let _ = std::panic::take_hook(); + let prev = self.hook.take().unwrap(); + let prev = match Arc::try_unwrap(prev) { + Ok(prev) => prev, + Err(_) => unreachable!(), + }; + std::panic::set_hook(prev); + } + + Self::half_restore(self.terminal.backend_mut()); + let _ = self.terminal.show_cursor(); + } + + fn half_restore(w: &mut impl io::Write) { let _ = disable_raw_mode(); - let _ = execute!(*self.0.backend_mut(), LeaveAlternateScreen, DisableMouseCapture); - let _ = self.0.show_cursor(); + let _ = execute!(*w, LeaveAlternateScreen, DisableMouseCapture); } } diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 1ac4b63e3d37..784cd5cd7bd8 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -38,12 +38,13 @@ impl ScriptArgs { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - let abs_path = source + let path = source .ast - .ok_or_else(|| eyre::eyre!("Source from artifact has no AST."))? + .ok_or_else(|| eyre::eyre!("source from artifact has no AST"))? .absolute_path; + let abs_path = project.root().join(path); let source_code = fs::read_to_string(abs_path).wrap_err_with(|| { - format!("Failed to read artifact source file for `{}`", id.identifier()) + format!("failed to read artifact source file for `{}`", id.identifier()) })?; let contract = artifact.clone().into_contract_bytecode(); let source_contract = compact_to_contract(contract)?; @@ -53,7 +54,7 @@ impl ScriptArgs { .or_default() .insert(source.id, (source_code, source_contract)); } else { - warn!("source not found for artifact={:?}", id); + warn!(?id, "source not found"); } Ok((id, artifact)) }) diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index b44cf71d0026..3fbdfc514950 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -6,7 +6,7 @@ use ethers_signers::Signer; use eyre::Result; use foundry_cli::utils::LoadConfig; use foundry_common::{contracts::flatten_contracts, try_get_http_provider, types::ToAlloy}; -use foundry_debugger::DebuggerBuilder; +use foundry_debugger::Debugger; use std::sync::Arc; /// Helper alias type for the collection of data changed due to the new sender. @@ -84,7 +84,7 @@ impl ScriptArgs { let mut decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; if self.debug { - let mut debugger = DebuggerBuilder::new() + let mut debugger = Debugger::builder() .debug_arenas(result.debug.as_deref().unwrap_or_default()) .decoder(&decoder) .sources(sources) diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index cb32bed9c50b..1f79b253267f 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -31,7 +31,7 @@ use foundry_config::{ }, get_available_profiles, Config, }; -use foundry_debugger::DebuggerBuilder; +use foundry_debugger::Debugger; use regex::Regex; use std::{collections::BTreeMap, fs, sync::mpsc::channel, time::Duration}; use watchexec::config::{InitConfig, RuntimeConfig}; @@ -297,10 +297,11 @@ impl TestArgs { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - let abs_path = source + let path = source .ast .ok_or_else(|| eyre::eyre!("Source from artifact has no AST."))? .absolute_path; + let abs_path = project.root().join(&path); let source_code = fs::read_to_string(abs_path)?; let contract = artifact.clone().into_contract_bytecode(); let source_contract = compact_to_contract(contract)?; @@ -315,7 +316,7 @@ impl TestArgs { let test = outcome.clone().into_tests().next().unwrap(); let result = test.result; // Run the debugger - let mut debugger = DebuggerBuilder::new() + let mut debugger = Debugger::builder() // TODO: `Option::as_slice` in 1.75 .debug_arenas(result.debug.as_ref().map(core::slice::from_ref).unwrap_or_default()) .decoders(&decoders) diff --git a/crates/forge/tests/cli/debug.rs b/crates/forge/tests/cli/debug.rs new file mode 100644 index 000000000000..3e3d08c7e2ad --- /dev/null +++ b/crates/forge/tests/cli/debug.rs @@ -0,0 +1,94 @@ +use itertools::Itertools; +use std::path::Path; + +// Sets up a debuggable test case. +// Run with `cargo test-debugger`. +forgetest_async!( + #[ignore = "ran manually"] + manual_debug_setup, + |prj, cmd| { + cmd.args(["init", "--force"]).arg(prj.root()).assert_non_empty_stdout(); + cmd.forge_fuse(); + + prj.add_source("Counter2.sol", r#" +contract A { + address public a; + uint public b; + int public c; + bytes32 public d; + bool public e; + bytes public f; + string public g; + + constructor(address _a, uint _b, int _c, bytes32 _d, bool _e, bytes memory _f, string memory _g) { + a = _a; + b = _b; + c = _c; + d = _d; + e = _e; + f = _f; + g = _g; + } + + function getA() public view returns (address) { + return a; + } + + function setA(address _a) public { + a = _a; + } +}"#, + ) + .unwrap(); + + let script = prj.add_script("Counter.s.sol", r#" +import "../src/Counter2.sol"; +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; + +contract B is A { + A public other; + address public self = address(this); + + constructor(address _a, uint _b, int _c, bytes32 _d, bool _e, bytes memory _f, string memory _g) + A(_a, _b, _c, _d, _e, _f, _g) + { + other = new A(_a, _b, _c, _d, _e, _f, _g); + } +} + +contract Script0 is Script, Test { + function run() external { + assertEq(uint256(1), uint256(1)); + + vm.startBroadcast(); + B b = new B(msg.sender, 2 ** 32, -1 * (2 ** 32), keccak256(abi.encode(1)), true, "abcdef", "hello"); + assertEq(b.getA(), msg.sender); + b.setA(tx.origin); + assertEq(b.getA(), tx.origin); + address _b = b.self(); + bytes32 _d = b.d(); + bytes32 _d2 = b.other().d(); + } +}"#, + ) + .unwrap(); + + cmd.args(["build"]).assert_success(); + cmd.forge_fuse(); + + cmd.args([ + "script", + script.to_str().unwrap(), + "--root", + prj.root().to_str().unwrap(), + "--tc=Script0", + "--debug", + ]); + eprintln!("root: {}", prj.root().display()); + let cmd_path = Path::new(cmd.cmd().get_program()).canonicalize().unwrap(); + let args = cmd.cmd().get_args().map(|s| s.to_str().unwrap()).format(" "); + eprintln!(" cmd: {} {args}", cmd_path.display()); + std::mem::forget(prj); + } +); diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index f8c9dcbbf0ca..d25297119e57 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -10,6 +10,7 @@ mod cmd; mod config; mod coverage; mod create; +mod debug; mod doc; mod multi_script; mod script; diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index c0265c3d6c74..078361fab8e0 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -1,4 +1,4 @@ -//! forge tests for cheat codes +//! Forge tests for cheatcodes. use crate::{ config::*, diff --git a/crates/forge/tests/it/config.rs b/crates/forge/tests/it/config.rs index cfb706d33fc3..899f0711a5f4 100644 --- a/crates/forge/tests/it/config.rs +++ b/crates/forge/tests/it/config.rs @@ -1,4 +1,4 @@ -//! Test setup +//! Test config. use crate::test_helpers::{COMPILED, EVM_OPTS, PROJECT}; use forge::{ @@ -16,7 +16,7 @@ use foundry_test_utils::{init_tracing, Filter}; use itertools::Itertools; use std::{collections::BTreeMap, path::Path}; -/// How to execute a a test run +/// How to execute a test run. pub struct TestConfig { pub runner: MultiContractRunner, pub should_fail: bool, diff --git a/crates/forge/tests/it/core.rs b/crates/forge/tests/it/core.rs index 07b89a6c14a7..eba2372c1df7 100644 --- a/crates/forge/tests/it/core.rs +++ b/crates/forge/tests/it/core.rs @@ -1,4 +1,4 @@ -//! forge tests for core functionality +//! Forge tests for core functionality. use crate::config::*; use forge::result::SuiteResult; diff --git a/crates/forge/tests/it/fork.rs b/crates/forge/tests/it/fork.rs index e66e975fdc86..814d1b09ebd2 100644 --- a/crates/forge/tests/it/fork.rs +++ b/crates/forge/tests/it/fork.rs @@ -1,4 +1,4 @@ -//! forge tests for cheat codes +//! Forge forking tests. use crate::{ config::*, diff --git a/crates/forge/tests/it/fs.rs b/crates/forge/tests/it/fs.rs index 9ab7711eda3c..29affe05c851 100644 --- a/crates/forge/tests/it/fs.rs +++ b/crates/forge/tests/it/fs.rs @@ -1,4 +1,4 @@ -//! Tests for reproducing issues +//! Filesystem tests. use crate::{config::*, test_helpers::PROJECT}; use foundry_config::{fs_permissions::PathPermission, Config, FsPermissions}; diff --git a/crates/forge/tests/it/fuzz.rs b/crates/forge/tests/it/fuzz.rs index 5654cb070a81..d16befa35064 100644 --- a/crates/forge/tests/it/fuzz.rs +++ b/crates/forge/tests/it/fuzz.rs @@ -1,4 +1,4 @@ -//! Tests for invariants +//! Fuzz tests. use crate::config::*; use alloy_primitives::U256; diff --git a/crates/forge/tests/it/inline.rs b/crates/forge/tests/it/inline.rs index 7fc2957a2a95..821077b704e6 100644 --- a/crates/forge/tests/it/inline.rs +++ b/crates/forge/tests/it/inline.rs @@ -1,3 +1,5 @@ +//! Inline configuration tests. + use crate::{ config::runner, test_helpers::{COMPILED, PROJECT}, diff --git a/crates/forge/tests/it/invariant.rs b/crates/forge/tests/it/invariant.rs index bfd28d1435b9..587eedaf8bc2 100644 --- a/crates/forge/tests/it/invariant.rs +++ b/crates/forge/tests/it/invariant.rs @@ -1,4 +1,4 @@ -//! Tests for invariants +//! Invariant tests. use crate::config::*; use alloy_primitives::U256; diff --git a/crates/forge/tests/it/spec.rs b/crates/forge/tests/it/spec.rs index 16ed24983309..724aaa0ff4ff 100644 --- a/crates/forge/tests/it/spec.rs +++ b/crates/forge/tests/it/spec.rs @@ -1,3 +1,5 @@ +//! Integration tests for EVM specifications. + use crate::config::*; use foundry_evm::revm::primitives::SpecId; use foundry_test_utils::Filter; diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index e0bac51b4ea6..31a330d2afb0 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -1,3 +1,5 @@ +//! Test helpers for Forge integration tests. + use alloy_primitives::U256; use foundry_compilers::{ artifacts::{Libraries, Settings},