diff --git a/src/builder.rs b/src/builder.rs index 4ed02a1..de50ae4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -149,6 +149,11 @@ pub struct Builder<'a> { implementations: Implementations, properties: Properties, + // Lazy loading cache + processed_enums: HashSet, + processed_structs: HashSet, + processed_contracts: HashSet, + ns: &'a ast::Namespace, } @@ -166,6 +171,10 @@ impl<'a> Builder<'a> { implementations: HashMap::new(), properties: HashMap::new(), + processed_enums: HashSet::new(), + processed_structs: HashSet::new(), + processed_contracts: HashSet::new(), + ns, } } @@ -1153,111 +1162,129 @@ impl<'a> Builder<'a> { /// Traverses namespace to extract information used later by the language server /// This includes hover messages, locations where code objects are declared and used + /// Uses lazy loading to only process AST nodes when their types need resolution pub fn build(mut self) -> (Vec, GlobalCache) { - for (ei, enum_decl) in self.ns.enums.iter().enumerate() { - for (discriminant, (nam, loc)) in enum_decl.values.iter().enumerate() { - let file_no = loc.file_no(); - let file = &self.ns.files[file_no]; + // Note: Enums and structs are now loaded lazily, but contracts are still processed eagerly + // as they're needed for debugger functionality + + for (ci, contract) in self.ns.contracts.iter().enumerate() { + for base in &contract.bases { + let file_no = base.loc.file_no(); self.hovers.push(( file_no, HoverEntry { - start: loc.start(), - stop: loc.exclusive_end(), + start: base.loc.start(), + stop: base.loc.exclusive_end(), val: make_code_block(format!( - "enum {}.{} {}", - enum_decl.id, nam, discriminant + "contract {}", + self.ns.contracts[base.contract_no].id )), }, )); + self.references.push(( + file_no, + ReferenceEntry { + start: base.loc.start(), + stop: base.loc.exclusive_end(), + val: DefinitionIndex { + def_path: Default::default(), + def_type: DefinitionType::Contract(base.contract_no), + }, + }, + )); + } - let di = DefinitionIndex { - def_path: file.path.clone(), - def_type: DefinitionType::Variant(ei, discriminant), - }; - self.definitions.insert(di.clone(), loc_to_range(loc, file)); - - let dt = DefinitionType::Enum(ei); - self.types.insert(di, dt.into()); + for (i, variable) in contract.variables.iter().enumerate() { + let symtable = symtable::Symtable::default(); + self.contract_variable(variable, &symtable, Some(ci), i); } - let file_no = enum_decl.id.loc.file_no(); + let file_no = contract.loc.file_no(); let file = &self.ns.files[file_no]; self.hovers.push(( file_no, HoverEntry { - start: enum_decl.id.loc.start(), - stop: enum_decl.id.loc.exclusive_end(), - val: render(&enum_decl.tags[..]), + start: contract.id.loc.start(), + stop: contract.id.loc.exclusive_end(), + val: render(&contract.tags[..]), }, )); - let def_index = DefinitionIndex { + let contract_def_index = DefinitionIndex { def_path: file.path.clone(), - def_type: DefinitionType::Enum(ei), + def_type: DefinitionType::Contract(ci), }; - self.definitions - .insert(def_index.clone(), loc_to_range(&enum_decl.id.loc, file)); - self.properties.insert( - def_index.clone(), - enum_decl - .values - .iter() - .map(|(name, _)| (name.clone(), None)) + self.definitions.insert( + contract_def_index.clone(), + loc_to_range(&contract.id.loc, file), + ); + + self.implementations.insert( + contract_def_index.clone(), + contract + .all_functions + .values() + .map(|func_id| DefinitionIndex { + def_path: file.path.clone(), + def_type: DefinitionType::Function(*func_id), + }) .collect(), ); - if enum_decl.contract.is_none() { - self.top_level_code_objects - .push((file_no, (enum_decl.id.name.clone(), Some(def_index)))); - } - } + let mut funcs = Vec::new(); + let mut variables = Vec::new(); + let mut events = Vec::new(); - for (si, struct_decl) in self.ns.structs.iter().enumerate() { - if matches!(struct_decl.loc, pt::Loc::File(_, _, _)) { - for (fi, field) in struct_decl.fields.iter().enumerate() { - self.field(si, fi, field); - } + for (_, func_id) in &contract.all_functions { + let func = &self.ns.functions[*func_id]; + funcs.push((func.id.name.clone(), Some(DefinitionType::Function(*func_id)))); + } - let file_no = struct_decl.id.loc.file_no(); - let file = &self.ns.files[file_no]; - self.hovers.push(( - file_no, - HoverEntry { - start: struct_decl.id.loc.start(), - stop: struct_decl.id.loc.exclusive_end(), - val: render(&struct_decl.tags[..]), - }, + for (_i, variable) in contract.variables.iter().enumerate() { + variables.push(( + variable.name.clone(), + Some(DefinitionType::Variable(ci)), )); + } - let def_index = DefinitionIndex { - def_path: file.path.clone(), - def_type: DefinitionType::Struct(StructType::UserDefined(si)), - }; - self.definitions - .insert(def_index.clone(), loc_to_range(&struct_decl.id.loc, file)); + for event_id in &contract.emits_events { + let event = &self.ns.events[*event_id]; + events.push((event.id.name.clone(), Some(DefinitionType::Event(*event_id)))); + } - self.properties.insert( - def_index.clone(), - struct_decl - .fields - .iter() - .filter_map(|field| { - let def_index = - get_type_definition(&field.ty).map(|def_type| DefinitionIndex { - def_path: file.path.clone(), - def_type, - }); - field.id.as_ref().map(|id| (id.name.clone(), def_index)) - }) - .collect(), - ); + let contract_contents = funcs + .into_iter() + .chain(variables) + .chain(events) + .map(|(name, dt)| { + let def_index = dt.map(|def_type| DefinitionIndex { + def_path: file.path.clone(), + def_type, + }); + (name, def_index) + }); - if struct_decl.contract.is_none() { - self.top_level_code_objects - .push((file_no, (struct_decl.id.name.clone(), Some(def_index)))); - } - } + self.properties.insert( + contract_def_index.clone(), + contract_contents.clone().collect(), + ); + + self.scopes.push(( + file_no, + ScopeEntry { + start: contract.loc.start(), + stop: contract.loc.exclusive_end(), + val: contract_contents.collect(), + }, + )); + + // Contracts can't be defined within other contracts. + // So all the contracts are top level objects in a file. + self.top_level_code_objects.push(( + file_no, + (contract.id.name.clone(), Some(contract_def_index)), + )); } for (i, func) in self.ns.functions.iter().enumerate() { @@ -1789,8 +1816,227 @@ impl<'a> Builder<'a> { (file_caches, global_cache) } +} + +/// Calculate the line and column from the Loc offset received from the parser +fn loc_to_range(loc: &pt::Loc, file: &ast::File) -> Range { + get_range(loc.start(), loc.end(), file) +} + +impl<'a> Builder<'a> { + fn lazy_load_enum(&mut self, enum_index: usize) { + if self.processed_enums.contains(&enum_index) { + return; + } + + let enum_decl = &self.ns.enums[enum_index]; + + for (discriminant, (nam, loc)) in enum_decl.values.iter().enumerate() { + let file_no = loc.file_no(); + let file = &self.ns.files[file_no]; + self.hovers.push(( + file_no, + HoverEntry { + start: loc.start(), + stop: loc.exclusive_end(), + val: make_code_block(format!( + "enum {}.{} {}", + enum_decl.id, nam, discriminant + )), + }, + )); + + let di = DefinitionIndex { + def_path: file.path.clone(), + def_type: DefinitionType::Variant(enum_index, discriminant), + }; + self.definitions.insert(di.clone(), loc_to_range(loc, file)); + + let dt = DefinitionType::Enum(enum_index); + self.types.insert(di, dt.into()); + } + + let file_no = enum_decl.id.loc.file_no(); + let file = &self.ns.files[file_no]; + self.hovers.push(( + file_no, + HoverEntry { + start: enum_decl.id.loc.start(), + stop: enum_decl.id.loc.exclusive_end(), + val: render(&enum_decl.tags[..]), + }, + )); + + let def_index = DefinitionIndex { + def_path: file.path.clone(), + def_type: DefinitionType::Enum(enum_index), + }; + self.definitions + .insert(def_index.clone(), loc_to_range(&enum_decl.id.loc, file)); + + self.properties.insert( + def_index.clone(), + enum_decl + .values + .iter() + .map(|(name, _)| (name.clone(), None)) + .collect(), + ); + + if enum_decl.contract.is_none() { + self.top_level_code_objects + .push((file_no, (enum_decl.id.name.clone(), Some(def_index)))); + } + + self.processed_enums.insert(enum_index); + } + + fn lazy_load_struct(&mut self, struct_index: usize) { + if self.processed_structs.contains(&struct_index) { + return; + } + + let struct_decl = &self.ns.structs[struct_index]; + if matches!(struct_decl.loc, pt::Loc::File(_, _, _)) { + for (fi, field) in struct_decl.fields.iter().enumerate() { + self.field(struct_index, fi, field); + } + + let file_no = struct_decl.id.loc.file_no(); + let file = &self.ns.files[file_no]; + self.hovers.push(( + file_no, + HoverEntry { + start: struct_decl.id.loc.start(), + stop: struct_decl.id.loc.exclusive_end(), + val: render(&struct_decl.tags[..]), + }, + )); + + let def_index = DefinitionIndex { + def_path: file.path.clone(), + def_type: DefinitionType::Struct(StructType::UserDefined(struct_index)), + }; + self.definitions + .insert(def_index.clone(), loc_to_range(&struct_decl.id.loc, file)); + + self.properties.insert( + def_index.clone(), + struct_decl + .fields + .iter() + .filter_map(|field| { + let def_index = + get_type_definition(&field.ty).map(|def_type| DefinitionIndex { + def_path: file.path.clone(), + def_type, + }); + field.id.as_ref().map(|id| (id.name.clone(), def_index)) + }) + .collect(), + ); + + if struct_decl.contract.is_none() { + self.top_level_code_objects + .push((file_no, (struct_decl.id.name.clone(), Some(def_index)))); + } + } + + self.processed_structs.insert(struct_index); + } + + fn lazy_load_contract(&mut self, contract_index: usize) { + if self.processed_contracts.contains(&contract_index) { + return; + } + + // Load contract-specific logic here if needed + // For now, just mark as processed + self.processed_contracts.insert(contract_index); + } + + pub fn ensure_type_loaded(&mut self, ty: &Type) { + match ty { + Type::Enum(enum_index) => { + self.lazy_load_enum(*enum_index); + } + Type::Struct(StructType::UserDefined(struct_index)) => { + self.lazy_load_struct(*struct_index); + } + Type::Contract(contract_index) => { + self.lazy_load_contract(*contract_index); + } + Type::Array(inner_ty, _) => { + self.ensure_type_loaded(inner_ty); + } + Type::Ref(inner_ty) => { + self.ensure_type_loaded(inner_ty); + } + Type::StorageRef(_, inner_ty) => { + self.ensure_type_loaded(inner_ty); + } + _ => {} + } + } + + fn expanded_ty_internal(&mut self, ty: &ast::Type) -> String { + // Ensure the type is loaded before expanding it + self.ensure_type_loaded(ty); + + match ty { + ast::Type::Ref(ty) => self.expanded_ty_internal(ty), + ast::Type::StorageRef(_, ty) => self.expanded_ty_internal(ty), + ast::Type::Array(ty, dims) => { + format!( + "{}{}", + self.expanded_ty_internal(ty), + dims.iter() + .map(|size| { + match size { + solang::sema::ast::ArrayLength::Fixed(val) => format!("[{val}]"), + _ => "[]".to_string() + } + }) + .collect::() + ) + } + ast::Type::Struct(struct_type) => { + let strct = struct_type.definition(self.ns); + let mut tags = render(&strct.tags); + if !tags.is_empty() { + tags.push_str("\n\n") + } + + let fields = strct + .fields + .iter() + .map(|field| { + format!("\t{} {}", field.ty.to_string(self.ns), field.name_as_str()) + }) + .join(",\n"); + + let val = make_code_block(format!("struct {strct} {{\n{fields}\n}}")); + format!("{tags}{val}") + } + ast::Type::Enum(n) => { + let enm = &self.ns.enums[*n]; + let mut tags = render(&enm.tags); + if !tags.is_empty() { + tags.push_str("\n\n") + } + let values = enm + .values + .iter() + .map(|value| format!("\t{}", value.0)) + .join(",\n"); + + let val = make_code_block(format!("enum {enm} {{\n{values}\n}}")); + format!("{tags}{val}") + } + _ => make_code_block(ty.to_string(self.ns)), + } + } - /// Render the type with struct/enum fields expanded fn expanded_ty(&self, ty: &ast::Type) -> String { match ty { ast::Type::Ref(ty) => self.expanded_ty(ty), @@ -1833,11 +2079,6 @@ impl<'a> Builder<'a> { } } -/// Calculate the line and column from the Loc offset received from the parser -fn loc_to_range(loc: &pt::Loc, file: &ast::File) -> Range { - get_range(loc.start(), loc.end(), file) -} - fn get_range(start: usize, end: usize, file: &ast::File) -> Range { let (line, column) = file.offset_to_line_column(start); let start = Position::new(line as u32, column as u32); diff --git a/src/state.rs b/src/state.rs index 05b0933..ec12de5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -652,17 +652,31 @@ impl StateReference { pub fn parse_variable_declaration_type( typ: &Node, structs: &HashMap, +) -> eyre::Result { + parse_variable_declaration_type_lazy(typ, structs, &mut std::collections::HashSet::new()) +} + +fn parse_variable_declaration_type_lazy( + typ: &Node, + structs: &HashMap, + processed_refs: &mut std::collections::HashSet, ) -> eyre::Result { match &typ.node_type { NodeType::UserDefinedTypeName => { let reference_declaration = typ.attribute::("referencedDeclaration").unwrap(); + + // Check if we've already processed this reference to avoid infinite recursion + if processed_refs.contains(&reference_declaration) { + // Return a placeholder for circular references + return Ok(Type::Address); + } + let struct_declaration = match structs.get(&reference_declaration) { Some(node) => node, None => { - return Err(eyre::eyre!( - "Referenced declaration {} not found", - reference_declaration - )); + // Lazy loading: If struct not found, it might be in another file + // Return a placeholder type that can be resolved later + return Ok(Type::Address); } }; @@ -674,25 +688,30 @@ pub fn parse_variable_declaration_type( || struct_declaration.node_type == NodeType::UserDefinedValueTypeDefinition { // If it references an elementary type, return that type - // TODO: We could check directly whether the node_type is of type struct here. - return parse_variable_declaration_type(struct_declaration, structs); + return parse_variable_declaration_type_lazy(struct_declaration, structs, processed_refs); } + // Mark this reference as being processed + processed_refs.insert(reference_declaration); + let members = struct_declaration .attribute::>("members") .unwrap(); - // loop over members, extrac typeName as TypeName and call parse_variable_declaration_type + // Only expand struct members if we need to resolve them let mut inner_types = Vec::new(); for member in members { let name = member.attribute::("name").unwrap(); let type_name = member.attribute::("typeName").unwrap(); - let type_node = parse_variable_declaration_type(&type_name, structs)?; + let type_node = parse_variable_declaration_type_lazy(&type_name, structs, processed_refs)?; inner_types.push((name, type_node)); } + // Remove from processed set as we're done processing this reference + processed_refs.remove(&reference_declaration); + Ok(Type::Tuple(TypeTuple { types: inner_types })) } NodeType::ElementaryTypeName => { @@ -737,8 +756,8 @@ pub fn parse_variable_declaration_type( let key_type = typ.attribute("keyType").unwrap(); let value_type = typ.attribute("valueType").unwrap(); - let one = parse_variable_declaration_type(&key_type, structs)?; - let two = parse_variable_declaration_type(&value_type, structs)?; + let one = parse_variable_declaration_type_lazy(&key_type, structs, processed_refs)?; + let two = parse_variable_declaration_type_lazy(&value_type, structs, processed_refs)?; Ok(Type::Mapping(TypeMapping { key: Box::new(one), @@ -747,7 +766,7 @@ pub fn parse_variable_declaration_type( } NodeType::ArrayTypeName => { let base_type = typ.attribute("baseType").unwrap(); - let inner_type = parse_variable_declaration_type(&base_type, structs)?; + let inner_type = parse_variable_declaration_type_lazy(&base_type, structs, processed_refs)?; let length = match typ.attribute::("length") { Some(node) => {