Skip to main content

soldb_ethdebug/
metadata.rs

1//! ETHDebug artifact model: instructions, source spans, and variable locations.
2//!
3//! An ETHDebug runtime artifact is a list of instructions, each carrying its program
4//! counter and a `context` describing the source range it was generated from. Mapping a
5//! program counter back to Solidity means finding its instruction and reading that span.
6//!
7//! Spans nest: a compiler attaches a whole-contract range to dispatcher and preamble
8//! instructions, so a span containing a given line is not the same thing as a span
9//! generated for it. Callers that resolve a source line to a program counter must rank
10//! candidates accordingly rather than taking the first intersecting span.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use soldb_core::{SoldbError, SoldbResult};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct SourceLocation {
20    pub source_id: u64,
21    pub offset: u64,
22    pub length: u64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct FunctionIdentity {
27    pub identifier: Option<String>,
28    pub declaration: Option<SourceLocation>,
29    pub activation: Option<String>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum FunctionExit {
35    Return,
36    Revert,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Instruction {
41    pub offset: u64,
42    pub operation: Value,
43    #[serde(default)]
44    pub context: Option<Value>,
45}
46
47impl Instruction {
48    #[must_use]
49    pub fn mnemonic(&self) -> Option<&str> {
50        self.operation
51            .get("mnemonic")
52            .and_then(serde_json::Value::as_str)
53    }
54
55    #[must_use]
56    pub fn arguments(&self) -> Vec<&str> {
57        self.operation
58            .get("arguments")
59            .and_then(serde_json::Value::as_array)
60            .map(|arguments| {
61                arguments
62                    .iter()
63                    .filter_map(serde_json::Value::as_str)
64                    .collect()
65            })
66            .unwrap_or_default()
67    }
68
69    #[must_use]
70    pub fn source_location(&self) -> Option<SourceLocation> {
71        let mut locations = self.source_locations();
72        (locations.len() == 1).then(|| locations.remove(0))
73    }
74
75    /// Returns every source alternative attached to this instruction.
76    #[must_use]
77    pub fn source_locations(&self) -> Vec<SourceLocation> {
78        let mut locations = Vec::new();
79        if let Some(context) = &self.context {
80            collect_source_locations(context, &mut locations);
81        }
82        locations
83    }
84
85    /// Returns every possible function invocation attached to this instruction.
86    #[must_use]
87    pub fn function_invocations(&self) -> Vec<FunctionIdentity> {
88        let mut functions = Vec::new();
89        if let Some(context) = &self.context {
90            collect_function_invocations(context, &mut functions);
91        }
92        functions
93    }
94
95    /// Returns the function exit attached to this instruction.
96    #[must_use]
97    pub fn function_exit(&self) -> Option<FunctionExit> {
98        let context = self.context.as_ref()?;
99        if context.get("return").is_some() {
100            Some(FunctionExit::Return)
101        } else if context.get("revert").is_some() {
102            Some(FunctionExit::Revert)
103        } else {
104            None
105        }
106    }
107
108    /// Legacy source-map modifier depth, when this instruction was adapted from one.
109    #[must_use]
110    pub fn modifier_depth(&self) -> Option<i64> {
111        self.context.as_ref()?.get("modifierDepth")?.as_i64()
112    }
113}
114
115fn collect_source_locations(context: &Value, locations: &mut Vec<SourceLocation>) {
116    if let Some(location) = context.get("code").and_then(parse_source_location) {
117        if !locations.contains(&location) {
118            locations.push(location);
119        }
120    }
121    if let Some(pick) = context.get("pick").and_then(Value::as_array) {
122        for alternative in pick {
123            collect_source_locations(alternative, locations);
124        }
125    }
126}
127
128fn collect_function_invocations(context: &Value, functions: &mut Vec<FunctionIdentity>) {
129    if let Some(invoke) = context.get("invoke") {
130        let function = FunctionIdentity {
131            identifier: invoke
132                .get("identifier")
133                .and_then(Value::as_str)
134                .map(str::to_owned),
135            declaration: invoke.get("declaration").and_then(parse_source_location),
136            activation: invoke
137                .get("activation")
138                .and_then(Value::as_str)
139                .map(str::to_owned),
140        };
141        if !functions.contains(&function) {
142            functions.push(function);
143        }
144    }
145    if let Some(pick) = context.get("pick").and_then(Value::as_array) {
146        for alternative in pick {
147            collect_function_invocations(alternative, functions);
148        }
149    }
150}
151
152fn parse_source_location(value: &Value) -> Option<SourceLocation> {
153    let source_id = value.get("source")?.get("id")?.as_u64()?;
154    let range = value.get("range")?;
155    Some(SourceLocation {
156        source_id,
157        offset: range.get("offset")?.as_u64()?,
158        length: range.get("length")?.as_u64()?,
159    })
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct VariableLocation {
164    pub name: String,
165    pub ty: String,
166    pub location_type: String,
167    pub offset: u64,
168    pub pc_range: (u64, u64),
169}
170
171impl VariableLocation {
172    #[must_use]
173    pub fn is_active_at_pc(&self, pc: u64) -> bool {
174        self.pc_range.0 <= pc && pc <= self.pc_range.1
175    }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct EthdebugInfo {
180    pub compilation: Value,
181    pub contract_name: String,
182    pub environment: String,
183    pub instructions: Vec<Instruction>,
184    pub sources: BTreeMap<u64, String>,
185    #[serde(default)]
186    pub variable_locations: BTreeMap<u64, Vec<VariableLocation>>,
187}
188
189impl EthdebugInfo {
190    /// Builds the debug info for one contract from already-parsed ETHDebug artifacts.
191    ///
192    /// `metadata` is the global resource file (`ethdebug.json` from older compilers,
193    /// `ethdebug_resources.json` from modern ones) and `program` is the contract's
194    /// program artifact (`<Contract>_ethdebug.json` for creation code,
195    /// `<Contract>_ethdebug-runtime.json` for runtime code). Nothing is read from disk
196    /// here, so the CLI, the DAP server, and any host that receives the artifacts as
197    /// strings all build the same value through this one path.
198    ///
199    /// A program without an `instructions` array yields no instructions rather than an
200    /// error; callers that need to know report it as a debug-info gap.
201    pub fn from_artifacts(
202        contract_name: &str,
203        environment: &str,
204        metadata: &Value,
205        program: &Value,
206    ) -> SoldbResult<Self> {
207        let instructions = program
208            .get("instructions")
209            .cloned()
210            .map(serde_json::from_value::<Vec<Instruction>>)
211            .transpose()
212            .map_err(|error| {
213                SoldbError::Message(format!(
214                    "invalid `instructions` in ETHDebug program artifact: {error}"
215                ))
216            })?
217            .unwrap_or_default();
218        let compilation = metadata
219            .get("compilation")
220            .cloned()
221            .unwrap_or_else(|| metadata.clone());
222        let sources = parse_compilation_sources(&compilation);
223        let variable_locations = parse_variable_locations(program)?;
224        Ok(Self {
225            compilation,
226            contract_name: contract_name.to_owned(),
227            environment: environment.to_owned(),
228            instructions,
229            sources,
230            variable_locations,
231        })
232    }
233
234    #[must_use]
235    pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
236        self.instructions
237            .iter()
238            .find(|instruction| instruction.offset == pc)
239    }
240
241    #[must_use]
242    pub fn source_info(&self, pc: u64) -> Option<(&str, u64, u64)> {
243        let instruction = self.instruction_at_pc(pc)?;
244        let source_location = instruction.source_location()?;
245        let source_path = self.sources.get(&source_location.source_id)?;
246        Some((source_path, source_location.offset, source_location.length))
247    }
248
249    /// Whether the artifact carries variable locations at all.
250    ///
251    /// No compiler release emits them yet, so this is usually false, and the difference
252    /// matters to a user: nothing in scope at one program counter is not the same as a
253    /// compiler that describes no variables anywhere.
254    #[must_use]
255    pub fn has_variable_locations(&self) -> bool {
256        !self.variable_locations.is_empty()
257    }
258
259    pub fn variables_at_pc(&self, pc: u64) -> Vec<&VariableLocation> {
260        if let Some(exact) = self.variable_locations.get(&pc) {
261            return exact.iter().collect();
262        }
263
264        self.variable_locations
265            .values()
266            .flat_map(|variables| variables.iter())
267            .filter(|variable| variable.is_active_at_pc(pc))
268            .collect()
269    }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct EthdebugSpec {
274    pub address: Option<String>,
275    pub name: Option<String>,
276    pub path: String,
277}
278
279pub fn parse_ethdebug_spec(input: &str) -> EthdebugSpec {
280    if let Ok(spec) = parse_single_contract_spec(input) {
281        return spec;
282    }
283
284    parse_multi_contract_spec(input).unwrap_or_else(|_| EthdebugSpec {
285        address: None,
286        name: None,
287        path: input.to_owned(),
288    })
289}
290
291pub fn parse_single_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
292    if !input.starts_with("0x") {
293        return Err(SoldbError::Message(format!(
294            "Must use format 'address:name:path' (got: {input})"
295        )));
296    }
297
298    let parts = input.splitn(3, ':').collect::<Vec<_>>();
299    if parts.len() != 3 || parts[1].is_empty() || parts[2].is_empty() {
300        return Err(SoldbError::Message(format!(
301            "Must use format 'address:name:path' (got: {input})"
302        )));
303    }
304
305    Ok(EthdebugSpec {
306        address: Some(parts[0].to_owned()),
307        name: Some(parts[1].to_owned()),
308        path: parts[2].to_owned(),
309    })
310}
311
312pub fn parse_multi_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
313    if input.is_empty() {
314        return Err(SoldbError::Message("Path cannot be empty".to_owned()));
315    }
316
317    if input.starts_with("0x") && input.contains(':') {
318        let parts = input.splitn(2, ':').collect::<Vec<_>>();
319        if parts.len() != 2 || parts[1].is_empty() {
320            return Err(SoldbError::Message(format!(
321                "Must use format 'address:path' (got: {input})"
322            )));
323        }
324
325        return Ok(EthdebugSpec {
326            address: Some(parts[0].to_owned()),
327            name: None,
328            path: parts[1].to_owned(),
329        });
330    }
331
332    Ok(EthdebugSpec {
333        address: None,
334        name: None,
335        path: input.to_owned(),
336    })
337}
338
339/// Maps every source id listed in an ETHDebug `compilation` object to its path.
340///
341/// Entries without a numeric `id` or a string `path` are skipped rather than failing the
342/// whole load, since one malformed source entry should not hide the mapping for the rest.
343#[must_use]
344pub fn parse_compilation_sources(compilation: &Value) -> BTreeMap<u64, String> {
345    compilation
346        .get("sources")
347        .and_then(Value::as_array)
348        .into_iter()
349        .flatten()
350        .filter_map(|source| {
351            Some((
352                source.get("id")?.as_u64()?,
353                source.get("path")?.as_str()?.to_owned(),
354            ))
355        })
356        .collect()
357}
358
359/// Returns the contents of a source embedded in an ETHDebug `compilation` object.
360///
361/// Some artifacts inline each source under `contents` (older builds used `content`);
362/// when they do, a frontend can show source without reading the file, which is the only
363/// option for a host without a filesystem.
364#[must_use]
365pub fn read_compilation_source(compilation: &Value, source_id: u64) -> Option<String> {
366    compilation
367        .get("sources")
368        .and_then(Value::as_array)?
369        .iter()
370        .find(|source| source.get("id").and_then(Value::as_u64) == Some(source_id))
371        .and_then(|source| {
372            source
373                .get("contents")
374                .or_else(|| source.get("content"))
375                .and_then(Value::as_str)
376        })
377        .map(str::to_owned)
378}
379
380pub fn parse_variable_locations(
381    contract_data: &Value,
382) -> SoldbResult<BTreeMap<u64, Vec<VariableLocation>>> {
383    let mut variable_locations = BTreeMap::<u64, Vec<VariableLocation>>::new();
384
385    if let Some(instructions) = contract_data
386        .get("instructions")
387        .and_then(serde_json::Value::as_array)
388    {
389        for instruction in instructions {
390            let pc = instruction
391                .get("offset")
392                .and_then(serde_json::Value::as_u64)
393                .ok_or_else(|| SoldbError::Message("Instruction missing offset".to_owned()))?;
394
395            let Some(variables) = instruction
396                .get("context")
397                .and_then(|context| context.get("variables"))
398                .and_then(serde_json::Value::as_array)
399            else {
400                continue;
401            };
402
403            for variable in variables {
404                variable_locations
405                    .entry(pc)
406                    .or_default()
407                    .push(parse_context_variable(variable, pc));
408            }
409        }
410    }
411
412    if let Some(variables) = contract_data
413        .get("variables")
414        .and_then(serde_json::Value::as_array)
415    {
416        for variable in variables {
417            let variable_location = parse_top_level_variable(variable);
418            for pc in variable_location.pc_range.0..=variable_location.pc_range.1 {
419                variable_locations
420                    .entry(pc)
421                    .or_default()
422                    .push(variable_location.clone());
423            }
424        }
425    }
426
427    Ok(variable_locations)
428}
429
430fn parse_context_variable(variable: &Value, pc: u64) -> VariableLocation {
431    let location = variable.get("location");
432    let scope = variable.get("scope");
433
434    VariableLocation {
435        name: get_string(variable, "name", "unknown"),
436        ty: get_string(variable, "type", "unknown"),
437        location_type: location
438            .and_then(|value| value.get("type"))
439            .and_then(serde_json::Value::as_str)
440            .unwrap_or("stack")
441            .to_owned(),
442        offset: location
443            .and_then(|value| value.get("offset"))
444            .and_then(serde_json::Value::as_u64)
445            .unwrap_or(0),
446        pc_range: (
447            scope
448                .and_then(|value| value.get("start"))
449                .and_then(serde_json::Value::as_u64)
450                .unwrap_or(pc),
451            scope
452                .and_then(|value| value.get("end"))
453                .and_then(serde_json::Value::as_u64)
454                .unwrap_or(pc),
455        ),
456    }
457}
458
459fn parse_top_level_variable(variable: &Value) -> VariableLocation {
460    VariableLocation {
461        name: get_string(variable, "name", "unknown"),
462        ty: get_string(variable, "type", "unknown"),
463        location_type: get_string(variable, "location_type", "stack"),
464        offset: variable
465            .get("offset")
466            .and_then(serde_json::Value::as_u64)
467            .unwrap_or(0),
468        pc_range: (
469            variable
470                .get("pc_start")
471                .and_then(serde_json::Value::as_u64)
472                .unwrap_or(0),
473            variable
474                .get("pc_end")
475                .and_then(serde_json::Value::as_u64)
476                .unwrap_or(0),
477        ),
478    }
479}
480
481fn get_string(value: &Value, key: &str, default: &str) -> String {
482    value
483        .get(key)
484        .and_then(serde_json::Value::as_str)
485        .unwrap_or(default)
486        .to_owned()
487}
488
489#[cfg(test)]
490mod tests {
491    use std::collections::BTreeMap;
492
493    use super::{
494        parse_compilation_sources, parse_ethdebug_spec, parse_multi_contract_spec,
495        parse_single_contract_spec, parse_variable_locations, read_compilation_source,
496        EthdebugInfo, Instruction,
497    };
498    use serde_json::json;
499
500    #[test]
501    fn reads_sources_embedded_in_the_compilation() {
502        let compilation = json!({
503            "sources": [
504                {"id": 0, "path": "A.sol", "contents": "contract A {}"},
505                {"id": 1, "path": "B.sol", "content": "contract B {}"},
506                {"id": 2, "path": "C.sol"}
507            ]
508        });
509
510        assert_eq!(
511            read_compilation_source(&compilation, 0).as_deref(),
512            Some("contract A {}")
513        );
514        assert_eq!(
515            read_compilation_source(&compilation, 1).as_deref(),
516            Some("contract B {}")
517        );
518        assert_eq!(read_compilation_source(&compilation, 2), None);
519        assert_eq!(read_compilation_source(&compilation, 9), None);
520        assert_eq!(read_compilation_source(&json!({}), 0), None);
521    }
522
523    #[test]
524    fn builds_info_from_parsed_artifacts() {
525        let metadata = json!({
526            "compilation": {
527                "sources": [
528                    {"id": 0, "path": "Counter.sol"},
529                    {"id": 1, "path": "lib/Math.sol"},
530                    {"path": "missing-id.sol"},
531                    {"id": 2}
532                ]
533            },
534            "types": {},
535            "pointers": {}
536        });
537        let program = json!({
538            "instructions": [
539                {
540                    "offset": 0,
541                    "operation": {"mnemonic": "PUSH1", "arguments": ["0x80"]},
542                    "context": {
543                        "code": {"source": {"id": 0}, "range": {"offset": 10, "length": 4}},
544                        "variables": [
545                            {"identifier": "x", "pointer": {"location": "stack", "slot": 0}}
546                        ]
547                    }
548                },
549                {"offset": 2, "operation": {"mnemonic": "STOP"}}
550            ]
551        });
552
553        let info = EthdebugInfo::from_artifacts("Counter", "runtime", &metadata, &program)
554            .expect("artifacts");
555
556        assert_eq!(info.contract_name, "Counter");
557        assert_eq!(info.environment, "runtime");
558        assert_eq!(info.compilation, metadata["compilation"]);
559        assert_eq!(
560            info.sources,
561            BTreeMap::from([
562                (0, "Counter.sol".to_owned()),
563                (1, "lib/Math.sol".to_owned())
564            ])
565        );
566        assert_eq!(info.instructions.len(), 2);
567        assert_eq!(info.source_info(0), Some(("Counter.sol", 10, 4)));
568        assert_eq!(info.variables_at_pc(0).len(), 1);
569        assert!(info.variables_at_pc(2).is_empty());
570    }
571
572    #[test]
573    fn treats_legacy_metadata_without_compilation_wrapper_as_the_compilation() {
574        let metadata = json!({"sources": [{"id": 3, "path": "Legacy.sol"}]});
575        let program = json!({});
576
577        let info = EthdebugInfo::from_artifacts("Legacy", "create", &metadata, &program)
578            .expect("artifacts");
579
580        assert_eq!(info.compilation, metadata);
581        assert_eq!(info.sources, BTreeMap::from([(3, "Legacy.sol".to_owned())]));
582        assert!(info.instructions.is_empty());
583        assert!(info.variable_locations.is_empty());
584    }
585
586    #[test]
587    fn rejects_malformed_instructions() {
588        let metadata = json!({"compilation": {"sources": []}});
589        let program = json!({"instructions": [{"operation": {"mnemonic": "STOP"}}]});
590
591        let error = EthdebugInfo::from_artifacts("Broken", "runtime", &metadata, &program)
592            .expect_err("missing offset must fail");
593
594        assert!(
595            error.to_string().contains("`instructions`"),
596            "unexpected error: {error}"
597        );
598    }
599
600    #[test]
601    fn parses_compilation_sources_skipping_incomplete_entries() {
602        let compilation = json!({
603            "sources": [
604                {"id": 0, "path": "A.sol"},
605                {"id": "1", "path": "B.sol"},
606                {"id": 2, "path": 7},
607                {"id": 3, "path": "D.sol"}
608            ]
609        });
610
611        assert_eq!(
612            parse_compilation_sources(&compilation),
613            BTreeMap::from([(0, "A.sol".to_owned()), (3, "D.sol".to_owned())])
614        );
615        assert!(parse_compilation_sources(&json!({})).is_empty());
616    }
617
618    #[test]
619    fn parses_single_and_multi_contract_specs() {
620        let spec = parse_single_contract_spec("0xabc:Token:out").expect("single spec");
621        assert_eq!(spec.address.as_deref(), Some("0xabc"));
622        assert_eq!(spec.name.as_deref(), Some("Token"));
623        assert_eq!(spec.path, "out");
624
625        let multi = parse_multi_contract_spec("0xabc:out").expect("multi spec");
626        assert_eq!(multi.address.as_deref(), Some("0xabc"));
627        assert_eq!(multi.name, None);
628        assert_eq!(multi.path, "out");
629
630        let plain = parse_ethdebug_spec("out");
631        assert_eq!(plain.address, None);
632        assert_eq!(plain.path, "out");
633
634        assert!(parse_single_contract_spec("out").is_err());
635        assert!(parse_multi_contract_spec("").is_err());
636    }
637
638    #[test]
639    fn extracts_instruction_source_locations() {
640        let instruction: Instruction = serde_json::from_value(json!({
641            "offset": 12,
642            "operation": {"mnemonic": "PUSH1", "arguments": ["0x2a"]},
643            "context": {
644                "code": {
645                    "source": {"id": 7},
646                    "range": {"offset": 20, "length": 4}
647                }
648            }
649        }))
650        .expect("instruction");
651
652        let source_location = instruction.source_location().expect("source location");
653        assert_eq!(instruction.mnemonic(), Some("PUSH1"));
654        assert_eq!(instruction.arguments(), ["0x2a"]);
655        assert_eq!(source_location.source_id, 7);
656        assert_eq!(source_location.offset, 20);
657        assert_eq!(source_location.length, 4);
658    }
659
660    #[test]
661    fn extracts_source_picks_and_function_events() {
662        let invoke: Instruction = serde_json::from_value(json!({
663            "offset": 12,
664            "operation": {"mnemonic": "JUMPDEST"},
665            "context": {
666                "invoke": {
667                    "identifier": "calculate",
668                    "declaration": {
669                        "source": {"id": 7},
670                        "range": {"offset": 100, "length": 80}
671                    },
672                    "jump": true
673                }
674            }
675        }))
676        .expect("invoke instruction");
677        let shared: Instruction = serde_json::from_value(json!({
678            "offset": 13,
679            "operation": {"mnemonic": "SSTORE"},
680            "context": {
681                "pick": [
682                    {"code": {
683                        "source": {"id": 7},
684                        "range": {"offset": 20, "length": 4}
685                    }},
686                    {"code": {
687                        "source": {"id": 7},
688                        "range": {"offset": 140, "length": 4}
689                    }}
690                ]
691            }
692        }))
693        .expect("shared instruction");
694        let returned: Instruction = serde_json::from_value(json!({
695            "offset": 14,
696            "operation": {"mnemonic": "STOP"},
697            "context": {"return": {}}
698        }))
699        .expect("return instruction");
700
701        let function = invoke.function_invocations().remove(0);
702        assert_eq!(function.identifier.as_deref(), Some("calculate"));
703        assert_eq!(function.declaration.map(|range| range.offset), Some(100));
704        assert_eq!(shared.source_location(), None);
705        assert_eq!(
706            shared
707                .source_locations()
708                .into_iter()
709                .map(|location| location.offset)
710                .collect::<Vec<_>>(),
711            [20, 140]
712        );
713        assert_eq!(returned.function_exit(), Some(super::FunctionExit::Return));
714    }
715
716    #[test]
717    fn reports_source_and_variable_info_at_pc() {
718        let instruction: Instruction = serde_json::from_value(json!({
719            "offset": 3,
720            "operation": {"mnemonic": "SLOAD"},
721            "context": {
722                "code": {
723                    "source": {"id": 0},
724                    "range": {"offset": 9, "length": 5}
725                }
726            }
727        }))
728        .expect("instruction");
729
730        let variable = super::VariableLocation {
731            name: "stored".to_owned(),
732            ty: "uint256".to_owned(),
733            location_type: "storage".to_owned(),
734            offset: 0,
735            pc_range: (2, 5),
736        };
737        let mut sources = BTreeMap::new();
738        sources.insert(0, "Counter.sol".to_owned());
739        let mut variable_locations = BTreeMap::new();
740        variable_locations.insert(2, vec![variable]);
741        let info = EthdebugInfo {
742            compilation: json!({}),
743            contract_name: "Counter".to_owned(),
744            environment: "runtime".to_owned(),
745            instructions: vec![instruction],
746            sources,
747            variable_locations,
748        };
749
750        assert_eq!(info.source_info(3), Some(("Counter.sol", 9, 5)));
751        assert_eq!(info.variables_at_pc(3)[0].name, "stored");
752        assert!(info.variables_at_pc(9).is_empty());
753    }
754
755    #[test]
756    fn parses_variable_locations_from_contract_data() {
757        let variables = parse_variable_locations(&json!({
758            "instructions": [
759                {
760                    "offset": 10,
761                    "context": {
762                        "variables": [
763                            {
764                                "name": "amount",
765                                "type": "uint256",
766                                "location": {"type": "stack", "offset": 1},
767                                "scope": {"start": 10, "end": 12}
768                            }
769                        ]
770                    }
771                }
772            ],
773            "variables": [
774                {
775                    "name": "stored",
776                    "type": "uint256",
777                    "location_type": "storage",
778                    "offset": 0,
779                    "pc_start": 20,
780                    "pc_end": 21
781                }
782            ]
783        }))
784        .expect("variables");
785
786        assert_eq!(variables.get(&10).expect("pc 10")[0].name, "amount");
787        assert_eq!(
788            variables.get(&20).expect("pc 20")[0].location_type,
789            "storage"
790        );
791        assert_eq!(variables.get(&21).expect("pc 21")[0].name, "stored");
792    }
793}