use crate::frontend::VisibilityKind;
use crate::solidity::{
ContractMetadata, Diagnostic, DiagnosticSeverity, FunctionKind, FunctionMetadata,
ParameterMetadata, StateMutability, StateVariableMetadata,
};
use crate::type_system::NeoType;
#[derive(Debug, Clone)]
pub struct SemanticModel {
pub functions: Vec<FunctionSymbol>,
pub state_variables: Vec<StateVariableSymbol>,
}
impl SemanticModel {
pub fn public_functions(&self) -> Vec<&FunctionSymbol> {
self.functions
.iter()
.filter(|f| {
matches!(
f.visibility,
VisibilityKind::Public | VisibilityKind::External
)
})
.collect()
}
pub fn get_function(&self, name: &str) -> Option<&FunctionSymbol> {
self.functions.iter().find(|f| f.name == name)
}
pub fn get_state_variables(&self) -> &[StateVariableSymbol] {
&self.state_variables
}
pub fn is_payable(&self) -> bool {
self.functions
.iter()
.any(|f| f.state_mutability == StateMutability::Payable)
}
}
#[derive(Debug, Clone)]
pub struct FunctionSymbol {
pub name: String,
pub kind: FunctionKind,
pub parameters: Vec<ParameterSymbol>,
pub returns: Vec<ParameterSymbol>,
pub state_mutability: StateMutability,
pub visibility: VisibilityKind,
}
#[derive(Debug, Clone)]
pub struct ParameterSymbol {
pub name: Option<String>,
pub ty: NeoType,
pub storage: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StateVariableSymbol {
pub name: Option<String>,
pub ty: NeoType,
pub is_constant: bool,
pub is_immutable: bool,
pub visibility: Option<String>,
}
pub fn build_semantic_model(metadata: &ContractMetadata) -> Result<SemanticModel, Vec<Diagnostic>> {
let mut diagnostics = Vec::new();
let mut functions = Vec::new();
for function in &metadata.methods {
match convert_function(function) {
Ok(symbol) => functions.push(symbol),
Err(mut diags) => diagnostics.append(&mut diags),
}
}
let mut state_variables = Vec::new();
for state in &metadata.state_variables {
match convert_state_variable(state) {
Ok(symbol) => state_variables.push(symbol),
Err(mut diags) => diagnostics.append(&mut diags),
}
}
let has_error = diagnostics
.iter()
.any(|diag| matches!(diag.severity, DiagnosticSeverity::Error));
if has_error {
Err(diagnostics)
} else {
Ok(SemanticModel {
functions,
state_variables,
})
}
}
fn convert_function(function: &FunctionMetadata) -> Result<FunctionSymbol, Vec<Diagnostic>> {
let mut diagnostics = Vec::new();
let allow_unsupported_internal_types = !matches!(
function.visibility,
VisibilityKind::Public | VisibilityKind::External
);
let mut parameters = Vec::new();
for param in &function.parameters {
match convert_parameter(
param,
FunctionSide::Parameter,
&function.name,
allow_unsupported_internal_types,
) {
Ok(symbol) => parameters.push(symbol),
Err(diag) => diagnostics.push(diag),
}
}
let mut returns = Vec::new();
for param in &function.return_parameters {
match convert_parameter(
param,
FunctionSide::Return,
&function.name,
allow_unsupported_internal_types,
) {
Ok(symbol) => returns.push(symbol),
Err(diag) => diagnostics.push(diag),
}
}
if diagnostics
.iter()
.any(|diag| matches!(diag.severity, DiagnosticSeverity::Error))
{
Err(diagnostics)
} else {
Ok(FunctionSymbol {
name: function.name.clone(),
kind: function.kind,
parameters,
returns,
state_mutability: function.state_mutability,
visibility: function.visibility,
})
}
}
fn convert_state_variable(
state: &StateVariableMetadata,
) -> Result<StateVariableSymbol, Vec<Diagnostic>> {
match &state.neo_type {
Some(neo_type) => Ok(StateVariableSymbol {
name: state.name.clone(),
ty: neo_type.clone(),
is_constant: state.is_constant,
is_immutable: state.is_immutable,
visibility: state.visibility.clone(),
}),
None => Err(vec![Diagnostic::error(format!(
"state variable '{}' has unsupported type '{}'",
state.name.as_deref().unwrap_or("<unnamed>"),
state.ty
))]),
}
}
enum FunctionSide {
Parameter,
Return,
}
fn convert_parameter(
param: &ParameterMetadata,
side: FunctionSide,
function_name: &str,
allow_unsupported_internal_types: bool,
) -> Result<ParameterSymbol, Diagnostic> {
match ¶m.neo_type {
Some(neo_type) => Ok(ParameterSymbol {
name: param.name.clone(),
ty: neo_type.clone(),
storage: param.storage.clone(),
}),
None if allow_unsupported_internal_types => Ok(ParameterSymbol {
name: param.name.clone(),
ty: NeoType::Any,
storage: param.storage.clone(),
}),
None if is_supported_tuple_type(¶m.ty) => Ok(ParameterSymbol {
name: param.name.clone(),
ty: NeoType::Any,
storage: param.storage.clone(),
}),
None if is_devpack_iterator_type(¶m.ty) => Ok(ParameterSymbol {
name: param.name.clone(),
ty: NeoType::Any,
storage: param.storage.clone(),
}),
None => Err(Diagnostic::error(match side {
FunctionSide::Parameter => format!(
"function '{}' parameter '{}' uses unsupported type '{}'",
function_name,
param
.name
.clone()
.unwrap_or_else(|| "<unnamed>".to_string()),
param.ty
),
FunctionSide::Return => format!(
"function '{}' return type '{}' is unsupported",
function_name, param.ty
),
})),
}
}
fn is_devpack_iterator_type(ty: &str) -> bool {
let lowered = ty.trim().to_ascii_lowercase();
matches!(
lowered.as_str(),
"iterator" | "syscalls.iterator" | "storage.iterator"
)
}
fn is_supported_tuple_type(ty: &str) -> bool {
let trimmed = ty.trim();
let bytes = trimmed.as_bytes();
if bytes.len() < 2 || bytes[0] != b'(' || bytes[bytes.len() - 1] != b')' {
return false;
}
let mut depth: i32 = 0;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 && i != bytes.len() - 1 {
return false;
}
}
_ => {}
}
}
if depth != 0 {
return false;
}
let inner = &trimmed[1..trimmed.len() - 1];
let parts = split_tuple_components_for_params(inner);
!parts.is_empty() && parts.iter().all(|p| is_supported_leaf_or_tuple(p))
}
fn split_tuple_components_for_params(body: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth: i32 = 0;
let mut start = 0usize;
let bytes = body.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' | b'[' => depth += 1,
b')' | b']' => depth -= 1,
b',' if depth == 0 => {
parts.push(body[start..i].trim().to_string());
start = i + 1;
}
_ => {}
}
}
if start < body.len() {
let last = body[start..].trim();
if !last.is_empty() {
parts.push(last.to_string());
}
}
parts
}
fn is_supported_leaf_or_tuple(ty: &str) -> bool {
let trimmed = ty.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
return is_supported_tuple_type(trimmed);
}
let lowered = trimmed.to_ascii_lowercase();
lowered.starts_with("uint")
|| lowered.starts_with("int")
|| lowered == "bool"
|| lowered == "string"
|| lowered == "address"
|| lowered == "bytes"
|| lowered == "bytearray"
|| lowered.starts_with("bytes")
|| lowered.ends_with("[]")
|| lowered.starts_with("mapping")
}