Skip to main content

soldb_debugger/
types.rs

1//! Type declarations found in the sources: structs, enums, and user-defined value types.
2//!
3//! A legacy source map says where a variable's slot is, and the declaration says its
4//! type; what the type *means* — which members a struct has, what an enum's values are
5//! called, what a `type Price is uint128` wraps — is declared elsewhere in the sources.
6//! This scanner collects those declarations so a value can be shown as `Color.Red` rather
7//! than `1`, and a memory struct as its members rather than as a pointer.
8//!
9//! Like the local scanner, it reads the text rather than parsing the language: it finds
10//! each declaration keyword outside comments and strings, reads the declaration that
11//! follows, and remembers which contract it was declared in and which contracts that one
12//! inherits from, so a bare `Item` resolves the way the language resolves it: in the
13//! contract whose code is executing, then in its bases, then at file level.
14
15use crate::locals::{skip_string, skip_trivia};
16use crate::{find_matching_delimiter, find_solidity_keyword, is_identifier, parse_identifier};
17
18/// One member of a struct, in declaration order.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct StructMember {
21    pub name: String,
22    /// The type as written, with whitespace collapsed: `uint256`, `Item[]`, `mapping(address => uint256)`.
23    pub ty: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27struct StructDecl {
28    name: String,
29    /// The contract, library, or interface the struct is declared in; `None` at file level.
30    scope: Option<String>,
31    members: Vec<StructMember>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35struct EnumDecl {
36    name: String,
37    scope: Option<String>,
38    variants: Vec<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42struct ValueTypeDecl {
43    name: String,
44    scope: Option<String>,
45    underlying: String,
46}
47
48/// A `contract`, `library`, or `interface` block.
49#[derive(Debug, Clone, PartialEq, Eq)]
50struct Scope {
51    name: String,
52    source_id: u64,
53    start: usize,
54    end: usize,
55    /// The contracts named after `is`, in order.
56    bases: Vec<String>,
57}
58
59/// The struct, enum, and user-defined value type declarations of a set of sources.
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct SourceTypes {
62    structs: Vec<StructDecl>,
63    enums: Vec<EnumDecl>,
64    value_types: Vec<ValueTypeDecl>,
65    scopes: Vec<Scope>,
66}
67
68impl SourceTypes {
69    /// The declarations of one source, given id 0.
70    #[must_use]
71    pub fn parse(source: &str) -> Self {
72        let mut types = Self::default();
73        types.add_source(0, source);
74        types
75    }
76
77    /// Adds the declarations of another source.
78    pub fn add_source(&mut self, source_id: u64, source: &str) {
79        let code = code_ranges(source);
80        let scopes = scan_scopes(source, source_id, &code);
81        let scope_of = |offset: usize| -> Option<String> {
82            scopes
83                .iter()
84                .filter(|scope| scope.start <= offset && offset < scope.end)
85                .min_by_key(|scope| scope.end - scope.start)
86                .map(|scope| scope.name.clone())
87        };
88        let mut cursor = 0;
89        while let Some(at) = find_keyword(source, &code, "struct", cursor) {
90            cursor = at + "struct".len();
91            let Some((name, open, close)) = braced_declaration(source, cursor) else {
92                continue;
93            };
94            self.structs.push(StructDecl {
95                name: name.to_owned(),
96                scope: scope_of(at),
97                members: parse_members(&source[open + 1..close]),
98            });
99            cursor = close + 1;
100        }
101        let mut cursor = 0;
102        while let Some(at) = find_keyword(source, &code, "enum", cursor) {
103            cursor = at + "enum".len();
104            let Some((name, open, close)) = braced_declaration(source, cursor) else {
105                continue;
106            };
107            let variants = source[open + 1..close]
108                .split(',')
109                .map(str::trim)
110                .filter(|variant| !variant.is_empty())
111                .map(str::to_owned)
112                .collect();
113            self.enums.push(EnumDecl {
114                name: name.to_owned(),
115                scope: scope_of(at),
116                variants,
117            });
118            cursor = close + 1;
119        }
120        let mut cursor = 0;
121        while let Some(at) = find_keyword(source, &code, "type", cursor) {
122            cursor = at + "type".len();
123            let Some((name, name_end)) = parse_identifier(source, skip_trivia(source, cursor))
124            else {
125                continue;
126            };
127            let after = skip_trivia(source, name_end);
128            let Some(("is", is_end)) = parse_identifier(source, after) else {
129                continue;
130            };
131            let underlying_start = skip_trivia(source, is_end);
132            let Some(end) = source[underlying_start..].find(';') else {
133                continue;
134            };
135            let underlying = source[underlying_start..underlying_start + end].trim();
136            if underlying.is_empty() {
137                continue;
138            }
139            self.value_types.push(ValueTypeDecl {
140                name: name.to_owned(),
141                scope: scope_of(at),
142                underlying: underlying.to_owned(),
143            });
144            cursor = underlying_start + end;
145        }
146        self.scopes.extend(scopes);
147    }
148
149    /// Whether any declaration was found.
150    #[must_use]
151    pub fn is_empty(&self) -> bool {
152        self.structs.is_empty() && self.enums.is_empty() && self.value_types.is_empty()
153    }
154
155    /// The contract, library, or interface whose block contains `offset` of the source,
156    /// innermost first: the scope a name written there resolves in.
157    #[must_use]
158    pub fn scope_at(&self, source_id: u64, offset: u64) -> Option<&str> {
159        let offset = usize::try_from(offset).ok()?;
160        self.scopes
161            .iter()
162            .filter(|scope| {
163                scope.source_id == source_id && scope.start <= offset && offset < scope.end
164            })
165            .min_by_key(|scope| scope.end - scope.start)
166            .map(|scope| scope.name.as_str())
167    }
168
169    /// The members of the struct `name` names, as written in `scope` (a contract name,
170    /// or `None` for file level); `name` may be qualified as `Contract.Name`.
171    #[must_use]
172    pub fn struct_members(&self, scope: Option<&str>, name: &str) -> Option<&[StructMember]> {
173        self.resolve(
174            self.structs.iter(),
175            |decl| (&decl.name, decl.scope.as_deref()),
176            scope,
177            name,
178        )
179        .map(|decl| decl.members.as_slice())
180    }
181
182    /// The variants of the enum `name` names, in declaration order.
183    #[must_use]
184    pub fn enum_variants(&self, scope: Option<&str>, name: &str) -> Option<&[String]> {
185        self.resolve(
186            self.enums.iter(),
187            |decl| (&decl.name, decl.scope.as_deref()),
188            scope,
189            name,
190        )
191        .map(|decl| decl.variants.as_slice())
192    }
193
194    /// The type a user-defined value type wraps.
195    #[must_use]
196    pub fn underlying(&self, scope: Option<&str>, name: &str) -> Option<&str> {
197        self.resolve(
198            self.value_types.iter(),
199            |decl| (&decl.name, decl.scope.as_deref()),
200            scope,
201            name,
202        )
203        .map(|decl| decl.underlying.as_str())
204    }
205
206    /// The value of an enum literal such as `Color.Red` or `Shop.Color.Red`.
207    #[must_use]
208    pub fn enum_literal(&self, scope: Option<&str>, path: &str) -> Option<u64> {
209        let (enum_name, variant) = path.rsplit_once('.')?;
210        let variants = self.enum_variants(scope, enum_name)?;
211        variants
212            .iter()
213            .position(|candidate| candidate == variant)
214            .map(|index| index as u64)
215    }
216
217    /// Whether `name` is a struct, however qualified.
218    #[must_use]
219    pub fn is_struct(&self, scope: Option<&str>, name: &str) -> bool {
220        self.struct_members(scope, name).is_some()
221    }
222
223    /// Every enum's variants, keyed the way a storage layout labels the type
224    /// (`Shop.Color`) and by its bare name (`Color`), for
225    /// [`soldb_ethdebug::StorageLayout::enum_variants`]. A bare name declared twice keeps
226    /// its first declaration.
227    #[must_use]
228    pub fn enum_variants_by_name(&self) -> std::collections::BTreeMap<String, Vec<String>> {
229        let mut names = std::collections::BTreeMap::new();
230        for decl in &self.enums {
231            if let Some(scope) = &decl.scope {
232                names
233                    .entry(format!("{scope}.{}", decl.name))
234                    .or_insert_with(|| decl.variants.clone());
235            }
236            names
237                .entry(decl.name.clone())
238                .or_insert_with(|| decl.variants.clone());
239        }
240        names
241    }
242
243    /// The contracts `scope` inherits from, nearest first, transitively.
244    fn bases(&self, scope: &str) -> Vec<&str> {
245        let mut seen = Vec::<&str>::new();
246        let mut queue = self
247            .scopes
248            .iter()
249            .filter(|candidate| candidate.name == scope)
250            .flat_map(|candidate| candidate.bases.iter().map(String::as_str))
251            .collect::<std::collections::VecDeque<_>>();
252        while let Some(base) = queue.pop_front() {
253            if seen.contains(&base) {
254                continue;
255            }
256            seen.push(base);
257            queue.extend(
258                self.scopes
259                    .iter()
260                    .filter(|candidate| candidate.name == base)
261                    .flat_map(|candidate| candidate.bases.iter().map(String::as_str)),
262            );
263        }
264        seen
265    }
266
267    /// Finds the declaration `name` refers to from `scope`. A qualified name must match
268    /// its scope; a bare name is looked up in `scope`, then in the contracts it inherits
269    /// from, then at file level, and failing all of those wherever it is declared.
270    fn resolve<'a, T>(
271        &'a self,
272        declarations: impl Iterator<Item = &'a T> + Clone,
273        key: impl Fn(&'a T) -> (&'a String, Option<&'a str>),
274        scope: Option<&str>,
275        name: &str,
276    ) -> Option<&'a T> {
277        let (qualifier, bare) = match name.rsplit_once('.') {
278            Some((qualifier, bare)) => (Some(qualifier), bare),
279            None => (None, name),
280        };
281        let candidates = declarations
282            .filter(|declaration| key(declaration).0 == bare)
283            .collect::<Vec<_>>();
284        if candidates.is_empty() {
285            return None;
286        }
287        let in_scope = |wanted: Option<&str>| {
288            candidates
289                .iter()
290                .copied()
291                .find(|declaration| key(declaration).1 == wanted)
292        };
293        if let Some(qualifier) = qualifier {
294            // `A.Item` where the declaration is `B.Item`: the wrong scope, unless nothing
295            // better turns up.
296            return in_scope(Some(qualifier)).or_else(|| candidates.first().copied());
297        }
298        if let Some(scope) = scope {
299            if let Some(found) = in_scope(Some(scope)) {
300                return Some(found);
301            }
302            for base in self.bases(scope) {
303                if let Some(found) = in_scope(Some(base)) {
304                    return Some(found);
305                }
306            }
307        }
308        in_scope(None).or_else(|| candidates.first().copied())
309    }
310}
311
312/// The byte ranges of a source that are code: outside comments and string literals.
313fn code_ranges(source: &str) -> Vec<(usize, usize)> {
314    let bytes = source.as_bytes();
315    let mut ranges = Vec::new();
316    let mut index = 0;
317    let mut start = 0;
318    while index < bytes.len() {
319        let skipped = skip_trivia(source, index);
320        if skipped != index {
321            if start < index {
322                ranges.push((start, index));
323            }
324            index = skipped;
325            start = index;
326            continue;
327        }
328        match bytes[index] {
329            b'"' | b'\'' => {
330                if start < index {
331                    ranges.push((start, index));
332                }
333                index = skip_string(source, index);
334                start = index;
335            }
336            _ => index += 1,
337        }
338    }
339    if start < bytes.len() {
340        ranges.push((start, bytes.len()));
341    }
342    ranges
343}
344
345/// The next `keyword` at or after `start` that is code rather than a comment or a string.
346fn find_keyword(
347    source: &str,
348    code: &[(usize, usize)],
349    keyword: &str,
350    start: usize,
351) -> Option<usize> {
352    let mut cursor = start;
353    while let Some(at) = find_solidity_keyword(source, keyword, cursor) {
354        if code.iter().any(|(from, to)| *from <= at && at < *to) {
355            return Some(at);
356        }
357        cursor = at + keyword.len();
358    }
359    None
360}
361
362/// The `contract`, `library`, and `interface` blocks of a source, with what they inherit.
363fn scan_scopes(source: &str, source_id: u64, code: &[(usize, usize)]) -> Vec<Scope> {
364    let mut scopes = Vec::new();
365    for keyword in ["contract", "library", "interface"] {
366        let mut cursor = 0;
367        while let Some(at) = find_keyword(source, code, keyword, cursor) {
368            cursor = at + keyword.len();
369            let Some((name, name_end)) = parse_identifier(source, skip_trivia(source, cursor))
370            else {
371                continue;
372            };
373            // `abstract contract X is Y, Z(1) {`: the block opens after the inheritance
374            // list, which names the bases.
375            let Some(open) = source[name_end..].find('{').map(|found| name_end + found) else {
376                continue;
377            };
378            let header = &source[name_end..open];
379            if header.contains(';') {
380                continue;
381            }
382            let Some(close) = find_matching_delimiter(source, open, b'{', b'}') else {
383                continue;
384            };
385            scopes.push(Scope {
386                name: name.to_owned(),
387                source_id,
388                start: open,
389                end: close,
390                bases: parse_bases(header),
391            });
392            cursor = open + 1;
393        }
394    }
395    scopes
396}
397
398/// The contracts named in an `is A, B(1), C` header, in order.
399fn parse_bases(header: &str) -> Vec<String> {
400    let trimmed = skip_trivia(header, 0);
401    let Some(rest) = header[trimmed..].strip_prefix("is") else {
402        return Vec::new();
403    };
404    if rest
405        .bytes()
406        .next()
407        .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphanumeric())
408    {
409        return Vec::new();
410    }
411    let mut bases = Vec::new();
412    let mut depth = 0_i32;
413    let mut start = 0;
414    let bytes = rest.as_bytes();
415    for index in 0..=bytes.len() {
416        match bytes.get(index) {
417            Some(b'(') => {
418                if depth == 0 {
419                    push_base(&mut bases, &rest[start..index]);
420                    start = bytes.len();
421                }
422                depth += 1;
423            }
424            Some(b')') => depth -= 1,
425            Some(b',') | None if depth == 0 => {
426                if start < index {
427                    push_base(&mut bases, &rest[start..index]);
428                }
429                start = index + 1;
430            }
431            _ => {}
432        }
433    }
434    bases
435}
436
437fn push_base(bases: &mut Vec<String>, text: &str) {
438    let name = text.trim();
439    if is_identifier(name) {
440        bases.push(name.to_owned());
441    }
442}
443
444/// `Name { ... }` after a `struct` or `enum` keyword ending at `after`: the name and the
445/// braces.
446fn braced_declaration(source: &str, after: usize) -> Option<(&str, usize, usize)> {
447    let (name, name_end) = parse_identifier(source, skip_trivia(source, after))?;
448    let open = skip_trivia(source, name_end);
449    if source.as_bytes().get(open) != Some(&b'{') {
450        return None;
451    }
452    let close = find_matching_delimiter(source, open, b'{', b'}')?;
453    Some((name, open, close))
454}
455
456/// The `type name;` members between a struct's braces.
457fn parse_members(body: &str) -> Vec<StructMember> {
458    let mut members = Vec::new();
459    let mut index = 0;
460    while index < body.len() {
461        index = skip_trivia(body, index);
462        if index >= body.len() {
463            break;
464        }
465        let Some(end) = find_member_end(body, index) else {
466            break;
467        };
468        if let Some((ty, name)) = split_member(&body[index..end]) {
469            members.push(StructMember { name, ty });
470        }
471        index = end + 1;
472    }
473    members
474}
475
476/// A member's `type name` text as its type, with whitespace collapsed, and its name.
477fn split_member(text: &str) -> Option<(String, String)> {
478    let text = text.trim();
479    let name_start = text
480        .rfind(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
481        .map_or(0, |index| index + 1);
482    let name = &text[name_start..];
483    let ty = text[..name_start]
484        .split_whitespace()
485        .collect::<Vec<_>>()
486        .join(" ");
487    if !is_identifier(name) || ty.is_empty() {
488        return None;
489    }
490    Some((ty, name.to_owned()))
491}
492
493/// The `;` that ends the member starting at `index`, outside any parentheses (a mapping
494/// type carries `=>` and could carry a nested mapping's parentheses).
495fn find_member_end(body: &str, index: usize) -> Option<usize> {
496    let bytes = body.as_bytes();
497    let mut depth = 0_i32;
498    for (offset, byte) in bytes.iter().enumerate().skip(index) {
499        match byte {
500            b'(' | b'[' => depth += 1,
501            b')' | b']' => depth -= 1,
502            b';' if depth <= 0 => return Some(offset),
503            _ => {}
504        }
505    }
506    None
507}
508
509#[cfg(test)]
510mod tests {
511    use super::{SourceTypes, StructMember};
512
513    const SOURCE: &str = "\
514// SPDX-License-Identifier: MIT
515pragma solidity ^0.8.20;
516
517type Price is uint128;
518
519struct Pair {
520    uint256 a;
521    uint256 b;
522}
523
524enum Level { Low, High }
525
526contract Shop {
527    enum Color { Red, Green, Blue }
528    struct Item {
529        uint256 id;
530        string name;
531        Color color;
532        uint256[] tags;
533        mapping(address => uint256) owners;
534        Pair[2] pairs;
535    }
536    type Money is uint256;
537    function f() public pure returns (uint256) { return 0; } // struct in a comment
538}
539
540library Math {
541    struct Item { uint8 x; }
542}
543
544abstract contract Base is Shop {
545    struct Item { uint16 y; }
546}
547
548contract Outlet is Base, Math(1) {
549    function g() public pure returns (uint256) { return 1; }
550}
551";
552
553    #[test]
554    fn finds_structs_enums_and_value_types_with_their_scopes() {
555        let types = SourceTypes::parse(SOURCE);
556        assert_eq!(
557            types.struct_members(None, "Pair"),
558            Some(
559                &[
560                    StructMember {
561                        name: "a".to_owned(),
562                        ty: "uint256".to_owned()
563                    },
564                    StructMember {
565                        name: "b".to_owned(),
566                        ty: "uint256".to_owned()
567                    },
568                ][..]
569            )
570        );
571        let item = types.struct_members(None, "Shop.Item").expect("Shop.Item");
572        assert_eq!(
573            item.iter()
574                .map(|member| (member.name.as_str(), member.ty.as_str()))
575                .collect::<Vec<_>>(),
576            [
577                ("id", "uint256"),
578                ("name", "string"),
579                ("color", "Color"),
580                ("tags", "uint256[]"),
581                ("owners", "mapping(address => uint256)"),
582                ("pairs", "Pair[2]"),
583            ]
584        );
585        assert_eq!(
586            types.struct_members(None, "Math.Item").map(<[_]>::len),
587            Some(1)
588        );
589        assert_eq!(
590            types
591                .enum_variants(None, "Color")
592                .map(|variants| variants.join(",")),
593            Some("Red,Green,Blue".to_owned())
594        );
595        assert_eq!(
596            types
597                .enum_variants(None, "Level")
598                .map(|variants| variants.join(",")),
599            Some("Low,High".to_owned())
600        );
601        assert_eq!(types.underlying(None, "Price"), Some("uint128"));
602        assert_eq!(types.underlying(None, "Shop.Money"), Some("uint256"));
603        assert_eq!(types.enum_literal(None, "Color.Green"), Some(1));
604        assert_eq!(types.enum_literal(None, "Shop.Color.Blue"), Some(2));
605        assert_eq!(types.enum_literal(None, "Color.Purple"), None);
606        assert_eq!(types.enum_literal(None, "Missing.Red"), None);
607        assert!(types.struct_members(None, "Nothing").is_none());
608        let names = types.enum_variants_by_name();
609        assert_eq!(names["Shop.Color"], ["Red", "Green", "Blue"]);
610        assert_eq!(names["Color"], ["Red", "Green", "Blue"]);
611        assert_eq!(names["Level"], ["Low", "High"]);
612    }
613
614    #[test]
615    fn a_bare_name_resolves_in_the_executing_contract_then_its_bases() {
616        let types = SourceTypes::parse(SOURCE);
617        // Which `Item` a bare name means depends on where it is written.
618        let width = |scope: Option<&str>| {
619            types
620                .struct_members(scope, "Item")
621                .map(|members| members[0].ty.clone())
622        };
623        assert_eq!(width(Some("Shop")).as_deref(), Some("uint256"));
624        assert_eq!(width(Some("Math")).as_deref(), Some("uint8"));
625        assert_eq!(width(Some("Base")).as_deref(), Some("uint16"));
626        // `Outlet` declares none: its nearest base `Base` does.
627        assert_eq!(width(Some("Outlet")).as_deref(), Some("uint16"));
628        // An unrelated contract falls back to the first declaration.
629        assert_eq!(width(Some("Other")).as_deref(), Some("uint256"));
630        assert_eq!(width(None).as_deref(), Some("uint256"));
631        // A file-level type wins over another contract's for a bare name.
632        assert_eq!(
633            types.struct_members(Some("Math"), "Pair").map(<[_]>::len),
634            Some(2)
635        );
636        // Inherited enums resolve the same way.
637        assert_eq!(types.enum_literal(Some("Outlet"), "Color.Blue"), Some(2));
638        // The scope of an offset is the innermost block containing it.
639        let offset = SOURCE.find("function g()").expect("g") as u64;
640        assert_eq!(types.scope_at(0, offset), Some("Outlet"));
641        assert_eq!(types.scope_at(0, 0), None);
642        assert_eq!(types.scope_at(1, offset), None);
643    }
644
645    #[test]
646    fn ignores_keywords_inside_identifiers_and_comments() {
647        let types = SourceTypes::parse(
648            "contract C { uint256 mystruct; /* struct Hidden { uint256 x; } */ enum E { A } }",
649        );
650        assert!(types.struct_members(None, "Hidden").is_none());
651        assert_eq!(types.enum_variants(None, "E").map(<[_]>::len), Some(1));
652    }
653}