Skip to main content

neo_devpack_solidity/
semantic_model.rs

1//! Semantic model extracted from Solidity metadata.
2
3use crate::frontend::VisibilityKind;
4use crate::solidity::{
5    ContractMetadata, Diagnostic, DiagnosticSeverity, FunctionKind, FunctionMetadata,
6    ParameterMetadata, StateMutability, StateVariableMetadata,
7};
8use crate::type_system::NeoType;
9
10#[derive(Debug, Clone)]
11pub struct SemanticModel {
12    pub functions: Vec<FunctionSymbol>,
13    pub state_variables: Vec<StateVariableSymbol>,
14}
15
16impl SemanticModel {
17    /// Get all public functions
18    pub fn public_functions(&self) -> Vec<&FunctionSymbol> {
19        self.functions
20            .iter()
21            .filter(|f| {
22                matches!(
23                    f.visibility,
24                    VisibilityKind::Public | VisibilityKind::External
25                )
26            })
27            .collect()
28    }
29
30    /// Get a function by name
31    pub fn get_function(&self, name: &str) -> Option<&FunctionSymbol> {
32        self.functions.iter().find(|f| f.name == name)
33    }
34
35    /// Get all state variables
36    pub fn get_state_variables(&self) -> &[StateVariableSymbol] {
37        &self.state_variables
38    }
39
40    /// Check if this is a payable contract
41    pub fn is_payable(&self) -> bool {
42        self.functions
43            .iter()
44            .any(|f| f.state_mutability == StateMutability::Payable)
45    }
46}
47
48#[derive(Debug, Clone)]
49pub struct FunctionSymbol {
50    pub name: String,
51    pub kind: FunctionKind,
52    pub parameters: Vec<ParameterSymbol>,
53    pub returns: Vec<ParameterSymbol>,
54    pub state_mutability: StateMutability,
55    pub visibility: VisibilityKind,
56}
57
58#[derive(Debug, Clone)]
59pub struct ParameterSymbol {
60    pub name: Option<String>,
61    pub ty: NeoType,
62    pub storage: Option<String>,
63}
64
65#[derive(Debug, Clone)]
66pub struct StateVariableSymbol {
67    pub name: Option<String>,
68    pub ty: NeoType,
69    pub is_constant: bool,
70    pub is_immutable: bool,
71    pub visibility: Option<String>,
72}
73
74pub fn build_semantic_model(metadata: &ContractMetadata) -> Result<SemanticModel, Vec<Diagnostic>> {
75    let mut diagnostics = Vec::new();
76
77    let mut functions = Vec::new();
78    for function in &metadata.methods {
79        match convert_function(function) {
80            Ok(symbol) => functions.push(symbol),
81            Err(mut diags) => diagnostics.append(&mut diags),
82        }
83    }
84
85    let mut state_variables = Vec::new();
86    for state in &metadata.state_variables {
87        match convert_state_variable(state) {
88            Ok(symbol) => state_variables.push(symbol),
89            Err(mut diags) => diagnostics.append(&mut diags),
90        }
91    }
92
93    let has_error = diagnostics
94        .iter()
95        .any(|diag| matches!(diag.severity, DiagnosticSeverity::Error));
96
97    if has_error {
98        Err(diagnostics)
99    } else {
100        Ok(SemanticModel {
101            functions,
102            state_variables,
103        })
104    }
105}
106
107fn convert_function(function: &FunctionMetadata) -> Result<FunctionSymbol, Vec<Diagnostic>> {
108    let mut diagnostics = Vec::new();
109    let allow_unsupported_internal_types = !matches!(
110        function.visibility,
111        VisibilityKind::Public | VisibilityKind::External
112    );
113
114    let mut parameters = Vec::new();
115    for param in &function.parameters {
116        match convert_parameter(
117            param,
118            FunctionSide::Parameter,
119            &function.name,
120            allow_unsupported_internal_types,
121        ) {
122            Ok(symbol) => parameters.push(symbol),
123            Err(diag) => diagnostics.push(diag),
124        }
125    }
126
127    let mut returns = Vec::new();
128    for param in &function.return_parameters {
129        match convert_parameter(
130            param,
131            FunctionSide::Return,
132            &function.name,
133            allow_unsupported_internal_types,
134        ) {
135            Ok(symbol) => returns.push(symbol),
136            Err(diag) => diagnostics.push(diag),
137        }
138    }
139
140    if diagnostics
141        .iter()
142        .any(|diag| matches!(diag.severity, DiagnosticSeverity::Error))
143    {
144        Err(diagnostics)
145    } else {
146        Ok(FunctionSymbol {
147            name: function.name.clone(),
148            kind: function.kind,
149            parameters,
150            returns,
151            state_mutability: function.state_mutability,
152            visibility: function.visibility,
153        })
154    }
155}
156
157fn convert_state_variable(
158    state: &StateVariableMetadata,
159) -> Result<StateVariableSymbol, Vec<Diagnostic>> {
160    match &state.neo_type {
161        Some(neo_type) => Ok(StateVariableSymbol {
162            name: state.name.clone(),
163            ty: neo_type.clone(),
164            is_constant: state.is_constant,
165            is_immutable: state.is_immutable,
166            visibility: state.visibility.clone(),
167        }),
168        None => Err(vec![Diagnostic::error(format!(
169            "state variable '{}' has unsupported type '{}'",
170            state.name.as_deref().unwrap_or("<unnamed>"),
171            state.ty
172        ))]),
173    }
174}
175
176enum FunctionSide {
177    Parameter,
178    Return,
179}
180
181fn convert_parameter(
182    param: &ParameterMetadata,
183    side: FunctionSide,
184    function_name: &str,
185    allow_unsupported_internal_types: bool,
186) -> Result<ParameterSymbol, Diagnostic> {
187    match &param.neo_type {
188        Some(neo_type) => Ok(ParameterSymbol {
189            name: param.name.clone(),
190            ty: neo_type.clone(),
191            storage: param.storage.clone(),
192        }),
193        None if allow_unsupported_internal_types => Ok(ParameterSymbol {
194            name: param.name.clone(),
195            ty: NeoType::Any,
196            storage: param.storage.clone(),
197        }),
198        // Task #94 — tolerate parenthesised tuple return/parameter types whose
199        // components are all individually supported. The frontend loses the
200        // structured tuple shape (solang flattens the top-level tuple into
201        // sibling ParameterIR entries), but nested tuples still arrive as a
202        // raw `(T1, T2, ...)` string here. Downstream IR lowering treats the
203        // slot as `NeoType::Any` and routes it through `abiEncode`, which
204        // produces the EVM-canonical flat head layout the spec calls for.
205        None if is_supported_tuple_type(&param.ty) => Ok(ParameterSymbol {
206            name: param.name.clone(),
207            ty: NeoType::Any,
208            storage: param.storage.clone(),
209        }),
210        // Gap `nep11` — the devpack NeoVM iterator handle (`Syscalls.Iterator`).
211        // The builtin helper libraries are never struct-merged into user
212        // contracts, so the type never resolves to a `NeoType::Struct`; treat
213        // the opaque handle as `Any` so NEP-11 `tokensOf`/`tokens` can declare
214        // it (manifest returntype `InteropInterface`).
215        None if is_devpack_iterator_type(&param.ty) => Ok(ParameterSymbol {
216            name: param.name.clone(),
217            ty: NeoType::Any,
218            storage: param.storage.clone(),
219        }),
220        None => Err(Diagnostic::error(match side {
221            FunctionSide::Parameter => format!(
222                "function '{}' parameter '{}' uses unsupported type '{}'",
223                function_name,
224                param
225                    .name
226                    .clone()
227                    .unwrap_or_else(|| "<unnamed>".to_string()),
228                param.ty
229            ),
230            FunctionSide::Return => format!(
231                "function '{}' return type '{}' is unsupported",
232                function_name, param.ty
233            ),
234        })),
235    }
236}
237
238/// Gap `nep11` — true iff `ty` names the devpack NeoVM iterator handle type
239/// (`Syscalls.Iterator` / `Storage.Iterator` / bare `Iterator`). Mirrors
240/// `is_devpack_iterator_type` in `validate/contract/return_types.rs`.
241fn is_devpack_iterator_type(ty: &str) -> bool {
242    let lowered = ty.trim().to_ascii_lowercase();
243    matches!(
244        lowered.as_str(),
245        "iterator" | "syscalls.iterator" | "storage.iterator"
246    )
247}
248
249/// Task #94 — recognise `(T1, T2, ...)` tuple-shaped type strings with all
250/// inner components individually supported by the scalar/array/bytes/mapping
251/// rules. Recursive on nested parens.
252fn is_supported_tuple_type(ty: &str) -> bool {
253    let trimmed = ty.trim();
254    let bytes = trimmed.as_bytes();
255    if bytes.len() < 2 || bytes[0] != b'(' || bytes[bytes.len() - 1] != b')' {
256        return false;
257    }
258    let mut depth: i32 = 0;
259    for (i, &b) in bytes.iter().enumerate() {
260        match b {
261            b'(' => depth += 1,
262            b')' => {
263                depth -= 1;
264                if depth == 0 && i != bytes.len() - 1 {
265                    return false;
266                }
267            }
268            _ => {}
269        }
270    }
271    if depth != 0 {
272        return false;
273    }
274    let inner = &trimmed[1..trimmed.len() - 1];
275    let parts = split_tuple_components_for_params(inner);
276    !parts.is_empty() && parts.iter().all(|p| is_supported_leaf_or_tuple(p))
277}
278
279fn split_tuple_components_for_params(body: &str) -> Vec<String> {
280    let mut parts = Vec::new();
281    let mut depth: i32 = 0;
282    let mut start = 0usize;
283    let bytes = body.as_bytes();
284    for (i, &b) in bytes.iter().enumerate() {
285        match b {
286            b'(' | b'[' => depth += 1,
287            b')' | b']' => depth -= 1,
288            b',' if depth == 0 => {
289                parts.push(body[start..i].trim().to_string());
290                start = i + 1;
291            }
292            _ => {}
293        }
294    }
295    if start < body.len() {
296        let last = body[start..].trim();
297        if !last.is_empty() {
298            parts.push(last.to_string());
299        }
300    }
301    parts
302}
303
304fn is_supported_leaf_or_tuple(ty: &str) -> bool {
305    let trimmed = ty.trim();
306    if trimmed.starts_with('(') && trimmed.ends_with(')') {
307        return is_supported_tuple_type(trimmed);
308    }
309    let lowered = trimmed.to_ascii_lowercase();
310    lowered.starts_with("uint")
311        || lowered.starts_with("int")
312        || lowered == "bool"
313        || lowered == "string"
314        || lowered == "address"
315        || lowered == "bytes"
316        || lowered == "bytearray"
317        || lowered.starts_with("bytes")
318        || lowered.ends_with("[]")
319        || lowered.starts_with("mapping")
320}