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