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
23pub mod condition;
24pub mod debug_diff;
25pub mod state;
26pub mod stepping;
27
28pub use condition::{Condition, ConditionContext, Evaluation};
29pub use debug_diff::{
30    capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
31    DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
32    DebugTraceSummary,
33};
34pub use soldb_ethdebug::StorageLayout;
35pub use state::{
36    short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
37    StateVariable, StorageTape, StorageWords,
38};
39pub use stepping::{
40    address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
41    Frame, FunctionId, JumpMarker, LineKey, ResolvedFunction, ResolvedLine, SourceListing,
42    StepLocation, StepMap,
43};
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct DebugSession {
47    pub trace: TransactionTrace,
48    pub ethdebug: Option<EthdebugInfo>,
49    #[serde(default)]
50    pub source_contents: BTreeMap<u64, String>,
51    #[serde(default)]
52    pub functions: Vec<SourceFunction>,
53}
54
55impl DebugSession {
56    #[must_use]
57    pub fn new(trace: TransactionTrace) -> Self {
58        Self {
59            trace,
60            ethdebug: None,
61            source_contents: BTreeMap::new(),
62            functions: Vec::new(),
63        }
64    }
65
66    #[must_use]
67    pub fn with_ethdebug(
68        trace: TransactionTrace,
69        ethdebug: EthdebugInfo,
70        source_contents: BTreeMap<u64, String>,
71    ) -> Self {
72        let mut session = Self::new(trace);
73        session.attach_ethdebug(ethdebug, source_contents);
74        session
75    }
76
77    /// Attaches debug info to a session that already holds a trace, replacing whatever
78    /// was attached before.
79    ///
80    /// The trace is left in place rather than rebuilt, so a frontend that keeps a large
81    /// trace resident, such as the WebAssembly module, can add or swap the contract's
82    /// artifacts without copying hundreds of thousands of steps.
83    pub fn attach_ethdebug(
84        &mut self,
85        ethdebug: EthdebugInfo,
86        source_contents: BTreeMap<u64, String>,
87    ) {
88        self.functions = source_contents
89            .iter()
90            .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
91            .collect();
92        self.ethdebug = Some(ethdebug);
93        self.source_contents = source_contents;
94    }
95
96    #[must_use]
97    pub fn step(&self, step_index: usize) -> Option<DebugStep> {
98        let step = self.trace.steps.get(step_index)?;
99        Some(DebugStep {
100            index: step_index,
101            pc: step.pc,
102            op: step.op.to_string(),
103            gas: step.gas,
104            gas_cost: step.gas_cost,
105            depth: step.depth,
106            source: self.source_span(step.pc),
107            function: self.function_at_pc(step.pc).cloned(),
108            snapshot: step.normalized_snapshot(),
109            variables: self.variables_at_step(step),
110        })
111    }
112
113    #[must_use]
114    pub fn steps(&self) -> Vec<DebugStep> {
115        (0..self.trace.steps.len())
116            .filter_map(|step_index| self.step(step_index))
117            .collect()
118    }
119
120    #[must_use]
121    pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
122        let ethdebug = self.ethdebug.as_ref()?;
123        let instruction = ethdebug.instruction_at_pc(pc)?;
124        let location = instruction.source_location()?;
125        let path = ethdebug.sources.get(&location.source_id)?.clone();
126        let position = self
127            .source_contents
128            .get(&location.source_id)
129            .map(|source| line_column_for_offset(source, location.offset as usize));
130        Some(SourceSpan {
131            source_id: location.source_id,
132            path,
133            offset: location.offset,
134            length: location.length,
135            line: position.map_or(0, |position| position.line),
136            column: position.map_or(0, |position| position.column),
137        })
138    }
139
140    #[must_use]
141    pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
142        let location = self
143            .ethdebug
144            .as_ref()?
145            .instruction_at_pc(pc)?
146            .source_location()?;
147        self.functions
148            .iter()
149            .filter(|function| {
150                function.source_id == location.source_id
151                    && function.declaration_start <= location.offset
152                    && location.offset <= function.body_end
153            })
154            .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
155    }
156
157    #[must_use]
158    pub fn variables_at_step(&self, step: &TraceStep) -> Vec<DebugVariable> {
159        let Some(ethdebug) = &self.ethdebug else {
160            return Vec::new();
161        };
162        variables_for_step(&self.trace, ethdebug, step)
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct DebugStep {
168    pub index: usize,
169    pub pc: u64,
170    pub op: String,
171    pub gas: u64,
172    pub gas_cost: u64,
173    pub depth: u64,
174    pub source: Option<SourceSpan>,
175    pub function: Option<SourceFunction>,
176    pub snapshot: StepSnapshot,
177    pub variables: Vec<DebugVariable>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct SourceSpan {
182    pub source_id: u64,
183    pub path: String,
184    pub offset: u64,
185    pub length: u64,
186    pub line: u64,
187    pub column: u64,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct SourceFunction {
192    pub source_id: u64,
193    /// The declared name; `constructor`, `fallback`, and `receive` for those, and the
194    /// modifier's name for a modifier.
195    pub name: String,
196    pub params: Vec<SourceParam>,
197    pub declaration_start: u64,
198    /// The one-based line the declaration begins on.
199    #[serde(default)]
200    pub declaration_line: u64,
201    pub body_end: u64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct SourceParam {
206    pub name: String,
207    /// The type without its data location, such as `uint256` or `string`.
208    pub ty: String,
209    /// The data location as declared: `memory`, `calldata`, or `storage`; `None` for a
210    /// value type.
211    #[serde(default)]
212    pub location: Option<String>,
213}
214
215/// One argument of a function frame, as it was on the stack when the frame was entered.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct FrameArgument {
218    pub name: String,
219    pub ty: String,
220    pub value: DebugValue,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct DebugVariable {
225    pub name: String,
226    pub ty: String,
227    pub location: DebugLocation,
228    pub value: DebugValue,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct DebugLocation {
233    pub kind: String,
234    pub offset: u64,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct DebugValue {
239    pub display: String,
240    pub raw: Option<String>,
241    pub status: DebugValueStatus,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum DebugValueStatus {
247    Decoded,
248    Raw,
249    Unavailable,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253struct SourcePosition {
254    line: u64,
255    column: u64,
256}
257
258#[must_use]
259pub fn variables_for_step(
260    trace: &TransactionTrace,
261    ethdebug: &EthdebugInfo,
262    step: &TraceStep,
263) -> Vec<DebugVariable> {
264    ethdebug
265        .variables_at_pc(step.pc)
266        .into_iter()
267        .map(|variable| decode_variable(trace, step, variable))
268        .collect()
269}
270
271fn decode_variable(
272    trace: &TransactionTrace,
273    step: &TraceStep,
274    variable: &VariableLocation,
275) -> DebugVariable {
276    let raw = raw_value_for_location(trace, step, variable);
277    let value = raw.map_or_else(
278        || DebugValue {
279            display: "<unavailable>".to_owned(),
280            raw: None,
281            status: DebugValueStatus::Unavailable,
282        },
283        |raw| decode_debug_value(&raw, &variable.ty),
284    );
285    DebugVariable {
286        name: variable.name.clone(),
287        ty: variable.ty.clone(),
288        location: DebugLocation {
289            kind: variable.location_type.clone(),
290            offset: variable.offset,
291        },
292        value,
293    }
294}
295
296fn raw_value_for_location(
297    trace: &TransactionTrace,
298    step: &TraceStep,
299    variable: &VariableLocation,
300) -> Option<String> {
301    let snapshot = step.snapshot_ref();
302    match variable.location_type.as_str() {
303        "stack" => snapshot
304            .stack
305            .get(variable.offset as usize)
306            .map(|value| normalize_hex(value)),
307        "memory" => word_from_hex_bytes(snapshot.memory?, variable.offset as usize),
308        "calldata" => word_from_hex_bytes(&trace.input_data, variable.offset as usize),
309        "storage" => storage_value(snapshot.storage, variable.offset),
310        _ => None,
311    }
312}
313
314fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
315    let ty = ty.trim();
316    let normalized = normalize_hex(raw);
317    let word = normalized.trim_start_matches("0x");
318    let decoded = decode_static_word(word, ty);
319    match decoded {
320        Some(display) => DebugValue {
321            display,
322            raw: Some(normalized),
323            status: DebugValueStatus::Decoded,
324        },
325        None => DebugValue {
326            display: normalized.clone(),
327            raw: Some(normalized),
328            status: DebugValueStatus::Raw,
329        },
330    }
331}
332
333fn decode_static_word(word: &str, ty: &str) -> Option<String> {
334    if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
335        return None;
336    }
337    if ty.starts_with("uint") {
338        return Some(format_uint_word(word));
339    }
340    if ty == "address" {
341        let padded = left_pad_word(word);
342        return Some(
343            format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
344        );
345    }
346    if ty == "bool" {
347        let trimmed = word.trim_start_matches('0');
348        return match trimmed {
349            "" | "0" => Some("false".to_owned()),
350            "1" => Some("true".to_owned()),
351            _ => None,
352        };
353    }
354    if ty == "bytes32" {
355        return Some(normalize_hex(word));
356    }
357    if let Some(size) = fixed_bytes_size(ty) {
358        let padded = right_pad_word(word);
359        return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
360    }
361    None
362}
363
364/// Where the compiler leaves a function's parameters on the stack when the function is
365/// entered. Solidity's two code generators disagree, and nothing in the artifacts says
366/// which produced the code, so [`StepMap`] proves it from the trace instead of assuming.
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum ArgumentOrder {
369    /// The first declared parameter is on top: the via-IR pipeline pushes them
370    /// right-to-left.
371    FirstOnTop,
372    /// The last declared parameter is on top: the legacy pipeline pushes them
373    /// left-to-right.
374    LastOnTop,
375}
376
377/// What this trace proved about how a contract's compiler passes arguments.
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub enum ArgumentLayout {
380    /// The parameters are the top words at the entry point, in this order.
381    Ordered(ArgumentOrder),
382    /// The parameters are the top words, but which is which was not established: the
383    /// evidence came from a one-parameter frame, where both orders look the same.
384    TopWords,
385}
386
387impl ArgumentOrder {
388    /// The stack index of parameter `index` of `count`, counting from the bottom of the
389    /// stack, given that the parameters occupy the top `count` words.
390    #[must_use]
391    fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
392        match self {
393            Self::FirstOnTop => stack_len - 1 - index,
394            Self::LastOnTop => stack_len - count + index,
395        }
396    }
397}
398
399/// What a frame's arguments are read from: the stack at its entry point, and the memory
400/// a pointer among them refers into.
401#[derive(Debug, Clone, Copy, Default)]
402pub struct FrameState<'a> {
403    pub stack: &'a [StackWord],
404    /// Memory at the entry step, as one unprefixed hex string, when the backend captured
405    /// it.
406    pub memory: Option<&'a str>,
407}
408
409/// The arguments a function was entered with, read off the stack at its entry point.
410///
411/// Every parameter read here occupies exactly one word: a value type is the value, and a
412/// `memory` or `storage` reference is a pointer, which memory's own layout then resolves.
413/// A `calldata` slice of a dynamic type is the exception — it is a pointer *and* a length,
414/// two words, and nothing in the trace proves that width the way the calldata proves the
415/// order — so a function taking one reports no arguments rather than words that may be
416/// misaligned.
417#[must_use]
418pub fn decode_arguments(
419    params: &[SourceParam],
420    state: FrameState<'_>,
421    layout: ArgumentLayout,
422) -> Vec<FrameArgument> {
423    let stack = state.stack;
424    if params.is_empty() || stack.len() < params.len() {
425        return Vec::new();
426    }
427    if !params.iter().all(readable_parameter) {
428        return Vec::new();
429    }
430    // One parameter is on top under either order; more than one needs the order proven.
431    let order = match layout {
432        ArgumentLayout::Ordered(order) => order,
433        ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
434        ArgumentLayout::TopWords => return Vec::new(),
435    };
436    params
437        .iter()
438        .enumerate()
439        .map(|(index, param)| {
440            let word = &stack[order.word_index(index, params.len(), stack.len())];
441            FrameArgument {
442                name: param.name.clone(),
443                ty: param.ty.clone(),
444                value: argument_value(param, word, state.memory),
445            }
446        })
447        .collect()
448}
449
450fn argument_value(param: &SourceParam, word: &str, memory: Option<&str>) -> DebugValue {
451    let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
452        return DebugValue {
453            display: "<unreadable stack word>".to_owned(),
454            raw: Some(word.to_owned()),
455            status: DebugValueStatus::Unavailable,
456        };
457    };
458    let raw = Some(short_hex(&parsed));
459    match param.location.as_deref() {
460        Some("memory") => {
461            let pointer = word_as_usize(&parsed);
462            let display = match (pointer, memory) {
463                (Some(pointer), Some(memory)) => read_memory_value(memory, pointer, &param.ty)
464                    .unwrap_or_else(|| {
465                        format!(
466                            "<{} in memory at {}, beyond what this step captured>",
467                            param.ty,
468                            short_hex(&parsed)
469                        )
470                    }),
471                (_, None) => format!(
472                    "<{} in memory at {}; this backend captured no memory>",
473                    param.ty,
474                    short_hex(&parsed)
475                ),
476                (None, _) => format!("<{} at {}>", param.ty, short_hex(&parsed)),
477            };
478            let decoded =
479                display.starts_with('"') || display.starts_with('[') || display.starts_with("0x");
480            DebugValue {
481                display,
482                raw,
483                status: if decoded {
484                    DebugValueStatus::Decoded
485                } else {
486                    DebugValueStatus::Raw
487                },
488            }
489        }
490        Some(location) => DebugValue {
491            display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
492            raw,
493            status: DebugValueStatus::Raw,
494        },
495        None => DebugValue {
496            display: decode_value(value_bytes(&parsed, &param.ty), &param.ty),
497            raw,
498            status: DebugValueStatus::Decoded,
499        },
500    }
501}
502
503/// Whether a parameter is one word the debugger can place: a value, or a pointer.
504pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
505    match param.location.as_deref() {
506        // A `calldata` slice of a dynamic type is two words; see `decode_arguments`.
507        Some("calldata") => !is_dynamic_type(&param.ty),
508        Some(_) => true,
509        None => is_value_type(&param.ty),
510    }
511}
512
513/// Whether a type is stored with a length, rather than a fixed number of words.
514fn is_dynamic_type(ty: &str) -> bool {
515    ty == "string" || ty == "bytes" || ty.ends_with("[]")
516}
517
518/// A value living in memory, read through Solidity's memory layout: a `string` or `bytes`
519/// is a length followed by its bytes, a dynamic array is a length followed by one word
520/// per element, and a fixed-size array is those words with no length.
521///
522/// The layout is the language's, not a guess — the same standing as the storage layout —
523/// but only value-type elements are decoded; anything else is a pointer this does not
524/// follow, and it says so rather than printing an offset as a number.
525fn read_memory_value(memory: &str, pointer: usize, ty: &str) -> Option<String> {
526    if ty == "string" || ty == "bytes" {
527        let length = word_as_usize(&memory_word(memory, pointer)?)?;
528        let bytes = memory_bytes(memory, pointer.checked_add(32)?, length)?;
529        if ty == "string" {
530            if let Ok(text) = std::str::from_utf8(&bytes) {
531                return Some(format!("{text:?}"));
532            }
533        }
534        return Some(format!("0x{}", hex_of(&bytes)));
535    }
536    let (element, count) = array_shape(ty)?;
537    if !is_value_type(element) {
538        return None;
539    }
540    let (first, count) = match count {
541        // Dynamic: the length is the first word, the elements follow it.
542        None => (
543            pointer.checked_add(32)?,
544            word_as_usize(&memory_word(memory, pointer)?)?,
545        ),
546        Some(count) => (pointer, count),
547    };
548    let shown = count.min(8);
549    let mut parts = Vec::with_capacity(shown);
550    for index in 0..shown {
551        let word = memory_word(memory, first.checked_add(index.checked_mul(32)?)?)?;
552        parts.push(decode_value(value_bytes(&word, element), element));
553    }
554    if count > shown {
555        parts.push(format!("... {} more", count - shown));
556    }
557    Some(format!("[{}]", parts.join(", ")))
558}
559
560/// An array type as its element type and its length: `None` for a dynamic array, which
561/// carries its length in memory.
562fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
563    let inner = ty.strip_suffix(']')?;
564    let open = inner.rfind('[')?;
565    let element = inner[..open].trim();
566    let length = &inner[open + 1..];
567    if length.is_empty() {
568        return Some((element, None));
569    }
570    Some((element, Some(length.parse::<usize>().ok()?)))
571}
572
573/// The 32 bytes at `offset` of a memory image, when it reaches that far.
574fn memory_word(memory: &str, offset: usize) -> Option<Word> {
575    let bytes = memory_bytes(memory, offset, 32)?;
576    let mut word = [0_u8; 32];
577    word.copy_from_slice(&bytes);
578    Some(word)
579}
580
581/// `length` bytes at `offset` of a memory image, which is two hex digits per byte.
582fn memory_bytes(memory: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
583    let start = offset.checked_mul(2)?;
584    let end = start.checked_add(length.checked_mul(2)?)?;
585    let digits = memory.get(start..end)?;
586    if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
587        return None;
588    }
589    Some(
590        digits
591            .as_bytes()
592            .chunks(2)
593            .map(|pair| {
594                u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
595            })
596            .collect(),
597    )
598}
599
600/// A word as an offset or a length, when it fits one.
601fn word_as_usize(word: &Word) -> Option<usize> {
602    if word[..24].iter().any(|byte| *byte != 0) {
603        return None;
604    }
605    usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
606}
607
608fn hex_of(bytes: &[u8]) -> String {
609    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
610}
611
612/// The bytes of a stack word a value of type `ty` occupies: a signed integer is its own
613/// width from the low end so the sign bit is the right one, fixed bytes are left-aligned,
614/// and everything else reads as a whole word.
615fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
616    if let Some(bits) = integer_bits(ty, "int") {
617        return &word[32 - bits / 8..];
618    }
619    if let Some(size) = fixed_bytes_size(ty) {
620        return &word[..size];
621    }
622    word
623}
624
625/// Whether a value of this type is one stack word the debugger can decode.
626#[must_use]
627pub fn is_value_type(ty: &str) -> bool {
628    matches!(ty, "bool" | "address" | "address payable")
629        || fixed_bytes_size(ty).is_some()
630        || integer_bits(ty, "uint").is_some()
631        || integer_bits(ty, "int").is_some()
632}
633
634/// The width of `uint<N>`/`int<N>` in bits, the bare names counting as 256.
635fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
636    let rest = ty.strip_prefix(prefix)?;
637    // `int8` must not match the `uint` prefix; `strip_prefix` already ruled that out.
638    if rest.is_empty() {
639        return Some(256);
640    }
641    let bits = rest.parse::<usize>().ok()?;
642    (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
643}
644
645fn fixed_bytes_size(ty: &str) -> Option<usize> {
646    let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
647    (1..=32).contains(&size).then_some(size)
648}
649
650fn format_uint_word(word: &str) -> String {
651    let trimmed = word.trim_start_matches('0');
652    if trimmed.is_empty() {
653        return "0".to_owned();
654    }
655    if trimmed.len() <= 32 {
656        return u128::from_str_radix(trimmed, 16)
657            .map(|value| value.to_string())
658            .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
659    }
660    format!("0x{}", trimmed.to_ascii_lowercase())
661}
662
663fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
664    storage
665        .get(&format!("0x{slot:x}"))
666        .or_else(|| storage.get(&format!("{slot:x}")))
667        .or_else(|| storage.get(&format!("0x{slot:064x}")))
668        .or_else(|| storage.get(&format!("{slot:064x}")))
669        .map(|value| normalize_hex(value))
670}
671
672fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
673    let hex = input.trim_start_matches("0x");
674    let start = byte_offset.checked_mul(2)?;
675    let end = start.checked_add(64)?;
676    let word = hex.get(start..end)?;
677    word.bytes()
678        .all(|byte| byte.is_ascii_hexdigit())
679        .then(|| format!("0x{}", word.to_ascii_lowercase()))
680}
681
682fn normalize_hex(value: &str) -> String {
683    let value = value.trim();
684    let hex = value.strip_prefix("0x").unwrap_or(value);
685    format!("0x{}", hex.to_ascii_lowercase())
686}
687
688fn left_pad_word(word: &str) -> String {
689    let word = word.trim_start_matches("0x").to_ascii_lowercase();
690    if word.len() >= 64 {
691        return word;
692    }
693    format!("{:0>64}", word)
694}
695
696fn right_pad_word(word: &str) -> String {
697    let word = word.trim_start_matches("0x").to_ascii_lowercase();
698    if word.len() >= 64 {
699        return word;
700    }
701    format!("{:0<64}", word)
702}
703
704fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
705    let mut line = 1;
706    let mut column = 1;
707    for byte in source.bytes().take(offset.min(source.len())) {
708        if byte == b'\n' {
709            line += 1;
710            column = 1;
711        } else {
712            column += 1;
713        }
714    }
715    SourcePosition { line, column }
716}
717
718/// The declarations that open a body of executable code in a Solidity source.
719const DECLARATION_KEYWORDS: [&str; 5] =
720    ["function", "modifier", "constructor", "fallback", "receive"];
721
722/// Finds every function-like declaration in a source file by scanning its text:
723/// functions, modifiers, constructors, `fallback`, and `receive`. This is what the
724/// debugger attributes steps to when the compiler emits no function boundaries.
725#[must_use]
726pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
727    let mut functions = Vec::new();
728    for keyword in DECLARATION_KEYWORDS {
729        collect_declarations(source_id, source, keyword, &mut functions);
730    }
731    functions.sort_by_key(|function| function.declaration_start);
732    functions
733}
734
735fn collect_declarations(
736    source_id: u64,
737    source: &str,
738    keyword: &str,
739    functions: &mut Vec<SourceFunction>,
740) {
741    let named = matches!(keyword, "function" | "modifier");
742    let mut cursor = 0;
743    while let Some(keyword_start) = find_solidity_keyword(source, keyword, cursor) {
744        let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
745        let name = if named {
746            let Some((name, name_end)) = parse_identifier(source, index) else {
747                cursor = index;
748                continue;
749            };
750            index = skip_ascii_whitespace(source, name_end);
751            name
752        } else {
753            keyword
754        };
755        if source.as_bytes().get(index) != Some(&b'(') {
756            cursor = index;
757            continue;
758        }
759        let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
760            cursor = index + 1;
761            continue;
762        };
763        let params = parse_source_params(&source[index + 1..params_end]);
764        let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
765            cursor = params_end + 1;
766            continue;
767        };
768        // A call such as `token.receive(x);` has a `;` before the next `{`; only a
769        // declaration runs straight from its parameter list into a body.
770        if source[params_end + 1..body_start].contains(';') {
771            cursor = params_end + 1;
772            continue;
773        }
774        let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
775            cursor = body_start + 1;
776            continue;
777        };
778
779        functions.push(SourceFunction {
780            source_id,
781            name: name.to_owned(),
782            params,
783            declaration_start: keyword_start as u64,
784            declaration_line: source[..keyword_start]
785                .bytes()
786                .filter(|byte| *byte == b'\n')
787                .count() as u64
788                + 1,
789            body_end: body_end as u64,
790        });
791        cursor = body_end + 1;
792    }
793}
794
795fn parse_source_params(params: &str) -> Vec<SourceParam> {
796    split_top_level_commas(params)
797        .into_iter()
798        .enumerate()
799        .filter_map(|(index, param)| {
800            let param = param.trim();
801            if param.is_empty() {
802                return None;
803            }
804            let mut tokens = param.split_whitespace().collect::<Vec<_>>();
805            let name = tokens
806                .last()
807                .copied()
808                .filter(|token| is_identifier(token))
809                .map_or_else(|| format!("arg{index}"), str::to_owned);
810            if tokens.last().copied() == Some(name.as_str()) && tokens.len() > 1 {
811                tokens.pop();
812            }
813            let location = tokens
814                .iter()
815                .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
816                .map(|token| (*token).to_owned());
817            let ty = tokens
818                .into_iter()
819                .filter(|token| !matches!(*token, "memory" | "calldata" | "storage" | "payable"))
820                .collect::<Vec<_>>()
821                .join(" ");
822            (!ty.is_empty()).then_some(SourceParam { name, ty, location })
823        })
824        .collect()
825}
826
827fn split_top_level_commas(input: &str) -> Vec<&str> {
828    let mut parts = Vec::new();
829    let mut start = 0;
830    let mut depth = 0_i32;
831    for (index, byte) in input.bytes().enumerate() {
832        match byte {
833            b'(' | b'[' => depth += 1,
834            b')' | b']' => depth -= 1,
835            b',' if depth == 0 => {
836                parts.push(&input[start..index]);
837                start = index + 1;
838            }
839            _ => {}
840        }
841    }
842    parts.push(&input[start..]);
843    parts
844}
845
846fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
847    let mut cursor = start;
848    while let Some(relative) = source[cursor..].find(keyword) {
849        let absolute = cursor + relative;
850        let before = absolute
851            .checked_sub(1)
852            .and_then(|index| source.as_bytes().get(index))
853            .copied();
854        let after = source.as_bytes().get(absolute + keyword.len()).copied();
855        if before.is_none_or(|byte| !is_identifier_byte(byte))
856            && after.is_none_or(|byte| !is_identifier_byte(byte))
857        {
858            return Some(absolute);
859        }
860        cursor = absolute + keyword.len();
861    }
862    None
863}
864
865fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
866    let bytes = source.as_bytes();
867    let first = *bytes.get(start)?;
868    if !is_identifier_start_byte(first) {
869        return None;
870    }
871    let mut end = start + 1;
872    while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
873        end += 1;
874    }
875    Some((&source[start..end], end))
876}
877
878fn is_identifier(input: &str) -> bool {
879    let mut bytes = input.bytes();
880    let Some(first) = bytes.next() else {
881        return false;
882    };
883    is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
884}
885
886fn is_identifier_start_byte(byte: u8) -> bool {
887    byte == b'_' || byte.is_ascii_alphabetic()
888}
889
890fn is_identifier_byte(byte: u8) -> bool {
891    is_identifier_start_byte(byte) || byte.is_ascii_digit()
892}
893
894fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
895    while source
896        .as_bytes()
897        .get(index)
898        .is_some_and(u8::is_ascii_whitespace)
899    {
900        index += 1;
901    }
902    index
903}
904
905fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
906    source
907        .as_bytes()
908        .iter()
909        .enumerate()
910        .skip(start)
911        .find_map(|(index, byte)| (*byte == needle).then_some(index))
912}
913
914fn find_matching_delimiter(source: &str, open_index: usize, open: u8, close: u8) -> Option<usize> {
915    let mut depth = 0_i32;
916    for (index, byte) in source.bytes().enumerate().skip(open_index) {
917        if byte == open {
918            depth += 1;
919        } else if byte == close {
920            depth -= 1;
921            if depth == 0 {
922                return Some(index);
923            }
924        }
925    }
926    None
927}
928
929#[cfg(test)]
930mod tests {
931    use std::collections::BTreeMap;
932
933    use serde_json::json;
934    use soldb_core::{TraceStep, TransactionTrace};
935    use soldb_ethdebug::{EthdebugInfo, Instruction, VariableLocation};
936
937    use super::{parse_source_functions, DebugSession, DebugValueStatus};
938
939    #[test]
940    fn builds_source_steps_and_decodes_active_variables() {
941        let source =
942            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
943        let offset = source.find("value = x").expect("source offset") as u64;
944        let trace = sample_trace();
945        let mut sources = BTreeMap::new();
946        sources.insert(0, "Counter.sol".to_owned());
947        let mut source_contents = BTreeMap::new();
948        source_contents.insert(0, source.to_owned());
949        let mut variable_locations = BTreeMap::new();
950        variable_locations.insert(
951            0,
952            vec![
953                VariableLocation {
954                    name: "x".to_owned(),
955                    ty: "uint256".to_owned(),
956                    location_type: "stack".to_owned(),
957                    offset: 0,
958                    pc_range: (0, 1),
959                },
960                VariableLocation {
961                    name: "sender".to_owned(),
962                    ty: "address".to_owned(),
963                    location_type: "calldata".to_owned(),
964                    offset: 4,
965                    pc_range: (0, 1),
966                },
967            ],
968        );
969        let info = EthdebugInfo {
970            compilation: json!({}),
971            contract_name: "Counter".to_owned(),
972            environment: "runtime".to_owned(),
973            instructions: vec![Instruction {
974                offset: 0,
975                operation: json!({"mnemonic": "PUSH1"}),
976                context: Some(json!({
977                    "code": {
978                        "source": {"id": 0},
979                        "range": {"offset": offset, "length": 9}
980                    }
981                })),
982            }],
983            sources,
984            variable_locations,
985        };
986
987        let session = DebugSession::with_ethdebug(trace, info, source_contents);
988        let step = session.step(0).expect("debug step");
989        assert_eq!(step.source.as_ref().expect("source").line, 3);
990        assert_eq!(step.function.as_ref().expect("function").name, "set");
991        assert_eq!(
992            step.snapshot
993                .stack
994                .iter()
995                .map(|word| &**word)
996                .collect::<Vec<_>>(),
997            ["0x2a"]
998        );
999        assert_eq!(step.variables[0].name, "x");
1000        assert_eq!(step.variables[0].value.display, "42");
1001        assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1002        assert_eq!(
1003            step.variables[1].value.display,
1004            "0x1111111111111111111111111111111111111111"
1005        );
1006    }
1007
1008    #[test]
1009    fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1010        let trace = sample_trace();
1011        let mut variable_locations = BTreeMap::new();
1012        variable_locations.insert(
1013            0,
1014            vec![
1015                VariableLocation {
1016                    name: "name".to_owned(),
1017                    ty: "string".to_owned(),
1018                    location_type: "stack".to_owned(),
1019                    offset: 0,
1020                    pc_range: (0, 0),
1021                },
1022                VariableLocation {
1023                    name: "missing".to_owned(),
1024                    ty: "uint256".to_owned(),
1025                    location_type: "stack".to_owned(),
1026                    offset: 99,
1027                    pc_range: (0, 0),
1028                },
1029            ],
1030        );
1031        let info = EthdebugInfo {
1032            compilation: json!({}),
1033            contract_name: "Counter".to_owned(),
1034            environment: "runtime".to_owned(),
1035            instructions: Vec::new(),
1036            sources: BTreeMap::new(),
1037            variable_locations,
1038        };
1039
1040        let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1041        let step = session.step(0).expect("debug step");
1042        assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1043        assert_eq!(step.variables[0].value.display, "0x2a");
1044        assert_eq!(
1045            step.variables[1].value.status,
1046            DebugValueStatus::Unavailable
1047        );
1048    }
1049
1050    #[test]
1051    fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1052        let source =
1053            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
1054        let offset = source.find("value = x").expect("source offset") as u64;
1055        let info = EthdebugInfo {
1056            compilation: json!({}),
1057            contract_name: "Counter".to_owned(),
1058            environment: "runtime".to_owned(),
1059            instructions: vec![Instruction {
1060                offset: 0,
1061                operation: json!({"mnemonic": "PUSH1"}),
1062                context: Some(json!({
1063                    "code": {"source": {"id": 0}, "range": {"offset": offset, "length": 9}}
1064                })),
1065            }],
1066            sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1067            variable_locations: BTreeMap::new(),
1068        };
1069        let source_contents = BTreeMap::from([(0, source.to_owned())]);
1070
1071        let constructed =
1072            DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1073        let mut attached = DebugSession::new(sample_trace());
1074        assert!(attached.ethdebug.is_none());
1075        assert!(attached.functions.is_empty());
1076        attached.attach_ethdebug(info, source_contents);
1077
1078        assert_eq!(attached, constructed);
1079        assert_eq!(attached.functions.len(), 1);
1080        assert_eq!(attached.functions[0].name, "set");
1081        assert_eq!(
1082            attached
1083                .step(0)
1084                .and_then(|step| step.source)
1085                .map(|span| span.line),
1086            Some(3)
1087        );
1088
1089        // Attaching again replaces the debug info wholesale rather than merging.
1090        let other = EthdebugInfo {
1091            compilation: json!({}),
1092            contract_name: "Other".to_owned(),
1093            environment: "runtime".to_owned(),
1094            instructions: Vec::new(),
1095            sources: BTreeMap::new(),
1096            variable_locations: BTreeMap::new(),
1097        };
1098        attached.attach_ethdebug(other, BTreeMap::new());
1099        assert_eq!(
1100            attached
1101                .ethdebug
1102                .as_ref()
1103                .map(|info| info.contract_name.as_str()),
1104            Some("Other")
1105        );
1106        assert!(attached.functions.is_empty());
1107        assert!(attached.source_contents.is_empty());
1108        assert_eq!(attached.step(0).and_then(|step| step.source), None);
1109        assert_eq!(attached.trace, sample_trace());
1110    }
1111
1112    #[test]
1113    fn parses_source_functions_with_tuple_and_array_params() {
1114        let functions = parse_source_functions(
1115            7,
1116            "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1117        );
1118        assert_eq!(functions[0].source_id, 7);
1119        assert_eq!(functions[0].name, "submit");
1120        assert_eq!(functions[0].params[0].name, "p");
1121        assert_eq!(functions[0].params[0].ty, "Person");
1122        assert_eq!(functions[0].params[1].name, "xs");
1123        assert_eq!(functions[0].params[1].ty, "uint256[2]");
1124    }
1125
1126    fn sample_trace() -> TransactionTrace {
1127        TransactionTrace {
1128            tx_hash: Some("0xabc".to_owned()),
1129            from_addr: "0x1".to_owned(),
1130            to_addr: Some("0x2".to_owned()),
1131            value: "0".to_owned(),
1132            input_data: format!(
1133                "0x12345678{}",
1134                "0000000000000000000000001111111111111111111111111111111111111111"
1135            ),
1136            gas_used: 1,
1137            output: "0x".to_owned(),
1138            success: true,
1139            error: None,
1140            debug_trace_available: true,
1141            contract_address: None,
1142            backend: Some("debug-rpc".to_owned()),
1143            capabilities: Default::default(),
1144            artifacts: Default::default(),
1145            steps: vec![TraceStep {
1146                pc: 0,
1147                op: "PUSH1".into(),
1148                gas: 1,
1149                gas_cost: 0,
1150                depth: 0,
1151                stack: vec!["0x2a".into()],
1152                memory: Some(format!("{}{}", "00".repeat(32), "2a".repeat(32))),
1153                storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1154                error: None,
1155                snapshot: Default::default(),
1156            }],
1157        }
1158    }
1159    /// Solidity memory: a `string` is a length then its bytes; an array is a length then
1160    /// one word per element; a fixed-size array is those words with no length.
1161    fn memory_image(words: &[&str]) -> String {
1162        words
1163            .iter()
1164            .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1165            .collect()
1166    }
1167
1168    #[test]
1169    fn reads_arguments_that_live_in_memory() {
1170        // 0x00: "hello" as a string (length 5), 0x40: [7, 8, 9] as a dynamic array.
1171        let memory = memory_image(&[
1172            "5",
1173            "68656c6c6f000000000000000000000000000000000000000000000000000000",
1174            "3",
1175            "7",
1176            "8",
1177            "9",
1178        ]);
1179        let string_at = |offset: usize| super::read_memory_value(&memory, offset, "string");
1180        assert_eq!(string_at(0).as_deref(), Some("\"hello\""));
1181        assert_eq!(
1182            super::read_memory_value(&memory, 0, "bytes").as_deref(),
1183            Some("0x68656c6c6f")
1184        );
1185        assert_eq!(
1186            super::read_memory_value(&memory, 64, "uint256[]").as_deref(),
1187            Some("[7, 8, 9]")
1188        );
1189        // A fixed-size array has no length word: it starts at the pointer.
1190        assert_eq!(
1191            super::read_memory_value(&memory, 96, "uint256[2]").as_deref(),
1192            Some("[7, 8]")
1193        );
1194        // Beyond what the step captured, and a type whose elements are not values: no
1195        // guess, and the caller says where the pointer pointed instead.
1196        assert_eq!(super::read_memory_value(&memory, 4096, "string"), None);
1197        assert_eq!(super::read_memory_value(&memory, 64, "string[]"), None);
1198    }
1199
1200    #[test]
1201    fn a_parameter_is_readable_only_when_its_width_is_known() {
1202        let param = |ty: &str, location: Option<&str>| super::SourceParam {
1203            name: "x".to_owned(),
1204            ty: ty.to_owned(),
1205            location: location.map(str::to_owned),
1206        };
1207        assert!(super::readable_parameter(&param("uint256", None)));
1208        assert!(super::readable_parameter(&param("string", Some("memory"))));
1209        assert!(super::readable_parameter(&param("Config", Some("storage"))));
1210        // One word: a calldata slice of a fixed-size type is a pointer.
1211        assert!(super::readable_parameter(&param(
1212            "uint256[2]",
1213            Some("calldata")
1214        )));
1215        // Two words, and nothing proves that width: not read.
1216        assert!(!super::readable_parameter(&param(
1217            "bytes",
1218            Some("calldata")
1219        )));
1220        assert!(!super::readable_parameter(&param(
1221            "uint256[]",
1222            Some("calldata")
1223        )));
1224        // A struct by value is not one word of anything the debugger can decode.
1225        assert!(!super::readable_parameter(&param("Config", None)));
1226    }
1227}