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