diff --git a/crates/hir/src/def_id.rs b/crates/hir/src/def_id.rs index d4aaaf85..bc1873d0 100644 --- a/crates/hir/src/def_id.rs +++ b/crates/hir/src/def_id.rs @@ -64,6 +64,7 @@ impl DefId { DefLoc::Decl(InContainer { cont_id, .. }) => cont_id, DefLoc::Typedef(InContainer { cont_id, .. }) => cont_id, DefLoc::Instance(InModule { module_id, .. }) => module_id.into(), + DefLoc::Modport(InModule { module_id, .. }) => module_id.into(), DefLoc::Stmt(InContainer { cont_id, .. }) => cont_id, } } @@ -106,6 +107,7 @@ impl DefId { } DefLoc::Typedef(_) => DefKind::Typedef, DefLoc::Instance(_) => DefKind::Instance, + DefLoc::Modport(_) => DefKind::Modport, DefLoc::Stmt(_) => DefKind::Stmt, } } @@ -148,6 +150,9 @@ impl DefId { DefLoc::Instance(InModule { value, module_id }) => { module_id.to_container(db).get(value).name.clone() } + DefLoc::Modport(InModule { value, module_id }) => { + module_id.to_container(db).get(value).name.clone() + } DefLoc::Stmt(InContainer { value, cont_id }) => { cont_id.to_container(db).get(value).label.clone() } @@ -220,6 +225,10 @@ impl DefId { let range = module_id.to_container_src_map(db).get(value)?.name_range()?; Some(InFile::new(module_id.file_id, range)) } + DefLoc::Modport(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.name_range()?; + Some(InFile::new(module_id.file_id, range)) + } DefLoc::Stmt(InContainer { value, cont_id }) => { let range = cont_id.to_container_src_map(db).get(value)?.name_range()?; Some(InFile::new(cont_id.file_id(db).into(), range)) @@ -290,6 +299,10 @@ impl DefId { let range = module_id.to_container_src_map(db).get(value)?.range(); InFile::new(module_id.file_id, range) } + DefLoc::Modport(InModule { value, module_id }) => { + let range = module_id.to_container_src_map(db).get(value)?.range(); + InFile::new(module_id.file_id, range) + } DefLoc::Stmt(InContainer { value, cont_id }) => { let range = cont_id.to_container_src_map(db).get(value)?.range(); InFile::new(cont_id.file_id(db).into(), range) diff --git a/crates/hir/src/display.rs b/crates/hir/src/display.rs index a3a0473b..dd5f1aab 100644 --- a/crates/hir/src/display.rs +++ b/crates/hir/src/display.rs @@ -130,6 +130,19 @@ impl HirDisplay for Ty { f.write_str("module") } } + Ty::VirtualInterface { def, modport } => { + f.write_str("virtual interface ")?; + if let Some(name) = def.name(f.db) { + f.write_str(&name)?; + } else { + f.write_str("interface")?; + } + if let Some(modport_name) = modport.and_then(|modport| modport.name(f.db)) { + f.write_str(".")?; + f.write_str(&modport_name)?; + } + Ok(()) + } Ty::GenerateBlock(generate_block_id) => { let block = f.db.generate_block(*generate_block_id); if let Some(name) = &block.name { diff --git a/crates/hir/src/hir_def/module.rs b/crates/hir/src/hir_def/module.rs index 40f1ba91..b3524cb5 100644 --- a/crates/hir/src/hir_def/module.rs +++ b/crates/hir/src/hir_def/module.rs @@ -8,6 +8,7 @@ use instantiation::{ ParamAssign, ParamAssignSrc, PortConn, PortConnSrc, impl_lower_instantiation, }; use la_arena::{Arena, Idx, IdxRange, RawIdx}; +use modport::{LowerModport, ModportDef, ModportId, ModportSrc}; use port::{ NonAnsiPort, NonAnsiPortId, NonAnsiPortSrc, PortDecl, PortDeclId, PortDeclSrc, PortRef, PortRefId, PortRefSrc, PortSrcs, Ports, @@ -67,6 +68,7 @@ pub mod continuous_assgin; pub mod defparam; pub mod generate; pub mod instantiation; +pub mod modport; pub mod port; pub mod specify; @@ -91,6 +93,7 @@ define_container! { typedefs: [Typedef], structs: [StructDef], subroutines: [Subroutine], + modports: [ModportDef], package_imports: [PackageImport], instantiations: [Instantiation], @@ -131,6 +134,7 @@ define_container! { typedef_srcs: [Typedef | TypedefSrc], struct_srcs: [StructDef | StructSrc], subroutine_srcs: [Subroutine | SubroutineSrc], + modport_srcs: [ModportDef | ModportSrc], instantiation_srcs: [Instantiation | InstantiationSrc], inst_param_assign_srcs: [ParamAssign | ParamAssignSrc], @@ -279,6 +283,7 @@ impl ModuleSourceMap { ModuleItem::PortDeclId(idx) => self.get(*idx)?.ptr(), ModuleItem::TypedefId(idx) => self.get(*idx)?.ptr(), ModuleItem::SubroutineId(idx) => self.get(*idx)?.node, + ModuleItem::ModportId(idx) => self.get(*idx)?.node, }) } } @@ -298,6 +303,7 @@ define_enum_deriving_from! { PortDeclId(PortDeclId), TypedefId(TypedefId), SubroutineId(LocalSubroutineId), + ModportId(ModportId), } } @@ -611,8 +617,14 @@ impl LowerModuleCtx<'_> { NetAlias(_) => continue, // Modport - ModportDeclaration(_) - | ModportClockingPort(_) + ModportDeclaration(modport) => { + for modport_id in self.lower_modport_declaration(modport) { + self.module_source_map.items.push(modport_id.into()); + } + self.region_tree.handle_node(member.syntax()); + continue; + } + ModportClockingPort(_) | ModportSimplePortList(_) | ModportSubroutinePortList(_) => continue, diff --git a/crates/hir/src/hir_def/module/modport.rs b/crates/hir/src/hir_def/module/modport.rs new file mode 100644 index 00000000..68a8002c --- /dev/null +++ b/crates/hir/src/hir_def/module/modport.rs @@ -0,0 +1,163 @@ +use la_arena::Idx; +use smallvec::SmallVec; +use syntax::{ + SyntaxKind, SyntaxToken, TokenKind, + ast::{self, AstNode}, + ptr::{SyntaxNodePtr, SyntaxTokenPtr}, + slang_ext::AstNodeExt, +}; +use utils::text_edit::TextRange; + +use super::{LowerModuleCtx, port::PortDirection}; +use crate::{ + hir_def::{Ident, alloc_idx_and_src, lower_ident_opt}, + source_map::{FromSourceAst, IsNamedSrc, IsSrc, SourceAst, root_token_in}, +}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ModportDef { + pub name: Option, + pub ports: SmallVec<[ModportPort; 4]>, +} + +pub type ModportId = Idx; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ModportPort { + pub name: Ident, + pub dir: PortDirection, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub struct ModportSrc { + pub node: SyntaxNodePtr, + pub name: Option, +} + +impl IsSrc for ModportSrc { + #[inline] + fn kind(&self) -> SyntaxKind { + self.node.kind() + } + + #[inline] + fn range(&self) -> TextRange { + self.node.range() + } +} + +impl IsNamedSrc for ModportSrc { + #[inline] + fn name_kind(&self) -> Option { + self.name.map(|name| name.kind()) + } + + #[inline] + fn name_range(&self) -> Option { + self.name.map(|name| name.range()) + } +} + +impl<'a> FromSourceAst<'a, ast::ModportItem<'a>> for ModportSrc { + fn from_source_ast(item: SourceAst>) -> Self { + let item = item.into_inner(); + let syntax = item.syntax(); + let name = item + .name() + .and_then(|name| root_token_in(syntax, name).map(SyntaxTokenPtr::from_token)); + Self { node: AstNodeExt::to_ptr(&item), name } + } +} + +pub(crate) trait LowerModport { + fn lower_modport_declaration( + &mut self, + modport: ast::ModportDeclaration<'_>, + ) -> SmallVec<[ModportId; 1]>; +} + +impl LowerModport for LowerModuleCtx<'_> { + fn lower_modport_declaration( + &mut self, + modport: ast::ModportDeclaration<'_>, + ) -> SmallVec<[ModportId; 1]> { + let mut lowered = SmallVec::new(); + for item in modport.items().children() { + let name = lower_ident_opt(item.name()); + let ports = lower_modport_ports(item); + let modport_id = alloc_idx_and_src! { + self.file_id; + ModportDef { name, ports } => self.module.modports, + item => self.module_source_map.modport_srcs, + }; + lowered.push(modport_id); + } + lowered + } +} + +fn lower_modport_ports(item: ast::ModportItem<'_>) -> SmallVec<[ModportPort; 4]> { + let mut ports = SmallVec::new(); + + // slang models a modport item as named entries whose `ports()` members are + // simple or subroutine port lists; each list then owns its inner ports. + for member in item.ports().ports().children() { + match member { + ast::Member::ModportSimplePortList(port_list) => { + let dir = lower_modport_dir(port_list.direction()).unwrap_or_default(); + for port in port_list.ports().children() { + if let Some(name) = lower_modport_port_name(port) { + ports.push(ModportPort { name, dir }); + } + } + } + ast::Member::ModportSubroutinePortList(port_list) => { + let dir = lower_modport_subroutine_dir(port_list.import_export()); + for port in port_list.ports().children() { + if let Some(name) = lower_modport_port_name(port) { + ports.push(ModportPort { name, dir }); + } + } + } + _ => {} + } + } + + ports +} + +fn lower_modport_port_name(port: ast::ModportPort<'_>) -> Option { + match port { + ast::ModportPort::ModportNamedPort(port) => lower_ident_opt(port.name()), + ast::ModportPort::ModportExplicitPort(port) => lower_ident_opt(port.name()), + ast::ModportPort::ModportSubroutinePort(port) => { + lower_ident_opt(rightmost_name_token(port.prototype().name())) + } + } +} + +fn rightmost_name_token(name: ast::Name<'_>) -> Option> { + match name { + ast::Name::IdentifierName(name) => name.identifier(), + ast::Name::IdentifierSelectName(name) => name.identifier(), + ast::Name::ScopedName(name) => rightmost_name_token(name.right()), + _ => None, + } +} + +fn lower_modport_dir(tok: Option>) -> Option { + tok.and_then(|tok| match tok.kind() { + TokenKind::INPUT_KEYWORD => Some(PortDirection::Input), + TokenKind::OUTPUT_KEYWORD => Some(PortDirection::Output), + TokenKind::IN_OUT_KEYWORD => Some(PortDirection::Inout), + TokenKind::REF_KEYWORD => Some(PortDirection::Ref), + _ => None, + }) +} + +fn lower_modport_subroutine_dir(tok: Option>) -> PortDirection { + match tok.map(|tok| tok.kind()) { + Some(TokenKind::EXPORT_KEYWORD) => PortDirection::Output, + _ => PortDirection::Input, + } +} diff --git a/crates/hir/src/scope.rs b/crates/hir/src/scope.rs index ec2fdaeb..9d9fd868 100644 --- a/crates/hir/src/scope.rs +++ b/crates/hir/src/scope.rs @@ -185,6 +185,10 @@ impl NameScope { scope.insert_value_opt(&subroutine.name, def_id(db, subroutine_id)); } + for (modport_id, modport) in module.modports.iter() { + scope.insert_value_opt(&modport.name, def_id(db, InModule::new(module_id, modport_id))); + } + insert_decls_and_typedefs( &mut scope, db, diff --git a/crates/hir/src/semantics/pathres.rs b/crates/hir/src/semantics/pathres.rs index 45a1a1d3..cc43c620 100644 --- a/crates/hir/src/semantics/pathres.rs +++ b/crates/hir/src/semantics/pathres.rs @@ -118,7 +118,7 @@ fn resolve_top_level_module_root( // segment value fallback: `top` alone remains a type-space module name. let type_res = resolve_name(db, cont_id, ident, NameContext::Type)?; let module_defs = - type_res.def_ids().iter().copied().filter(|def_id| def_id.kind(db) == DefKind::Module); + type_res.def_ids().iter().copied().filter(|def_id| def_id.kind(db).is_instantiable_def()); PathResolution::from_def_ids(module_defs) } @@ -147,7 +147,7 @@ fn resolve_child_name( pub fn descend_scope(db: &dyn HirDb, def_id: DefId) -> Option { match def_id.kind(db) { - DefKind::Module => def_id.as_module(db).map(Into::into), + kind if kind.is_instantiable_def() => def_id.as_module(db).map(Into::into), DefKind::Instance => { let instance = def_id.as_instance(db)?; instance_target_module_id(db, instance.module_id, instance.value).map(Into::into) @@ -460,4 +460,46 @@ endmodule DefKind::Net ); } + + #[test] + fn resolve_path_descends_interface_instances_to_modports() { + let db = db_with_root_text( + r#" +interface bus_if; + wire clk; + modport host(input clk); +endinterface + +module top; + bus_if u_if(); +endmodule +"#, + ); + + let top = db + .unit_scope() + .module_ids(&db, &ident("top")) + .unique() + .expect("top module should resolve uniquely"); + + let res = resolve_path(&db, top.into(), &path(&["u_if", "host"]), NameContext::Value) + .expect("interface instance modport should resolve"); + + let def = res.primary_def_id().expect("modport should produce a definition"); + assert_eq!(def.name(&db).as_deref(), Some("host")); + assert_eq!(def.kind(&db), DefKind::Modport); + assert_eq!( + resolved_kind(&db, top.into(), &["u_if", "clk"], NameContext::Value), + DefKind::Net + ); + assert_eq!( + resolved_kind( + &db, + ScopeId::File(HirFileId::File(TOP)), + &["top", "u_if", "host"], + NameContext::Value, + ), + DefKind::Modport + ); + } } diff --git a/crates/hir/src/symbol.rs b/crates/hir/src/symbol.rs index 37b76ddf..3e2b9b3e 100644 --- a/crates/hir/src/symbol.rs +++ b/crates/hir/src/symbol.rs @@ -12,7 +12,8 @@ use crate::{ expr::declarator::DeclId, file::{config::ConfigDeclId, library::LibraryDeclId, udp::UdpDeclId}, module::{ - ModuleId, generate::GenerateBlockId, instantiation::InstanceId, port::NonAnsiPortId, + ModuleId, generate::GenerateBlockId, instantiation::InstanceId, modport::ModportId, + port::NonAnsiPortId, }, stmt::StmtId, subroutine::{LocalSubroutineId, SubroutinePortId}, @@ -25,6 +26,7 @@ use crate::{ pub struct DefId(pub salsa::InternId); #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] pub enum DefLoc { Module(ModuleId), Config(InFile), @@ -38,6 +40,7 @@ pub enum DefLoc { Decl(InContainer), Typedef(InContainer), Instance(InModule), + Modport(InModule), Stmt(InContainer), } @@ -54,6 +57,7 @@ impl_from! { DefLoc => Decl(InContainer), Typedef(InContainer), Instance(InModule), + Modport(InModule), Stmt(InContainer), } @@ -150,6 +154,13 @@ impl DefId { } } + pub fn as_modport(self, db: &dyn InternDb) -> Option> { + match self.loc(db) { + DefLoc::Modport(id) => Some(id), + _ => None, + } + } + pub fn as_stmt(self, db: &dyn InternDb) -> Option> { match self.loc(db) { DefLoc::Stmt(id) => Some(id), @@ -181,10 +192,15 @@ pub enum DefKind { Genvar, Specparam, Instance, + Modport, Stmt, } impl DefKind { + pub fn is_instantiable_def(self) -> bool { + matches!(self, DefKind::Module | DefKind::Interface | DefKind::Program) + } + pub fn symbol_kind(self) -> SymbolKind { match self { DefKind::Module => SymbolKind::Module, @@ -205,6 +221,7 @@ impl DefKind { DefKind::Genvar => SymbolKind::Genvar, DefKind::Specparam => SymbolKind::Specparam, DefKind::Instance => SymbolKind::Instance, + DefKind::Modport => SymbolKind::Unknown, DefKind::Stmt => SymbolKind::Stmt, } } @@ -348,7 +365,7 @@ impl NameScope { .get(ident) .into_iter() .flat_map(|defs| defs.iter()) - .filter(|def_id| def_id.kind(db) == DefKind::Module) + .filter(|def_id| def_id.kind(db).is_instantiable_def()) .filter_map(|def_id| def_id.as_module(db)) .collect::>(); @@ -383,7 +400,9 @@ impl NameScope { pub fn module_names<'a>(&'a self, db: &'a dyn HirDb) -> impl Iterator + 'a { self.types.iter().filter_map(move |(ident, defs)| { defs.iter() - .any(|def_id| def_id.kind(db) == DefKind::Module && def_id.as_module(db).is_some()) + .any(|def_id| { + def_id.kind(db).is_instantiable_def() && def_id.as_module(db).is_some() + }) .then_some(ident) }) } diff --git a/crates/hir/src/type_infer.rs b/crates/hir/src/type_infer.rs index 35d9dba3..41022245 100644 --- a/crates/hir/src/type_infer.rs +++ b/crates/hir/src/type_infer.rs @@ -45,6 +45,7 @@ pub enum Ty { Chandle, Alias { typedef: InContainer, target: Box }, Module(ModuleId), + VirtualInterface { def: DefId, modport: Option }, GenerateBlock(GenerateBlockId), Block(crate::hir_def::block::BlockId), } @@ -157,6 +158,7 @@ fn type_of_def_id(db: &dyn HirDb, def_id: DefId) -> TyResult { .as_module(db) .map(|module_id| TyResult::new(Ty::Module(module_id))) .unwrap_or_else(|| TyResult::new(Ty::Unknown)), + DefKind::Interface => TyResult::new(Ty::VirtualInterface { def: def_id, modport: None }), DefKind::Port | DefKind::Variable | DefKind::Net @@ -177,7 +179,26 @@ fn type_of_def_id(db: &dyn HirDb, def_id: DefId) -> TyResult { DefKind::Instance => def_id .as_instance(db) .and_then(|instance| instance_target_module_id(db, instance.module_id, instance.value)) - .map(|module_id| TyResult::new(Ty::Module(module_id))) + .map(|module_id| { + let module_kind = db.hir_file(module_id.file_id).get(module_id.value).kind; + if module_kind == ModuleKind::Interface { + TyResult::new(Ty::VirtualInterface { + def: DefId::new(db, module_id), + modport: None, + }) + } else { + TyResult::new(Ty::Module(module_id)) + } + }) + .unwrap_or_else(|| TyResult::new(Ty::Unknown)), + DefKind::Modport => def_id + .as_modport(db) + .map(|modport| { + TyResult::new(Ty::VirtualInterface { + def: DefId::new(db, modport.module_id), + modport: Some(def_id), + }) + }) .unwrap_or_else(|| TyResult::new(Ty::Unknown)), DefKind::GenerateBlock => def_id .as_generate_block(db) @@ -187,8 +208,7 @@ fn type_of_def_id(db: &dyn HirDb, def_id: DefId) -> TyResult { .as_block(db) .map(|block_id| TyResult::new(Ty::Block(block_id))) .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Interface - | DefKind::Program + DefKind::Program | DefKind::Udp | DefKind::Config | DefKind::Library @@ -231,6 +251,9 @@ pub fn members_of_ty(db: &dyn HirDb, ty: &Ty) -> Vec { Ty::Struct(struct_id) => struct_members(db, *struct_id), Ty::Union(def_id) => union_members(db, *def_id), Ty::Module(module_id) => module_members(db, *module_id), + Ty::VirtualInterface { def, .. } => { + def.as_module(db).map(|module_id| module_members(db, module_id)).unwrap_or_default() + } Ty::GenerateBlock(generate_block_id) => generate_block_members(db, *generate_block_id), Ty::Block(block_id) => block_members(db, *block_id), Ty::Unknown @@ -275,6 +298,7 @@ pub fn type_class(db: &dyn HirDb, ty: &Ty) -> Option { | Ty::Event | Ty::Chandle | Ty::Module(_) + | Ty::VirtualInterface { .. } | Ty::GenerateBlock(_) | Ty::Block(_) => None, } @@ -347,6 +371,7 @@ pub fn packed_bit_width(db: &dyn HirDb, ty: &Ty) -> Option { | Ty::Event | Ty::Chandle | Ty::Module(_) + | Ty::VirtualInterface { .. } | Ty::GenerateBlock(_) | Ty::Block(_) => None, } @@ -911,6 +936,18 @@ mod tests { db.type_of_decl(decl).ty.clone() } + fn path_ty(db: &TestDb, module_id: ModuleId, segments: &[&str]) -> Ty { + let path = segments.iter().map(|segment| ident(segment)).collect::>(); + let res = crate::semantics::pathres::resolve_path( + db, + module_id.into(), + &path, + NameContext::Value, + ) + .unwrap_or_else(|| panic!("path {segments:?} should resolve")); + db.type_of_path_resolution(res).ty.clone() + } + fn typedef_ty(db: &TestDb, module_id: ModuleId, name: &str) -> Ty { let res = resolve_name(db, module_id.into(), &ident(name), NameContext::Type) .expect("typedef should resolve"); @@ -1031,4 +1068,30 @@ endmodule ] ); } + + #[test] + fn virtual_interface_type_display_covers_instance_and_modport() { + let db = db_with_root_text( + r#" +interface bus_if; + wire clk; + modport host(input clk); +endinterface + +module top; + bus_if u_if(); +endmodule +"#, + ); + let top = module_id(&db, "top"); + + assert_eq!( + path_ty(&db, top, &["u_if"]).display_source(&db).unwrap(), + "virtual interface bus_if" + ); + assert_eq!( + path_ty(&db, top, &["u_if", "host"]).display_source(&db).unwrap(), + "virtual interface bus_if.host" + ); + } } diff --git a/crates/ide/src/document_symbols.rs b/crates/ide/src/document_symbols.rs index 4d46faca..16730388 100644 --- a/crates/ide/src/document_symbols.rs +++ b/crates/ide/src/document_symbols.rs @@ -327,6 +327,13 @@ fn collect_module_items( ModuleItem::SubroutineId(subroutine_id) => { build_subroutine(collector, subroutine_id, module, src_map) } + ModuleItem::ModportId(modport_id) => { + let modport = module.get(modport_id); + if let Some(src) = src_map.get(modport_id) { + collector.push_symbol(&modport.name, src); + collector.pop(); + } + } ModuleItem::StructId(struct_id) => build_struct(collector, struct_id, module, src_map), } } diff --git a/crates/ide/src/render.rs b/crates/ide/src/render.rs index cd574bd0..662b9164 100644 --- a/crates/ide/src/render.rs +++ b/crates/ide/src/render.rs @@ -13,7 +13,7 @@ use hir::{ }, literal::Literal, module::{ - ModuleId, + ModuleId, ModuleKind, instantiation::InstanceId, port::{NonAnsiPortId, Ports}, }, @@ -284,7 +284,13 @@ fn render_def_origin( fn render_definition_title(db: &RootDb, origin: &DefId) -> Option { let name = origin.name(db)?; let kind = match origin.loc(db) { - DefLoc::Module(_) => "Module", + DefLoc::Module(module_id) => match db.hir_file(module_id.file_id).get(module_id.value).kind + { + ModuleKind::Module => "Module", + ModuleKind::Interface => "Interface", + ModuleKind::Program => "Program", + ModuleKind::Package => "Package", + }, DefLoc::Config(_) => "Config", DefLoc::Library(_) => "Library", DefLoc::Udp(_) => "Primitive", @@ -298,7 +304,9 @@ fn render_definition_title(db: &RootDb, origin: &DefId) -> Option { DefLoc::Decl(decl_id) => render_decl_title_kind(db, decl_id)?, DefLoc::Typedef(_) => "Typedef", DefLoc::Instance(_) => "Instance", + DefLoc::Modport(_) => "Modport", DefLoc::Stmt(_) => "Statement", + _ => return None, }; Some(format!("{kind} {}", inline_code(name.as_str()))) @@ -341,7 +349,13 @@ fn render_signature(sema: &Semantics, origin: &DefId) -> Option fn render_module_signature(db: &RootDb, module_id: ModuleId) -> Option { let module = db.module(module_id); let name = module.name.as_ref()?; - let mut signature = format!("module {name}"); + let keyword = match db.hir_file(module_id.file_id).get(module_id.value).kind { + ModuleKind::Module => "module", + ModuleKind::Interface => "interface", + ModuleKind::Program => "program", + ModuleKind::Package => "package", + }; + let mut signature = format!("{keyword} {name}"); let params = render_module_param_ports(db, module_id); if !params.is_empty() { @@ -628,6 +642,7 @@ fn render_label_signature(db: &RootDb, origin: &DefId) -> Option { DefLoc::Block(_) => "block", DefLoc::GenerateBlock(_) => "generate", DefLoc::Instance(_) => "instance", + DefLoc::Modport(_) => "modport", DefLoc::Stmt(_) => "statement", DefLoc::Typedef(_) => "typedef", DefLoc::Module(_) @@ -635,6 +650,7 @@ fn render_label_signature(db: &RootDb, origin: &DefId) -> Option { | DefLoc::SubroutinePort(_) | DefLoc::NonAnsiPort(_) | DefLoc::Decl(_) => return None, + _ => return None, }; Some(format!("{kind} {name}")) } diff --git a/crates/ide/src/semantic_index.rs b/crates/ide/src/semantic_index.rs index c7322f54..bd83501d 100644 --- a/crates/ide/src/semantic_index.rs +++ b/crates/ide/src/semantic_index.rs @@ -10,7 +10,7 @@ use hir::{ hir_def::{Ident, module::ModuleId}, semantics::Semantics, source_map::IsSrc, - symbol::{DefId, DefKind}, + symbol::DefId, }; use itertools::Itertools; use rustc_hash::FxHashMap; @@ -120,7 +120,7 @@ impl ModuleIndex { for (_, defs) in db.file_scope(hir_file_id).iter_listing() { for module_id in defs .iter() - .filter(|def_id| def_id.kind(db) == DefKind::Module) + .filter(|def_id| def_id.kind(db).is_instantiable_def()) .filter_map(|def_id| def_id.as_module(db)) { let Some(module) = SemanticModuleDefinition::new(db, module_id) else { diff --git a/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__instance.snap b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__instance.snap new file mode 100644 index 00000000..39f6b4f0 --- /dev/null +++ b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__instance.snap @@ -0,0 +1,16 @@ +--- +source: crates/ide/src/verilog_2005.rs +expression: normalize_hover_snapshot(instance_hover.info.as_str()) +--- +Instance `u_if` + +```systemverilog +instance u_if of bus_if + +interface bus_if () +``` + + +--- + +in `top` from [feature.v]() line 8 diff --git a/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__modport.snap b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__modport.snap new file mode 100644 index 00000000..2484ecab --- /dev/null +++ b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_interface_modport_hover_and_navigation__modport.snap @@ -0,0 +1,14 @@ +--- +source: crates/ide/src/verilog_2005.rs +expression: normalize_hover_snapshot(modport_hover.info.as_str()) +--- +Modport `host` + +```systemverilog +modport host +``` + + +--- + +in `bus_if` from [feature.v]() line 4 diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index b51503d3..ddc1e48e 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -2700,6 +2700,80 @@ endmodule ); } +#[test] +fn systemverilog_interface_modport_hover_and_navigation() { + let text = r#" +interface /*marker:iface_def*/bus_if; + wire /*marker:clk_def*/clk; + modport /*marker:modport_def*/host(input clk); +endinterface + +module top; + /*marker:iface_ref*/bus_if /*marker:inst_ref*/u_if(); + assign u_if./*marker:clk_ref*/clk = 1'b0; + initial begin + u_if./*marker:modport_ref*/host; + end +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let iface_def_range = marked_range(&markers, "iface_def", TextSize::of("bus_if")); + let iface_nav = analysis + .goto_definition(position(file_id, &markers, "iface_ref")) + .unwrap() + .expect("interface definition expected"); + assert!( + iface_nav.info.iter().any(|target| target.focus_range == Some(iface_def_range)), + "interface reference should navigate to interface definition: {iface_nav:?}" + ); + + let modport_def_range = marked_range(&markers, "modport_def", TextSize::of("host")); + let modport_nav = analysis + .goto_definition(position(file_id, &markers, "modport_ref")) + .unwrap() + .expect("modport definition expected"); + assert!( + modport_nav.info.iter().any(|target| target.focus_range == Some(modport_def_range)), + "modport reference should navigate to modport definition: {modport_nav:?}" + ); + + let clk_def_range = marked_range(&markers, "clk_def", TextSize::of("clk")); + let clk_nav = analysis + .goto_definition(position(file_id, &markers, "clk_ref")) + .unwrap() + .expect("interface member definition expected"); + assert!( + clk_nav.info.iter().any(|target| target.focus_range == Some(clk_def_range)), + "interface member reference should navigate to net definition: {clk_nav:?}" + ); + + let instance_hover = analysis + .hover( + position(file_id, &markers, "inst_ref"), + HoverConfig { format: HoverFormat::PlainText }, + ) + .unwrap() + .expect("interface instance hover expected"); + assert_hover_snapshot!( + "systemverilog_interface_modport_hover_and_navigation__instance", + instance_hover.info.as_str(), + ); + + let modport_hover = analysis + .hover( + position(file_id, &markers, "modport_ref"), + HoverConfig { format: HoverFormat::PlainText }, + ) + .unwrap() + .expect("modport hover expected"); + assert_hover_snapshot!( + "systemverilog_interface_modport_hover_and_navigation__modport", + modport_hover.info.as_str(), + ); +} + #[test] fn verilog_2005_hover_uses_relative_source_label_for_absolute_workspace_path() { let dir = TestDir::new("hover-source-label"); @@ -2833,6 +2907,9 @@ fn semantic_index_groups_modules_and_references_by_definition() { ( "/a.sv", r#" +interface /*marker:a_iface_def*/bus_if; +endinterface + module /*marker:a_module_def*/mod_a; wire /*marker:a_shared_def*/shared; assign y = /*marker:a_shared_ref*/shared; @@ -2866,6 +2943,10 @@ endmodule assert_eq!(modules.len(), 1, "module index should contain mod_a exactly once"); assert_eq!(modules[0].file_id, *file_a); assert_eq!(modules[0].name_range, marked_range(markers_a, "a_module_def", 5)); + let interfaces = module_index.module_definitions(&"bus_if".into()); + assert_eq!(interfaces.len(), 1, "module index should contain bus_if exactly once"); + assert_eq!(interfaces[0].file_id, *file_a); + assert_eq!(interfaces[0].name_range, marked_range(markers_a, "a_iface_def", 6)); let groups = index.reference_groups_named("shared"); assert_eq!(groups.len(), 2, "same-name definitions should be separate reference groups");