1use std::borrow::Cow;
13use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16use serde_json::{json, Value};
17use soldb_core::{SoldbError, SoldbResult};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct SourceLocation {
21 pub source_id: u64,
22 pub offset: u64,
23 pub length: u64,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct FunctionIdentity {
28 pub identifier: Option<String>,
29 pub declaration: Option<SourceLocation>,
30 pub activation: Option<String>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum FunctionExit {
36 Return,
37 Revert,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(from = "InstructionJson", into = "InstructionJson")]
49pub struct Instruction {
50 pub offset: u64,
51 operation: Operation,
52 context: Option<Context>,
53}
54
55#[derive(Serialize, Deserialize)]
57struct InstructionJson {
58 offset: u64,
59 operation: Value,
60 #[serde(default)]
61 context: Option<Value>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65enum Operation {
66 Mnemonic(Cow<'static, str>),
68 Json(Value),
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73enum Context {
74 SourceMap(SourceMapContext),
76 Json(Value),
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub struct SourceMapContext {
84 pub code: Option<SourceLocation>,
86 pub jump: SourceMapJump,
88 pub modifier_depth: i64,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
94pub enum SourceMapJump {
95 #[default]
97 Regular,
98 Into,
100 Out,
102}
103
104impl Instruction {
105 #[must_use]
108 pub fn new(offset: u64, mnemonic: &str, context: Option<Value>) -> Self {
109 Self {
110 offset,
111 operation: Operation::Mnemonic(Cow::Owned(mnemonic.to_owned())),
112 context: context.map(Context::from_json),
113 }
114 }
115
116 #[must_use]
119 pub fn from_source_map(
120 offset: u64,
121 mnemonic: impl Into<Cow<'static, str>>,
122 context: SourceMapContext,
123 ) -> Self {
124 Self {
125 offset,
126 operation: Operation::Mnemonic(mnemonic.into()),
127 context: Some(Context::SourceMap(context)),
128 }
129 }
130
131 #[must_use]
132 pub fn mnemonic(&self) -> Option<&str> {
133 match &self.operation {
134 Operation::Mnemonic(mnemonic) => Some(mnemonic),
135 Operation::Json(operation) => operation.get("mnemonic").and_then(Value::as_str),
136 }
137 }
138
139 #[must_use]
140 pub fn arguments(&self) -> Vec<&str> {
141 let Operation::Json(operation) = &self.operation else {
142 return Vec::new();
143 };
144 operation
145 .get("arguments")
146 .and_then(Value::as_array)
147 .map(|arguments| arguments.iter().filter_map(Value::as_str).collect())
148 .unwrap_or_default()
149 }
150
151 #[must_use]
152 pub fn source_location(&self) -> Option<SourceLocation> {
153 match &self.context {
154 Some(Context::SourceMap(context)) => context.code.clone(),
155 _ => {
156 let mut locations = self.source_locations();
157 (locations.len() == 1).then(|| locations.remove(0))
158 }
159 }
160 }
161
162 #[must_use]
164 pub fn source_locations(&self) -> Vec<SourceLocation> {
165 let mut locations = Vec::new();
166 match &self.context {
167 Some(Context::SourceMap(context)) => locations.extend(context.code.clone()),
168 Some(Context::Json(context)) => collect_source_locations(context, &mut locations),
169 None => {}
170 }
171 locations
172 }
173
174 #[must_use]
176 pub fn function_invocations(&self) -> Vec<FunctionIdentity> {
177 let mut functions = Vec::new();
178 match &self.context {
179 Some(Context::SourceMap(context)) if context.jump == SourceMapJump::Into => {
180 functions.push(FunctionIdentity {
181 identifier: None,
182 declaration: None,
183 activation: None,
184 });
185 }
186 Some(Context::Json(context)) => collect_function_invocations(context, &mut functions),
187 _ => {}
188 }
189 functions
190 }
191
192 #[must_use]
194 pub fn function_exit(&self) -> Option<FunctionExit> {
195 match self.context.as_ref()? {
196 Context::SourceMap(context) => {
197 (context.jump == SourceMapJump::Out).then_some(FunctionExit::Return)
198 }
199 Context::Json(context) => {
200 if context.get("return").is_some() {
201 Some(FunctionExit::Return)
202 } else if context.get("revert").is_some() {
203 Some(FunctionExit::Revert)
204 } else {
205 None
206 }
207 }
208 }
209 }
210
211 #[must_use]
213 pub fn modifier_depth(&self) -> Option<i64> {
214 match self.context.as_ref()? {
215 Context::SourceMap(context) => Some(context.modifier_depth),
216 Context::Json(context) => context.get("modifierDepth")?.as_i64(),
217 }
218 }
219}
220
221impl From<InstructionJson> for Instruction {
222 fn from(instruction: InstructionJson) -> Self {
223 Self {
224 offset: instruction.offset,
225 operation: Operation::from_json(instruction.operation),
226 context: instruction.context.map(Context::from_json),
227 }
228 }
229}
230
231impl From<Instruction> for InstructionJson {
232 fn from(instruction: Instruction) -> Self {
233 Self {
234 offset: instruction.offset,
235 operation: match instruction.operation {
236 Operation::Mnemonic(mnemonic) => json!({"mnemonic": mnemonic}),
237 Operation::Json(operation) => operation,
238 },
239 context: instruction.context.map(|context| match context {
240 Context::SourceMap(context) => context.to_json(),
241 Context::Json(context) => context,
242 }),
243 }
244 }
245}
246
247impl Operation {
248 fn from_json(operation: Value) -> Self {
251 if let Value::Object(fields) = &operation {
252 if let (1, Some(Value::String(mnemonic))) = (fields.len(), fields.get("mnemonic")) {
253 return Self::Mnemonic(Cow::Owned(mnemonic.clone()));
254 }
255 }
256 Self::Json(operation)
257 }
258}
259
260impl Context {
261 fn from_json(context: Value) -> Self {
264 SourceMapContext::from_json(&context).map_or(Self::Json(context), Self::SourceMap)
265 }
266}
267
268impl SourceMapContext {
269 fn from_json(context: &Value) -> Option<Self> {
270 let fields = context.as_object()?;
271 let modifier_depth = fields.get("modifierDepth")?.as_i64()?;
272 let mut code = None;
273 let mut jump = SourceMapJump::Regular;
274 for (key, value) in fields {
275 match key.as_str() {
276 "modifierDepth" => {}
277 "code" => code = Some(source_range_of(value)?),
278 "invoke" | "return" if empty_object(value) && jump == SourceMapJump::Regular => {
279 jump = if key == "invoke" {
280 SourceMapJump::Into
281 } else {
282 SourceMapJump::Out
283 };
284 }
285 _ => return None,
286 }
287 }
288 Some(Self {
289 code,
290 jump,
291 modifier_depth,
292 })
293 }
294
295 fn to_json(&self) -> Value {
296 let mut context = serde_json::Map::new();
297 context.insert("modifierDepth".to_owned(), json!(self.modifier_depth));
298 if let Some(code) = &self.code {
299 context.insert(
300 "code".to_owned(),
301 source_range(code.source_id, code.offset, code.length),
302 );
303 }
304 match self.jump {
305 SourceMapJump::Regular => {}
306 SourceMapJump::Into => {
307 context.insert("invoke".to_owned(), json!({}));
308 }
309 SourceMapJump::Out => {
310 context.insert("return".to_owned(), json!({}));
311 }
312 }
313 Value::Object(context)
314 }
315}
316
317fn source_range_of(value: &Value) -> Option<SourceLocation> {
319 let fields = value.as_object()?;
320 let source = fields.get("source")?.as_object()?;
321 let range = fields.get("range")?.as_object()?;
322 if fields.len() != 2 || source.len() != 1 || range.len() != 2 {
323 return None;
324 }
325 Some(SourceLocation {
326 source_id: source.get("id")?.as_u64()?,
327 offset: range.get("offset")?.as_u64()?,
328 length: range.get("length")?.as_u64()?,
329 })
330}
331
332fn empty_object(value: &Value) -> bool {
333 value.as_object().is_some_and(serde_json::Map::is_empty)
334}
335
336fn collect_source_locations(context: &Value, locations: &mut Vec<SourceLocation>) {
337 if let Some(location) = context.get("code").and_then(parse_source_location) {
338 if !locations.contains(&location) {
339 locations.push(location);
340 }
341 }
342 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
343 for alternative in pick {
344 collect_source_locations(alternative, locations);
345 }
346 }
347}
348
349fn collect_function_invocations(context: &Value, functions: &mut Vec<FunctionIdentity>) {
350 if let Some(invoke) = context.get("invoke") {
351 let function = FunctionIdentity {
352 identifier: invoke
353 .get("identifier")
354 .and_then(Value::as_str)
355 .map(str::to_owned),
356 declaration: invoke.get("declaration").and_then(parse_source_location),
357 activation: invoke
358 .get("activation")
359 .and_then(Value::as_str)
360 .map(str::to_owned),
361 };
362 if !functions.contains(&function) {
363 functions.push(function);
364 }
365 }
366 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
367 for alternative in pick {
368 collect_function_invocations(alternative, functions);
369 }
370 }
371}
372
373#[must_use]
376pub fn source_range(source_id: u64, offset: u64, length: u64) -> Value {
377 json!({"source": {"id": source_id}, "range": {"offset": offset, "length": length}})
378}
379
380fn parse_source_location(value: &Value) -> Option<SourceLocation> {
381 let source_id = value.get("source")?.get("id")?.as_u64()?;
382 let range = value.get("range")?;
383 Some(SourceLocation {
384 source_id,
385 offset: range.get("offset")?.as_u64()?,
386 length: range.get("length")?.as_u64()?,
387 })
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct VariableLocation {
392 pub name: String,
393 pub ty: String,
394 pub location_type: String,
395 pub offset: u64,
396 pub pc_range: (u64, u64),
397}
398
399impl VariableLocation {
400 #[must_use]
401 pub fn is_active_at_pc(&self, pc: u64) -> bool {
402 self.pc_range.0 <= pc && pc <= self.pc_range.1
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407pub struct EthdebugInfo {
408 pub compilation: Value,
409 pub contract_name: String,
410 pub environment: String,
411 pub instructions: Vec<Instruction>,
412 pub sources: BTreeMap<u64, String>,
413 #[serde(default)]
414 pub variable_locations: BTreeMap<u64, Vec<VariableLocation>>,
415}
416
417impl EthdebugInfo {
418 pub fn from_artifacts(
430 contract_name: &str,
431 environment: &str,
432 metadata: &Value,
433 program: &Value,
434 ) -> SoldbResult<Self> {
435 let instructions = program
436 .get("instructions")
437 .cloned()
438 .map(serde_json::from_value::<Vec<Instruction>>)
439 .transpose()
440 .map_err(|error| {
441 SoldbError::Message(format!(
442 "invalid `instructions` in ETHDebug program artifact: {error}"
443 ))
444 })?
445 .unwrap_or_default();
446 let compilation = metadata
447 .get("compilation")
448 .cloned()
449 .unwrap_or_else(|| metadata.clone());
450 let sources = parse_compilation_sources(&compilation);
451 let variable_locations = parse_variable_locations(program)?;
452 Ok(Self {
453 compilation,
454 contract_name: contract_name.to_owned(),
455 environment: environment.to_owned(),
456 instructions,
457 sources,
458 variable_locations,
459 })
460 }
461
462 #[must_use]
463 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
464 self.instructions
465 .iter()
466 .find(|instruction| instruction.offset == pc)
467 }
468
469 #[must_use]
470 pub fn source_info(&self, pc: u64) -> Option<(&str, u64, u64)> {
471 let instruction = self.instruction_at_pc(pc)?;
472 let source_location = instruction.source_location()?;
473 let source_path = self.sources.get(&source_location.source_id)?;
474 Some((source_path, source_location.offset, source_location.length))
475 }
476
477 #[must_use]
483 pub fn has_variable_locations(&self) -> bool {
484 !self.variable_locations.is_empty()
485 }
486
487 pub fn variables_at_pc(&self, pc: u64) -> Vec<&VariableLocation> {
488 if let Some(exact) = self.variable_locations.get(&pc) {
489 return exact.iter().collect();
490 }
491
492 self.variable_locations
493 .values()
494 .flat_map(|variables| variables.iter())
495 .filter(|variable| variable.is_active_at_pc(pc))
496 .collect()
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct EthdebugSpec {
502 pub address: Option<String>,
503 pub name: Option<String>,
504 pub path: String,
505}
506
507pub fn parse_ethdebug_spec(input: &str) -> EthdebugSpec {
508 if let Ok(spec) = parse_single_contract_spec(input) {
509 return spec;
510 }
511
512 parse_multi_contract_spec(input).unwrap_or_else(|_| EthdebugSpec {
513 address: None,
514 name: None,
515 path: input.to_owned(),
516 })
517}
518
519pub fn parse_single_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
520 if !input.starts_with("0x") {
521 return Err(SoldbError::Message(format!(
522 "Must use format 'address:name:path' (got: {input})"
523 )));
524 }
525
526 let parts = input.splitn(3, ':').collect::<Vec<_>>();
527 if parts.len() != 3 || parts[1].is_empty() || parts[2].is_empty() {
528 return Err(SoldbError::Message(format!(
529 "Must use format 'address:name:path' (got: {input})"
530 )));
531 }
532
533 Ok(EthdebugSpec {
534 address: Some(parts[0].to_owned()),
535 name: Some(parts[1].to_owned()),
536 path: parts[2].to_owned(),
537 })
538}
539
540pub fn parse_multi_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
541 if input.is_empty() {
542 return Err(SoldbError::Message("Path cannot be empty".to_owned()));
543 }
544
545 if input.starts_with("0x") && input.contains(':') {
546 let parts = input.splitn(2, ':').collect::<Vec<_>>();
547 if parts.len() != 2 || parts[1].is_empty() {
548 return Err(SoldbError::Message(format!(
549 "Must use format 'address:path' (got: {input})"
550 )));
551 }
552
553 return Ok(EthdebugSpec {
554 address: Some(parts[0].to_owned()),
555 name: None,
556 path: parts[1].to_owned(),
557 });
558 }
559
560 Ok(EthdebugSpec {
561 address: None,
562 name: None,
563 path: input.to_owned(),
564 })
565}
566
567#[must_use]
572pub fn parse_compilation_sources(compilation: &Value) -> BTreeMap<u64, String> {
573 compilation
574 .get("sources")
575 .and_then(Value::as_array)
576 .into_iter()
577 .flatten()
578 .filter_map(|source| {
579 Some((
580 source.get("id")?.as_u64()?,
581 source.get("path")?.as_str()?.to_owned(),
582 ))
583 })
584 .collect()
585}
586
587#[must_use]
593pub fn read_compilation_source(compilation: &Value, source_id: u64) -> Option<String> {
594 compilation
595 .get("sources")
596 .and_then(Value::as_array)?
597 .iter()
598 .find(|source| source.get("id").and_then(Value::as_u64) == Some(source_id))
599 .and_then(|source| {
600 source
601 .get("contents")
602 .or_else(|| source.get("content"))
603 .and_then(Value::as_str)
604 })
605 .map(str::to_owned)
606}
607
608pub fn parse_variable_locations(
609 contract_data: &Value,
610) -> SoldbResult<BTreeMap<u64, Vec<VariableLocation>>> {
611 let mut variable_locations = BTreeMap::<u64, Vec<VariableLocation>>::new();
612
613 if let Some(instructions) = contract_data
614 .get("instructions")
615 .and_then(serde_json::Value::as_array)
616 {
617 for instruction in instructions {
618 let pc = instruction
619 .get("offset")
620 .and_then(serde_json::Value::as_u64)
621 .ok_or_else(|| SoldbError::Message("Instruction missing offset".to_owned()))?;
622
623 let Some(variables) = instruction
624 .get("context")
625 .and_then(|context| context.get("variables"))
626 .and_then(serde_json::Value::as_array)
627 else {
628 continue;
629 };
630
631 for variable in variables {
632 variable_locations
633 .entry(pc)
634 .or_default()
635 .push(parse_context_variable(variable, pc));
636 }
637 }
638 }
639
640 if let Some(variables) = contract_data
641 .get("variables")
642 .and_then(serde_json::Value::as_array)
643 {
644 for variable in variables {
645 let variable_location = parse_top_level_variable(variable);
646 for pc in variable_location.pc_range.0..=variable_location.pc_range.1 {
647 variable_locations
648 .entry(pc)
649 .or_default()
650 .push(variable_location.clone());
651 }
652 }
653 }
654
655 Ok(variable_locations)
656}
657
658fn parse_context_variable(variable: &Value, pc: u64) -> VariableLocation {
659 let location = variable.get("location");
660 let scope = variable.get("scope");
661
662 VariableLocation {
663 name: get_string(variable, "name", "unknown"),
664 ty: get_string(variable, "type", "unknown"),
665 location_type: location
666 .and_then(|value| value.get("type"))
667 .and_then(serde_json::Value::as_str)
668 .unwrap_or("stack")
669 .to_owned(),
670 offset: location
671 .and_then(|value| value.get("offset"))
672 .and_then(serde_json::Value::as_u64)
673 .unwrap_or(0),
674 pc_range: (
675 scope
676 .and_then(|value| value.get("start"))
677 .and_then(serde_json::Value::as_u64)
678 .unwrap_or(pc),
679 scope
680 .and_then(|value| value.get("end"))
681 .and_then(serde_json::Value::as_u64)
682 .unwrap_or(pc),
683 ),
684 }
685}
686
687fn parse_top_level_variable(variable: &Value) -> VariableLocation {
688 VariableLocation {
689 name: get_string(variable, "name", "unknown"),
690 ty: get_string(variable, "type", "unknown"),
691 location_type: get_string(variable, "location_type", "stack"),
692 offset: variable
693 .get("offset")
694 .and_then(serde_json::Value::as_u64)
695 .unwrap_or(0),
696 pc_range: (
697 variable
698 .get("pc_start")
699 .and_then(serde_json::Value::as_u64)
700 .unwrap_or(0),
701 variable
702 .get("pc_end")
703 .and_then(serde_json::Value::as_u64)
704 .unwrap_or(0),
705 ),
706 }
707}
708
709fn get_string(value: &Value, key: &str, default: &str) -> String {
710 value
711 .get(key)
712 .and_then(serde_json::Value::as_str)
713 .unwrap_or(default)
714 .to_owned()
715}
716
717#[cfg(test)]
718mod tests {
719 use std::collections::BTreeMap;
720
721 use super::{
722 parse_compilation_sources, parse_ethdebug_spec, parse_multi_contract_spec,
723 parse_single_contract_spec, parse_variable_locations, read_compilation_source,
724 EthdebugInfo, FunctionExit, Instruction, SourceLocation, SourceMapContext, SourceMapJump,
725 };
726 use serde_json::json;
727
728 #[test]
729 fn reads_sources_embedded_in_the_compilation() {
730 let compilation = json!({
731 "sources": [
732 {"id": 0, "path": "A.sol", "contents": "contract A {}"},
733 {"id": 1, "path": "B.sol", "content": "contract B {}"},
734 {"id": 2, "path": "C.sol"}
735 ]
736 });
737
738 assert_eq!(
739 read_compilation_source(&compilation, 0).as_deref(),
740 Some("contract A {}")
741 );
742 assert_eq!(
743 read_compilation_source(&compilation, 1).as_deref(),
744 Some("contract B {}")
745 );
746 assert_eq!(read_compilation_source(&compilation, 2), None);
747 assert_eq!(read_compilation_source(&compilation, 9), None);
748 assert_eq!(read_compilation_source(&json!({}), 0), None);
749 }
750
751 #[test]
752 fn builds_info_from_parsed_artifacts() {
753 let metadata = json!({
754 "compilation": {
755 "sources": [
756 {"id": 0, "path": "Counter.sol"},
757 {"id": 1, "path": "lib/Math.sol"},
758 {"path": "missing-id.sol"},
759 {"id": 2}
760 ]
761 },
762 "types": {},
763 "pointers": {}
764 });
765 let program = json!({
766 "instructions": [
767 {
768 "offset": 0,
769 "operation": {"mnemonic": "PUSH1", "arguments": ["0x80"]},
770 "context": {
771 "code": {"source": {"id": 0}, "range": {"offset": 10, "length": 4}},
772 "variables": [
773 {"identifier": "x", "pointer": {"location": "stack", "slot": 0}}
774 ]
775 }
776 },
777 {"offset": 2, "operation": {"mnemonic": "STOP"}}
778 ]
779 });
780
781 let info = EthdebugInfo::from_artifacts("Counter", "runtime", &metadata, &program)
782 .expect("artifacts");
783
784 assert_eq!(info.contract_name, "Counter");
785 assert_eq!(info.environment, "runtime");
786 assert_eq!(info.compilation, metadata["compilation"]);
787 assert_eq!(
788 info.sources,
789 BTreeMap::from([
790 (0, "Counter.sol".to_owned()),
791 (1, "lib/Math.sol".to_owned())
792 ])
793 );
794 assert_eq!(info.instructions.len(), 2);
795 assert_eq!(info.source_info(0), Some(("Counter.sol", 10, 4)));
796 assert_eq!(info.variables_at_pc(0).len(), 1);
797 assert!(info.variables_at_pc(2).is_empty());
798 }
799
800 #[test]
801 fn treats_legacy_metadata_without_compilation_wrapper_as_the_compilation() {
802 let metadata = json!({"sources": [{"id": 3, "path": "Legacy.sol"}]});
803 let program = json!({});
804
805 let info = EthdebugInfo::from_artifacts("Legacy", "create", &metadata, &program)
806 .expect("artifacts");
807
808 assert_eq!(info.compilation, metadata);
809 assert_eq!(info.sources, BTreeMap::from([(3, "Legacy.sol".to_owned())]));
810 assert!(info.instructions.is_empty());
811 assert!(info.variable_locations.is_empty());
812 }
813
814 #[test]
815 fn rejects_malformed_instructions() {
816 let metadata = json!({"compilation": {"sources": []}});
817 let program = json!({"instructions": [{"operation": {"mnemonic": "STOP"}}]});
818
819 let error = EthdebugInfo::from_artifacts("Broken", "runtime", &metadata, &program)
820 .expect_err("missing offset must fail");
821
822 assert!(
823 error.to_string().contains("`instructions`"),
824 "unexpected error: {error}"
825 );
826 }
827
828 #[test]
829 fn parses_compilation_sources_skipping_incomplete_entries() {
830 let compilation = json!({
831 "sources": [
832 {"id": 0, "path": "A.sol"},
833 {"id": "1", "path": "B.sol"},
834 {"id": 2, "path": 7},
835 {"id": 3, "path": "D.sol"}
836 ]
837 });
838
839 assert_eq!(
840 parse_compilation_sources(&compilation),
841 BTreeMap::from([(0, "A.sol".to_owned()), (3, "D.sol".to_owned())])
842 );
843 assert!(parse_compilation_sources(&json!({})).is_empty());
844 }
845
846 #[test]
847 fn parses_single_and_multi_contract_specs() {
848 let spec = parse_single_contract_spec("0xabc:Token:out").expect("single spec");
849 assert_eq!(spec.address.as_deref(), Some("0xabc"));
850 assert_eq!(spec.name.as_deref(), Some("Token"));
851 assert_eq!(spec.path, "out");
852
853 let multi = parse_multi_contract_spec("0xabc:out").expect("multi spec");
854 assert_eq!(multi.address.as_deref(), Some("0xabc"));
855 assert_eq!(multi.name, None);
856 assert_eq!(multi.path, "out");
857
858 let plain = parse_ethdebug_spec("out");
859 assert_eq!(plain.address, None);
860 assert_eq!(plain.path, "out");
861
862 assert!(parse_single_contract_spec("out").is_err());
863 assert!(parse_multi_contract_spec("").is_err());
864 }
865
866 #[test]
867 fn extracts_instruction_source_locations() {
868 let instruction: Instruction = serde_json::from_value(json!({
869 "offset": 12,
870 "operation": {"mnemonic": "PUSH1", "arguments": ["0x2a"]},
871 "context": {
872 "code": {
873 "source": {"id": 7},
874 "range": {"offset": 20, "length": 4}
875 }
876 }
877 }))
878 .expect("instruction");
879
880 let source_location = instruction.source_location().expect("source location");
881 assert_eq!(instruction.mnemonic(), Some("PUSH1"));
882 assert_eq!(instruction.arguments(), ["0x2a"]);
883 assert_eq!(source_location.source_id, 7);
884 assert_eq!(source_location.offset, 20);
885 assert_eq!(source_location.length, 4);
886 }
887
888 #[test]
889 fn source_map_instructions_read_and_write_as_their_json() {
890 let code = SourceLocation {
891 source_id: 3,
892 offset: 40,
893 length: 9,
894 };
895 let typed = Instruction::from_source_map(
896 7,
897 "JUMP",
898 SourceMapContext {
899 code: Some(code.clone()),
900 jump: SourceMapJump::Into,
901 modifier_depth: 1,
902 },
903 );
904 let json = json!({
905 "offset": 7,
906 "operation": {"mnemonic": "JUMP"},
907 "context": {
908 "modifierDepth": 1,
909 "code": {"source": {"id": 3}, "range": {"offset": 40, "length": 9}},
910 "invoke": {}
911 }
912 });
913 assert_eq!(serde_json::to_value(&typed).expect("json"), json);
914 let read: Instruction = serde_json::from_value(json.clone()).expect("instruction");
915 assert_eq!(read, typed);
916 let context = json.get("context").cloned();
917 assert_eq!(Instruction::new(7, "JUMP", context), typed);
918
919 assert_eq!(typed.mnemonic(), Some("JUMP"));
920 assert!(typed.arguments().is_empty());
921 assert_eq!(typed.source_location(), Some(code.clone()));
922 assert_eq!(typed.source_locations(), [code]);
923 assert_eq!(typed.function_invocations().len(), 1);
924 assert_eq!(typed.function_exit(), None);
925 assert_eq!(typed.modifier_depth(), Some(1));
926
927 let generated = Instruction::from_source_map(
928 8,
929 "JUMP",
930 SourceMapContext {
931 code: None,
932 jump: SourceMapJump::Out,
933 modifier_depth: 0,
934 },
935 );
936 assert_eq!(generated.source_location(), None);
937 assert!(generated.function_invocations().is_empty());
938 assert_eq!(generated.function_exit(), Some(FunctionExit::Return));
939 assert_eq!(
940 serde_json::to_value(&generated).expect("json")["context"],
941 json!({"modifierDepth": 0, "return": {}})
942 );
943 }
944
945 #[test]
946 fn keeps_contexts_a_source_map_cannot_say_as_written() {
947 for context in [
948 json!({"code": {"source": {"id": 1}, "range": {"offset": 0, "length": 1}}}),
950 json!({"modifierDepth": 0, "remark": "x"}),
951 json!({"modifierDepth": 0, "invoke": {"identifier": "f"}}),
952 json!({"modifierDepth": 0, "invoke": {}, "return": {}}),
953 json!({"modifierDepth": 0, "code": {"source": {"id": 1, "path": "a.sol"},
954 "range": {"offset": 0, "length": 1}}}),
955 ] {
956 let instruction = Instruction::new(0, "STOP", Some(context.clone()));
957 let written = serde_json::to_value(&instruction).expect("json");
958 assert_eq!(written["context"], context);
959 }
960 let push: Instruction = serde_json::from_value(json!({
961 "offset": 0,
962 "operation": {"mnemonic": "PUSH1", "arguments": ["0x01"]}
963 }))
964 .expect("instruction");
965 assert_eq!(
966 serde_json::to_value(&push).expect("json")["operation"],
967 json!({"mnemonic": "PUSH1", "arguments": ["0x01"]})
968 );
969 }
970
971 #[test]
972 fn extracts_source_picks_and_function_events() {
973 let invoke: Instruction = serde_json::from_value(json!({
974 "offset": 12,
975 "operation": {"mnemonic": "JUMPDEST"},
976 "context": {
977 "invoke": {
978 "identifier": "calculate",
979 "declaration": {
980 "source": {"id": 7},
981 "range": {"offset": 100, "length": 80}
982 },
983 "jump": true
984 }
985 }
986 }))
987 .expect("invoke instruction");
988 let shared: Instruction = serde_json::from_value(json!({
989 "offset": 13,
990 "operation": {"mnemonic": "SSTORE"},
991 "context": {
992 "pick": [
993 {"code": {
994 "source": {"id": 7},
995 "range": {"offset": 20, "length": 4}
996 }},
997 {"code": {
998 "source": {"id": 7},
999 "range": {"offset": 140, "length": 4}
1000 }}
1001 ]
1002 }
1003 }))
1004 .expect("shared instruction");
1005 let returned: Instruction = serde_json::from_value(json!({
1006 "offset": 14,
1007 "operation": {"mnemonic": "STOP"},
1008 "context": {"return": {}}
1009 }))
1010 .expect("return instruction");
1011
1012 let function = invoke.function_invocations().remove(0);
1013 assert_eq!(function.identifier.as_deref(), Some("calculate"));
1014 assert_eq!(function.declaration.map(|range| range.offset), Some(100));
1015 assert_eq!(shared.source_location(), None);
1016 assert_eq!(
1017 shared
1018 .source_locations()
1019 .into_iter()
1020 .map(|location| location.offset)
1021 .collect::<Vec<_>>(),
1022 [20, 140]
1023 );
1024 assert_eq!(returned.function_exit(), Some(super::FunctionExit::Return));
1025 }
1026
1027 #[test]
1028 fn reports_source_and_variable_info_at_pc() {
1029 let instruction: Instruction = serde_json::from_value(json!({
1030 "offset": 3,
1031 "operation": {"mnemonic": "SLOAD"},
1032 "context": {
1033 "code": {
1034 "source": {"id": 0},
1035 "range": {"offset": 9, "length": 5}
1036 }
1037 }
1038 }))
1039 .expect("instruction");
1040
1041 let variable = super::VariableLocation {
1042 name: "stored".to_owned(),
1043 ty: "uint256".to_owned(),
1044 location_type: "storage".to_owned(),
1045 offset: 0,
1046 pc_range: (2, 5),
1047 };
1048 let mut sources = BTreeMap::new();
1049 sources.insert(0, "Counter.sol".to_owned());
1050 let mut variable_locations = BTreeMap::new();
1051 variable_locations.insert(2, vec![variable]);
1052 let info = EthdebugInfo {
1053 compilation: json!({}),
1054 contract_name: "Counter".to_owned(),
1055 environment: "runtime".to_owned(),
1056 instructions: vec![instruction],
1057 sources,
1058 variable_locations,
1059 };
1060
1061 assert_eq!(info.source_info(3), Some(("Counter.sol", 9, 5)));
1062 assert_eq!(info.variables_at_pc(3)[0].name, "stored");
1063 assert!(info.variables_at_pc(9).is_empty());
1064 }
1065
1066 #[test]
1067 fn parses_variable_locations_from_contract_data() {
1068 let variables = parse_variable_locations(&json!({
1069 "instructions": [
1070 {
1071 "offset": 10,
1072 "context": {
1073 "variables": [
1074 {
1075 "name": "amount",
1076 "type": "uint256",
1077 "location": {"type": "stack", "offset": 1},
1078 "scope": {"start": 10, "end": 12}
1079 }
1080 ]
1081 }
1082 }
1083 ],
1084 "variables": [
1085 {
1086 "name": "stored",
1087 "type": "uint256",
1088 "location_type": "storage",
1089 "offset": 0,
1090 "pc_start": 20,
1091 "pc_end": 21
1092 }
1093 ]
1094 }))
1095 .expect("variables");
1096
1097 assert_eq!(variables.get(&10).expect("pc 10")[0].name, "amount");
1098 assert_eq!(
1099 variables.get(&20).expect("pc 20")[0].location_type,
1100 "storage"
1101 );
1102 assert_eq!(variables.get(&21).expect("pc 21")[0].name, "stored");
1103 }
1104}