Skip to main content

soldb_debugger/
lib.rs

1//! Source-level debugging over an execution trace.
2//!
3//! Given a [`soldb_core::TransactionTrace`] and the ETHDebug metadata for the contract
4//! it executed, this crate answers the questions a source-level debugger asks: which
5//! source span a step is at, which function contains a program counter, and which
6//! variables are live there with what values.
7//!
8//! It is frontend-agnostic on purpose. [`variables_for_step`] is what backs both the CLI
9//! REPL's `vars`/`print` commands and the DAP server's variables view, so the terminal
10//! and the editor decode identically.
11//!
12//! Variable values are only as good as the debug info: a location the backend did not
13//! capture decodes to [`DebugValueStatus::Unavailable`], and a value whose declared type
14//! is not decodable is reported as [`DebugValueStatus::Raw`] rather than guessed at.
15
16use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use soldb_core::{StepSnapshot, TraceStep, TransactionTrace, Word as StackWord};
21use soldb_ethdebug::{decode_value, parse_word, EthdebugInfo, VariableLocation, Word};
22
23use crate::decode::ValueReader;
24use crate::roots::root_calls;
25use crate::stepping::calldata_for_step;
26
27pub mod condition;
28pub mod debug_diff;
29mod decode;
30mod locals;
31mod roots;
32pub mod state;
33pub mod stepping;
34mod types;
35
36pub use condition::{Condition, ConditionContext, Evaluation};
37pub use debug_diff::{
38    capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
39    DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
40    DebugTraceSummary,
41};
42pub use roots::check_root_calls;
43pub use soldb_ethdebug::{CodeGenerator, StorageLayout};
44pub use state::{
45    short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
46    StateVariable, StorageTape, StorageWords,
47};
48pub use stepping::{
49    address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
50    Frame, FunctionId, InferredVariable, JumpMarker, LineKey, LocalsStatus, ResolvedFunction,
51    ResolvedLine, SourceListing, StepLocation, StepMap, VariableKind, INFERRED_LOCALS_NOTE,
52    INFERRED_LOCALS_WARNING,
53};
54pub use types::{SourceTypes, StructMember};
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct DebugSession {
58    pub trace: TransactionTrace,
59    pub ethdebug: Option<EthdebugInfo>,
60    #[serde(default)]
61    pub source_contents: BTreeMap<u64, String>,
62    #[serde(default)]
63    pub functions: Vec<SourceFunction>,
64}
65
66impl DebugSession {
67    #[must_use]
68    pub fn new(trace: TransactionTrace) -> Self {
69        Self {
70            trace,
71            ethdebug: None,
72            source_contents: BTreeMap::new(),
73            functions: Vec::new(),
74        }
75    }
76
77    #[must_use]
78    pub fn with_ethdebug(
79        trace: TransactionTrace,
80        ethdebug: EthdebugInfo,
81        source_contents: BTreeMap<u64, String>,
82    ) -> Self {
83        let mut session = Self::new(trace);
84        session.attach_ethdebug(ethdebug, source_contents);
85        session
86    }
87
88    /// Attaches debug info to a session that already holds a trace, replacing whatever
89    /// was attached before.
90    ///
91    /// The trace is left in place rather than rebuilt, so a frontend that keeps a large
92    /// trace resident can add or swap the contract's artifacts without copying hundreds
93    /// of thousands of steps.
94    pub fn attach_ethdebug(
95        &mut self,
96        ethdebug: EthdebugInfo,
97        source_contents: BTreeMap<u64, String>,
98    ) {
99        self.functions = source_contents
100            .iter()
101            .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
102            .collect();
103        self.ethdebug = Some(ethdebug);
104        self.source_contents = source_contents;
105    }
106
107    #[must_use]
108    pub fn step(&self, step_index: usize) -> Option<DebugStep> {
109        let step = self.trace.steps.get(step_index)?;
110        Some(DebugStep {
111            index: step_index,
112            pc: step.pc,
113            op: step.op.to_string(),
114            gas: step.gas,
115            gas_cost: step.gas_cost,
116            depth: step.depth,
117            source: self.source_span(step.pc),
118            function: self.function_at_pc(step.pc).cloned(),
119            snapshot: step.normalized_snapshot(),
120            variables: self.variables_at_step(step_index),
121        })
122    }
123
124    #[must_use]
125    pub fn steps(&self) -> Vec<DebugStep> {
126        (0..self.trace.steps.len())
127            .filter_map(|step_index| self.step(step_index))
128            .collect()
129    }
130
131    #[must_use]
132    pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
133        let ethdebug = self.ethdebug.as_ref()?;
134        let instruction = ethdebug.instruction_at_pc(pc)?;
135        let location = instruction.source_location()?;
136        let path = ethdebug.sources.get(&location.source_id)?.clone();
137        let position = self
138            .source_contents
139            .get(&location.source_id)
140            .map(|source| line_column_for_offset(source, location.offset as usize));
141        Some(SourceSpan {
142            source_id: location.source_id,
143            path,
144            offset: location.offset,
145            length: location.length,
146            line: position.map_or(0, |position| position.line),
147            column: position.map_or(0, |position| position.column),
148        })
149    }
150
151    #[must_use]
152    pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
153        let location = self
154            .ethdebug
155            .as_ref()?
156            .instruction_at_pc(pc)?
157            .source_location()?;
158        self.functions
159            .iter()
160            .filter(|function| {
161                function.source_id == location.source_id
162                    && function.declaration_start <= location.offset
163                    && location.offset <= function.body_end
164            })
165            .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
166    }
167
168    #[must_use]
169    pub fn variables_at_step(&self, step_index: usize) -> Vec<DebugVariable> {
170        let Some(ethdebug) = &self.ethdebug else {
171            return Vec::new();
172        };
173        variables_for_step(&self.trace, ethdebug, step_index)
174    }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct DebugStep {
179    pub index: usize,
180    pub pc: u64,
181    pub op: String,
182    pub gas: u64,
183    pub gas_cost: u64,
184    pub depth: u64,
185    pub source: Option<SourceSpan>,
186    pub function: Option<SourceFunction>,
187    pub snapshot: StepSnapshot,
188    pub variables: Vec<DebugVariable>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct SourceSpan {
193    pub source_id: u64,
194    pub path: String,
195    pub offset: u64,
196    pub length: u64,
197    pub line: u64,
198    pub column: u64,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct SourceFunction {
203    pub source_id: u64,
204    /// The declared name; `constructor`, `fallback`, and `receive` for those, and the
205    /// modifier's name for a modifier.
206    pub name: String,
207    pub params: Vec<SourceParam>,
208    pub declaration_start: u64,
209    /// The one-based line the declaration begins on.
210    #[serde(default)]
211    pub declaration_line: u64,
212    /// The offset of the body's opening brace.
213    #[serde(default)]
214    pub body_start: u64,
215    pub body_end: u64,
216    /// The return parameters, named or not, in declaration order.
217    #[serde(default)]
218    pub returns: Vec<SourceLocal>,
219    /// The local variables declared in the body, in declaration order.
220    #[serde(default)]
221    pub locals: Vec<SourceLocal>,
222    /// Whether the header invokes modifiers, which run inlined around the body and reserve
223    /// stack slots of their own.
224    #[serde(default)]
225    pub has_modifiers: bool,
226    /// The modifiers the header invokes, in the order they run.
227    #[serde(default)]
228    pub modifiers: Vec<String>,
229    /// For a modifier, the offset of its `_;` placeholder, where the function body runs.
230    #[serde(default)]
231    pub placeholder: Option<u64>,
232}
233
234/// A half-open byte range in a source.
235#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ByteRange {
237    pub start: u64,
238    pub end: u64,
239}
240
241impl ByteRange {
242    #[must_use]
243    pub const fn contains(self, offset: u64) -> bool {
244        self.start <= offset && offset < self.end
245    }
246
247    /// Whether a span of `length` bytes at `offset` lies inside this range.
248    #[must_use]
249    pub const fn covers(self, offset: u64, length: u64) -> bool {
250        self.start <= offset && offset.saturating_add(length) <= self.end
251    }
252
253    /// Whether this range contains the whole of `other`.
254    #[must_use]
255    pub const fn encloses(self, other: Self) -> bool {
256        self.start <= other.start && other.end <= self.end
257    }
258}
259
260/// A variable a function declares in its source: a return parameter or a local.
261///
262/// The spans are what ties the variable to the trace. solc's legacy code generator reserves
263/// the variable's stack slot under `declaration`, the `type [location] name` text, as the
264/// first instructions of `statement`; the slot lives until execution leaves `scope`.
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct SourceLocal {
267    pub name: String,
268    /// The type without its data location, such as `uint256` or `string`.
269    pub ty: String,
270    /// The data location as declared, when the type has one.
271    #[serde(default)]
272    pub location: Option<String>,
273    pub declaration: ByteRange,
274    /// The declaration statement, from its first token to before its `;`.
275    pub statement: ByteRange,
276    /// The variable's index among those its statement declares: each takes the next slot.
277    pub position: usize,
278    pub scope: ByteRange,
279}
280
281impl SourceLocal {
282    /// The type as declared, with its data location.
283    #[must_use]
284    pub fn declared_type(&self) -> String {
285        match &self.location {
286            Some(location) => format!("{} {location}", self.ty),
287            None => self.ty.clone(),
288        }
289    }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293pub struct SourceParam {
294    pub name: String,
295    /// The type without its data location, such as `uint256` or `string`.
296    pub ty: String,
297    /// The data location as declared: `memory`, `calldata`, or `storage`; `None` for a
298    /// value type.
299    #[serde(default)]
300    pub location: Option<String>,
301}
302
303/// One argument of a function frame, as it was on the stack when the frame was entered.
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct FrameArgument {
306    pub name: String,
307    pub ty: String,
308    pub value: DebugValue,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct DebugVariable {
313    pub name: String,
314    pub ty: String,
315    pub location: DebugLocation,
316    pub value: DebugValue,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320pub struct DebugLocation {
321    pub kind: String,
322    pub offset: u64,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct DebugValue {
327    pub display: String,
328    pub raw: Option<String>,
329    pub status: DebugValueStatus,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "snake_case")]
334pub enum DebugValueStatus {
335    Decoded,
336    Raw,
337    Unavailable,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341struct SourcePosition {
342    line: u64,
343    column: u64,
344}
345
346#[must_use]
347pub fn variables_for_step(
348    trace: &TransactionTrace,
349    ethdebug: &EthdebugInfo,
350    step_index: usize,
351) -> Vec<DebugVariable> {
352    let Some(step) = trace.steps.get(step_index) else {
353        return Vec::new();
354    };
355    let variables = ethdebug.variables_at_pc(step.pc);
356    if variables.is_empty() {
357        return Vec::new();
358    }
359    // The top-level calls are found only when a variable may read calldata.
360    let roots = root_calls(trace);
361    let calldata = calldata_for_step(trace, &roots, step_index);
362    variables
363        .into_iter()
364        .map(|variable| decode_variable(step, calldata, variable))
365        .collect()
366}
367
368fn decode_variable(step: &TraceStep, calldata: &str, variable: &VariableLocation) -> DebugVariable {
369    let raw = raw_value_for_location(step, calldata, variable);
370    let value = raw.map_or_else(
371        || DebugValue {
372            display: "<unavailable>".to_owned(),
373            raw: None,
374            status: DebugValueStatus::Unavailable,
375        },
376        |raw| decode_debug_value(&raw, &variable.ty),
377    );
378    DebugVariable {
379        name: variable.name.clone(),
380        ty: variable.ty.clone(),
381        location: DebugLocation {
382            kind: variable.location_type.clone(),
383            offset: variable.offset,
384        },
385        value,
386    }
387}
388
389fn raw_value_for_location(
390    step: &TraceStep,
391    calldata: &str,
392    variable: &VariableLocation,
393) -> Option<String> {
394    let snapshot = step.snapshot_ref();
395    match variable.location_type.as_str() {
396        "stack" => snapshot
397            .stack
398            .get(variable.offset as usize)
399            .map(|value| normalize_hex(value)),
400        "memory" => word_from_hex_bytes(snapshot.memory?, variable.offset as usize),
401        "calldata" => word_from_hex_bytes(calldata, variable.offset as usize),
402        "storage" => storage_value(snapshot.storage, variable.offset),
403        _ => None,
404    }
405}
406
407pub(crate) fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
408    let ty = ty.trim();
409    let normalized = normalize_hex(raw);
410    let word = normalized.trim_start_matches("0x");
411    let decoded = decode_static_word(word, ty);
412    match decoded {
413        Some(display) => DebugValue {
414            display,
415            raw: Some(normalized),
416            status: DebugValueStatus::Decoded,
417        },
418        None => DebugValue {
419            display: normalized.clone(),
420            raw: Some(normalized),
421            status: DebugValueStatus::Raw,
422        },
423    }
424}
425
426pub(crate) fn decode_static_word(word: &str, ty: &str) -> Option<String> {
427    if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
428        return None;
429    }
430    if ty.starts_with("uint") {
431        return Some(format_uint_word(word));
432    }
433    if ty.starts_with("int") {
434        return Some(format_int_word(word));
435    }
436    if ty == "address" {
437        let padded = left_pad_word(word);
438        return Some(
439            format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
440        );
441    }
442    if ty == "bool" {
443        let trimmed = word.trim_start_matches('0');
444        return match trimmed {
445            "" | "0" => Some("false".to_owned()),
446            "1" => Some("true".to_owned()),
447            _ => None,
448        };
449    }
450    if ty == "bytes32" {
451        return Some(normalize_hex(word));
452    }
453    if let Some(size) = fixed_bytes_size(ty) {
454        let padded = right_pad_word(word);
455        return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
456    }
457    None
458}
459
460/// Where the compiler leaves a function's parameters on the stack when the function is
461/// entered. Solidity's two code generators disagree, and nothing in the artifacts says
462/// which produced the code, so [`StepMap`] proves it from the trace instead of assuming.
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub enum ArgumentOrder {
465    /// The first declared parameter is on top: the via-IR pipeline pushes them
466    /// right-to-left.
467    FirstOnTop,
468    /// The last declared parameter is on top: the legacy pipeline pushes them
469    /// left-to-right.
470    LastOnTop,
471}
472
473/// What this trace proved about how a contract's compiler passes arguments.
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum ArgumentLayout {
476    /// The parameters are the top words at the entry point, in this order.
477    Ordered(ArgumentOrder),
478    /// The parameters are the top words, but which is which was not established: the
479    /// evidence came from a one-parameter frame, where both orders look the same.
480    TopWords,
481}
482
483impl ArgumentOrder {
484    /// The stack index of parameter `index` of `count`, counting from the bottom of the
485    /// stack, given that the parameters occupy the top `count` words.
486    #[must_use]
487    fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
488        match self {
489            Self::FirstOnTop => stack_len - 1 - index,
490            Self::LastOnTop => stack_len - count + index,
491        }
492    }
493}
494
495/// What a frame's arguments are read from: the stack at its entry point, and the memory
496/// a pointer among them refers into.
497#[derive(Debug, Clone, Copy, Default)]
498pub struct FrameState<'a> {
499    pub stack: &'a [StackWord],
500    /// Memory at the entry step, as one unprefixed hex string, when the backend captured
501    /// it.
502    pub memory: Option<&'a str>,
503}
504
505/// The arguments a function was entered with, read off the stack at its entry point.
506///
507/// Every parameter read here occupies exactly one word: a value type is the value, and a
508/// `memory` or `storage` reference is a pointer, which memory's own layout then resolves.
509/// A `calldata` slice of a dynamic type is the exception — it is a pointer *and* a length,
510/// two words, and nothing in the trace proves that width the way the calldata proves the
511/// order — so a function taking one reports no arguments rather than words that may be
512/// misaligned.
513#[must_use]
514pub fn decode_arguments(
515    params: &[SourceParam],
516    state: FrameState<'_>,
517    layout: ArgumentLayout,
518    types: &SourceTypes,
519    scope: Option<&str>,
520) -> Vec<FrameArgument> {
521    let stack = state.stack;
522    if params.is_empty() || stack.len() < params.len() {
523        return Vec::new();
524    }
525    if !params.iter().all(readable_parameter) {
526        return Vec::new();
527    }
528    // One parameter is on top under either order; more than one needs the order proven.
529    let order = match layout {
530        ArgumentLayout::Ordered(order) => order,
531        ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
532        ArgumentLayout::TopWords => return Vec::new(),
533    };
534    params
535        .iter()
536        .enumerate()
537        .map(|(index, param)| {
538            let word = &stack[order.word_index(index, params.len(), stack.len())];
539            FrameArgument {
540                name: param.name.clone(),
541                ty: param.ty.clone(),
542                value: argument_value(param, word, state.memory, types, scope),
543            }
544        })
545        .collect()
546}
547
548fn argument_value(
549    param: &SourceParam,
550    word: &str,
551    memory: Option<&str>,
552    types: &SourceTypes,
553    scope: Option<&str>,
554) -> DebugValue {
555    let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
556        return DebugValue {
557            display: "<unreadable stack word>".to_owned(),
558            raw: Some(word.to_owned()),
559            status: DebugValueStatus::Unavailable,
560        };
561    };
562    let reader = ValueReader {
563        memory,
564        calldata: "",
565        storage: None,
566        layout: None,
567        types,
568        scope,
569    };
570    let raw = Some(short_hex(&parsed));
571    match param.location.as_deref() {
572        Some("memory") => reader.variable(&format!("{} memory", param.ty), &[word]),
573        Some(location) => DebugValue {
574            display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
575            raw,
576            status: DebugValueStatus::Raw,
577        },
578        None => DebugValue {
579            display: decode_value(value_bytes(&parsed, &param.ty), &param.ty),
580            raw,
581            status: DebugValueStatus::Decoded,
582        },
583    }
584}
585
586/// Whether a parameter is one word the debugger can place: a value, or a pointer.
587pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
588    match param.location.as_deref() {
589        // A `calldata` slice of a dynamic type is two words; see `decode_arguments`.
590        Some("calldata") => !is_dynamic_type(&param.ty),
591        Some(_) => true,
592        None => is_value_type(&param.ty),
593    }
594}
595
596/// Whether a type is stored with a length, rather than a fixed number of words.
597fn is_dynamic_type(ty: &str) -> bool {
598    ty == "string" || ty == "bytes" || ty.ends_with("[]")
599}
600
601/// An array type as its element type and its length: `None` for a dynamic array, which
602/// carries its length in memory.
603pub(crate) fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
604    let inner = ty.strip_suffix(']')?;
605    let open = inner.rfind('[')?;
606    let element = inner[..open].trim();
607    let length = &inner[open + 1..];
608    if length.is_empty() {
609        return Some((element, None));
610    }
611    Some((element, Some(length.parse::<usize>().ok()?)))
612}
613
614/// The 32 bytes at `offset` of a memory image, when it reaches that far.
615pub(crate) fn memory_word(memory: &str, offset: usize) -> Option<Word> {
616    let bytes = memory_bytes(memory, offset, 32)?;
617    let mut word = [0_u8; 32];
618    word.copy_from_slice(&bytes);
619    Some(word)
620}
621
622/// `length` bytes at `offset` of a memory image, which is two hex digits per byte.
623pub(crate) fn memory_bytes(memory: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
624    let start = offset.checked_mul(2)?;
625    let end = start.checked_add(length.checked_mul(2)?)?;
626    let digits = memory.get(start..end)?;
627    if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
628        return None;
629    }
630    Some(
631        digits
632            .as_bytes()
633            .chunks(2)
634            .map(|pair| {
635                u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
636            })
637            .collect(),
638    )
639}
640
641/// A word as an offset or a length, when it fits one.
642pub(crate) fn word_as_usize(word: &Word) -> Option<usize> {
643    if word[..24].iter().any(|byte| *byte != 0) {
644        return None;
645    }
646    usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
647}
648
649pub(crate) fn hex_of(bytes: &[u8]) -> String {
650    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
651}
652
653/// The bytes of a stack word a value of type `ty` occupies: a signed integer is its own
654/// width from the low end so the sign bit is the right one, fixed bytes are left-aligned,
655/// and everything else reads as a whole word.
656pub(crate) fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
657    if let Some(bits) = integer_bits(ty, "int") {
658        return &word[32 - bits / 8..];
659    }
660    if let Some(size) = fixed_bytes_size(ty) {
661        return &word[..size];
662    }
663    word
664}
665
666/// Whether a value of this type is one stack word the debugger can decode.
667#[must_use]
668pub fn is_value_type(ty: &str) -> bool {
669    matches!(ty, "bool" | "address" | "address payable")
670        || fixed_bytes_size(ty).is_some()
671        || integer_bits(ty, "uint").is_some()
672        || integer_bits(ty, "int").is_some()
673}
674
675/// The width of `uint<N>`/`int<N>` in bits, the bare names counting as 256.
676fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
677    let rest = ty.strip_prefix(prefix)?;
678    // `int8` must not match the `uint` prefix; `strip_prefix` already ruled that out.
679    if rest.is_empty() {
680        return Some(256);
681    }
682    let bits = rest.parse::<usize>().ok()?;
683    (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
684}
685
686fn fixed_bytes_size(ty: &str) -> Option<usize> {
687    let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
688    (1..=32).contains(&size).then_some(size)
689}
690
691fn format_uint_word(word: &str) -> String {
692    let trimmed = word.trim_start_matches('0');
693    if trimmed.is_empty() {
694        return "0".to_owned();
695    }
696    if trimmed.len() <= 32 {
697        return u128::from_str_radix(trimmed, 16)
698            .map(|value| value.to_string())
699            .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
700    }
701    format!("0x{}", trimmed.to_ascii_lowercase())
702}
703
704/// A two's-complement word as a signed decimal, or as hex when it does not fit `i128`.
705fn format_int_word(word: &str) -> String {
706    let padded = left_pad_word(word);
707    let Ok(bytes) = (0..32)
708        .map(|index| u8::from_str_radix(&padded[index * 2..index * 2 + 2], 16))
709        .collect::<Result<Vec<_>, _>>()
710    else {
711        return format!("0x{}", word.to_ascii_lowercase());
712    };
713    if bytes[0] & 0x80 == 0 {
714        return format_uint_word(word);
715    }
716    // Negate: invert every byte and add one, from the least significant end.
717    let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
718    for byte in magnitude.iter_mut().rev() {
719        let (sum, carry) = byte.overflowing_add(1);
720        *byte = sum;
721        if !carry {
722            break;
723        }
724    }
725    let hex = magnitude
726        .iter()
727        .map(|byte| format!("{byte:02x}"))
728        .collect::<String>();
729    let trimmed = hex.trim_start_matches('0');
730    if trimmed.len() <= 32 {
731        if let Ok(value) = u128::from_str_radix(trimmed, 16) {
732            return format!("-{value}");
733        }
734    }
735    format!("-0x{trimmed}")
736}
737
738fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
739    storage
740        .get(&format!("0x{slot:x}"))
741        .or_else(|| storage.get(&format!("{slot:x}")))
742        .or_else(|| storage.get(&format!("0x{slot:064x}")))
743        .or_else(|| storage.get(&format!("{slot:064x}")))
744        .map(|value| normalize_hex(value))
745}
746
747fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
748    let hex = input.trim_start_matches("0x");
749    let start = byte_offset.checked_mul(2)?;
750    let end = start.checked_add(64)?;
751    let word = hex.get(start..end)?;
752    word.bytes()
753        .all(|byte| byte.is_ascii_hexdigit())
754        .then(|| format!("0x{}", word.to_ascii_lowercase()))
755}
756
757fn normalize_hex(value: &str) -> String {
758    let value = value.trim();
759    let hex = value.strip_prefix("0x").unwrap_or(value);
760    format!("0x{}", hex.to_ascii_lowercase())
761}
762
763fn left_pad_word(word: &str) -> String {
764    let word = word.trim_start_matches("0x").to_ascii_lowercase();
765    if word.len() >= 64 {
766        return word;
767    }
768    format!("{:0>64}", word)
769}
770
771fn right_pad_word(word: &str) -> String {
772    let word = word.trim_start_matches("0x").to_ascii_lowercase();
773    if word.len() >= 64 {
774        return word;
775    }
776    format!("{:0<64}", word)
777}
778
779fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
780    let mut line = 1;
781    let mut column = 1;
782    for byte in source.bytes().take(offset.min(source.len())) {
783        if byte == b'\n' {
784            line += 1;
785            column = 1;
786        } else {
787            column += 1;
788        }
789    }
790    SourcePosition { line, column }
791}
792
793/// The declarations that open a body of executable code in a Solidity source.
794const DECLARATION_KEYWORDS: [&str; 5] =
795    ["function", "modifier", "constructor", "fallback", "receive"];
796
797/// Finds every function-like declaration in a source file by scanning its text:
798/// functions, modifiers, constructors, `fallback`, and `receive`. This is what the
799/// debugger attributes steps to when the compiler emits no function boundaries.
800#[must_use]
801pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
802    let mut functions = Vec::new();
803    for keyword in DECLARATION_KEYWORDS {
804        collect_declarations(source_id, source, keyword, &mut functions);
805    }
806    functions.sort_by_key(|function| function.declaration_start);
807    functions
808}
809
810fn collect_declarations(
811    source_id: u64,
812    source: &str,
813    keyword: &str,
814    functions: &mut Vec<SourceFunction>,
815) {
816    let named = matches!(keyword, "function" | "modifier");
817    let mut cursor = 0;
818    while let Some(keyword_start) = find_solidity_keyword(source, keyword, cursor) {
819        let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
820        let name = if named {
821            let Some((name, name_end)) = parse_identifier(source, index) else {
822                cursor = index;
823                continue;
824            };
825            index = skip_ascii_whitespace(source, name_end);
826            name
827        } else {
828            keyword
829        };
830        if source.as_bytes().get(index) != Some(&b'(') {
831            cursor = index;
832            continue;
833        }
834        let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
835            cursor = index + 1;
836            continue;
837        };
838        let params = parse_source_params(&source[index + 1..params_end]);
839        let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
840            cursor = params_end + 1;
841            continue;
842        };
843        // A call such as `token.receive(x);` has a `;` before the next `{`; only a
844        // declaration runs straight from its parameter list into a body.
845        if source[params_end + 1..body_start].contains(';') {
846            cursor = params_end + 1;
847            continue;
848        }
849        let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
850            cursor = body_start + 1;
851            continue;
852        };
853        let (returns, modifiers) = parse_header(source, params_end + 1, body_start);
854        let modifiers = if keyword == "modifier" {
855            Vec::new()
856        } else {
857            modifiers
858        };
859        let returns = returns
860            .into_iter()
861            .map(|variable| SourceLocal {
862                name: variable.name,
863                ty: variable.ty,
864                location: variable.location,
865                declaration: variable.span,
866                statement: variable.span,
867                position: 0,
868                scope: ByteRange {
869                    start: body_start as u64,
870                    end: body_end as u64,
871                },
872            })
873            .collect();
874
875        functions.push(SourceFunction {
876            source_id,
877            name: name.to_owned(),
878            params,
879            declaration_start: keyword_start as u64,
880            declaration_line: source[..keyword_start]
881                .bytes()
882                .filter(|byte| *byte == b'\n')
883                .count() as u64
884                + 1,
885            body_start: body_start as u64,
886            body_end: body_end as u64,
887            returns,
888            locals: locals::scan_locals(source, body_start, body_end),
889            has_modifiers: !modifiers.is_empty(),
890            modifiers,
891            placeholder: (keyword == "modifier")
892                .then(|| locals::find_placeholder(source, body_start, body_end))
893                .flatten(),
894        });
895        cursor = body_end + 1;
896    }
897}
898
899/// The words a function header may carry between its parameter list and its body
900/// without invoking a modifier.
901const HEADER_KEYWORDS: [&str; 11] = [
902    "public", "private", "internal", "external", "pure", "view", "payable", "virtual", "override",
903    "returns", "constant",
904];
905
906/// The return parameters declared in a function header, and the modifiers it invokes.
907///
908/// The header is the text between the parameter list and the body. A `returns (...)`
909/// clause holds the return parameters, each with the span the compiler reserves its slot
910/// under; any other identifier that is not a visibility or mutability keyword is a
911/// modifier invocation, and they run in the order written.
912fn parse_header(source: &str, start: usize, end: usize) -> (Vec<locals::Declared>, Vec<String>) {
913    let bytes = source.as_bytes();
914    let mut returns = Vec::new();
915    let mut modifiers = Vec::new();
916    let mut index = start;
917    while index < end {
918        index = locals::skip_trivia(source, index);
919        if index >= end {
920            break;
921        }
922        let Some((word, word_end)) = parse_identifier(source, index) else {
923            index += 1;
924            continue;
925        };
926        let after = locals::skip_trivia(source, word_end);
927        let arguments = (bytes.get(after) == Some(&b'('))
928            .then(|| find_matching_delimiter(source, after, b'(', b')'))
929            .flatten()
930            .filter(|close| *close < end);
931        if word == "returns" {
932            if let Some(close) = arguments {
933                returns = parse_return_list(source, after, close);
934            }
935        } else if !HEADER_KEYWORDS.contains(&word) {
936            modifiers.push(word.to_owned());
937        }
938        index = arguments.map_or(word_end, |close| close + 1);
939    }
940    (returns, modifiers)
941}
942
943/// The return parameters between the parentheses at `open` and `close`.
944fn parse_return_list(source: &str, open: usize, close: usize) -> Vec<locals::Declared> {
945    let mut returns = Vec::new();
946    let mut start = open + 1;
947    let mut depth = 0_i32;
948    for index in open + 1..=close {
949        match source.as_bytes()[index] {
950            b'(' | b'[' => depth += 1,
951            b')' | b']' if depth > 0 => depth -= 1,
952            b',' | b')' => {
953                let component = source[start..index].trim();
954                if !component.is_empty() {
955                    let component_start = start
956                        + (source[start..index].len() - source[start..index].trim_start().len());
957                    let component_start = locals::skip_trivia(source, component_start);
958                    let component_end = component_start + component.len();
959                    if let Some(declared) =
960                        locals::parse_return(source, component_start, component_end, returns.len())
961                    {
962                        returns.push(declared);
963                    }
964                }
965                start = index + 1;
966            }
967            _ => {}
968        }
969    }
970    returns
971}
972
973/// The words that may follow a parameter's type without naming it.
974const PARAM_KEYWORDS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
975
976fn parse_source_params(params: &str) -> Vec<SourceParam> {
977    let params = strip_param_comments(params);
978    split_top_level_commas(&params)
979        .into_iter()
980        .enumerate()
981        .filter_map(|(index, param)| {
982            let param = param.trim();
983            if param.is_empty() {
984                return None;
985            }
986            let mut tokens = param.split_whitespace().collect::<Vec<_>>();
987            // A parameter is `<type> [location] [name]`, and the name is optional:
988            // `function f(address, uint256 amount)` leaves the first one unnamed. A
989            // trailing token names the parameter only when it is an identifier, is not
990            // the whole parameter, and is not one of the words that may follow a type
991            // without naming it; otherwise the parameter is unnamed and takes `argN`.
992            let trailing = tokens.last().copied();
993            let name = match trailing {
994                Some(token)
995                    if tokens.len() > 1
996                        && is_identifier(token)
997                        && !PARAM_KEYWORDS.contains(&token) =>
998                {
999                    tokens.pop();
1000                    token.to_owned()
1001                }
1002                _ => format!("arg{index}"),
1003            };
1004            let location = tokens
1005                .iter()
1006                .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
1007                .map(|token| (*token).to_owned());
1008            let ty = tokens
1009                .into_iter()
1010                .filter(|token| !PARAM_KEYWORDS.contains(token))
1011                .collect::<Vec<_>>()
1012                .join(" ");
1013            (!ty.is_empty()).then_some(SourceParam { name, ty, location })
1014        })
1015        .collect()
1016}
1017
1018/// A parameter list with its comments removed.
1019///
1020/// Solidity allows a comment anywhere in a parameter list, and an unnamed parameter is
1021/// commonly documented with one:
1022///
1023/// ```solidity
1024/// function burnFrom_withCaller(
1025///     address caller,
1026///     address, //from
1027///     uint256, //amount
1028/// ) external {}
1029/// ```
1030///
1031/// Left in, a comment's words are split on whitespace like any other token, so `//from`
1032/// parses as a type and the type on the following line parses as its name. Only the text
1033/// between the parentheses is stripped, so no byte offset recorded for a declaration
1034/// moves.
1035fn strip_param_comments(params: &str) -> String {
1036    let mut output = String::with_capacity(params.len());
1037    let mut chars = params.chars().peekable();
1038    while let Some(character) = chars.next() {
1039        if character == '/' {
1040            match chars.peek() {
1041                Some('/') => {
1042                    // Drop to the end of the line, keeping the newline so the tokens on
1043                    // either side of it stay separate.
1044                    for next in chars.by_ref() {
1045                        if next == '\n' {
1046                            output.push('\n');
1047                            break;
1048                        }
1049                    }
1050                    continue;
1051                }
1052                Some('*') => {
1053                    chars.next();
1054                    let mut previous = None;
1055                    for next in chars.by_ref() {
1056                        if previous == Some('*') && next == '/' {
1057                            break;
1058                        }
1059                        previous = Some(next);
1060                    }
1061                    // A block comment can sit between a type and its name, so it leaves
1062                    // a separator behind rather than joining them.
1063                    output.push(' ');
1064                    continue;
1065                }
1066                _ => {}
1067            }
1068        }
1069        output.push(character);
1070    }
1071    output
1072}
1073
1074fn split_top_level_commas(input: &str) -> Vec<&str> {
1075    let mut parts = Vec::new();
1076    let mut start = 0;
1077    let mut depth = 0_i32;
1078    for (index, byte) in input.bytes().enumerate() {
1079        match byte {
1080            b'(' | b'[' => depth += 1,
1081            b')' | b']' => depth -= 1,
1082            b',' if depth == 0 => {
1083                parts.push(&input[start..index]);
1084                start = index + 1;
1085            }
1086            _ => {}
1087        }
1088    }
1089    parts.push(&input[start..]);
1090    parts
1091}
1092
1093pub(crate) fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
1094    let mut cursor = start;
1095    while let Some(relative) = source[cursor..].find(keyword) {
1096        let absolute = cursor + relative;
1097        let before = absolute
1098            .checked_sub(1)
1099            .and_then(|index| source.as_bytes().get(index))
1100            .copied();
1101        let after = source.as_bytes().get(absolute + keyword.len()).copied();
1102        if before.is_none_or(|byte| !is_identifier_byte(byte))
1103            && after.is_none_or(|byte| !is_identifier_byte(byte))
1104        {
1105            return Some(absolute);
1106        }
1107        cursor = absolute + keyword.len();
1108    }
1109    None
1110}
1111
1112pub(crate) fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
1113    let bytes = source.as_bytes();
1114    let first = *bytes.get(start)?;
1115    if !is_identifier_start_byte(first) {
1116        return None;
1117    }
1118    let mut end = start + 1;
1119    while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
1120        end += 1;
1121    }
1122    Some((&source[start..end], end))
1123}
1124
1125pub(crate) fn is_identifier(input: &str) -> bool {
1126    let mut bytes = input.bytes();
1127    let Some(first) = bytes.next() else {
1128        return false;
1129    };
1130    is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
1131}
1132
1133fn is_identifier_start_byte(byte: u8) -> bool {
1134    byte == b'_' || byte.is_ascii_alphabetic()
1135}
1136
1137pub(crate) fn is_identifier_byte(byte: u8) -> bool {
1138    is_identifier_start_byte(byte) || byte.is_ascii_digit()
1139}
1140
1141pub(crate) fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
1142    while source
1143        .as_bytes()
1144        .get(index)
1145        .is_some_and(u8::is_ascii_whitespace)
1146    {
1147        index += 1;
1148    }
1149    index
1150}
1151
1152fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
1153    source
1154        .as_bytes()
1155        .iter()
1156        .enumerate()
1157        .skip(start)
1158        .find_map(|(index, byte)| (*byte == needle).then_some(index))
1159}
1160
1161pub(crate) fn find_matching_delimiter(
1162    source: &str,
1163    open_index: usize,
1164    open: u8,
1165    close: u8,
1166) -> Option<usize> {
1167    let mut depth = 0_i32;
1168    for (index, byte) in source.bytes().enumerate().skip(open_index) {
1169        if byte == open {
1170            depth += 1;
1171        } else if byte == close {
1172            depth -= 1;
1173            if depth == 0 {
1174                return Some(index);
1175            }
1176        }
1177    }
1178    None
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183    use std::collections::BTreeMap;
1184
1185    use serde_json::json;
1186    use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
1187    use soldb_ethdebug::{EthdebugInfo, Instruction, VariableLocation};
1188
1189    use super::{parse_source_functions, DebugSession, DebugValueStatus};
1190
1191    #[test]
1192    fn builds_source_steps_and_decodes_active_variables() {
1193        let source =
1194            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
1195        let offset = source.find("value = x").expect("source offset") as u64;
1196        let trace = sample_trace();
1197        let mut sources = BTreeMap::new();
1198        sources.insert(0, "Counter.sol".to_owned());
1199        let mut source_contents = BTreeMap::new();
1200        source_contents.insert(0, source.to_owned());
1201        let mut variable_locations = BTreeMap::new();
1202        variable_locations.insert(
1203            0,
1204            vec![
1205                VariableLocation {
1206                    name: "x".to_owned(),
1207                    ty: "uint256".to_owned(),
1208                    location_type: "stack".to_owned(),
1209                    offset: 0,
1210                    pc_range: (0, 1),
1211                },
1212                VariableLocation {
1213                    name: "sender".to_owned(),
1214                    ty: "address".to_owned(),
1215                    location_type: "calldata".to_owned(),
1216                    offset: 4,
1217                    pc_range: (0, 1),
1218                },
1219            ],
1220        );
1221        let info = EthdebugInfo {
1222            compilation: json!({}),
1223            contract_name: "Counter".to_owned(),
1224            environment: "runtime".to_owned(),
1225            instructions: vec![Instruction {
1226                offset: 0,
1227                operation: json!({"mnemonic": "PUSH1"}),
1228                context: Some(json!({
1229                    "code": {
1230                        "source": {"id": 0},
1231                        "range": {"offset": offset, "length": 9}
1232                    }
1233                })),
1234            }],
1235            sources,
1236            variable_locations,
1237        };
1238
1239        let session = DebugSession::with_ethdebug(trace, info, source_contents);
1240        let step = session.step(0).expect("debug step");
1241        assert_eq!(step.source.as_ref().expect("source").line, 3);
1242        assert_eq!(step.function.as_ref().expect("function").name, "set");
1243        assert_eq!(
1244            step.snapshot
1245                .stack
1246                .iter()
1247                .map(|word| &**word)
1248                .collect::<Vec<_>>(),
1249            ["0x2a"]
1250        );
1251        assert_eq!(step.variables[0].name, "x");
1252        assert_eq!(step.variables[0].value.display, "42");
1253        assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1254        assert_eq!(
1255            step.variables[1].value.display,
1256            "0x1111111111111111111111111111111111111111"
1257        );
1258    }
1259
1260    #[test]
1261    fn calldata_variables_read_the_input_of_their_top_level_call() {
1262        let root = |id: usize, entry: usize, input: String| ExecutionCall {
1263            id,
1264            parent_id: None,
1265            depth: 1,
1266            entry_step: Some(entry),
1267            exit_step: Some(entry + 1),
1268            call_type: "CALL".to_owned(),
1269            from: "0x1".to_owned(),
1270            to: "0x2".to_owned(),
1271            bytecode_address: "0x2".to_owned(),
1272            value: "0x0".to_owned(),
1273            input,
1274            gas_limit: 0,
1275            gas_used: None,
1276            output: None,
1277            success: Some(true),
1278            error: None,
1279        };
1280        let mut trace = sample_trace();
1281        trace.steps.push(trace.steps[0].clone());
1282        let second_input = format!(
1283            "0x12345678{:0>64}",
1284            "2222222222222222222222222222222222222222"
1285        );
1286        trace.artifacts.calls = vec![
1287            root(0, 0, trace.input_data.clone()),
1288            root(1, 1, second_input),
1289        ];
1290        let info = EthdebugInfo {
1291            compilation: json!({}),
1292            contract_name: "Counter".to_owned(),
1293            environment: "runtime".to_owned(),
1294            instructions: Vec::new(),
1295            sources: BTreeMap::new(),
1296            variable_locations: BTreeMap::from([(
1297                0,
1298                vec![VariableLocation {
1299                    name: "sender".to_owned(),
1300                    ty: "address".to_owned(),
1301                    location_type: "calldata".to_owned(),
1302                    offset: 4,
1303                    pc_range: (0, 1),
1304                }],
1305            )]),
1306        };
1307        let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1308        let sender = |step: usize| {
1309            session.step(step).expect("debug step").variables[0]
1310                .value
1311                .display
1312                .clone()
1313        };
1314        assert_eq!(sender(0), "0x1111111111111111111111111111111111111111");
1315        assert_eq!(sender(1), "0x2222222222222222222222222222222222222222");
1316    }
1317
1318    #[test]
1319    fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1320        let trace = sample_trace();
1321        let mut variable_locations = BTreeMap::new();
1322        variable_locations.insert(
1323            0,
1324            vec![
1325                VariableLocation {
1326                    name: "name".to_owned(),
1327                    ty: "string".to_owned(),
1328                    location_type: "stack".to_owned(),
1329                    offset: 0,
1330                    pc_range: (0, 0),
1331                },
1332                VariableLocation {
1333                    name: "missing".to_owned(),
1334                    ty: "uint256".to_owned(),
1335                    location_type: "stack".to_owned(),
1336                    offset: 99,
1337                    pc_range: (0, 0),
1338                },
1339            ],
1340        );
1341        let info = EthdebugInfo {
1342            compilation: json!({}),
1343            contract_name: "Counter".to_owned(),
1344            environment: "runtime".to_owned(),
1345            instructions: Vec::new(),
1346            sources: BTreeMap::new(),
1347            variable_locations,
1348        };
1349
1350        let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1351        let step = session.step(0).expect("debug step");
1352        assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1353        assert_eq!(step.variables[0].value.display, "0x2a");
1354        assert_eq!(
1355            step.variables[1].value.status,
1356            DebugValueStatus::Unavailable
1357        );
1358    }
1359
1360    #[test]
1361    fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1362        let source =
1363            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
1364        let offset = source.find("value = x").expect("source offset") as u64;
1365        let info = EthdebugInfo {
1366            compilation: json!({}),
1367            contract_name: "Counter".to_owned(),
1368            environment: "runtime".to_owned(),
1369            instructions: vec![Instruction {
1370                offset: 0,
1371                operation: json!({"mnemonic": "PUSH1"}),
1372                context: Some(json!({
1373                    "code": {"source": {"id": 0}, "range": {"offset": offset, "length": 9}}
1374                })),
1375            }],
1376            sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1377            variable_locations: BTreeMap::new(),
1378        };
1379        let source_contents = BTreeMap::from([(0, source.to_owned())]);
1380
1381        let constructed =
1382            DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1383        let mut attached = DebugSession::new(sample_trace());
1384        assert!(attached.ethdebug.is_none());
1385        assert!(attached.functions.is_empty());
1386        attached.attach_ethdebug(info, source_contents);
1387
1388        assert_eq!(attached, constructed);
1389        assert_eq!(attached.functions.len(), 1);
1390        assert_eq!(attached.functions[0].name, "set");
1391        assert_eq!(
1392            attached
1393                .step(0)
1394                .and_then(|step| step.source)
1395                .map(|span| span.line),
1396            Some(3)
1397        );
1398
1399        // Attaching again replaces the debug info wholesale rather than merging.
1400        let other = EthdebugInfo {
1401            compilation: json!({}),
1402            contract_name: "Other".to_owned(),
1403            environment: "runtime".to_owned(),
1404            instructions: Vec::new(),
1405            sources: BTreeMap::new(),
1406            variable_locations: BTreeMap::new(),
1407        };
1408        attached.attach_ethdebug(other, BTreeMap::new());
1409        assert_eq!(
1410            attached
1411                .ethdebug
1412                .as_ref()
1413                .map(|info| info.contract_name.as_str()),
1414            Some("Other")
1415        );
1416        assert!(attached.functions.is_empty());
1417        assert!(attached.source_contents.is_empty());
1418        assert_eq!(attached.step(0).and_then(|step| step.source), None);
1419        assert_eq!(attached.trace, sample_trace());
1420    }
1421
1422    #[test]
1423    fn parses_source_functions_with_tuple_and_array_params() {
1424        let functions = parse_source_functions(
1425            7,
1426            "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1427        );
1428        assert_eq!(functions[0].source_id, 7);
1429        assert_eq!(functions[0].name, "submit");
1430        assert_eq!(functions[0].params[0].name, "p");
1431        assert_eq!(functions[0].params[0].ty, "Person");
1432        assert_eq!(functions[0].params[1].name, "xs");
1433        assert_eq!(functions[0].params[1].ty, "uint256[2]");
1434    }
1435
1436    #[test]
1437    fn unnamed_params_documented_by_comments_do_not_become_parameters() {
1438        // The shape this comes from, verbatim: unnamed parameters whose names live in
1439        // trailing comments. Before comments were stripped, `//from` parsed as a type and
1440        // the type on the next line parsed as its name.
1441        let functions = parse_source_functions(
1442            0,
1443            "contract C {\n    function burnFrom_withCaller(\n        address caller,\n                     address, //from\n        uint256, //amount\n        bytes32 //h\n    ) external              view returns (bool) {}\n}",
1444        );
1445        let params = &functions[0].params;
1446        assert_eq!(functions[0].name, "burnFrom_withCaller");
1447        assert_eq!(params.len(), 4);
1448        assert_eq!(
1449            (params[0].ty.as_str(), params[0].name.as_str()),
1450            ("address", "caller")
1451        );
1452        assert_eq!(
1453            (params[1].ty.as_str(), params[1].name.as_str()),
1454            ("address", "arg1")
1455        );
1456        assert_eq!(
1457            (params[2].ty.as_str(), params[2].name.as_str()),
1458            ("uint256", "arg2")
1459        );
1460        assert_eq!(
1461            (params[3].ty.as_str(), params[3].name.as_str()),
1462            ("bytes32", "arg3")
1463        );
1464    }
1465
1466    #[test]
1467    fn unnamed_params_take_positional_names_not_their_type() {
1468        let functions = parse_source_functions(
1469            0,
1470            "contract C { function burn(address from, uint256 amount, bytes32, bytes memory              signature) public {} }",
1471        );
1472        let params = &functions[0].params;
1473        assert_eq!(
1474            (params[1].ty.as_str(), params[1].name.as_str()),
1475            ("uint256", "amount")
1476        );
1477        // Previously reported as `bytes32 bytes32`, the type standing in as the name.
1478        assert_eq!(
1479            (params[2].ty.as_str(), params[2].name.as_str()),
1480            ("bytes32", "arg2")
1481        );
1482        assert_eq!(
1483            (params[3].ty.as_str(), params[3].name.as_str()),
1484            ("bytes", "signature")
1485        );
1486        assert_eq!(params[3].location.as_deref(), Some("memory"));
1487    }
1488
1489    #[test]
1490    fn a_trailing_declaration_keyword_does_not_name_a_param() {
1491        let functions = parse_source_functions(
1492            0,
1493            "contract C { function f(string memory, address payable, bytes calldata data) public              {} }",
1494        );
1495        let params = &functions[0].params;
1496        assert_eq!(
1497            (params[0].ty.as_str(), params[0].name.as_str()),
1498            ("string", "arg0")
1499        );
1500        assert_eq!(params[0].location.as_deref(), Some("memory"));
1501        assert_eq!(
1502            (params[1].ty.as_str(), params[1].name.as_str()),
1503            ("address", "arg1")
1504        );
1505        assert_eq!(
1506            (params[2].ty.as_str(), params[2].name.as_str()),
1507            ("bytes", "data")
1508        );
1509        assert_eq!(params[2].location.as_deref(), Some("calldata"));
1510    }
1511
1512    #[test]
1513    fn block_comments_in_a_param_list_separate_rather_than_join() {
1514        let functions = parse_source_functions(
1515            0,
1516            "contract C { function f(uint256 /* wei */ amount, address /*to*/) public {} }",
1517        );
1518        let params = &functions[0].params;
1519        assert_eq!(
1520            (params[0].ty.as_str(), params[0].name.as_str()),
1521            ("uint256", "amount")
1522        );
1523        assert_eq!(
1524            (params[1].ty.as_str(), params[1].name.as_str()),
1525            ("address", "arg1")
1526        );
1527    }
1528
1529    #[test]
1530    fn stripping_comments_leaves_declaration_offsets_alone() {
1531        // Only the text between the parentheses is stripped, so the offsets the PC
1532        // mapping reads still point into the original source.
1533        let source = "contract C {\n    function f(address, //from\n        uint256 amount\n                          ) public {}\n}";
1534        let functions = parse_source_functions(0, source);
1535        let start = functions[0].declaration_start as usize;
1536        assert!(source[start..].starts_with("function f("));
1537        assert_eq!(functions[0].declaration_line, 2);
1538        assert_eq!(source.as_bytes()[functions[0].body_end as usize], b'}');
1539    }
1540
1541    fn sample_trace() -> TransactionTrace {
1542        TransactionTrace {
1543            tx_hash: Some("0xabc".to_owned()),
1544            from_addr: "0x1".to_owned(),
1545            to_addr: Some("0x2".to_owned()),
1546            value: "0".to_owned(),
1547            input_data: format!(
1548                "0x12345678{}",
1549                "0000000000000000000000001111111111111111111111111111111111111111"
1550            ),
1551            gas_used: 1,
1552            output: "0x".to_owned(),
1553            success: true,
1554            error: None,
1555            debug_trace_available: true,
1556            contract_address: None,
1557            backend: Some("debug-rpc".to_owned()),
1558            capabilities: Default::default(),
1559            artifacts: Default::default(),
1560            steps: vec![TraceStep {
1561                pc: 0,
1562                op: "PUSH1".into(),
1563                gas: 1,
1564                gas_cost: 0,
1565                depth: 0,
1566                stack: vec!["0x2a".into()],
1567                memory: Some(format!("{}{}", "00".repeat(32), "2a".repeat(32))),
1568                storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1569                error: None,
1570                snapshot: Default::default(),
1571            }],
1572        }
1573    }
1574    /// Solidity memory: a `string` is a length then its bytes; an array is a length then
1575    /// one word per element; a fixed-size array is those words with no length.
1576    fn memory_image(words: &[&str]) -> String {
1577        words
1578            .iter()
1579            .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1580            .collect()
1581    }
1582
1583    #[test]
1584    fn reads_arguments_that_live_in_memory() {
1585        // 0x00: "hello" as a string (length 5), 0x40: [7, 8, 9] as a dynamic array.
1586        let memory = memory_image(&[
1587            "5",
1588            "68656c6c6f000000000000000000000000000000000000000000000000000000",
1589            "3",
1590            "7",
1591            "8",
1592            "9",
1593        ]);
1594        let types = super::SourceTypes::default();
1595        let reader = super::ValueReader {
1596            memory: Some(&memory),
1597            calldata: "",
1598            storage: None,
1599            layout: None,
1600            types: &types,
1601            scope: None,
1602        };
1603        let read = |offset: usize, ty: &str| reader.read_memory(offset, ty, 0);
1604        assert_eq!(read(0, "string").as_deref(), Some("\"hello\""));
1605        assert_eq!(read(0, "bytes").as_deref(), Some("0x68656c6c6f"));
1606        assert_eq!(read(64, "uint256[]").as_deref(), Some("[7, 8, 9]"));
1607        // A fixed-size array has no length word: it starts at the pointer.
1608        assert_eq!(read(96, "uint256[2]").as_deref(), Some("[7, 8]"));
1609        // Beyond what the step captured: no guess, and the caller says where the pointer
1610        // pointed instead. An array of strings follows each element's pointer, and says
1611        // where one pointed when it cannot.
1612        assert_eq!(read(4096, "string"), None);
1613        assert_eq!(
1614            read(64, "string[]").as_deref(),
1615            Some("[<string at 0x7>, <string at 0x8>, <string at 0x9>]")
1616        );
1617    }
1618
1619    #[test]
1620    fn a_parameter_is_readable_only_when_its_width_is_known() {
1621        let param = |ty: &str, location: Option<&str>| super::SourceParam {
1622            name: "x".to_owned(),
1623            ty: ty.to_owned(),
1624            location: location.map(str::to_owned),
1625        };
1626        assert!(super::readable_parameter(&param("uint256", None)));
1627        assert!(super::readable_parameter(&param("string", Some("memory"))));
1628        assert!(super::readable_parameter(&param("Config", Some("storage"))));
1629        // One word: a calldata slice of a fixed-size type is a pointer.
1630        assert!(super::readable_parameter(&param(
1631            "uint256[2]",
1632            Some("calldata")
1633        )));
1634        // Two words, and nothing proves that width: not read.
1635        assert!(!super::readable_parameter(&param(
1636            "bytes",
1637            Some("calldata")
1638        )));
1639        assert!(!super::readable_parameter(&param(
1640            "uint256[]",
1641            Some("calldata")
1642        )));
1643        // A struct by value is not one word of anything the debugger can decode.
1644        assert!(!super::readable_parameter(&param("Config", None)));
1645    }
1646}