From ea357080c4066c6e09a7f7e7e75637da6cbe616f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 27 Jun 2026 05:20:35 +0800 Subject: [PATCH 1/2] feat(hir): resolve hierarchical paths (cherry picked from commit f89300d1dd29afb612e1beb807584dd719140065) --- crates/hir/src/semantics/hir_to_def.rs | 113 +++---- crates/hir/src/semantics/pathres.rs | 301 +++++++++++++++++- crates/hir/src/type_infer.rs | 14 +- crates/ide/src/completion/engine/member.rs | 11 +- crates/ide/src/completion/engine/tests.rs | 22 ++ crates/ide/src/definitions.rs | 61 ++++ crates/ide/src/semantic_tokens.rs | 81 ++++- ...ilog_2005__verilog_2005_lsp_snapshots.snap | 1 + 8 files changed, 517 insertions(+), 87 deletions(-) diff --git a/crates/hir/src/semantics/hir_to_def.rs b/crates/hir/src/semantics/hir_to_def.rs index 76f88e27..a189e5a2 100644 --- a/crates/hir/src/semantics/hir_to_def.rs +++ b/crates/hir/src/semantics/hir_to_def.rs @@ -6,12 +6,10 @@ use crate::{ container::{InContainer, ScopeId}, hir_def::{ Ident, - block::BlockId, expr::{Expr, ExprId}, - module::{ModuleId, generate::GenerateBlockId, instantiation::InstanceId}, }, - semantics::pathres::{name_scope, resolve_name}, - symbol::{DefLoc, NameContext}, + semantics::pathres::{descend_scope, name_scope, resolve_name, resolve_path}, + symbol::NameContext, }; #[derive(Default, Debug)] @@ -30,12 +28,20 @@ impl Source2DefCtx<'_, '_> { let mut resolve = |expr: &Expr| match expr { Expr::Field { receiver, field } => { let field = field.as_ref()?; + if let Some(res) = self.resolve_expr_path(cont_id, expr_id, NameContext::Value) { + self.hir_cache.expr_map.insert(InContainer::new(cont_id, expr_id), res.clone()); + return Some(res); + } let receiver_res = self.expr_to_def(InContainer::new(cont_id, *receiver))?; let res = self.resolve_member_from_resolution(receiver_res, field)?; self.hir_cache.expr_map.insert(InContainer::new(cont_id, expr_id), res.clone()); Some(res) } Expr::ElementSelect { receiver, .. } => { + if let Some(res) = self.resolve_expr_path(cont_id, expr_id, NameContext::Value) { + self.hir_cache.expr_map.insert(InContainer::new(cont_id, expr_id), res.clone()); + return Some(res); + } let res = self.expr_to_def(InContainer::new(cont_id, *receiver))?; self.hir_cache.expr_map.insert(InContainer::new(cont_id, expr_id), res.clone()); Some(res) @@ -89,76 +95,53 @@ impl Source2DefCtx<'_, '_> { field: &Ident, ) -> Option { for def_id in res.def_ids() { - match def_id.loc(self.db) { - DefLoc::Module(module_id) => { - if let Some(res) = self.resolve_member_in_module(module_id, field) { - return Some(res); - } - } - DefLoc::Instance(instance) => { - let target_module = - self.instance_target_module_id(instance.module_id, instance.value)?; - if let Some(res) = self.resolve_member_in_module(target_module, field) { - return Some(res); - } - } - DefLoc::Block(block_id) => { - if let Some(res) = self.resolve_member_in_block(block_id, field) { - return Some(res); - } - } - DefLoc::GenerateBlock(generate_block_id) => { - if let Some(res) = - self.resolve_member_in_generate_block(generate_block_id, field) - { - return Some(res); - } - } - _ => {} + let Some(scope_id) = descend_scope(self.db, *def_id) else { + continue; + }; + if let Some(res) = name_scope(self.db, scope_id) + .lookup(NameContext::Value, field) + .and_then(PathResolution::from_def_ids) + { + return Some(res); } } None } - fn resolve_member_in_module( - &mut self, - module_id: ModuleId, - field: &Ident, - ) -> Option { - name_scope(self.db, module_id.into()) - .lookup(NameContext::Value, field) - .and_then(PathResolution::from_def_ids) - } - - fn resolve_member_in_block( - &mut self, - block_id: BlockId, - field: &Ident, + fn resolve_expr_path( + &self, + cont_id: ScopeId, + expr_id: ExprId, + ctx: NameContext, ) -> Option { - name_scope(self.db, block_id.into()) - .lookup(NameContext::Value, field) - .and_then(PathResolution::from_def_ids) + let path = self.expr_path(cont_id, expr_id)?; + resolve_path(self.db, cont_id, &path, ctx) } - fn resolve_member_in_generate_block( - &mut self, - generate_block_id: GenerateBlockId, - field: &Ident, - ) -> Option { - name_scope(self.db, generate_block_id.into()) - .lookup(NameContext::Value, field) - .and_then(PathResolution::from_def_ids) + fn expr_path(&self, cont_id: ScopeId, expr_id: ExprId) -> Option> { + match self.expr_in_container(cont_id, expr_id)? { + Expr::Ident(ident) => Some(vec![ident]), + Expr::Field { receiver, field } => { + let mut path = self.expr_path(cont_id, receiver)?; + path.push(field?); + Some(path) + } + Expr::ElementSelect { receiver, .. } => self.expr_path(cont_id, receiver), + _ => None, + } } - fn instance_target_module_id( - &mut self, - module_id: ModuleId, - instance_id: InstanceId, - ) -> Option { - let module = self.db.module(module_id); - let instance = module.get(instance_id); - let instantiation = module.get(instance.parent); - let module_name = instantiation.module_name.as_ref()?; - self.db.unit_scope().module_ids(self.db, module_name).unique() + fn expr_in_container(&self, cont_id: ScopeId, expr_id: ExprId) -> Option { + match cont_id { + ScopeId::File(file_id) => Some(self.db.hir_file(file_id).get(expr_id).clone()), + ScopeId::Module(module_id) => Some(self.db.module(module_id).get(expr_id).clone()), + ScopeId::Block(block_id) => Some(self.db.block(block_id).get(expr_id).clone()), + ScopeId::GenerateBlock(generate_block_id) => { + Some(self.db.generate_block(generate_block_id).get(expr_id).clone()) + } + ScopeId::Subroutine(subroutine_id) => { + Some(self.db.subroutine(subroutine_id.as_in_container()).get(expr_id).clone()) + } + } } } diff --git a/crates/hir/src/semantics/pathres.rs b/crates/hir/src/semantics/pathres.rs index a733f91a..dd9e511d 100644 --- a/crates/hir/src/semantics/pathres.rs +++ b/crates/hir/src/semantics/pathres.rs @@ -1,6 +1,7 @@ use smallvec::SmallVec; use syntax::{SyntaxNode, SyntaxTokenWithParent}; use triomphe::Arc; +use utils::get::GetRef; use super::SemanticsImpl; use crate::{ @@ -8,10 +9,25 @@ use crate::{ db::HirDb, def_id::{ModuleDef, ModuleDefId}, file::HirFileId, - hir_def::{Ident, lower_ident_opt}, - symbol::{DefId, NameContext, NameScope}, + hir_def::{ + Ident, lower_ident_opt, + module::{ModuleId, instantiation::InstanceId}, + }, + symbol::{DefId, DefKind, NameContext, NameScope}, }; +// SystemVerilog name AST note for path resolution: +// +// slang models simple names as `IdentifierName`, names with unpacked selects +// as `IdentifierSelectName { identifier, selectors }`, and qualified names as +// `ScopedName { left, separator, right }`. The `separator` token is the only +// raw-AST distinction between `a.b` hierarchical selection and `a::b` +// package/class scoping. HIR lowering turns dot-style member access and +// `ScopedName` with an identifier right side into `Expr::Field`, and +// `IdentifierSelectName` into `Expr::ElementSelect`; C3's `resolve_path` +// handles the hierarchical dot/select shape only. Package/class `::` remains +// outside this resolver until those constructs are lowered. + impl SemanticsImpl<'_> { pub fn nameres_ident( &self, @@ -66,6 +82,98 @@ pub fn resolve_name( db.unit_scope().lookup(ctx, ident).and_then(PathResolution::from_def_ids) } +pub fn resolve_path( + db: &dyn HirDb, + cont_id: ScopeId, + path: &[Ident], + ctx: NameContext, +) -> Option { + let (first, rest) = path.split_first()?; + let mut current = resolve_name(db, cont_id, first, ctx).or_else(|| { + resolve_top_level_module_root(db, cont_id, first, ctx, !rest.is_empty()) + })?; + + for (idx, segment) in rest.iter().enumerate() { + let segment_ctx = if idx + 1 == rest.len() { ctx } else { NameContext::Value }; + current = resolve_child_name(db, ¤t, segment, segment_ctx)?; + } + + Some(current) +} + +fn resolve_top_level_module_root( + db: &dyn HirDb, + cont_id: ScopeId, + ident: &Ident, + ctx: NameContext, + has_child_segment: bool, +) -> Option { + if !has_child_segment || ctx != NameContext::Value { + return None; + } + + // IEEE 1800 hierarchical names can start at a top-level module instance. + // Vide has module definitions in the type namespace and no separate + // elaborated top-instance DefId yet, so a multi-segment value path may use + // a module definition as an explicit hierarchy root. This is not a single + // 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); + PathResolution::from_def_ids(module_defs) +} + +fn resolve_child_name( + db: &dyn HirDb, + parent: &PathResolution, + ident: &Ident, + ctx: NameContext, +) -> Option { + let mut defs = SmallVec::<[DefId; 3]>::new(); + for def_id in parent.def_ids() { + let Some(scope_id) = descend_scope(db, *def_id) else { + continue; + }; + let Some(child_defs) = name_scope(db, scope_id).lookup(ctx, ident) else { + continue; + }; + for child_def_id in child_defs { + if !defs.contains(&child_def_id) { + defs.push(child_def_id); + } + } + } + PathResolution::from_def_ids(defs) +} + +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), + DefKind::Instance => { + let instance = def_id.as_instance(db)?; + instance_target_module_id(db, instance.module_id, instance.value).map(Into::into) + } + DefKind::Block => def_id.as_block(db).map(Into::into), + DefKind::GenerateBlock => def_id.as_generate_block(db).map(Into::into), + _ => None, + } +} + +pub(crate) fn instance_target_module_id( + db: &dyn HirDb, + module_id: ModuleId, + instance_id: InstanceId, +) -> Option { + let module = db.module(module_id); + let instance = module.get(instance_id); + let instantiation = module.get(instance.parent); + let module_name = instantiation.module_name.as_ref()?; + db.unit_scope().module_ids(db, module_name).unique() +} + pub(crate) fn name_scope(db: &dyn HirDb, scope_id: ScopeId) -> Arc { match scope_id { ScopeId::File(file_id) => db.file_scope(file_id), @@ -171,3 +279,192 @@ impl From for PathResolution { Self::from_def_id(def_id) } } + +#[cfg(test)] +mod tests { + use std::fmt; + + use rustc_hash::FxHashSet; + use smol_str::SmolStr; + use triomphe::Arc; + use utils::paths::{AbsPathBuf, Utf8PathBuf}; + use vfs::{FileId, FileSet, VfsPath, anchored_path::AnchoredPath}; + + use super::*; + use crate::{ + base_db::{ + diagnostics_config::DiagnosticsConfig, + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + salsa::{self, Durability}, + source_db::{ + FileLoader, SourceDb, SourceDbStorage, SourceFileKind, SourceRootDb, + SourceRootDbStorage, + }, + source_root::{SourceRoot, SourceRootId}, + }, + container::ScopeId, + db::{HirDb, HirDbStorage, InternDbStorage}, + file::HirFileId, + hir_def::Ident, + symbol::{DefKind, NameContext}, + }; + + const TOP: FileId = FileId(0); + const ROOT: SourceRootId = SourceRootId(0); + const PROFILE: CompilationProfileId = CompilationProfileId(0); + + #[salsa::database(SourceDbStorage, SourceRootDbStorage, InternDbStorage, HirDbStorage)] + #[derive(Default)] + struct TestDb { + storage: salsa::Storage, + } + + impl salsa::Database for TestDb {} + + impl fmt::Debug for TestDb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestDb").finish() + } + } + + impl FileLoader for TestDb { + fn resolve_path(&self, path: AnchoredPath<'_>) -> Option { + let source_root_id = SourceRootDb::source_root_id(self, path.anchor_id); + SourceRootDb::source_root(self, source_root_id).resolve_path(path) + } + } + + fn db_with_root_text(root_text: &str) -> TestDb { + let top_path = abs_path("rtl/top.sv"); + let mut file_set = FileSet::default(); + file_set.insert(TOP, VfsPath::from(top_path.clone())); + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + let mut files = FxHashSet::default(); + files.insert(TOP); + + let preprocess = PreprocessConfig::default(); + let project_config = ProjectConfig::new( + vec![Some(PROFILE)], + vec![CompilationProfile { + source_roots: vec![ROOT], + top_modules: Vec::new(), + preprocess: preprocess.clone(), + }], + ); + + let mut db = TestDb::default(); + db.set_files_with_durability(Box::new(files), Durability::HIGH); + db.set_project_config_with_durability(Arc::new(project_config), Durability::HIGH); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::HIGH, + ); + db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); + db.set_source_root_id_with_durability(TOP, ROOT, Durability::LOW); + db.set_file_path_with_durability(TOP, Some(top_path), Durability::LOW); + db.set_file_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); + db.set_file_text_with_durability(TOP, Arc::from(root_text), Durability::LOW); + db.set_file_preprocess_config_with_durability(TOP, Arc::new(preprocess), Durability::LOW); + db + } + + fn abs_path(path: &str) -> AbsPathBuf { + let prefix = if cfg!(windows) { "C:/repo" } else { "/repo" }; + AbsPathBuf::assert(Utf8PathBuf::from(format!("{prefix}/{path}"))) + } + + fn ident(name: &str) -> Ident { + SmolStr::new(name) + } + + fn path(segments: &[&str]) -> Vec { + segments.iter().map(|segment| ident(segment)).collect() + } + + fn resolved_kind( + db: &TestDb, + scope_id: ScopeId, + segments: &[&str], + ctx: NameContext, + ) -> DefKind { + let path = path(segments); + resolve_path(db, scope_id, &path, ctx) + .and_then(|res| res.primary_def_id()) + .map(|def_id| def_id.kind(db)) + .unwrap_or_else(|| panic!("path {segments:?} should resolve")) + } + + #[test] + fn resolve_path_descends_instances_blocks_and_generate_blocks() { + let db = db_with_root_text( + r#" +module child; + wire sig; +endmodule + +module top; + child u(); + child arr [1:0] (); + + initial begin : b + integer local_sig; + end + + generate + if (1) begin : g + wire gen_sig; + end + endgenerate +endmodule +"#, + ); + + let top = db + .unit_scope() + .module_ids(&db, &ident("top")) + .unique() + .expect("top module should resolve uniquely"); + + assert_eq!( + resolved_kind(&db, top.into(), &["u", "sig"], NameContext::Value), + DefKind::Net + ); + assert_eq!( + resolved_kind(&db, top.into(), &["arr", "sig"], NameContext::Value), + DefKind::Net + ); + assert_eq!( + resolved_kind(&db, top.into(), &["b", "local_sig"], NameContext::Value), + DefKind::Variable + ); + assert_eq!( + resolved_kind(&db, top.into(), &["g", "gen_sig"], NameContext::Value), + DefKind::Net + ); + } + + #[test] + fn resolve_path_treats_top_level_module_as_hierarchical_root() { + let db = db_with_root_text( + r#" +module child; + wire sig; +endmodule + +module top; + child u(); +endmodule +"#, + ); + + assert_eq!( + resolved_kind( + &db, + ScopeId::File(HirFileId::File(TOP)), + &["top", "u", "sig"], + NameContext::Value, + ), + DefKind::Net + ); + } +} diff --git a/crates/hir/src/type_infer.rs b/crates/hir/src/type_infer.rs index abeeb05a..7efc0273 100644 --- a/crates/hir/src/type_infer.rs +++ b/crates/hir/src/type_infer.rs @@ -20,7 +20,7 @@ use crate::{ subroutine::SubroutinePortId, typedef::TypedefId, }, - semantics::pathres::{PathResolution, resolve_name}, + semantics::pathres::{PathResolution, instance_target_module_id, resolve_name}, symbol::{DefId, DefKind, NameContext}, }; @@ -504,18 +504,6 @@ fn type_of_subroutine_port_impl(db: &dyn HirDb, port: InSubroutine Option { - let module = db.module(module_id); - let instance = module.get(instance_id); - let instantiation = module.get(instance.parent); - let module_name = instantiation.module_name.as_ref()?; - db.unit_scope().module_ids(db, module_name).unique() -} - fn int_kind_width(kind: IntKind) -> usize { match kind { IntKind::Byte => 8, diff --git a/crates/ide/src/completion/engine/member.rs b/crates/ide/src/completion/engine/member.rs index 6f356c6e..9264cb59 100644 --- a/crates/ide/src/completion/engine/member.rs +++ b/crates/ide/src/completion/engine/member.rs @@ -126,8 +126,15 @@ fn members_for_expr( file_id: HirFileId, expr: ast::Expression<'_>, ) -> Option> { - let ty = db.type_of_expr(sema.resolve_expr(file_id, expr)?); - let members = members_of_ty(db, &ty.ty); + let expr_id = sema.resolve_expr(file_id, expr)?; + let ty = db.type_of_expr(expr_id); + let mut members = members_of_ty(db, &ty.ty); + if members.is_empty() + && let Some(res) = sema.expr_to_def(expr_id) + { + let ty = db.type_of_path_resolution(res); + members = members_of_ty(db, &ty.ty); + } (!members.is_empty()).then_some(members) } diff --git a/crates/ide/src/completion/engine/tests.rs b/crates/ide/src/completion/engine/tests.rs index 4f51c47e..8931edcd 100644 --- a/crates/ide/src/completion/engine/tests.rs +++ b/crates/ide/src/completion/engine/tests.rs @@ -129,6 +129,28 @@ fn manual_and_triggered_at_use_same_sensitivity_expectation_behavior() { assert!(labels(&manual).contains(&"*"), "sensitivity completions expected: {manual:?}"); } +#[test] +fn completes_hierarchical_module_root_member_access() { + let text = r#" +module leaf; + wire leaf_wire; +endmodule + +module top; + leaf u0(); + initial begin + top.u0./*caret*/ + end +endmodule +"#; + + let items = completions_in_text(text, Some(TriggerChar::Dot)); + assert!( + labels(&items).contains(&"leaf_wire"), + "hierarchical member completion should include child module members: {items:?}" + ); +} + #[test] fn completion_fixtures() { insta::glob!("fixtures/*.v", |path| { diff --git a/crates/ide/src/definitions.rs b/crates/ide/src/definitions.rs index b1989ffe..7fb14c71 100644 --- a/crates/ide/src/definitions.rs +++ b/crates/ide/src/definitions.rs @@ -56,6 +56,10 @@ impl DefinitionClass { return Some(def); } + if token_is_in_non_dot_scoped_name(parent) { + return None; + } + let res = match_ast! { parent, ast::NamedParamAssignment[it] if it.name() == Some(tok) => { resolve_named_param_assignment(sema.db, file_id.file_id(), it) @@ -133,6 +137,9 @@ fn resolve_member_or_scoped_name( } let scoped = SyntaxAncestors::start_from(parent).find_map(ast::ScopedName::cast)?; + if !scoped_uses_dot(scoped) { + return None; + } let right_tok = scoped_right_token(scoped)?; if right_tok != tok { return None; @@ -204,6 +211,20 @@ fn scoped_right_token(scoped: ast::ScopedName<'_>) -> Option> { } } +fn scoped_uses_dot(scoped: ast::ScopedName<'_>) -> bool { + scoped + .syntax() + .children() + .filter_map(|elem| elem.as_token()) + .any(|tok| tok.kind() == syntax::Token![.]) +} + +fn token_is_in_non_dot_scoped_name(parent: syntax::SyntaxNode<'_>) -> bool { + SyntaxAncestors::start_from(parent) + .find_map(ast::ScopedName::cast) + .is_some_and(|scoped| !scoped_uses_dot(scoped)) +} + #[cfg(test)] mod tests { use std::fmt::Write; @@ -309,4 +330,44 @@ mod tests { insta::assert_snapshot!(report); } + + #[test] + fn definition_resolves_hierarchical_path_leaf() { + let text = r#" +module leaf; + wire leaf_wire; +endmodule + +module top; + leaf u0(); + initial begin + top.u0.leaf_/*caret*/wire; + end +endmodule +"#; + let offset = TextSize::from(text.find("/*caret*/").unwrap() as u32); + let text = text.replace("/*caret*/", ""); + let (host, file_id) = host_with_file(&text); + let db = host.raw_db(); + let sema = Semantics::::new(db); + let parsed_file = sema.parse_file(file_id); + let file = parsed_file.compilation_unit().unwrap(); + let token = file + .syntax() + .token_at_offset(offset) + .pick_bext_token(crate::goto_definition::token_precedence) + .unwrap(); + + let DefinitionClass::Definition(def) = + DefinitionClass::resolve(&sema, file_id.into(), token).unwrap() + else { + panic!("expected plain definition for hierarchical leaf"); + }; + + let origins = def.origins(db); + assert!( + origins.iter().any(|origin| origin.kind(db) == DefKind::Net), + "hierarchical leaf should resolve to child net, got {origins:?}" + ); + } } diff --git a/crates/ide/src/semantic_tokens.rs b/crates/ide/src/semantic_tokens.rs index 8374d5ea..5e00c29e 100644 --- a/crates/ide/src/semantic_tokens.rs +++ b/crates/ide/src/semantic_tokens.rs @@ -8,7 +8,7 @@ use hir::{ Ident, block::{BlockId, BlockInfo}, expr::{ - Expr, ExprId, + Expr, ExprId, ExprSrc, data_ty::{DataTy, NamedDataTy}, declarator::DeclaratorParent, }, @@ -25,7 +25,11 @@ use hir::{ }; use rustc_hash::FxHashSet; use smol_str::SmolStr; -use syntax::{ast, has_text_range::HasTextRange}; +use syntax::{ + SyntaxTree, + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; use utils::{ get::{Get, GetRef}, text_edit::TextRange, @@ -187,6 +191,7 @@ fn collect_file( collector: &mut SemaTokenCollector, ) { let (hir_file, file_src_map) = sema.db.hir_file_with_source_map(file_id); + let tree = sema.db.parse(file_id); for (local_module_id, _) in hir_file.modules.iter() { let Some(range) = file_src_map.get(local_module_id).map(|src| src.range()) else { @@ -232,7 +237,12 @@ fn collect_file( continue; } match expr { - Expr::Field { .. } => {} + Expr::Field { .. } => { + let _: Option<()> = try { + let src = file_src_map.get(expr_id)?; + collect_field_like(sema, file_id.into(), expr_id, src, &tree, collector)?; + }; + } Expr::Ident(name) => { let Some(range) = file_src_map.get(expr_id).map(|src| src.range()) else { continue; @@ -285,6 +295,7 @@ fn collect_module( let db = sema.db; let (module, module_src_map) = db.module_with_source_map(module_id); let (module, module_src_map) = (module.as_ref(), module_src_map.as_ref()); + let tree = db.parse(module_id.file_id); port::collect_port(sema, module_id, collector); let collect_ident_like = @@ -335,7 +346,12 @@ fn collect_module( continue; } match expr { - Expr::Field { .. } => {} + Expr::Field { .. } => { + let _: Option<()> = try { + let src = module_src_map.get(expr_id)?; + collect_field_like(sema, module_id.into(), expr_id, src, &tree, collector)?; + }; + } Expr::Ident(name) => { let Some(range) = module_src_map.get(expr_id).map(|src| src.range()) else { continue; @@ -388,6 +404,7 @@ fn collect_block( let db = sema.db; let (block, block_src_map) = db.block_with_source_map(block_id); let (block, block_src_map) = (block.as_ref(), block_src_map.as_ref()); + let tree = db.parse(HirFileId::File(block_id.file_id(db))); let collect_ident_like = |name: &SmolStr, range: TextRange, collector: &mut SemaTokenCollector| { @@ -425,7 +442,12 @@ fn collect_block( continue; } match expr { - Expr::Field { .. } => {} + Expr::Field { .. } => { + let _: Option<()> = try { + let src = block_src_map.get(expr_id)?; + collect_field_like(sema, block_id.into(), expr_id, src, &tree, collector)?; + }; + } Expr::Ident(name) => { let Some(range) = block_src_map.get(expr_id).map(|src| src.range()) else { continue; @@ -560,6 +582,55 @@ fn collect_type_ident_like( collect_resolved_path(sema, res, range, collector) } +fn collect_field_like( + sema: &Semantics<'_, RootDb>, + cont_id: ScopeId, + expr_id: ExprId, + src: ExprSrc, + tree: &SyntaxTree, + collector: &mut SemaTokenCollector, +) -> Option<()> { + let expr = src.to_node(tree)?; + let range = field_name_range(expr)?; + if !collector.range.intersect(range).is_some() { + return None; + } + let res = sema.expr_to_def(InContainer::new(cont_id, expr_id))?; + collect_resolved_path(sema, res, range, collector) +} + +fn field_name_range(expr: ast::Expression<'_>) -> Option { + if let Some(access) = ast::MemberAccessExpression::cast(expr.syntax()) { + return access.name()?.text_range_in(access.syntax()); + } + + if let Some(scoped) = ast::ScopedName::cast(expr.syntax()) { + if !scoped_uses_dot(scoped) { + return None; + } + return scoped_right_token(scoped)?.text_range_in(scoped.syntax()); + } + + None +} + +fn scoped_right_token(scoped: ast::ScopedName<'_>) -> Option> { + use ast::Name::*; + match scoped.right() { + IdentifierName(ident) => ident.identifier(), + IdentifierSelectName(ident) => ident.identifier(), + _ => None, + } +} + +fn scoped_uses_dot(scoped: ast::ScopedName<'_>) -> bool { + scoped + .syntax() + .children() + .filter_map(|elem| elem.as_token()) + .any(|tok| tok.kind() == syntax::Token![.]) +} + fn named_data_ty_expr_id(ty: DataTy) -> Option { match ty { DataTy::Named(NamedDataTy::Ident(expr_id) | NamedDataTy::Field(expr_id)) => Some(expr_id), diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_lsp_snapshots.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_lsp_snapshots.snap index b1de3003..4c7b83f8 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_lsp_snapshots.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_lsp_snapshots.snap @@ -34,6 +34,7 @@ cfg_top Config container=None 264..267 Port(Clk) SemaTokenModifier(READ) 520..523 Port(Clk) SemaTokenModifier(READ) 626..633 Instance SemaTokenModifier(0x0) +634..635 Port(Others) SemaTokenModifier(WRITE) # navigation module_ref: ["Some(\"child\"):Some(Module)"] From 7a6c6791ed0200d5fc75669d3b250b1b1576cd8f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 27 Jun 2026 13:33:19 +0800 Subject: [PATCH 2/2] chore: satisfy workspace lint gates Apply rustfmt fixes for the hierarchical path resolution lowering. --- crates/hir/src/semantics/pathres.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/hir/src/semantics/pathres.rs b/crates/hir/src/semantics/pathres.rs index dd9e511d..45a1a1d3 100644 --- a/crates/hir/src/semantics/pathres.rs +++ b/crates/hir/src/semantics/pathres.rs @@ -89,9 +89,8 @@ pub fn resolve_path( ctx: NameContext, ) -> Option { let (first, rest) = path.split_first()?; - let mut current = resolve_name(db, cont_id, first, ctx).or_else(|| { - resolve_top_level_module_root(db, cont_id, first, ctx, !rest.is_empty()) - })?; + let mut current = resolve_name(db, cont_id, first, ctx) + .or_else(|| resolve_top_level_module_root(db, cont_id, first, ctx, !rest.is_empty()))?; for (idx, segment) in rest.iter().enumerate() { let segment_ctx = if idx + 1 == rest.len() { ctx } else { NameContext::Value }; @@ -118,11 +117,8 @@ fn resolve_top_level_module_root( // a module definition as an explicit hierarchy root. This is not a single // 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); + let module_defs = + type_res.def_ids().iter().copied().filter(|def_id| def_id.kind(db) == DefKind::Module); PathResolution::from_def_ids(module_defs) } @@ -425,10 +421,7 @@ endmodule .unique() .expect("top module should resolve uniquely"); - assert_eq!( - resolved_kind(&db, top.into(), &["u", "sig"], NameContext::Value), - DefKind::Net - ); + assert_eq!(resolved_kind(&db, top.into(), &["u", "sig"], NameContext::Value), DefKind::Net); assert_eq!( resolved_kind(&db, top.into(), &["arr", "sig"], NameContext::Value), DefKind::Net