Преглед на файлове

Fix recursive types in sema (#1131)

Cyrill Leutwiler преди 2 години
родител
ревизия
07b02e072a
променени са 46 файла, в които са добавени 737 реда и са изтрити 109 реда
  1. 1 0
      Cargo.toml
  2. 1 0
      src/codegen/expression.rs
  3. 1 0
      src/codegen/statements.rs
  4. 4 2
      src/sema/ast.rs
  5. 7 0
      src/sema/builtin.rs
  6. 13 0
      src/sema/builtin_structs.rs
  7. 1 1
      src/sema/expression/function_call.rs
  8. 14 0
      src/sema/functions.rs
  9. 3 2
      src/sema/namespace.rs
  10. 6 0
      src/sema/statements.rs
  11. 1 0
      src/sema/tests/mod.rs
  12. 294 103
      src/sema/types.rs
  13. 3 0
      src/sema/variables.rs
  14. 1 0
      src/sema/yul/expression.rs
  15. 1 0
      src/sema/yul/functions.rs
  16. 2 0
      src/sema/yul/tests/expression.rs
  17. 1 1
      tests/contract_testcases/evm/public_internal_function.sol
  18. 8 0
      tests/contract_testcases/solana/structs/recursive_01.dot
  19. 6 0
      tests/contract_testcases/solana/structs/recursive_01.sol
  20. 36 0
      tests/contract_testcases/solana/structs/recursive_02.dot
  21. 21 0
      tests/contract_testcases/solana/structs/recursive_02.sol
  22. 14 0
      tests/contract_testcases/solana/structs/recursive_04.dot
  23. 4 0
      tests/contract_testcases/solana/structs/recursive_04.sol
  24. 14 0
      tests/contract_testcases/substrate/function_types/recursive_01.dot
  25. 8 0
      tests/contract_testcases/substrate/function_types/recursive_01.sol
  26. 8 0
      tests/contract_testcases/substrate/function_types/recursive_02.dot
  27. 14 0
      tests/contract_testcases/substrate/function_types/recursive_02.sol
  28. 24 0
      tests/contract_testcases/substrate/function_types/recursive_03.dot
  29. 10 0
      tests/contract_testcases/substrate/function_types/recursive_03.sol
  30. 8 0
      tests/contract_testcases/substrate/structs/parse_structs_10.dot
  31. 16 0
      tests/contract_testcases/substrate/structs/parse_structs_16.dot
  32. 11 0
      tests/contract_testcases/substrate/structs/parse_structs_16.sol
  33. 14 0
      tests/contract_testcases/substrate/structs/parse_structs_17.dot
  34. 5 0
      tests/contract_testcases/substrate/structs/parse_structs_17.sol
  35. 28 0
      tests/contract_testcases/substrate/structs/parse_structs_18.dot
  36. 5 0
      tests/contract_testcases/substrate/structs/parse_structs_18.sol
  37. 14 0
      tests/contract_testcases/substrate/structs/parse_structs_19.dot
  38. 2 0
      tests/contract_testcases/substrate/structs/parse_structs_19.sol
  39. 6 0
      tests/contract_testcases/substrate/structs/parse_structs_20.dot
  40. 2 0
      tests/contract_testcases/substrate/structs/parse_structs_20.sol
  41. 46 0
      tests/contract_testcases/substrate/structs/parse_structs_21.dot
  42. 7 0
      tests/contract_testcases/substrate/structs/parse_structs_21.sol
  43. 24 0
      tests/contract_testcases/substrate/structs/parse_structs_22.dot
  44. 11 0
      tests/contract_testcases/substrate/structs/parse_structs_22.sol
  45. 12 0
      tests/contract_testcases/substrate/structs/parse_structs_23.dot
  46. 5 0
      tests/contract_testcases/substrate/structs/parse_structs_23.sol

+ 1 - 0
Cargo.toml

@@ -57,6 +57,7 @@ parse-display = "0.8.0"
 parity-scale-codec = "3.3"
 ink = "4.0.0-beta.1"
 scale-info = "2.3"
+petgraph = "0.6.3"
 
 [dev-dependencies]
 num-derive = "0.3"

+ 1 - 0
src/codegen/expression.rs

@@ -594,6 +594,7 @@ pub fn expression(
                 } else {
                     struct_ty.definition(ns).fields[..*field_no]
                         .iter()
+                        .filter(|field| !field.infinite_size)
                         .map(|field| field.ty.storage_slots(ns))
                         .sum()
                 };

+ 1 - 0
src/codegen/statements.rs

@@ -1105,6 +1105,7 @@ fn try_catch(
                             loc: pt::Loc::Codegen,
                             indexed: false,
                             readonly: false,
+                            infinite_size: false,
                             recursive: false,
                         })
                         .collect();

+ 4 - 2
src/sema/ast.rs

@@ -228,8 +228,10 @@ pub struct Parameter {
     pub indexed: bool,
     /// Some builtin structs have readonly fields
     pub readonly: bool,
-    /// A struct may contain itself which make the struct infinite size in
-    /// memory. This boolean specifies which field introduces the recursion.
+    /// A recursive struct may contain itself which make the struct infinite size in memory.
+    pub infinite_size: bool,
+    /// Is this struct field recursive. Recursive does not mean infinite size in all cases:
+    /// `struct S { S[] s }` is recursive but not of infinite size.
     pub recursive: bool,
 }
 

+ 7 - 0
src/sema/builtin.rs

@@ -1498,6 +1498,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -1507,6 +1508,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],
@@ -1517,6 +1519,7 @@ impl Namespace {
                 ty_loc: None,
                 readonly: false,
                 indexed: false,
+                infinite_size: false,
                 recursive: false,
             }],
             self,
@@ -1558,6 +1561,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -1567,6 +1571,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],
@@ -1578,6 +1583,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -1587,6 +1593,7 @@ impl Namespace {
                     ty_loc: None,
                     readonly: false,
                     indexed: false,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],

+ 13 - 0
src/sema/builtin_structs.rs

@@ -22,6 +22,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -34,6 +35,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -46,6 +48,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -58,6 +61,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -70,6 +74,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -82,6 +87,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -94,6 +100,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -106,6 +113,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: true,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],
@@ -128,6 +136,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -140,6 +149,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -152,6 +162,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],
@@ -171,6 +182,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 Parameter {
@@ -180,6 +192,7 @@ static BUILTIN_STRUCTS: Lazy<[StructDecl; 3]> = Lazy::new(|| {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
             ],

+ 1 - 1
src/sema/expression/function_call.rs

@@ -1379,7 +1379,7 @@ fn try_type_method(
                     Type::Bytes(_) => {
                         args_ty = Type::DynamicBytes;
                     }
-                    Type::Array(..) | Type::Struct(..) if !args_ty.is_dynamic(ns) => (),
+                    Type::Array(..) | Type::Struct(..) if !args_ty.is_dynamic(ns) => {}
                     _ => {
                         diagnostics.push(Diagnostic::error(
                             args.loc(),

+ 14 - 0
src/sema/functions.rs

@@ -452,6 +452,16 @@ pub fn contract_function(
         ns,
     );
 
+    if matches!(
+        visibility,
+        pt::Visibility::External(_) | pt::Visibility::Public(_) if params.iter().any(|p| p.ty.is_recursive(ns)))
+    {
+        ns.diagnostics.push(Diagnostic::error(
+            func.loc,
+            "Recursive parameter not allowed for public or external functions.".into(),
+        ))
+    }
+
     let mut fdecl = Function::new(
         func.loc,
         name,
@@ -915,6 +925,7 @@ pub fn resolve_params(
                     ty_loc: Some(ty_loc),
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 });
             }
@@ -1027,6 +1038,7 @@ pub fn resolve_returns(
                     ty_loc: Some(ty_loc),
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 });
             }
@@ -1066,6 +1078,7 @@ fn signatures() {
                 ty_loc: None,
                 indexed: false,
                 readonly: false,
+                infinite_size: false,
                 recursive: false,
             },
             Parameter {
@@ -1075,6 +1088,7 @@ fn signatures() {
                 ty_loc: None,
                 indexed: false,
                 readonly: false,
+                infinite_size: false,
                 recursive: false,
             },
         ],

+ 3 - 2
src/sema/namespace.rs

@@ -1381,8 +1381,9 @@ impl Namespace {
         }
     }
 
-    /// Generate the signature for the given name and parameters. Can be used
-    /// for both events and functions
+    /// Generate the signature for the given name and parameters; can be used for events and functions.
+    ///
+    /// Recursive arguments are invalid and default to a signature of `#recursive` to avoid stack overflows.
     pub fn signature(&self, name: &str, params: &[Parameter]) -> String {
         format!(
             "{}({})",

+ 6 - 0
src/sema/statements.rs

@@ -381,6 +381,7 @@ fn statement(
                         id: Some(decl.name.clone().unwrap()),
                         indexed: false,
                         readonly: false,
+                        infinite_size: false,
                         recursive: false,
                     },
                     initializer,
@@ -1331,6 +1332,7 @@ fn destructure(
                             ty_loc: Some(ty_loc),
                             indexed: false,
                             readonly: false,
+                            infinite_size: false,
                             recursive: false,
                         },
                     ));
@@ -2062,6 +2064,7 @@ fn try_catch(
                                 id: Some(name.clone()),
                                 indexed: false,
                                 readonly: false,
+                                infinite_size: false,
                                 recursive: false,
                             },
                         ));
@@ -2076,6 +2079,7 @@ fn try_catch(
                             indexed: false,
                             id: None,
                             readonly: false,
+                            infinite_size: false,
                             recursive: false,
                         },
                     ));
@@ -2158,6 +2162,7 @@ fn try_catch(
                         id: None,
                         indexed: false,
                         readonly: false,
+                        infinite_size: false,
                         recursive: false,
                     };
 
@@ -2240,6 +2245,7 @@ fn try_catch(
                     id: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 };
 

+ 1 - 0
src/sema/tests/mod.rs

@@ -35,6 +35,7 @@ fn test_statement_reachable() {
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 },
                 None,

+ 294 - 103
src/sema/types.rs

@@ -13,12 +13,19 @@ use super::{
 use crate::Target;
 use base58::{FromBase58, FromBase58Error};
 use indexmap::IndexMap;
+use itertools::Itertools;
 use num_bigint::BigInt;
 use num_traits::{One, Zero};
+use petgraph::algo::{all_simple_paths, tarjan_scc};
+use petgraph::stable_graph::IndexType;
+use petgraph::Directed;
+use solang_parser::diagnostics::Note;
 use solang_parser::{doccomment::DocComment, pt, pt::CodeLocation};
 use std::collections::HashSet;
 use std::{fmt::Write, ops::Mul};
 
+type Graph = petgraph::Graph<(), usize, Directed, usize>;
+
 /// List the types which should be resolved later
 pub struct ResolveFields<'a> {
     structs: Vec<ResolveStructFields<'a>>,
@@ -197,37 +204,125 @@ fn type_decl(
     });
 }
 
-/// check if a struct contains itself. This function calls itself recursively
-fn find_struct_recursion(struct_no: usize, structs_visited: &mut Vec<usize>, ns: &mut Namespace) {
-    let def = ns.structs[struct_no].clone();
-    let mut types_seen: HashSet<usize> = HashSet::new();
-
-    for (field_no, field) in def.fields.iter().enumerate() {
-        if let Type::Struct(StructType::UserDefined(field_struct_no)) = field.ty {
-            if types_seen.contains(&field_struct_no) {
-                continue;
+/// A struct field is considered to be of infinite size, if it contains itself infinite times (not in a vector).
+///
+/// This function sets the `infinitie_size` flag accordingly for all connections between `nodes`.
+/// `nodes` is assumed to be a set of strongly connected nodes from within the `graph`.
+///
+/// Any node (struct) can have one or more edges (types) to some other node (struct).
+/// A struct field is not of infinite size, if there are any 2 connecting nodes,
+/// where all edges between the 2 connecting nodes are mappings or dynamic arrays.
+///
+/// ```solidity
+/// struct A { B b; }                           // finite memory size
+/// struct B { A[] a; mapping (uint => A) m; }  // finite memory size
+///
+/// struct C { D d; }                           // infinite memory size
+/// struct D { C[] c1; C c2; }                  // infinite memory size
+/// ```
+fn check_infinite_struct_size(graph: &Graph, nodes: Vec<usize>, ns: &mut Namespace) {
+    let mut infinite_size = true;
+    let mut offenders = HashSet::new();
+    for (a, b) in nodes.windows(2).map(|w| (w[0], w[1])) {
+        let mut infinite_edge = false;
+        for edge in graph.edges_connecting(a.into(), b.into()) {
+            match &ns.structs[a].fields[*edge.weight()].ty {
+                Type::Array(_, dims) if dims.first() != Some(&ArrayLength::Dynamic) => {}
+                Type::Struct(StructType::UserDefined(_)) => {}
+                _ => continue,
             }
+            infinite_edge = true;
+            offenders.insert((a, *edge.weight()));
+        }
+        infinite_size &= infinite_edge;
+    }
+    if infinite_size {
+        for (struct_no, field_no) in offenders {
+            ns.structs[struct_no].fields[field_no].infinite_size = true;
+        }
+    }
+}
 
-            types_seen.insert(field_struct_no);
-
-            if structs_visited.contains(&field_struct_no) {
-                ns.diagnostics.push(Diagnostic::error_with_note(
-                    def.loc,
-                    format!("struct '{}' has infinite size", def.name),
-                    field.loc,
-                    format!("recursive field '{}'", field.name_as_str()),
-                ));
+/// A struct field is recursive, if it is connected to a cyclic path.
+///
+/// This function checks all structs in the `ns` for any paths leading into the given `node`.
+/// `node` is supposed to be inside a cycle.
+/// All affected struct fields will be flagged as recursive (and infinite size as well, if they are).
+fn check_recursive_struct_field(node: usize, graph: &Graph, ns: &mut Namespace) {
+    for n in 0..ns.structs.len() {
+        for path in all_simple_paths::<Vec<_>, &Graph>(graph, n.into(), node.into(), 0, None) {
+            for (a, b) in path.windows(2).map(|a_b| (a_b[0], a_b[1])) {
+                for edge in graph.edges_connecting(a, b) {
+                    ns.structs[a.index()].fields[*edge.weight()].recursive = true;
+                    if ns.structs[b.index()].fields.iter().any(|f| f.infinite_size) {
+                        ns.structs[a.index()].fields[*edge.weight()].infinite_size = true;
+                    }
+                }
+            }
+        }
+    }
+}
 
-                ns.structs[struct_no].fields[field_no].recursive = true;
-            } else {
-                structs_visited.push(field_struct_no);
-                find_struct_recursion(field_struct_no, structs_visited, ns);
-                structs_visited.pop();
+/// Find all other structs a given user struct may reach.
+///
+/// `edges` is a set with tuples of 3 dimensions. The first two are the connecting nodes (struct numbers).
+/// The last dimension is the field number of the first struct where the connection originates.
+fn collect_struct_edges(no: usize, edges: &mut HashSet<(usize, usize, usize)>, ns: &Namespace) {
+    for (field_no, field) in ns.structs[no].fields.iter().enumerate() {
+        for reaching in field.ty.user_struct_no(ns) {
+            if edges.insert((no, reaching, field_no)) {
+                collect_struct_edges(reaching, edges, ns)
             }
         }
     }
 }
 
+/// Checks for
+///   - Structs containing recursive (cycling) fields
+///   - Cycling struct fields of infinite size
+///
+/// The algorithm consists of these steps:
+///   1. All structs in the namespace are parsed into a graph.
+///      Nodes in the graph represent the structs.
+///      Edges develop when a struct encapsulates another struct.
+///      Edges have the originating struct field number as their weight.
+///      So we known from which struct field the connection originated later on.
+///   2. Find all Strongly Connected Components (SCC) in the graph.
+///   3. For any node inside in any SCC, if there is a non-trivial path from the node to itself, we've detected a cycle.
+///   4. For every cycle, check if it is of infinite memory size and flag involved struct fields accordingly.
+///   5. For any struct in the namespace, check if there are any cyclic paths stemming from it.
+///      If there are, flag the corresponding struct field as `recursive`.
+fn find_struct_recursion(ns: &mut Namespace) {
+    let mut edges = HashSet::new();
+    for n in 0..ns.structs.len() {
+        collect_struct_edges(n, &mut edges, ns);
+    }
+    let graph = Graph::from_edges(edges);
+    for n in tarjan_scc(&graph).iter().flatten().dedup() {
+        // Don't use None. It'll default to `node_count() - 1` and fail to find path for graphs like this: `A <-> B`
+        let max_len = Some(graph.node_count());
+        if let Some(cycle) = all_simple_paths::<Vec<_>, &Graph>(&graph, *n, *n, 0, max_len).next() {
+            check_infinite_struct_size(&graph, cycle.iter().map(|p| p.index()).collect(), ns);
+            check_recursive_struct_field(n.index(), &graph, ns);
+        }
+    }
+    for n in 0..ns.structs.len() {
+        let mut notes = vec![];
+        for field in ns.structs[n].fields.iter().filter(|f| f.infinite_size) {
+            let loc = field.loc;
+            let message = format!("recursive field '{}'", field.name_as_str());
+            notes.push(Note { loc, message });
+        }
+        if !notes.is_empty() {
+            ns.diagnostics.push(Diagnostic::error_with_notes(
+                ns.structs[n].loc,
+                format!("struct '{}' has infinite size", ns.structs[n].name),
+                notes,
+            ));
+        }
+    }
+}
+
 pub fn resolve_fields(delay: ResolveFields, file_no: usize, ns: &mut Namespace) {
     // now we can resolve the fields for the structs
     for resolve in delay.structs {
@@ -238,10 +333,8 @@ pub fn resolve_fields(delay: ResolveFields, file_no: usize, ns: &mut Namespace)
         ns.structs[resolve.struct_no].fields = fields;
     }
 
-    // struct can contain other structs, and we have to check for recursiveness,
-    // i.e. "struct a { b f1; } struct b { a f1; }"
-    (0..ns.structs.len())
-        .for_each(|struct_no| find_struct_recursion(struct_no, &mut vec![struct_no], ns));
+    // Handle recursive struct fields.
+    find_struct_recursion(ns);
 
     // Calculate the offset of each field in all the struct types
     struct_offsets(ns);
@@ -541,6 +634,7 @@ pub fn struct_decl(
             ty_loc: Some(field.ty.loc()),
             indexed: false,
             readonly: false,
+            infinite_size: false,
             recursive: false,
         });
     }
@@ -647,6 +741,7 @@ fn event_decl(
             ty_loc: Some(field.ty.loc()),
             indexed: field.indexed,
             readonly: false,
+            infinite_size: false,
             recursive: false,
         });
     }
@@ -797,7 +892,7 @@ fn struct_offsets(ns: &mut Namespace) {
 
                 offsets.push(offset.clone());
 
-                if !field.recursive {
+                if !field.infinite_size && ns.target == Target::Solana {
                     offset += field.ty.solana_storage_size(ns);
                 }
             }
@@ -823,7 +918,7 @@ fn struct_offsets(ns: &mut Namespace) {
             let mut largest_alignment = BigInt::zero();
 
             for field in &ns.structs[struct_no].fields {
-                if !field.recursive {
+                if !field.infinite_size {
                     let alignment = field.ty.storage_align(ns);
                     largest_alignment = std::cmp::max(alignment.clone(), largest_alignment.clone());
                     let remainder = offset.clone() % alignment.clone();
@@ -862,6 +957,23 @@ fn struct_offsets(ns: &mut Namespace) {
 }
 
 impl Type {
+    /// Return the set of user defined structs this type encapsulates.
+    pub fn user_struct_no(&self, ns: &Namespace) -> HashSet<usize> {
+        match self {
+            Type::Struct(StructType::UserDefined(n)) => HashSet::from([*n]),
+            Type::Mapping(Mapping { key, value, .. }) => {
+                let mut result = key.user_struct_no(ns);
+                result.extend(value.user_struct_no(ns));
+                result
+            }
+            Type::Array(ty, _) | Type::Ref(ty) | Type::Slice(ty) | Type::StorageRef(_, ty) => {
+                ty.user_struct_no(ns)
+            }
+            Type::UserType(no) => ns.user_types[*no].ty.user_struct_no(ns),
+            _ => HashSet::new(),
+        }
+    }
+
     pub fn to_string(&self, ns: &Namespace) -> String {
         match self {
             Type::Bool => "bool".to_string(),
@@ -948,7 +1060,9 @@ impl Type {
             Type::Contract(n) => format!("contract {}", ns.contracts[*n].name),
             Type::UserType(n) => format!("usertype {}", ns.user_types[*n]),
             Type::Ref(r) => r.to_string(ns),
-            Type::StorageRef(_, ty) => format!("{} storage", ty.to_string(ns)),
+            Type::StorageRef(_, ty) => {
+                format!("{} storage", ty.to_string(ns))
+            }
             Type::Void => "void".into(),
             Type::Unreachable => "unreachable".into(),
             // A slice of bytes1 is like bytes
@@ -1011,7 +1125,11 @@ impl Type {
                         .definition(ns)
                         .fields
                         .iter()
-                        .map(|f| f.ty.to_signature_string(say_tuple, ns))
+                        .map(|f| if f.recursive {
+                            "#recursive".into() // recursive types in public interfaces are not allowed
+                        } else {
+                            f.ty.to_signature_string(say_tuple, ns)
+                        })
                         .collect::<Vec<String>>()
                         .join(",")
                 )
@@ -1060,7 +1178,7 @@ impl Type {
             Type::Bytes(_) => false,
             Type::Enum(_) => false,
             Type::Struct(_) => true,
-            Type::Array(_, dims) => matches!(dims.last(), Some(ArrayLength::Fixed(_))),
+            Type::Array(_, dims) => !dims.iter().any(|d| *d == ArrayLength::Dynamic),
             Type::DynamicBytes => false,
             Type::String => false,
             Type::Mapping(..) => false,
@@ -1129,13 +1247,16 @@ impl Type {
             Type::Value => BigInt::from(ns.value_length),
             Type::Uint(n) | Type::Int(n) => BigInt::from(n / 8),
             Type::Rational => unreachable!(),
+            Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => {
+                (ns.target.ptr_size() / 8).into()
+            }
             Type::Array(ty, dims) => {
-                let pointer_size = BigInt::from(ns.target.ptr_size() / 8);
+                let pointer_size = (ns.target.ptr_size() / 8).into();
                 ty.memory_size_of(ns).mul(
                     dims.iter()
                         .map(|d| match d {
-                            ArrayLength::Dynamic => &pointer_size,
                             ArrayLength::Fixed(n) => n,
+                            ArrayLength::Dynamic => &pointer_size,
                             ArrayLength::AnyFixed => unreachable!(),
                         })
                         .product::<BigInt>(),
@@ -1219,18 +1340,15 @@ impl Type {
     /// which is not accounted for.
     pub fn solana_storage_size(&self, ns: &Namespace) -> BigInt {
         match self {
-            Type::Array(ty, dims) => {
-                let pointer_size = BigInt::from(4);
-                ty.solana_storage_size(ns).mul(
-                    dims.iter()
-                        .map(|d| match d {
-                            ArrayLength::Dynamic => &pointer_size,
-                            ArrayLength::Fixed(d) => d,
-                            ArrayLength::AnyFixed => panic!("unknown length"),
-                        })
-                        .product::<BigInt>(),
-                )
-            }
+            Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => 4.into(),
+            Type::Array(ty, dims) => ty.solana_storage_size(ns).mul(
+                dims.iter()
+                    .map(|d| match d {
+                        ArrayLength::Fixed(d) => d,
+                        _ => panic!("unknown length"),
+                    })
+                    .product::<BigInt>(),
+            ),
             Type::Struct(str_ty) => str_ty
                 .definition(ns)
                 .offsets
@@ -1261,9 +1379,10 @@ impl Type {
                 .definition(ns)
                 .fields
                 .iter()
-                .map(|f| if f.recursive { 1 } else { f.ty.align_of(ns) })
+                .filter(|f| !f.infinite_size) // Can't calculate alignment for structs with infinite recursion
+                .map(|f| f.ty.align_of(ns))
                 .max()
-                .unwrap(),
+                .unwrap_or(1), // All fields had infinite size, so we just pretend the alignment is one
             Type::InternalFunction { .. } => ns.target.ptr_size().into(),
             _ => 1,
         }
@@ -1328,9 +1447,6 @@ impl Type {
             Type::Rational => true,
             Type::Ref(r) => r.is_rational(),
             Type::StorageRef(_, r) => r.is_rational(),
-            Type::Int(_) => false,
-            Type::Uint(_) => false,
-            Type::Value => false,
             _ => false,
         }
     }
@@ -1347,7 +1463,7 @@ impl Type {
                 Type::Value => BigInt::from(ns.value_length),
                 Type::Uint(n) | Type::Int(n) => BigInt::from(n / 8),
                 Type::Rational => unreachable!(),
-                Type::Array(_, dims) if dims.last() == Some(&ArrayLength::Dynamic) => {
+                Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => {
                     BigInt::from(4)
                 }
                 Type::Array(ty, dims) => {
@@ -1393,17 +1509,12 @@ impl Type {
                     .definition(ns)
                     .fields
                     .iter()
-                    .map(|f| {
-                        if f.recursive {
-                            BigInt::one()
-                        } else {
-                            f.ty.storage_slots(ns)
-                        }
-                    })
+                    .filter(|f| !f.infinite_size)
+                    .map(|f| f.ty.storage_slots(ns))
                     .sum(),
+                Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => 1.into(),
                 Type::Array(ty, dims) => {
-                    let one = BigInt::one();
-
+                    let one = 1.into();
                     ty.storage_slots(ns)
                         * dims
                             .iter()
@@ -1432,7 +1543,7 @@ impl Type {
                 Type::Value => BigInt::from(ns.value_length),
                 Type::Uint(n) | Type::Int(n) => BigInt::from(n / 8),
                 Type::Rational => unreachable!(),
-                Type::Array(_, dims) if dims.last() == Some(&ArrayLength::Dynamic) => {
+                Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => {
                     BigInt::from(4)
                 }
                 Type::Array(ty, _) => {
@@ -1446,15 +1557,10 @@ impl Type {
                     .definition(ns)
                     .fields
                     .iter()
-                    .map(|field| {
-                        if field.recursive {
-                            BigInt::one()
-                        } else {
-                            field.ty.storage_align(ns)
-                        }
-                    })
+                    .filter(|field| !field.infinite_size)
+                    .map(|field| field.ty.storage_align(ns))
                     .max()
-                    .unwrap(),
+                    .unwrap_or_else(|| 1.into()), // All fields have infinite size, so we pretend one storage slot.
                 Type::String | Type::DynamicBytes => BigInt::from(4),
                 Type::InternalFunction { .. } => BigInt::from(ns.target.ptr_size()),
                 Type::ExternalFunction { .. } => BigInt::from(ns.address_length),
@@ -1503,25 +1609,28 @@ impl Type {
 
     /// Does this type contain any types which are variable-length
     pub fn is_dynamic(&self, ns: &Namespace) -> bool {
-        match self {
+        self.is_dynamic_internal(ns, &mut HashSet::new())
+    }
+
+    fn is_dynamic_internal(&self, ns: &Namespace, structs_visited: &mut HashSet<usize>) -> bool {
+        self.guarded_recursion(structs_visited, false, |structs_visited| match self {
             Type::String | Type::DynamicBytes => true,
-            Type::Ref(r) => r.is_dynamic(ns),
+            Type::Ref(r) => r.is_dynamic_internal(ns, structs_visited),
             Type::Array(ty, dim) => {
                 if dim.iter().any(|d| d == &ArrayLength::Dynamic) {
                     return true;
                 }
-
-                ty.is_dynamic(ns)
+                ty.is_dynamic_internal(ns, structs_visited)
             }
             Type::Struct(str_ty) => str_ty
                 .definition(ns)
                 .fields
                 .iter()
-                .any(|f| f.ty.is_dynamic(ns)),
-            Type::StorageRef(_, r) => r.is_dynamic(ns),
+                .any(|f| f.ty.is_dynamic_internal(ns, structs_visited)),
+            Type::StorageRef(_, r) => r.is_dynamic_internal(ns, structs_visited),
             Type::Slice(_) => true,
             _ => false,
-        }
+        })
     }
 
     /// Can this type have a calldata, memory, or storage location. This is to be
@@ -1580,32 +1689,50 @@ impl Type {
 
     /// Does the type contain any mapping type
     pub fn contains_mapping(&self, ns: &Namespace) -> bool {
-        match self {
+        self.contains_mapping_internal(ns, &mut HashSet::new())
+    }
+
+    fn contains_mapping_internal(
+        &self,
+        ns: &Namespace,
+        structs_visited: &mut HashSet<usize>,
+    ) -> bool {
+        self.guarded_recursion(structs_visited, false, |structs_visited| match self {
             Type::Mapping(..) => true,
-            Type::Array(ty, _) => ty.contains_mapping(ns),
+            Type::Array(ty, _) => ty.contains_mapping_internal(ns, structs_visited),
             Type::Struct(str_ty) => str_ty
                 .definition(ns)
                 .fields
                 .iter()
-                .any(|f| !f.recursive && f.ty.contains_mapping(ns)),
-            Type::StorageRef(_, r) | Type::Ref(r) => r.contains_mapping(ns),
+                .any(|f| f.ty.contains_mapping_internal(ns, structs_visited)),
+            Type::StorageRef(_, r) | Type::Ref(r) => {
+                r.contains_mapping_internal(ns, structs_visited)
+            }
             _ => false,
-        }
+        })
     }
 
     /// Does the type contain any internal function type
     pub fn contains_internal_function(&self, ns: &Namespace) -> bool {
-        match self {
+        self.contains_internal_function_internal(ns, &mut HashSet::new())
+    }
+
+    fn contains_internal_function_internal(
+        &self,
+        ns: &Namespace,
+        structs_visited: &mut HashSet<usize>,
+    ) -> bool {
+        self.guarded_recursion(structs_visited, false, |structs_visited| match self {
             Type::InternalFunction { .. } => true,
-            Type::Array(ty, _) => ty.contains_internal_function(ns),
-            Type::Struct(str_ty) => str_ty
-                .definition(ns)
-                .fields
-                .iter()
-                .any(|f| !f.recursive && f.ty.contains_internal_function(ns)),
-            Type::StorageRef(_, r) | Type::Ref(r) => r.contains_internal_function(ns),
+            Type::Array(ty, _) => ty.contains_internal_function_internal(ns, structs_visited),
+            Type::Struct(str_ty) => str_ty.definition(ns).fields.iter().any(|f| {
+                f.ty.contains_internal_function_internal(ns, structs_visited)
+            }),
+            Type::StorageRef(_, r) | Type::Ref(r) => {
+                r.contains_internal_function_internal(ns, structs_visited)
+            }
             _ => false,
-        }
+        })
     }
 
     /// Is this structure a builtin
@@ -1629,22 +1756,29 @@ impl Type {
         ns: &'a Namespace,
         builtin: &StructType,
     ) -> Option<&'a Type> {
-        match self {
-            Type::Array(ty, _) => ty.contains_builtins(ns, builtin),
+        self.contains_builtins_internal(ns, builtin, &mut HashSet::new())
+    }
+
+    fn contains_builtins_internal<'a>(
+        &'a self,
+        ns: &'a Namespace,
+        builtin: &StructType,
+        structs_visited: &mut HashSet<usize>,
+    ) -> Option<&'a Type> {
+        self.guarded_recursion(structs_visited, None, |structs_visited| match self {
+            Type::Array(ty, _) => ty.contains_builtins_internal(ns, builtin, structs_visited),
             Type::Mapping(Mapping { key, value, .. }) => key
-                .contains_builtins(ns, builtin)
-                .or_else(|| value.contains_builtins(ns, builtin)),
+                .contains_builtins_internal(ns, builtin, structs_visited)
+                .or_else(|| value.contains_builtins_internal(ns, builtin, structs_visited)),
             Type::Struct(str_ty) if str_ty == builtin => Some(self),
             Type::Struct(str_ty) => str_ty.definition(ns).fields.iter().find_map(|f| {
-                if f.recursive {
-                    None
-                } else {
-                    f.ty.contains_builtins(ns, builtin)
-                }
+                f.ty.contains_builtins_internal(ns, builtin, structs_visited)
             }),
-            Type::StorageRef(_, r) | Type::Ref(r) => r.contains_builtins(ns, builtin),
+            Type::StorageRef(_, r) | Type::Ref(r) => {
+                r.contains_builtins_internal(ns, builtin, structs_visited)
+            }
             _ => None,
-        }
+        })
     }
 
     /// If the type is Ref or StorageRef, get the underlying type
@@ -1718,7 +1852,7 @@ impl Type {
     pub fn is_sparse_solana(&self, ns: &Namespace) -> bool {
         match self.deref_any() {
             Type::Mapping(..) => true,
-            Type::Array(_, dims) if dims.last() == Some(&ArrayLength::Dynamic) => false,
+            Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => false,
             Type::Array(ty, dims) => {
                 let pointer_size = BigInt::from(4);
                 let len = ty.storage_slots(ns).mul(
@@ -1735,6 +1869,63 @@ impl Type {
             _ => false,
         }
     }
+
+    // Does this type contain itself
+    pub fn is_recursive(&self, ns: &Namespace) -> bool {
+        match self {
+            Type::Struct(StructType::UserDefined(n)) => {
+                ns.structs[*n].fields.iter().any(|f| f.recursive)
+            }
+            Type::Mapping(Mapping { key, value, .. }) => {
+                key.is_recursive(ns) || value.is_recursive(ns)
+            }
+            Type::Array(ty, _) | Type::Ref(ty) | Type::Slice(ty) | Type::StorageRef(_, ty) => {
+                ty.is_recursive(ns)
+            }
+            Type::UserType(no) => ns.user_types[*no].ty.is_recursive(ns),
+            _ => false,
+        }
+    }
+
+    /// Helper function to safely recurse over a `Type`, preventing stack overflows.
+    ///
+    /// `F` is expected to be a closure that recursively walks the `Type`.
+    /// `O` is the output type of the closure.
+    ///
+    /// `structs_visited` is the set of already visited structs. It is automatically updated for each struct already seen.
+    /// `bail` is the value that should be returned in case an infinite recursion occured.
+    /// `f` is the closure being called by this function.
+    ///
+    /// This function is useful in the various scenarios.
+    ///
+    /// Naturally, it can be used to detect recursive types (see `fn Type::is_recursive()`).
+    ///
+    /// Moreover, functions like `fn Type::contains_mapping()` need to recursively check if the type contains mappings.
+    /// Consider the following valid type:
+    ///
+    /// ```solidity
+    /// struct A { B b; }
+    /// struct B { A[] a; }
+    /// ```
+    ///
+    /// Looking at nested or referential types individually does not work here. This can only be done recursively;
+    /// however, a naive recursion will run indefinitely.
+    /// Now, thanks to the `Type::guarded_recursion()` wrapper, instead of overflowing the stack,
+    /// `fn Type::contains_mapping()` safely bails out using a value of `false`.
+    /// This makes sense because:
+    /// - In `Type::contains_mapping`, the mapping type is the only type to return true
+    /// - Mappings do not recursively call `contains_mapping`
+    fn guarded_recursion<F, O>(&self, structs_visited: &mut HashSet<usize>, bail: O, f: F) -> O
+    where
+        F: FnOnce(&mut HashSet<usize>) -> O,
+    {
+        if let Type::Struct(StructType::UserDefined(n)) = self {
+            if !structs_visited.insert(*n) {
+                return bail;
+            }
+        }
+        f(structs_visited)
+    }
 }
 
 /// These names cannot be used on Windows, even with an extension.

+ 3 - 0
src/sema/variables.rs

@@ -584,6 +584,7 @@ fn collect_parameters(
                 ty_loc: None,
                 indexed: false,
                 readonly: false,
+                infinite_size: false,
                 recursive: false,
             });
 
@@ -634,6 +635,7 @@ fn collect_parameters(
                     ty_loc: None,
                     indexed: false,
                     readonly: false,
+                    infinite_size: false,
                     recursive: false,
                 });
             }
@@ -651,6 +653,7 @@ fn collect_parameters(
             ty_loc: None,
             indexed: false,
             readonly: false,
+            infinite_size: false,
             recursive: false,
         },
     }

+ 1 - 0
src/sema/yul/expression.rs

@@ -423,6 +423,7 @@ pub(crate) fn resolve_function_call(
             ty_loc: None,
             indexed: false,
             readonly: false,
+            infinite_size: false,
             recursive: false,
         };
 

+ 1 - 0
src/sema/yul/functions.rs

@@ -155,6 +155,7 @@ fn process_parameters(parameters: &[pt::YulTypedIdentifier], ns: &mut Namespace)
             indexed: false,
             id: Some(item.id.clone()),
             readonly: false,
+            infinite_size: false,
             recursive: false,
         });
     }

+ 2 - 0
src/sema/yul/tests/expression.rs

@@ -732,6 +732,7 @@ fn check_arguments() {
                 ty_loc: None,
                 indexed: false,
                 readonly: false,
+                infinite_size: false,
                 recursive: false,
             },
             Parameter {
@@ -744,6 +745,7 @@ fn check_arguments() {
                 ty_loc: None,
                 indexed: false,
                 readonly: false,
+                infinite_size: false,
                 recursive: false,
             },
         ],

+ 1 - 1
tests/contract_testcases/evm/public_internal_function.sol

@@ -4,4 +4,4 @@ contract foo {
     }
 
     A[] public map;
-}
+}

+ 8 - 0
tests/contract_testcases/solana/structs/recursive_01.dot

@@ -0,0 +1,8 @@
+strict digraph "tests/contract_testcases/solana/structs/recursive_01.sol" {
+	S [label="name:S\ncontract: C\ntests/contract_testcases/solana/structs/recursive_01.sol:2:9-10\nfield name:f1 ty:int256\nfield name:f2 ty:struct C.S[]"]
+	contract [label="contract C\ntests/contract_testcases/solana/structs/recursive_01.sol:1:1-6:2"]
+	diagnostic [label="found contract 'C'\nlevel Debug\ntests/contract_testcases/solana/structs/recursive_01.sol:1:1-6:2"]
+	structs -> S
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+}

+ 6 - 0
tests/contract_testcases/solana/structs/recursive_01.sol

@@ -0,0 +1,6 @@
+contract C {
+	struct S {
+		int f1;
+		S[] f2;
+	}
+}

+ 36 - 0
tests/contract_testcases/solana/structs/recursive_02.dot

@@ -0,0 +1,36 @@
+strict digraph "tests/contract_testcases/solana/structs/recursive_02.sol" {
+	C [label="name:C\ncontract: con\ntests/contract_testcases/solana/structs/recursive_02.sol:3:20-21\nfield name:val ty:uint256\nfield name:b ty:struct con.B"]
+	D [label="name:D\ncontract: con\ntests/contract_testcases/solana/structs/recursive_02.sol:8:20-21\nfield name:c ty:struct con.C"]
+	B [label="name:B\ncontract: con\ntests/contract_testcases/solana/structs/recursive_02.sol:12:20-21\nfield name:d ty:struct con.D"]
+	A [label="name:A\ncontract: con\ntests/contract_testcases/solana/structs/recursive_02.sol:16:20-21\nfield name:d ty:struct con.D\nfield name:b ty:struct con.B\nfield name:c ty:struct con.C"]
+	contract [label="contract con\ntests/contract_testcases/solana/structs/recursive_02.sol:2:9-21:10"]
+	diagnostic [label="found contract 'con'\nlevel Debug\ntests/contract_testcases/solana/structs/recursive_02.sol:2:9-21:10"]
+	diagnostic_9 [label="contract name 'con' is reserved file name on Windows\nlevel Error\ntests/contract_testcases/solana/structs/recursive_02.sol:2:18-21"]
+	diagnostic_10 [label="struct 'C' has infinite size\nlevel Error\ntests/contract_testcases/solana/structs/recursive_02.sol:3:20-21"]
+	note [label="recursive field 'b'\ntests/contract_testcases/solana/structs/recursive_02.sol:5:17-20"]
+	diagnostic_12 [label="struct 'D' has infinite size\nlevel Error\ntests/contract_testcases/solana/structs/recursive_02.sol:8:20-21"]
+	note_13 [label="recursive field 'c'\ntests/contract_testcases/solana/structs/recursive_02.sol:9:17-20"]
+	diagnostic_14 [label="struct 'B' has infinite size\nlevel Error\ntests/contract_testcases/solana/structs/recursive_02.sol:12:20-21"]
+	note_15 [label="recursive field 'd'\ntests/contract_testcases/solana/structs/recursive_02.sol:13:17-20"]
+	diagnostic_16 [label="struct 'A' has infinite size\nlevel Error\ntests/contract_testcases/solana/structs/recursive_02.sol:16:20-21"]
+	note_17 [label="recursive field 'd'\ntests/contract_testcases/solana/structs/recursive_02.sol:17:17-20"]
+	note_18 [label="recursive field 'b'\ntests/contract_testcases/solana/structs/recursive_02.sol:18:17-20"]
+	note_19 [label="recursive field 'c'\ntests/contract_testcases/solana/structs/recursive_02.sol:19:17-20"]
+	structs -> C
+	structs -> D
+	structs -> B
+	structs -> A
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_9 [label="Error"]
+	diagnostics -> diagnostic_10 [label="Error"]
+	diagnostic_10 -> note [label="note"]
+	diagnostics -> diagnostic_12 [label="Error"]
+	diagnostic_12 -> note_13 [label="note"]
+	diagnostics -> diagnostic_14 [label="Error"]
+	diagnostic_14 -> note_15 [label="note"]
+	diagnostics -> diagnostic_16 [label="Error"]
+	diagnostic_16 -> note_17 [label="note"]
+	diagnostic_16 -> note_18 [label="note"]
+	diagnostic_16 -> note_19 [label="note"]
+}

+ 21 - 0
tests/contract_testcases/solana/structs/recursive_02.sol

@@ -0,0 +1,21 @@
+
+        contract con {
+            struct C {
+                uint256 val;
+                B b;
+            }
+
+            struct D {
+                C c;
+            }
+
+            struct B {
+                D d;
+            }
+
+            struct A {
+                D d;
+                B b;
+                C c;
+            }
+        }

+ 14 - 0
tests/contract_testcases/solana/structs/recursive_04.dot

@@ -0,0 +1,14 @@
+strict digraph "tests/contract_testcases/solana/structs/recursive_04.sol" {
+	B [label="name:B\ncontract: Recursive\ntests/contract_testcases/solana/structs/recursive_04.sol:2:12-13\nfield name:b ty:struct Recursive.B[]"]
+	C [label="name:C\ncontract: Recursive\ntests/contract_testcases/solana/structs/recursive_04.sol:3:12-13\nfield name:b ty:struct Recursive.B[]\nfield name:c ty:struct Recursive.C"]
+	contract [label="contract Recursive\ntests/contract_testcases/solana/structs/recursive_04.sol:1:1-4:2"]
+	diagnostic [label="found abstract contract 'Recursive'\nlevel Debug\ntests/contract_testcases/solana/structs/recursive_04.sol:1:1-4:2"]
+	diagnostic_7 [label="struct 'C' has infinite size\nlevel Error\ntests/contract_testcases/solana/structs/recursive_04.sol:3:12-13"]
+	note [label="recursive field 'c'\ntests/contract_testcases/solana/structs/recursive_04.sol:3:23-26"]
+	structs -> B
+	structs -> C
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_7 [label="Error"]
+	diagnostic_7 -> note [label="note"]
+}

+ 4 - 0
tests/contract_testcases/solana/structs/recursive_04.sol

@@ -0,0 +1,4 @@
+abstract contract Recursive {
+    struct B { B[] b; }
+    struct C { B[] b; C c; }
+}

+ 14 - 0
tests/contract_testcases/substrate/function_types/recursive_01.dot

@@ -0,0 +1,14 @@
+strict digraph "tests/contract_testcases/substrate/function_types/recursive_01.sol" {
+	S [label="name:S\ncontract: C\ntests/contract_testcases/substrate/function_types/recursive_01.sol:2:9-10\nfield name:f1 ty:int256\nfield name:f2 ty:struct C.S[]"]
+	contract [label="contract C\ntests/contract_testcases/substrate/function_types/recursive_01.sol:1:1-8:2"]
+	foo [label="function foo\ncontract: C\ntests/contract_testcases/substrate/function_types/recursive_01.sol:7:2-26\nsignature foo((int256,#recursive))\nvisibility public\nmutability nonpayable"]
+	parameters [label="parameters\nstruct C.S s"]
+	diagnostic [label="found contract 'C'\nlevel Debug\ntests/contract_testcases/substrate/function_types/recursive_01.sol:1:1-8:2"]
+	diagnostic_8 [label="Recursive parameter not allowed for public or external functions.\nlevel Error\ntests/contract_testcases/substrate/function_types/recursive_01.sol:7:2-26"]
+	structs -> S
+	contracts -> contract
+	contract -> foo [label="function"]
+	foo -> parameters [label="parameters"]
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_8 [label="Error"]
+}

+ 8 - 0
tests/contract_testcases/substrate/function_types/recursive_01.sol

@@ -0,0 +1,8 @@
+contract C {
+	struct S {
+		int f1;
+		S[] f2;
+	}
+
+	function foo(S s) public {}
+}

+ 8 - 0
tests/contract_testcases/substrate/function_types/recursive_02.dot

@@ -0,0 +1,8 @@
+strict digraph "tests/contract_testcases/substrate/function_types/recursive_02.sol" {
+	S [label="name:S\ntests/contract_testcases/substrate/function_types/recursive_02.sol:1:8-9\nfield name:f1 ty:int256\nfield name:f2 ty:struct S[]"]
+	foo [label="function foo\ntests/contract_testcases/substrate/function_types/recursive_02.sol:6:1-30\nsignature foo((int256,#recursive))\nvisibility internal\nmutability pure"]
+	parameters [label="parameters\nstruct S s"]
+	structs -> S
+	free_functions -> foo [label="function"]
+	foo -> parameters [label="parameters"]
+}

+ 14 - 0
tests/contract_testcases/substrate/function_types/recursive_02.sol

@@ -0,0 +1,14 @@
+struct S {
+    int256 f1;
+    S[] f2;
+}
+
+function foo(S memory s) pure {}
+
+// FIXME (stack overrun in emit):
+// contract Foo {
+//	function bar() public {
+//		S memory s = S({ f1: 1, f2: new S[](0) });
+//		foo(s);
+//	}
+// }

+ 24 - 0
tests/contract_testcases/substrate/function_types/recursive_03.dot

@@ -0,0 +1,24 @@
+strict digraph "tests/contract_testcases/substrate/function_types/recursive_03.sol" {
+	S [label="name:S\ncontract: Test\ntests/contract_testcases/substrate/function_types/recursive_03.sol:2:9-10\nfield name:foo ty:int256\nfield name:s ty:struct Test.S[]"]
+	contract [label="contract Test\ntests/contract_testcases/substrate/function_types/recursive_03.sol:1:1-10:2"]
+	test [label="function test\ncontract: Test\ntests/contract_testcases/substrate/function_types/recursive_03.sol:7:2-49\nsignature test(int256,(int256,#recursive)[])\nvisibility public\nmutability nonpayable"]
+	parameters [label="parameters\nint256 f\nstruct Test.S[] ss"]
+	returns [label="returns\nstruct Test.S "]
+	return [label="return\ntests/contract_testcases/substrate/function_types/recursive_03.sol:8:3-18"]
+	struct_literal [label="struct literal: struct Test.S\ntests/contract_testcases/substrate/function_types/recursive_03.sol:8:10-18"]
+	variable [label="variable: f\nint256\ntests/contract_testcases/substrate/function_types/recursive_03.sol:8:12-13"]
+	variable_10 [label="variable: ss\nstruct Test.S[]\ntests/contract_testcases/substrate/function_types/recursive_03.sol:8:15-17"]
+	diagnostic [label="found contract 'Test'\nlevel Debug\ntests/contract_testcases/substrate/function_types/recursive_03.sol:1:1-10:2"]
+	diagnostic_13 [label="Recursive parameter not allowed for public or external functions.\nlevel Error\ntests/contract_testcases/substrate/function_types/recursive_03.sol:7:2-49"]
+	structs -> S
+	contracts -> contract
+	contract -> test [label="function"]
+	test -> parameters [label="parameters"]
+	test -> returns [label="returns"]
+	test -> return [label="body"]
+	return -> struct_literal [label="expr"]
+	struct_literal -> variable [label="arg #0"]
+	struct_literal -> variable_10 [label="arg #1"]
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_13 [label="Error"]
+}

+ 10 - 0
tests/contract_testcases/substrate/function_types/recursive_03.sol

@@ -0,0 +1,10 @@
+contract Test {
+	struct S {
+		int foo;
+		S[] s;
+	}
+
+	function test(int f, S[] ss) public returns (S) {
+		return S(f, ss);
+	}
+}

+ 8 - 0
tests/contract_testcases/substrate/structs/parse_structs_10.dot

@@ -12,6 +12,10 @@ strict digraph "tests/contract_testcases/substrate/structs/parse_structs_10.sol"
 	note_13 [label="recursive field 'c'\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:9:17-20"]
 	diagnostic_14 [label="struct 'B' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:12:20-21"]
 	note_15 [label="recursive field 'd'\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:13:17-20"]
+	diagnostic_16 [label="struct 'A' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:16:20-21"]
+	note_17 [label="recursive field 'd'\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:17:17-20"]
+	note_18 [label="recursive field 'b'\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:18:17-20"]
+	note_19 [label="recursive field 'c'\ntests/contract_testcases/substrate/structs/parse_structs_10.sol:19:17-20"]
 	structs -> C
 	structs -> D
 	structs -> B
@@ -25,4 +29,8 @@ strict digraph "tests/contract_testcases/substrate/structs/parse_structs_10.sol"
 	diagnostic_12 -> note_13 [label="note"]
 	diagnostics -> diagnostic_14 [label="Error"]
 	diagnostic_14 -> note_15 [label="note"]
+	diagnostics -> diagnostic_16 [label="Error"]
+	diagnostic_16 -> note_17 [label="note"]
+	diagnostic_16 -> note_18 [label="note"]
+	diagnostic_16 -> note_19 [label="note"]
 }

+ 16 - 0
tests/contract_testcases/substrate/structs/parse_structs_16.dot

@@ -0,0 +1,16 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_16.sol" {
+	MapTree [label="name:MapTree\ncontract: AddressTree\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:2:12-19\nfield name:Items ty:mapping(uint256 => struct AddressTree.MapTree)"]
+	ArrayTreeDynamic [label="name:ArrayTreeDynamic\ncontract: AddressTree\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:5:12-28\nfield name:Arr ty:struct AddressTree.ArrayTreeDynamic[]"]
+	ArrayTreeFixed [label="name:ArrayTreeFixed\ncontract: AddressTree\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:8:12-26\nfield name:Arr ty:struct AddressTree.ArrayTreeFixed[2]"]
+	contract [label="contract AddressTree\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:1:1-11:2"]
+	diagnostic [label="found abstract contract 'AddressTree'\nlevel Debug\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:1:1-11:2"]
+	diagnostic_8 [label="struct 'ArrayTreeFixed' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:8:12-26"]
+	note [label="recursive field 'Arr'\ntests/contract_testcases/substrate/structs/parse_structs_16.sol:9:9-30"]
+	structs -> MapTree
+	structs -> ArrayTreeDynamic
+	structs -> ArrayTreeFixed
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_8 [label="Error"]
+	diagnostic_8 -> note [label="note"]
+}

+ 11 - 0
tests/contract_testcases/substrate/structs/parse_structs_16.sol

@@ -0,0 +1,11 @@
+abstract contract AddressTree {
+    struct MapTree {
+        mapping(uint256 => MapTree) Items; // OK
+    }
+    struct ArrayTreeDynamic {
+        ArrayTreeDynamic[] Arr; // OK
+    }
+    struct ArrayTreeFixed {
+        ArrayTreeFixed[2] Arr; // Recursive struct definision
+    }
+}

+ 14 - 0
tests/contract_testcases/substrate/structs/parse_structs_17.dot

@@ -0,0 +1,14 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_17.sol" {
+	A [label="name:A\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:1:8-9\nfield name:b ty:struct B"]
+	B [label="name:B\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:2:8-9\nfield name:a ty:struct A[]\nfield name:m ty:mapping(uint256 => struct A)"]
+	C [label="name:C\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:3:8-9\nfield name:b ty:struct B\nfield name:d ty:struct D"]
+	D [label="name:D\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:4:8-9\nfield name:e ty:uint256"]
+	contract [label="contract Foo\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:5:1-25"]
+	diagnostic [label="found abstract contract 'Foo'\nlevel Debug\ntests/contract_testcases/substrate/structs/parse_structs_17.sol:5:1-25"]
+	structs -> A
+	structs -> B
+	structs -> C
+	structs -> D
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+}

+ 5 - 0
tests/contract_testcases/substrate/structs/parse_structs_17.sol

@@ -0,0 +1,5 @@
+struct A { B b; }
+struct B { A[] a; mapping(uint=>A) m; }
+struct C { B b; D d; }
+struct D { uint e; }
+abstract contract Foo {}

+ 28 - 0
tests/contract_testcases/substrate/structs/parse_structs_18.dot

@@ -0,0 +1,28 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_18.sol" {
+	A [label="name:A\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:1:8-9\nfield name:b ty:struct B"]
+	B [label="name:B\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:2:8-9\nfield name:a ty:struct A\nfield name:m ty:mapping(uint256 => struct A)"]
+	C [label="name:C\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:3:8-9\nfield name:b ty:struct B\nfield name:d ty:struct D"]
+	D [label="name:D\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:4:8-9\nfield name:e ty:uint256"]
+	contract [label="contract Foo\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:5:1-25"]
+	diagnostic [label="struct 'A' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:1:8-9"]
+	note [label="recursive field 'b'\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:1:12-15"]
+	diagnostic_10 [label="struct 'B' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:2:8-9"]
+	note_11 [label="recursive field 'a'\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:2:12-15"]
+	note_12 [label="recursive field 'm'\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:2:17-35"]
+	diagnostic_13 [label="struct 'C' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:3:8-9"]
+	note_14 [label="recursive field 'b'\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:3:12-15"]
+	diagnostic_15 [label="found abstract contract 'Foo'\nlevel Debug\ntests/contract_testcases/substrate/structs/parse_structs_18.sol:5:1-25"]
+	structs -> A
+	structs -> B
+	structs -> C
+	structs -> D
+	contracts -> contract
+	diagnostics -> diagnostic [label="Error"]
+	diagnostic -> note [label="note"]
+	diagnostics -> diagnostic_10 [label="Error"]
+	diagnostic_10 -> note_11 [label="note"]
+	diagnostic_10 -> note_12 [label="note"]
+	diagnostics -> diagnostic_13 [label="Error"]
+	diagnostic_13 -> note_14 [label="note"]
+	diagnostics -> diagnostic_15 [label="Debug"]
+}

+ 5 - 0
tests/contract_testcases/substrate/structs/parse_structs_18.sol

@@ -0,0 +1,5 @@
+struct A { B b; }
+struct B { A a; mapping(uint=>A) m; }
+struct C { B b; D d; }
+struct D { uint e; }
+abstract contract Foo {}

+ 14 - 0
tests/contract_testcases/substrate/structs/parse_structs_19.dot

@@ -0,0 +1,14 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_19.sol" {
+	A [label="name:A\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:1:8-9\nfield name:b ty:struct B"]
+	B [label="name:B\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:2:8-9\nfield name:a ty:struct A"]
+	diagnostic [label="struct 'A' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:1:8-9"]
+	note [label="recursive field 'b'\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:1:12-15"]
+	diagnostic_7 [label="struct 'B' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:2:8-9"]
+	note_8 [label="recursive field 'a'\ntests/contract_testcases/substrate/structs/parse_structs_19.sol:2:12-15"]
+	structs -> A
+	structs -> B
+	diagnostics -> diagnostic [label="Error"]
+	diagnostic -> note [label="note"]
+	diagnostics -> diagnostic_7 [label="Error"]
+	diagnostic_7 -> note_8 [label="note"]
+}

+ 2 - 0
tests/contract_testcases/substrate/structs/parse_structs_19.sol

@@ -0,0 +1,2 @@
+struct A { B b; }
+struct B { A a; }

+ 6 - 0
tests/contract_testcases/substrate/structs/parse_structs_20.dot

@@ -0,0 +1,6 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_20.sol" {
+	A [label="name:A\ntests/contract_testcases/substrate/structs/parse_structs_20.sol:1:8-9\nfield name:b ty:struct B"]
+	B [label="name:B\ntests/contract_testcases/substrate/structs/parse_structs_20.sol:2:8-9\nfield name:a ty:struct A[]"]
+	structs -> A
+	structs -> B
+}

+ 2 - 0
tests/contract_testcases/substrate/structs/parse_structs_20.sol

@@ -0,0 +1,2 @@
+struct A { B b; }
+struct B { A[] a; }

+ 46 - 0
tests/contract_testcases/substrate/structs/parse_structs_21.dot

@@ -0,0 +1,46 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_21.sol" {
+	A [label="name:A\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:1:8-9\nfield name:b ty:struct B\nfield name:f ty:struct F"]
+	B [label="name:B\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:2:8-9\nfield name:c ty:struct C"]
+	C [label="name:C\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:3:8-9\nfield name:d ty:struct D"]
+	D [label="name:D\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:4:8-9\nfield name:a ty:struct A"]
+	F [label="name:F\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:5:8-9\nfield name:g ty:struct G"]
+	G [label="name:G\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:6:8-9\nfield name:h ty:struct H"]
+	H [label="name:H\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:7:8-9\nfield name:a ty:struct A"]
+	diagnostic [label="struct 'A' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:1:8-9"]
+	note [label="recursive field 'b'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:1:12-15"]
+	note_12 [label="recursive field 'f'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:1:17-20"]
+	diagnostic_13 [label="struct 'B' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:2:8-9"]
+	note_14 [label="recursive field 'c'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:2:12-15"]
+	diagnostic_15 [label="struct 'C' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:3:8-9"]
+	note_16 [label="recursive field 'd'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:3:12-15"]
+	diagnostic_17 [label="struct 'D' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:4:8-9"]
+	note_18 [label="recursive field 'a'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:4:12-15"]
+	diagnostic_19 [label="struct 'F' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:5:8-9"]
+	note_20 [label="recursive field 'g'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:5:12-15"]
+	diagnostic_21 [label="struct 'G' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:6:8-9"]
+	note_22 [label="recursive field 'h'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:6:12-15"]
+	diagnostic_23 [label="struct 'H' has infinite size\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:7:8-9"]
+	note_24 [label="recursive field 'a'\ntests/contract_testcases/substrate/structs/parse_structs_21.sol:7:12-15"]
+	structs -> A
+	structs -> B
+	structs -> C
+	structs -> D
+	structs -> F
+	structs -> G
+	structs -> H
+	diagnostics -> diagnostic [label="Error"]
+	diagnostic -> note [label="note"]
+	diagnostic -> note_12 [label="note"]
+	diagnostics -> diagnostic_13 [label="Error"]
+	diagnostic_13 -> note_14 [label="note"]
+	diagnostics -> diagnostic_15 [label="Error"]
+	diagnostic_15 -> note_16 [label="note"]
+	diagnostics -> diagnostic_17 [label="Error"]
+	diagnostic_17 -> note_18 [label="note"]
+	diagnostics -> diagnostic_19 [label="Error"]
+	diagnostic_19 -> note_20 [label="note"]
+	diagnostics -> diagnostic_21 [label="Error"]
+	diagnostic_21 -> note_22 [label="note"]
+	diagnostics -> diagnostic_23 [label="Error"]
+	diagnostic_23 -> note_24 [label="note"]
+}

+ 7 - 0
tests/contract_testcases/substrate/structs/parse_structs_21.sol

@@ -0,0 +1,7 @@
+struct A { B b; F f; }
+struct B { C c; }
+struct C { D d; }
+struct D { A a; }
+struct F { G g; }
+struct G { H h; }
+struct H { A a; }

+ 24 - 0
tests/contract_testcases/substrate/structs/parse_structs_22.dot

@@ -0,0 +1,24 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_22.sol" {
+	S [label="name:S\ncontract: Test\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:2:9-10\nfield name:foo ty:int256\nfield name:s ty:struct Test.S[][2]"]
+	contract [label="contract Test\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:1:1-10:2"]
+	test [label="function test\ncontract: Test\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:7:2-52\nsignature test(int256,(int256,#recursive)[][2])\nvisibility public\nmutability nonpayable"]
+	parameters [label="parameters\nint256 f\nstruct Test.S[][2] ss"]
+	returns [label="returns\nstruct Test.S "]
+	return [label="return\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:8:3-18"]
+	struct_literal [label="struct literal: struct Test.S\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:8:10-18"]
+	variable [label="variable: f\nint256\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:8:12-13"]
+	variable_10 [label="variable: ss\nstruct Test.S[][2]\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:8:15-17"]
+	diagnostic [label="found contract 'Test'\nlevel Debug\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:1:1-10:2"]
+	diagnostic_13 [label="Recursive parameter not allowed for public or external functions.\nlevel Error\ntests/contract_testcases/substrate/structs/parse_structs_22.sol:7:2-52"]
+	structs -> S
+	contracts -> contract
+	contract -> test [label="function"]
+	test -> parameters [label="parameters"]
+	test -> returns [label="returns"]
+	test -> return [label="body"]
+	return -> struct_literal [label="expr"]
+	struct_literal -> variable [label="arg #0"]
+	struct_literal -> variable_10 [label="arg #1"]
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_13 [label="Error"]
+}

+ 11 - 0
tests/contract_testcases/substrate/structs/parse_structs_22.sol

@@ -0,0 +1,11 @@
+contract Test {
+	struct S {
+		int foo;
+		S[][2] s;
+	}
+
+	function test(int f, S[][2] ss) public returns (S) {
+		return S(f, ss);
+	}
+}
+

+ 12 - 0
tests/contract_testcases/substrate/structs/parse_structs_23.dot

@@ -0,0 +1,12 @@
+strict digraph "tests/contract_testcases/substrate/structs/parse_structs_23.sol" {
+	S [label="name:S\ncontract: Fptr\ntests/contract_testcases/substrate/structs/parse_structs_23.sol:2:12-13\nfield name:ff ty:int256\nfield name:fptr ty:function(struct Fptr.S) external"]
+	contract [label="contract Fptr\ntests/contract_testcases/substrate/structs/parse_structs_23.sol:1:1-4:2"]
+	func [label="function func\ncontract: Fptr\ntests/contract_testcases/substrate/structs/parse_structs_23.sol:3:5-40\nsignature func((int256,function))\nvisibility public\nmutability pure"]
+	parameters [label="parameters\nstruct Fptr.S "]
+	diagnostic [label="found contract 'Fptr'\nlevel Debug\ntests/contract_testcases/substrate/structs/parse_structs_23.sol:1:1-4:2"]
+	structs -> S
+	contracts -> contract
+	contract -> func [label="function"]
+	func -> parameters [label="parameters"]
+	diagnostics -> diagnostic [label="Debug"]
+}

+ 5 - 0
tests/contract_testcases/substrate/structs/parse_structs_23.sol

@@ -0,0 +1,5 @@
+contract Fptr {
+    struct S { int ff; function (S memory) external fptr; }
+    function func(S memory) public pure {}
+}
+