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