1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use soldb_core::{StepSnapshot, TraceStep, TransactionTrace, Word as StackWord};
21use soldb_ethdebug::{decode_value, parse_word, EthdebugInfo, VariableLocation, Word};
22
23use crate::decode::ValueReader;
24use crate::roots::root_calls;
25use crate::stepping::calldata_for_step;
26
27pub mod condition;
28pub mod debug_diff;
29mod decode;
30mod locals;
31mod roots;
32pub mod state;
33pub mod stepping;
34mod types;
35
36pub use condition::{Condition, ConditionContext, Evaluation};
37pub use debug_diff::{
38 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
39 DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
40 DebugTraceSummary,
41};
42pub use roots::check_root_calls;
43pub use soldb_ethdebug::{CodeGenerator, StorageLayout};
44pub use state::{
45 short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
46 StateVariable, StorageTape, StorageWords,
47};
48pub use stepping::{
49 address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
50 Frame, FunctionId, InferredVariable, JumpMarker, LineKey, LocalsStatus, ResolvedFunction,
51 ResolvedLine, SourceListing, StepLocation, StepMap, VariableKind, INFERRED_LOCALS_NOTE,
52 INFERRED_LOCALS_WARNING,
53};
54pub use types::{SourceTypes, StructMember};
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct DebugSession {
58 pub trace: TransactionTrace,
59 pub ethdebug: Option<EthdebugInfo>,
60 #[serde(default)]
61 pub source_contents: BTreeMap<u64, String>,
62 #[serde(default)]
63 pub functions: Vec<SourceFunction>,
64}
65
66impl DebugSession {
67 #[must_use]
68 pub fn new(trace: TransactionTrace) -> Self {
69 Self {
70 trace,
71 ethdebug: None,
72 source_contents: BTreeMap::new(),
73 functions: Vec::new(),
74 }
75 }
76
77 #[must_use]
78 pub fn with_ethdebug(
79 trace: TransactionTrace,
80 ethdebug: EthdebugInfo,
81 source_contents: BTreeMap<u64, String>,
82 ) -> Self {
83 let mut session = Self::new(trace);
84 session.attach_ethdebug(ethdebug, source_contents);
85 session
86 }
87
88 pub fn attach_ethdebug(
95 &mut self,
96 ethdebug: EthdebugInfo,
97 source_contents: BTreeMap<u64, String>,
98 ) {
99 self.functions = source_contents
100 .iter()
101 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
102 .collect();
103 self.ethdebug = Some(ethdebug);
104 self.source_contents = source_contents;
105 }
106
107 #[must_use]
108 pub fn step(&self, step_index: usize) -> Option<DebugStep> {
109 let step = self.trace.steps.get(step_index)?;
110 Some(DebugStep {
111 index: step_index,
112 pc: step.pc,
113 op: step.op.to_string(),
114 gas: step.gas,
115 gas_cost: step.gas_cost,
116 depth: step.depth,
117 source: self.source_span(step.pc),
118 function: self.function_at_pc(step.pc).cloned(),
119 snapshot: step.normalized_snapshot(),
120 variables: self.variables_at_step(step_index),
121 })
122 }
123
124 #[must_use]
125 pub fn steps(&self) -> Vec<DebugStep> {
126 (0..self.trace.steps.len())
127 .filter_map(|step_index| self.step(step_index))
128 .collect()
129 }
130
131 #[must_use]
132 pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
133 let ethdebug = self.ethdebug.as_ref()?;
134 let instruction = ethdebug.instruction_at_pc(pc)?;
135 let location = instruction.source_location()?;
136 let path = ethdebug.sources.get(&location.source_id)?.clone();
137 let position = self
138 .source_contents
139 .get(&location.source_id)
140 .map(|source| line_column_for_offset(source, location.offset as usize));
141 Some(SourceSpan {
142 source_id: location.source_id,
143 path,
144 offset: location.offset,
145 length: location.length,
146 line: position.map_or(0, |position| position.line),
147 column: position.map_or(0, |position| position.column),
148 })
149 }
150
151 #[must_use]
152 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
153 let location = self
154 .ethdebug
155 .as_ref()?
156 .instruction_at_pc(pc)?
157 .source_location()?;
158 self.functions
159 .iter()
160 .filter(|function| {
161 function.source_id == location.source_id
162 && function.declaration_start <= location.offset
163 && location.offset <= function.body_end
164 })
165 .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
166 }
167
168 #[must_use]
169 pub fn variables_at_step(&self, step_index: usize) -> Vec<DebugVariable> {
170 let Some(ethdebug) = &self.ethdebug else {
171 return Vec::new();
172 };
173 variables_for_step(&self.trace, ethdebug, step_index)
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct DebugStep {
179 pub index: usize,
180 pub pc: u64,
181 pub op: String,
182 pub gas: u64,
183 pub gas_cost: u64,
184 pub depth: u64,
185 pub source: Option<SourceSpan>,
186 pub function: Option<SourceFunction>,
187 pub snapshot: StepSnapshot,
188 pub variables: Vec<DebugVariable>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct SourceSpan {
193 pub source_id: u64,
194 pub path: String,
195 pub offset: u64,
196 pub length: u64,
197 pub line: u64,
198 pub column: u64,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct SourceFunction {
203 pub source_id: u64,
204 pub name: String,
207 pub params: Vec<SourceParam>,
208 pub declaration_start: u64,
209 #[serde(default)]
211 pub declaration_line: u64,
212 #[serde(default)]
214 pub body_start: u64,
215 pub body_end: u64,
216 #[serde(default)]
218 pub returns: Vec<SourceLocal>,
219 #[serde(default)]
221 pub locals: Vec<SourceLocal>,
222 #[serde(default)]
225 pub has_modifiers: bool,
226 #[serde(default)]
228 pub modifiers: Vec<String>,
229 #[serde(default)]
231 pub placeholder: Option<u64>,
232}
233
234#[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 #[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 #[must_use]
255 pub const fn encloses(self, other: Self) -> bool {
256 self.start <= other.start && other.end <= self.end
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct SourceLocal {
267 pub name: String,
268 pub ty: String,
270 #[serde(default)]
272 pub location: Option<String>,
273 pub declaration: ByteRange,
274 pub statement: ByteRange,
276 pub position: usize,
278 pub scope: ByteRange,
279}
280
281impl SourceLocal {
282 #[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 pub ty: String,
297 #[serde(default)]
300 pub location: Option<String>,
301}
302
303#[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 let roots = root_calls(trace);
361 let calldata = calldata_for_step(trace, &roots, step_index);
362 variables
363 .into_iter()
364 .map(|variable| decode_variable(step, calldata, variable))
365 .collect()
366}
367
368fn decode_variable(step: &TraceStep, calldata: &str, variable: &VariableLocation) -> DebugVariable {
369 let raw = raw_value_for_location(step, calldata, variable);
370 let value = raw.map_or_else(
371 || DebugValue {
372 display: "<unavailable>".to_owned(),
373 raw: None,
374 status: DebugValueStatus::Unavailable,
375 },
376 |raw| decode_debug_value(&raw, &variable.ty),
377 );
378 DebugVariable {
379 name: variable.name.clone(),
380 ty: variable.ty.clone(),
381 location: DebugLocation {
382 kind: variable.location_type.clone(),
383 offset: variable.offset,
384 },
385 value,
386 }
387}
388
389fn raw_value_for_location(
390 step: &TraceStep,
391 calldata: &str,
392 variable: &VariableLocation,
393) -> Option<String> {
394 let snapshot = step.snapshot_ref();
395 match variable.location_type.as_str() {
396 "stack" => snapshot
397 .stack
398 .get(variable.offset as usize)
399 .map(|value| normalize_hex(value)),
400 "memory" => word_from_hex_bytes(snapshot.memory?, variable.offset as usize),
401 "calldata" => word_from_hex_bytes(calldata, variable.offset as usize),
402 "storage" => storage_value(snapshot.storage, variable.offset),
403 _ => None,
404 }
405}
406
407pub(crate) fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
408 let ty = ty.trim();
409 let normalized = normalize_hex(raw);
410 let word = normalized.trim_start_matches("0x");
411 let decoded = decode_static_word(word, ty);
412 match decoded {
413 Some(display) => DebugValue {
414 display,
415 raw: Some(normalized),
416 status: DebugValueStatus::Decoded,
417 },
418 None => DebugValue {
419 display: normalized.clone(),
420 raw: Some(normalized),
421 status: DebugValueStatus::Raw,
422 },
423 }
424}
425
426pub(crate) fn decode_static_word(word: &str, ty: &str) -> Option<String> {
427 if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
428 return None;
429 }
430 if ty.starts_with("uint") {
431 return Some(format_uint_word(word));
432 }
433 if ty.starts_with("int") {
434 return Some(format_int_word(word));
435 }
436 if ty == "address" {
437 let padded = left_pad_word(word);
438 return Some(
439 format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
440 );
441 }
442 if ty == "bool" {
443 let trimmed = word.trim_start_matches('0');
444 return match trimmed {
445 "" | "0" => Some("false".to_owned()),
446 "1" => Some("true".to_owned()),
447 _ => None,
448 };
449 }
450 if ty == "bytes32" {
451 return Some(normalize_hex(word));
452 }
453 if let Some(size) = fixed_bytes_size(ty) {
454 let padded = right_pad_word(word);
455 return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
456 }
457 None
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub enum ArgumentOrder {
465 FirstOnTop,
468 LastOnTop,
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum ArgumentLayout {
476 Ordered(ArgumentOrder),
478 TopWords,
481}
482
483impl ArgumentOrder {
484 #[must_use]
487 fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
488 match self {
489 Self::FirstOnTop => stack_len - 1 - index,
490 Self::LastOnTop => stack_len - count + index,
491 }
492 }
493}
494
495#[derive(Debug, Clone, Copy, Default)]
498pub struct FrameState<'a> {
499 pub stack: &'a [StackWord],
500 pub memory: Option<&'a str>,
503}
504
505#[must_use]
514pub fn decode_arguments(
515 params: &[SourceParam],
516 state: FrameState<'_>,
517 layout: ArgumentLayout,
518 types: &SourceTypes,
519 scope: Option<&str>,
520) -> Vec<FrameArgument> {
521 let stack = state.stack;
522 if params.is_empty() || stack.len() < params.len() {
523 return Vec::new();
524 }
525 if !params.iter().all(readable_parameter) {
526 return Vec::new();
527 }
528 let order = match layout {
530 ArgumentLayout::Ordered(order) => order,
531 ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
532 ArgumentLayout::TopWords => return Vec::new(),
533 };
534 params
535 .iter()
536 .enumerate()
537 .map(|(index, param)| {
538 let word = &stack[order.word_index(index, params.len(), stack.len())];
539 FrameArgument {
540 name: param.name.clone(),
541 ty: param.ty.clone(),
542 value: argument_value(param, word, state.memory, types, scope),
543 }
544 })
545 .collect()
546}
547
548fn argument_value(
549 param: &SourceParam,
550 word: &str,
551 memory: Option<&str>,
552 types: &SourceTypes,
553 scope: Option<&str>,
554) -> DebugValue {
555 let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
556 return DebugValue {
557 display: "<unreadable stack word>".to_owned(),
558 raw: Some(word.to_owned()),
559 status: DebugValueStatus::Unavailable,
560 };
561 };
562 let reader = ValueReader {
563 memory,
564 calldata: "",
565 storage: None,
566 layout: None,
567 types,
568 scope,
569 };
570 let raw = Some(short_hex(&parsed));
571 match param.location.as_deref() {
572 Some("memory") => reader.variable(&format!("{} memory", param.ty), &[word]),
573 Some(location) => DebugValue {
574 display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
575 raw,
576 status: DebugValueStatus::Raw,
577 },
578 None => DebugValue {
579 display: decode_value(value_bytes(&parsed, ¶m.ty), ¶m.ty),
580 raw,
581 status: DebugValueStatus::Decoded,
582 },
583 }
584}
585
586pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
588 match param.location.as_deref() {
589 Some("calldata") => !is_dynamic_type(¶m.ty),
591 Some(_) => true,
592 None => is_value_type(¶m.ty),
593 }
594}
595
596fn is_dynamic_type(ty: &str) -> bool {
598 ty == "string" || ty == "bytes" || ty.ends_with("[]")
599}
600
601pub(crate) fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
604 let inner = ty.strip_suffix(']')?;
605 let open = inner.rfind('[')?;
606 let element = inner[..open].trim();
607 let length = &inner[open + 1..];
608 if length.is_empty() {
609 return Some((element, None));
610 }
611 Some((element, Some(length.parse::<usize>().ok()?)))
612}
613
614pub(crate) fn memory_word(memory: &str, offset: usize) -> Option<Word> {
616 let bytes = memory_bytes(memory, offset, 32)?;
617 let mut word = [0_u8; 32];
618 word.copy_from_slice(&bytes);
619 Some(word)
620}
621
622pub(crate) fn memory_bytes(memory: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
624 let start = offset.checked_mul(2)?;
625 let end = start.checked_add(length.checked_mul(2)?)?;
626 let digits = memory.get(start..end)?;
627 if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
628 return None;
629 }
630 Some(
631 digits
632 .as_bytes()
633 .chunks(2)
634 .map(|pair| {
635 u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
636 })
637 .collect(),
638 )
639}
640
641pub(crate) fn word_as_usize(word: &Word) -> Option<usize> {
643 if word[..24].iter().any(|byte| *byte != 0) {
644 return None;
645 }
646 usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
647}
648
649pub(crate) fn hex_of(bytes: &[u8]) -> String {
650 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
651}
652
653pub(crate) fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
657 if let Some(bits) = integer_bits(ty, "int") {
658 return &word[32 - bits / 8..];
659 }
660 if let Some(size) = fixed_bytes_size(ty) {
661 return &word[..size];
662 }
663 word
664}
665
666#[must_use]
668pub fn is_value_type(ty: &str) -> bool {
669 matches!(ty, "bool" | "address" | "address payable")
670 || fixed_bytes_size(ty).is_some()
671 || integer_bits(ty, "uint").is_some()
672 || integer_bits(ty, "int").is_some()
673}
674
675fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
677 let rest = ty.strip_prefix(prefix)?;
678 if rest.is_empty() {
680 return Some(256);
681 }
682 let bits = rest.parse::<usize>().ok()?;
683 (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
684}
685
686fn fixed_bytes_size(ty: &str) -> Option<usize> {
687 let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
688 (1..=32).contains(&size).then_some(size)
689}
690
691fn format_uint_word(word: &str) -> String {
692 let trimmed = word.trim_start_matches('0');
693 if trimmed.is_empty() {
694 return "0".to_owned();
695 }
696 if trimmed.len() <= 32 {
697 return u128::from_str_radix(trimmed, 16)
698 .map(|value| value.to_string())
699 .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
700 }
701 format!("0x{}", trimmed.to_ascii_lowercase())
702}
703
704fn format_int_word(word: &str) -> String {
706 let padded = left_pad_word(word);
707 let Ok(bytes) = (0..32)
708 .map(|index| u8::from_str_radix(&padded[index * 2..index * 2 + 2], 16))
709 .collect::<Result<Vec<_>, _>>()
710 else {
711 return format!("0x{}", word.to_ascii_lowercase());
712 };
713 if bytes[0] & 0x80 == 0 {
714 return format_uint_word(word);
715 }
716 let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
718 for byte in magnitude.iter_mut().rev() {
719 let (sum, carry) = byte.overflowing_add(1);
720 *byte = sum;
721 if !carry {
722 break;
723 }
724 }
725 let hex = magnitude
726 .iter()
727 .map(|byte| format!("{byte:02x}"))
728 .collect::<String>();
729 let trimmed = hex.trim_start_matches('0');
730 if trimmed.len() <= 32 {
731 if let Ok(value) = u128::from_str_radix(trimmed, 16) {
732 return format!("-{value}");
733 }
734 }
735 format!("-0x{trimmed}")
736}
737
738fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
739 storage
740 .get(&format!("0x{slot:x}"))
741 .or_else(|| storage.get(&format!("{slot:x}")))
742 .or_else(|| storage.get(&format!("0x{slot:064x}")))
743 .or_else(|| storage.get(&format!("{slot:064x}")))
744 .map(|value| normalize_hex(value))
745}
746
747fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
748 let hex = input.trim_start_matches("0x");
749 let start = byte_offset.checked_mul(2)?;
750 let end = start.checked_add(64)?;
751 let word = hex.get(start..end)?;
752 word.bytes()
753 .all(|byte| byte.is_ascii_hexdigit())
754 .then(|| format!("0x{}", word.to_ascii_lowercase()))
755}
756
757fn normalize_hex(value: &str) -> String {
758 let value = value.trim();
759 let hex = value.strip_prefix("0x").unwrap_or(value);
760 format!("0x{}", hex.to_ascii_lowercase())
761}
762
763fn left_pad_word(word: &str) -> String {
764 let word = word.trim_start_matches("0x").to_ascii_lowercase();
765 if word.len() >= 64 {
766 return word;
767 }
768 format!("{:0>64}", word)
769}
770
771fn right_pad_word(word: &str) -> String {
772 let word = word.trim_start_matches("0x").to_ascii_lowercase();
773 if word.len() >= 64 {
774 return word;
775 }
776 format!("{:0<64}", word)
777}
778
779fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
780 let mut line = 1;
781 let mut column = 1;
782 for byte in source.bytes().take(offset.min(source.len())) {
783 if byte == b'\n' {
784 line += 1;
785 column = 1;
786 } else {
787 column += 1;
788 }
789 }
790 SourcePosition { line, column }
791}
792
793const DECLARATION_KEYWORDS: [&str; 5] =
795 ["function", "modifier", "constructor", "fallback", "receive"];
796
797#[must_use]
801pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
802 let mut functions = Vec::new();
803 for keyword in DECLARATION_KEYWORDS {
804 collect_declarations(source_id, source, keyword, &mut functions);
805 }
806 functions.sort_by_key(|function| function.declaration_start);
807 functions
808}
809
810fn collect_declarations(
811 source_id: u64,
812 source: &str,
813 keyword: &str,
814 functions: &mut Vec<SourceFunction>,
815) {
816 let named = matches!(keyword, "function" | "modifier");
817 let mut cursor = 0;
818 while let Some(keyword_start) = find_solidity_keyword(source, keyword, cursor) {
819 let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
820 let name = if named {
821 let Some((name, name_end)) = parse_identifier(source, index) else {
822 cursor = index;
823 continue;
824 };
825 index = skip_ascii_whitespace(source, name_end);
826 name
827 } else {
828 keyword
829 };
830 if source.as_bytes().get(index) != Some(&b'(') {
831 cursor = index;
832 continue;
833 }
834 let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
835 cursor = index + 1;
836 continue;
837 };
838 let params = parse_source_params(&source[index + 1..params_end]);
839 let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
840 cursor = params_end + 1;
841 continue;
842 };
843 if source[params_end + 1..body_start].contains(';') {
846 cursor = params_end + 1;
847 continue;
848 }
849 let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
850 cursor = body_start + 1;
851 continue;
852 };
853 let (returns, modifiers) = parse_header(source, params_end + 1, body_start);
854 let modifiers = if keyword == "modifier" {
855 Vec::new()
856 } else {
857 modifiers
858 };
859 let returns = returns
860 .into_iter()
861 .map(|variable| SourceLocal {
862 name: variable.name,
863 ty: variable.ty,
864 location: variable.location,
865 declaration: variable.span,
866 statement: variable.span,
867 position: 0,
868 scope: ByteRange {
869 start: body_start as u64,
870 end: body_end as u64,
871 },
872 })
873 .collect();
874
875 functions.push(SourceFunction {
876 source_id,
877 name: name.to_owned(),
878 params,
879 declaration_start: keyword_start as u64,
880 declaration_line: source[..keyword_start]
881 .bytes()
882 .filter(|byte| *byte == b'\n')
883 .count() as u64
884 + 1,
885 body_start: body_start as u64,
886 body_end: body_end as u64,
887 returns,
888 locals: locals::scan_locals(source, body_start, body_end),
889 has_modifiers: !modifiers.is_empty(),
890 modifiers,
891 placeholder: (keyword == "modifier")
892 .then(|| locals::find_placeholder(source, body_start, body_end))
893 .flatten(),
894 });
895 cursor = body_end + 1;
896 }
897}
898
899const HEADER_KEYWORDS: [&str; 11] = [
902 "public", "private", "internal", "external", "pure", "view", "payable", "virtual", "override",
903 "returns", "constant",
904];
905
906fn parse_header(source: &str, start: usize, end: usize) -> (Vec<locals::Declared>, Vec<String>) {
913 let bytes = source.as_bytes();
914 let mut returns = Vec::new();
915 let mut modifiers = Vec::new();
916 let mut index = start;
917 while index < end {
918 index = locals::skip_trivia(source, index);
919 if index >= end {
920 break;
921 }
922 let Some((word, word_end)) = parse_identifier(source, index) else {
923 index += 1;
924 continue;
925 };
926 let after = locals::skip_trivia(source, word_end);
927 let arguments = (bytes.get(after) == Some(&b'('))
928 .then(|| find_matching_delimiter(source, after, b'(', b')'))
929 .flatten()
930 .filter(|close| *close < end);
931 if word == "returns" {
932 if let Some(close) = arguments {
933 returns = parse_return_list(source, after, close);
934 }
935 } else if !HEADER_KEYWORDS.contains(&word) {
936 modifiers.push(word.to_owned());
937 }
938 index = arguments.map_or(word_end, |close| close + 1);
939 }
940 (returns, modifiers)
941}
942
943fn parse_return_list(source: &str, open: usize, close: usize) -> Vec<locals::Declared> {
945 let mut returns = Vec::new();
946 let mut start = open + 1;
947 let mut depth = 0_i32;
948 for index in open + 1..=close {
949 match source.as_bytes()[index] {
950 b'(' | b'[' => depth += 1,
951 b')' | b']' if depth > 0 => depth -= 1,
952 b',' | b')' => {
953 let component = source[start..index].trim();
954 if !component.is_empty() {
955 let component_start = start
956 + (source[start..index].len() - source[start..index].trim_start().len());
957 let component_start = locals::skip_trivia(source, component_start);
958 let component_end = component_start + component.len();
959 if let Some(declared) =
960 locals::parse_return(source, component_start, component_end, returns.len())
961 {
962 returns.push(declared);
963 }
964 }
965 start = index + 1;
966 }
967 _ => {}
968 }
969 }
970 returns
971}
972
973const PARAM_KEYWORDS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
975
976fn parse_source_params(params: &str) -> Vec<SourceParam> {
977 let params = strip_param_comments(params);
978 split_top_level_commas(¶ms)
979 .into_iter()
980 .enumerate()
981 .filter_map(|(index, param)| {
982 let param = param.trim();
983 if param.is_empty() {
984 return None;
985 }
986 let mut tokens = param.split_whitespace().collect::<Vec<_>>();
987 let trailing = tokens.last().copied();
993 let name = match trailing {
994 Some(token)
995 if tokens.len() > 1
996 && is_identifier(token)
997 && !PARAM_KEYWORDS.contains(&token) =>
998 {
999 tokens.pop();
1000 token.to_owned()
1001 }
1002 _ => format!("arg{index}"),
1003 };
1004 let location = tokens
1005 .iter()
1006 .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
1007 .map(|token| (*token).to_owned());
1008 let ty = tokens
1009 .into_iter()
1010 .filter(|token| !PARAM_KEYWORDS.contains(token))
1011 .collect::<Vec<_>>()
1012 .join(" ");
1013 (!ty.is_empty()).then_some(SourceParam { name, ty, location })
1014 })
1015 .collect()
1016}
1017
1018fn strip_param_comments(params: &str) -> String {
1036 let mut output = String::with_capacity(params.len());
1037 let mut chars = params.chars().peekable();
1038 while let Some(character) = chars.next() {
1039 if character == '/' {
1040 match chars.peek() {
1041 Some('/') => {
1042 for next in chars.by_ref() {
1045 if next == '\n' {
1046 output.push('\n');
1047 break;
1048 }
1049 }
1050 continue;
1051 }
1052 Some('*') => {
1053 chars.next();
1054 let mut previous = None;
1055 for next in chars.by_ref() {
1056 if previous == Some('*') && next == '/' {
1057 break;
1058 }
1059 previous = Some(next);
1060 }
1061 output.push(' ');
1064 continue;
1065 }
1066 _ => {}
1067 }
1068 }
1069 output.push(character);
1070 }
1071 output
1072}
1073
1074fn split_top_level_commas(input: &str) -> Vec<&str> {
1075 let mut parts = Vec::new();
1076 let mut start = 0;
1077 let mut depth = 0_i32;
1078 for (index, byte) in input.bytes().enumerate() {
1079 match byte {
1080 b'(' | b'[' => depth += 1,
1081 b')' | b']' => depth -= 1,
1082 b',' if depth == 0 => {
1083 parts.push(&input[start..index]);
1084 start = index + 1;
1085 }
1086 _ => {}
1087 }
1088 }
1089 parts.push(&input[start..]);
1090 parts
1091}
1092
1093pub(crate) fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
1094 let mut cursor = start;
1095 while let Some(relative) = source[cursor..].find(keyword) {
1096 let absolute = cursor + relative;
1097 let before = absolute
1098 .checked_sub(1)
1099 .and_then(|index| source.as_bytes().get(index))
1100 .copied();
1101 let after = source.as_bytes().get(absolute + keyword.len()).copied();
1102 if before.is_none_or(|byte| !is_identifier_byte(byte))
1103 && after.is_none_or(|byte| !is_identifier_byte(byte))
1104 {
1105 return Some(absolute);
1106 }
1107 cursor = absolute + keyword.len();
1108 }
1109 None
1110}
1111
1112pub(crate) fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
1113 let bytes = source.as_bytes();
1114 let first = *bytes.get(start)?;
1115 if !is_identifier_start_byte(first) {
1116 return None;
1117 }
1118 let mut end = start + 1;
1119 while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
1120 end += 1;
1121 }
1122 Some((&source[start..end], end))
1123}
1124
1125pub(crate) fn is_identifier(input: &str) -> bool {
1126 let mut bytes = input.bytes();
1127 let Some(first) = bytes.next() else {
1128 return false;
1129 };
1130 is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
1131}
1132
1133fn is_identifier_start_byte(byte: u8) -> bool {
1134 byte == b'_' || byte.is_ascii_alphabetic()
1135}
1136
1137pub(crate) fn is_identifier_byte(byte: u8) -> bool {
1138 is_identifier_start_byte(byte) || byte.is_ascii_digit()
1139}
1140
1141pub(crate) fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
1142 while source
1143 .as_bytes()
1144 .get(index)
1145 .is_some_and(u8::is_ascii_whitespace)
1146 {
1147 index += 1;
1148 }
1149 index
1150}
1151
1152fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
1153 source
1154 .as_bytes()
1155 .iter()
1156 .enumerate()
1157 .skip(start)
1158 .find_map(|(index, byte)| (*byte == needle).then_some(index))
1159}
1160
1161pub(crate) fn find_matching_delimiter(
1162 source: &str,
1163 open_index: usize,
1164 open: u8,
1165 close: u8,
1166) -> Option<usize> {
1167 let mut depth = 0_i32;
1168 for (index, byte) in source.bytes().enumerate().skip(open_index) {
1169 if byte == open {
1170 depth += 1;
1171 } else if byte == close {
1172 depth -= 1;
1173 if depth == 0 {
1174 return Some(index);
1175 }
1176 }
1177 }
1178 None
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use std::collections::BTreeMap;
1184
1185 use serde_json::json;
1186 use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
1187 use soldb_ethdebug::{EthdebugInfo, Instruction, VariableLocation};
1188
1189 use super::{parse_source_functions, DebugSession, DebugValueStatus};
1190
1191 #[test]
1192 fn builds_source_steps_and_decodes_active_variables() {
1193 let source =
1194 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1195 let offset = source.find("value = x").expect("source offset") as u64;
1196 let trace = sample_trace();
1197 let mut sources = BTreeMap::new();
1198 sources.insert(0, "Counter.sol".to_owned());
1199 let mut source_contents = BTreeMap::new();
1200 source_contents.insert(0, source.to_owned());
1201 let mut variable_locations = BTreeMap::new();
1202 variable_locations.insert(
1203 0,
1204 vec![
1205 VariableLocation {
1206 name: "x".to_owned(),
1207 ty: "uint256".to_owned(),
1208 location_type: "stack".to_owned(),
1209 offset: 0,
1210 pc_range: (0, 1),
1211 },
1212 VariableLocation {
1213 name: "sender".to_owned(),
1214 ty: "address".to_owned(),
1215 location_type: "calldata".to_owned(),
1216 offset: 4,
1217 pc_range: (0, 1),
1218 },
1219 ],
1220 );
1221 let info = EthdebugInfo {
1222 compilation: json!({}),
1223 contract_name: "Counter".to_owned(),
1224 environment: "runtime".to_owned(),
1225 instructions: vec![Instruction {
1226 offset: 0,
1227 operation: json!({"mnemonic": "PUSH1"}),
1228 context: Some(json!({
1229 "code": {
1230 "source": {"id": 0},
1231 "range": {"offset": offset, "length": 9}
1232 }
1233 })),
1234 }],
1235 sources,
1236 variable_locations,
1237 };
1238
1239 let session = DebugSession::with_ethdebug(trace, info, source_contents);
1240 let step = session.step(0).expect("debug step");
1241 assert_eq!(step.source.as_ref().expect("source").line, 3);
1242 assert_eq!(step.function.as_ref().expect("function").name, "set");
1243 assert_eq!(
1244 step.snapshot
1245 .stack
1246 .iter()
1247 .map(|word| &**word)
1248 .collect::<Vec<_>>(),
1249 ["0x2a"]
1250 );
1251 assert_eq!(step.variables[0].name, "x");
1252 assert_eq!(step.variables[0].value.display, "42");
1253 assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1254 assert_eq!(
1255 step.variables[1].value.display,
1256 "0x1111111111111111111111111111111111111111"
1257 );
1258 }
1259
1260 #[test]
1261 fn calldata_variables_read_the_input_of_their_top_level_call() {
1262 let root = |id: usize, entry: usize, input: String| ExecutionCall {
1263 id,
1264 parent_id: None,
1265 depth: 1,
1266 entry_step: Some(entry),
1267 exit_step: Some(entry + 1),
1268 call_type: "CALL".to_owned(),
1269 from: "0x1".to_owned(),
1270 to: "0x2".to_owned(),
1271 bytecode_address: "0x2".to_owned(),
1272 value: "0x0".to_owned(),
1273 input,
1274 gas_limit: 0,
1275 gas_used: None,
1276 output: None,
1277 success: Some(true),
1278 error: None,
1279 };
1280 let mut trace = sample_trace();
1281 trace.steps.push(trace.steps[0].clone());
1282 let second_input = format!(
1283 "0x12345678{:0>64}",
1284 "2222222222222222222222222222222222222222"
1285 );
1286 trace.artifacts.calls = vec![
1287 root(0, 0, trace.input_data.clone()),
1288 root(1, 1, second_input),
1289 ];
1290 let info = EthdebugInfo {
1291 compilation: json!({}),
1292 contract_name: "Counter".to_owned(),
1293 environment: "runtime".to_owned(),
1294 instructions: Vec::new(),
1295 sources: BTreeMap::new(),
1296 variable_locations: BTreeMap::from([(
1297 0,
1298 vec![VariableLocation {
1299 name: "sender".to_owned(),
1300 ty: "address".to_owned(),
1301 location_type: "calldata".to_owned(),
1302 offset: 4,
1303 pc_range: (0, 1),
1304 }],
1305 )]),
1306 };
1307 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1308 let sender = |step: usize| {
1309 session.step(step).expect("debug step").variables[0]
1310 .value
1311 .display
1312 .clone()
1313 };
1314 assert_eq!(sender(0), "0x1111111111111111111111111111111111111111");
1315 assert_eq!(sender(1), "0x2222222222222222222222222222222222222222");
1316 }
1317
1318 #[test]
1319 fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1320 let trace = sample_trace();
1321 let mut variable_locations = BTreeMap::new();
1322 variable_locations.insert(
1323 0,
1324 vec![
1325 VariableLocation {
1326 name: "name".to_owned(),
1327 ty: "string".to_owned(),
1328 location_type: "stack".to_owned(),
1329 offset: 0,
1330 pc_range: (0, 0),
1331 },
1332 VariableLocation {
1333 name: "missing".to_owned(),
1334 ty: "uint256".to_owned(),
1335 location_type: "stack".to_owned(),
1336 offset: 99,
1337 pc_range: (0, 0),
1338 },
1339 ],
1340 );
1341 let info = EthdebugInfo {
1342 compilation: json!({}),
1343 contract_name: "Counter".to_owned(),
1344 environment: "runtime".to_owned(),
1345 instructions: Vec::new(),
1346 sources: BTreeMap::new(),
1347 variable_locations,
1348 };
1349
1350 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1351 let step = session.step(0).expect("debug step");
1352 assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1353 assert_eq!(step.variables[0].value.display, "0x2a");
1354 assert_eq!(
1355 step.variables[1].value.status,
1356 DebugValueStatus::Unavailable
1357 );
1358 }
1359
1360 #[test]
1361 fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1362 let source =
1363 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1364 let offset = source.find("value = x").expect("source offset") as u64;
1365 let info = EthdebugInfo {
1366 compilation: json!({}),
1367 contract_name: "Counter".to_owned(),
1368 environment: "runtime".to_owned(),
1369 instructions: vec![Instruction {
1370 offset: 0,
1371 operation: json!({"mnemonic": "PUSH1"}),
1372 context: Some(json!({
1373 "code": {"source": {"id": 0}, "range": {"offset": offset, "length": 9}}
1374 })),
1375 }],
1376 sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1377 variable_locations: BTreeMap::new(),
1378 };
1379 let source_contents = BTreeMap::from([(0, source.to_owned())]);
1380
1381 let constructed =
1382 DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1383 let mut attached = DebugSession::new(sample_trace());
1384 assert!(attached.ethdebug.is_none());
1385 assert!(attached.functions.is_empty());
1386 attached.attach_ethdebug(info, source_contents);
1387
1388 assert_eq!(attached, constructed);
1389 assert_eq!(attached.functions.len(), 1);
1390 assert_eq!(attached.functions[0].name, "set");
1391 assert_eq!(
1392 attached
1393 .step(0)
1394 .and_then(|step| step.source)
1395 .map(|span| span.line),
1396 Some(3)
1397 );
1398
1399 let other = EthdebugInfo {
1401 compilation: json!({}),
1402 contract_name: "Other".to_owned(),
1403 environment: "runtime".to_owned(),
1404 instructions: Vec::new(),
1405 sources: BTreeMap::new(),
1406 variable_locations: BTreeMap::new(),
1407 };
1408 attached.attach_ethdebug(other, BTreeMap::new());
1409 assert_eq!(
1410 attached
1411 .ethdebug
1412 .as_ref()
1413 .map(|info| info.contract_name.as_str()),
1414 Some("Other")
1415 );
1416 assert!(attached.functions.is_empty());
1417 assert!(attached.source_contents.is_empty());
1418 assert_eq!(attached.step(0).and_then(|step| step.source), None);
1419 assert_eq!(attached.trace, sample_trace());
1420 }
1421
1422 #[test]
1423 fn parses_source_functions_with_tuple_and_array_params() {
1424 let functions = parse_source_functions(
1425 7,
1426 "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1427 );
1428 assert_eq!(functions[0].source_id, 7);
1429 assert_eq!(functions[0].name, "submit");
1430 assert_eq!(functions[0].params[0].name, "p");
1431 assert_eq!(functions[0].params[0].ty, "Person");
1432 assert_eq!(functions[0].params[1].name, "xs");
1433 assert_eq!(functions[0].params[1].ty, "uint256[2]");
1434 }
1435
1436 #[test]
1437 fn unnamed_params_documented_by_comments_do_not_become_parameters() {
1438 let functions = parse_source_functions(
1442 0,
1443 "contract C {\n function burnFrom_withCaller(\n address caller,\n address, //from\n uint256, //amount\n bytes32 //h\n ) external view returns (bool) {}\n}",
1444 );
1445 let params = &functions[0].params;
1446 assert_eq!(functions[0].name, "burnFrom_withCaller");
1447 assert_eq!(params.len(), 4);
1448 assert_eq!(
1449 (params[0].ty.as_str(), params[0].name.as_str()),
1450 ("address", "caller")
1451 );
1452 assert_eq!(
1453 (params[1].ty.as_str(), params[1].name.as_str()),
1454 ("address", "arg1")
1455 );
1456 assert_eq!(
1457 (params[2].ty.as_str(), params[2].name.as_str()),
1458 ("uint256", "arg2")
1459 );
1460 assert_eq!(
1461 (params[3].ty.as_str(), params[3].name.as_str()),
1462 ("bytes32", "arg3")
1463 );
1464 }
1465
1466 #[test]
1467 fn unnamed_params_take_positional_names_not_their_type() {
1468 let functions = parse_source_functions(
1469 0,
1470 "contract C { function burn(address from, uint256 amount, bytes32, bytes memory signature) public {} }",
1471 );
1472 let params = &functions[0].params;
1473 assert_eq!(
1474 (params[1].ty.as_str(), params[1].name.as_str()),
1475 ("uint256", "amount")
1476 );
1477 assert_eq!(
1479 (params[2].ty.as_str(), params[2].name.as_str()),
1480 ("bytes32", "arg2")
1481 );
1482 assert_eq!(
1483 (params[3].ty.as_str(), params[3].name.as_str()),
1484 ("bytes", "signature")
1485 );
1486 assert_eq!(params[3].location.as_deref(), Some("memory"));
1487 }
1488
1489 #[test]
1490 fn a_trailing_declaration_keyword_does_not_name_a_param() {
1491 let functions = parse_source_functions(
1492 0,
1493 "contract C { function f(string memory, address payable, bytes calldata data) public {} }",
1494 );
1495 let params = &functions[0].params;
1496 assert_eq!(
1497 (params[0].ty.as_str(), params[0].name.as_str()),
1498 ("string", "arg0")
1499 );
1500 assert_eq!(params[0].location.as_deref(), Some("memory"));
1501 assert_eq!(
1502 (params[1].ty.as_str(), params[1].name.as_str()),
1503 ("address", "arg1")
1504 );
1505 assert_eq!(
1506 (params[2].ty.as_str(), params[2].name.as_str()),
1507 ("bytes", "data")
1508 );
1509 assert_eq!(params[2].location.as_deref(), Some("calldata"));
1510 }
1511
1512 #[test]
1513 fn block_comments_in_a_param_list_separate_rather_than_join() {
1514 let functions = parse_source_functions(
1515 0,
1516 "contract C { function f(uint256 /* wei */ amount, address /*to*/) public {} }",
1517 );
1518 let params = &functions[0].params;
1519 assert_eq!(
1520 (params[0].ty.as_str(), params[0].name.as_str()),
1521 ("uint256", "amount")
1522 );
1523 assert_eq!(
1524 (params[1].ty.as_str(), params[1].name.as_str()),
1525 ("address", "arg1")
1526 );
1527 }
1528
1529 #[test]
1530 fn stripping_comments_leaves_declaration_offsets_alone() {
1531 let source = "contract C {\n function f(address, //from\n uint256 amount\n ) public {}\n}";
1534 let functions = parse_source_functions(0, source);
1535 let start = functions[0].declaration_start as usize;
1536 assert!(source[start..].starts_with("function f("));
1537 assert_eq!(functions[0].declaration_line, 2);
1538 assert_eq!(source.as_bytes()[functions[0].body_end as usize], b'}');
1539 }
1540
1541 fn sample_trace() -> TransactionTrace {
1542 TransactionTrace {
1543 tx_hash: Some("0xabc".to_owned()),
1544 from_addr: "0x1".to_owned(),
1545 to_addr: Some("0x2".to_owned()),
1546 value: "0".to_owned(),
1547 input_data: format!(
1548 "0x12345678{}",
1549 "0000000000000000000000001111111111111111111111111111111111111111"
1550 ),
1551 gas_used: 1,
1552 output: "0x".to_owned(),
1553 success: true,
1554 error: None,
1555 debug_trace_available: true,
1556 contract_address: None,
1557 backend: Some("debug-rpc".to_owned()),
1558 capabilities: Default::default(),
1559 artifacts: Default::default(),
1560 steps: vec![TraceStep {
1561 pc: 0,
1562 op: "PUSH1".into(),
1563 gas: 1,
1564 gas_cost: 0,
1565 depth: 0,
1566 stack: vec!["0x2a".into()],
1567 memory: Some(format!("{}{}", "00".repeat(32), "2a".repeat(32))),
1568 storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1569 error: None,
1570 snapshot: Default::default(),
1571 }],
1572 }
1573 }
1574 fn memory_image(words: &[&str]) -> String {
1577 words
1578 .iter()
1579 .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1580 .collect()
1581 }
1582
1583 #[test]
1584 fn reads_arguments_that_live_in_memory() {
1585 let memory = memory_image(&[
1587 "5",
1588 "68656c6c6f000000000000000000000000000000000000000000000000000000",
1589 "3",
1590 "7",
1591 "8",
1592 "9",
1593 ]);
1594 let types = super::SourceTypes::default();
1595 let reader = super::ValueReader {
1596 memory: Some(&memory),
1597 calldata: "",
1598 storage: None,
1599 layout: None,
1600 types: &types,
1601 scope: None,
1602 };
1603 let read = |offset: usize, ty: &str| reader.read_memory(offset, ty, 0);
1604 assert_eq!(read(0, "string").as_deref(), Some("\"hello\""));
1605 assert_eq!(read(0, "bytes").as_deref(), Some("0x68656c6c6f"));
1606 assert_eq!(read(64, "uint256[]").as_deref(), Some("[7, 8, 9]"));
1607 assert_eq!(read(96, "uint256[2]").as_deref(), Some("[7, 8]"));
1609 assert_eq!(read(4096, "string"), None);
1613 assert_eq!(
1614 read(64, "string[]").as_deref(),
1615 Some("[<string at 0x7>, <string at 0x8>, <string at 0x9>]")
1616 );
1617 }
1618
1619 #[test]
1620 fn a_parameter_is_readable_only_when_its_width_is_known() {
1621 let param = |ty: &str, location: Option<&str>| super::SourceParam {
1622 name: "x".to_owned(),
1623 ty: ty.to_owned(),
1624 location: location.map(str::to_owned),
1625 };
1626 assert!(super::readable_parameter(¶m("uint256", None)));
1627 assert!(super::readable_parameter(¶m("string", Some("memory"))));
1628 assert!(super::readable_parameter(¶m("Config", Some("storage"))));
1629 assert!(super::readable_parameter(¶m(
1631 "uint256[2]",
1632 Some("calldata")
1633 )));
1634 assert!(!super::readable_parameter(¶m(
1636 "bytes",
1637 Some("calldata")
1638 )));
1639 assert!(!super::readable_parameter(¶m(
1640 "uint256[]",
1641 Some("calldata")
1642 )));
1643 assert!(!super::readable_parameter(¶m("Config", None)));
1645 }
1646}