diff --git a/.gitignore b/.gitignore index e2f9da936..58e9ee3eb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,7 @@ tags # generated files generated.rs -docs/public/vide-lab/ -docs/public/schemas/ -docs/hir-def/ +/docs/ editors/zed/extension.wasm editors/zed/grammars/systemverilog/ diff --git a/Cargo.toml b/Cargo.toml index d5a7bbe73..2b38e0bfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,9 @@ description = "Language server for Verilog and System-Verilog" members = [ ".", "crates/base-db", + "crates/design-graph", "crates/hir-def", "crates/hir-semantics", - "crates/hir-ty", "crates/ide", "crates/preproc", "crates/preproc-expand", @@ -78,9 +78,9 @@ triomphe.workspace = true [workspace.dependencies] base-db = { path = "./crates/base-db/", version = "0.0.0" } +design-graph = { path = "./crates/design-graph/", version = "0.0.0" } hir-def = { path = "./crates/hir-def/", version = "0.0.0" } hir-semantics = { path = "./crates/hir-semantics/", version = "0.0.0" } -hir-ty = { path = "./crates/hir-ty/", version = "0.0.0" } ide = { path = "./crates/ide/", version = "0.0.0" } preproc = { path = "./crates/preproc/", version = "0.0.0" } preproc-expand = { path = "./crates/preproc-expand/", version = "0.0.0" } diff --git a/crates/base-db/src/change.rs b/crates/base-db/src/change.rs index b95f639ce..c62d3f6e1 100644 --- a/crates/base-db/src/change.rs +++ b/crates/base-db/src/change.rs @@ -71,7 +71,14 @@ impl Change { } let text = changed_file.text().unwrap_or_else(|| Arc::from("")); - db.set_file_kind_with_durability(file_id, kind, durability); + // Salsa treats every input write as a new revision, even when the + // value is unchanged. Rewriting kind on a body-only Modify dirties + // every query that reads `file_kind` (workspace catalogs, + // `unit_scope`, fold filters). Skip the write when the salsa + // input already exists and already holds this kind. + if !db.files().contains(&file_id) || db.file_kind(file_id) != kind { + db.set_file_kind_with_durability(file_id, kind, durability); + } db.set_file_text_with_durability(file_id, text, durability); } diff --git a/crates/hir-ty/Cargo.toml b/crates/design-graph/Cargo.toml similarity index 70% rename from crates/hir-ty/Cargo.toml rename to crates/design-graph/Cargo.toml index 0a3ddb6f5..ff4085d18 100644 --- a/crates/hir-ty/Cargo.toml +++ b/crates/design-graph/Cargo.toml @@ -1,18 +1,16 @@ [package] -name = "hir-ty" +name = "design-graph" version = "0.0.0" +description = "Compilation-unit design-unit facts and name join" edition.workspace = true [dependencies] base-db.workspace = true -hir-def.workspace = true rustc-hash.workspace = true salsa.workspace = true +smallvec.workspace = true smol_str.workspace = true syntax.workspace = true triomphe.workspace = true utils.workspace = true vfs.workspace = true - -[dev-dependencies] -preproc-expand.workspace = true diff --git a/crates/design-graph/src/db.rs b/crates/design-graph/src/db.rs new file mode 100644 index 000000000..a4820dfb8 --- /dev/null +++ b/crates/design-graph/src/db.rs @@ -0,0 +1,126 @@ +//! Salsa `file_facts` over an unexpanded parse. + +use std::cell::Cell; + +use base_db::{salsa, source_db::SourceRootDb}; +use syntax::{SyntaxTree, SyntaxTreeOptions}; +use triomphe::Arc; +use vfs::FileId; + +use crate::{ + facts::{DeclIndex, FileFacts, extract}, + graph::{GeneratedUnits, UnitCatalog}, +}; + +thread_local! { + pub static SOURCE_CATALOG_RUNS: Cell = const { Cell::new(0) }; +} + +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub struct FileFactsKey { + #[returns(copy)] + pub file_id: FileId, +} + +/// Workspace database that can extract unexpanded design-unit facts. +#[salsa::db] +pub trait DesignGraphDb: SourceRootDb {} + +fn default_source_buffer_path(db: &dyn SourceRootDb, file_id: FileId) -> String { + db.file_path(file_id).map(|path| path.to_string()).unwrap_or_else(|| { + if cfg!(windows) { + format!(r"C:\__vide_virtual__\{}", file_id.index()) + } else { + format!("/__vide_virtual__/{}", file_id.index()) + } + }) +} + +#[salsa::tracked(lru = 256, returns(clone))] +pub fn file_facts_query(db: &dyn DesignGraphDb, key: FileFactsKey) -> Arc { + let file_id = key.file_id(db); + let text = db.file_text(file_id); + let path = default_source_buffer_path(db, file_id); + let name = + db.file_path(file_id).map(|path| path.to_string()).unwrap_or_else(|| "source".into()); + let profile_id = db.file_compilation_profile(file_id); + let predefines = db.project_config().preprocess_for_profile(profile_id).predefine_strings(); + // U2: profile predefines, no include expansion. This is not + // `SyntaxTreeOptions::without_include_expansion()`: that helper ships + // empty predefines so `source_model` (U1) stays file-local. FileFacts + // must see the same `ifdef` view the profile will compile, or gated + // units disappear from the name catalog. It also cannot share U3 + // (`literal_include_targets`): that scan needs a preprocessor `Trace`, + // and attaching a Trace here would make every L0 fact pay for include + // resolution. Sharing one salsa query would hide gated units, invalidate + // the file-local preprocessor model, or both. + // + // `preprocessor_independent` is `syntax::preprocessor_independent` — + // the same directive-trivia walk U1 uses. The boolean cannot diverge; + // the trees can, because predefines differ. + let options = SyntaxTreeOptions { + predefines, + include_paths: Vec::new(), + include_buffers: Vec::new(), + expand_includes: false, + collect_expected_syntax: false, + expected_syntax_offset: None, + }; + syntax::record_unexpanded_parse("file_facts"); + let tree = SyntaxTree::from_file_in_memory_with_options(&text, &name, &path, &options); + Arc::new(extract::from_tree(file_id, &tree, &text)) +} + +/// Position-free and small: must not share the parse LRU with `file_facts`. +/// A workspace larger than that LRU would otherwise re-extract every evicted +/// file's decls on the next revision, which re-parses `file_facts` with them. +#[salsa::tracked(returns(clone))] +pub fn file_decls_query(db: &dyn DesignGraphDb, key: FileFactsKey) -> Arc { + Arc::new(file_facts_query(db, key).decls()) +} + +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub struct UnitCatalogKey { + #[returns(copy)] + pub _unit: (), +} + +/// L0 name catalog of source decls. Production resolution uses this as a +/// name → file locator. Generated names are not merged here; they live on +/// the paid-parse owner table (`HirFileId::Macro`). +#[salsa::tracked(lru = 4, returns(clone))] +pub fn source_unit_catalog_query( + db: &dyn DesignGraphDb, + _key: UnitCatalogKey, +) -> triomphe::Arc { + SOURCE_CATALOG_RUNS.with(|runs| runs.set(runs.get() + 1)); + let decls: Vec<_> = db + .files() + .iter() + .copied() + .filter(|&file_id| db.file_kind(file_id).is_semantic_compilation_unit()) + .map(|file_id| db.file_decls(file_id)) + .collect(); + triomphe::Arc::new(UnitCatalog::from_decls( + decls.iter().map(std::convert::AsRef::as_ref), + &GeneratedUnits::default(), + )) +} + +pub fn set_file_facts_lru_capacity(db: &mut dyn DesignGraphDb, capacity: usize) { + file_facts_query::set_lru_capacity(db, capacity); +} + +impl dyn DesignGraphDb + '_ { + pub fn file_facts(&self, file_id: FileId) -> Arc { + file_facts_query(self, FileFactsKey::new(self, file_id)) + } + + pub fn file_decls(&self, file_id: FileId) -> Arc { + file_decls_query(self, FileFactsKey::new(self, file_id)) + } + + pub fn source_unit_catalog(&self) -> triomphe::Arc { + source_unit_catalog_query(self, UnitCatalogKey::new(self, ())) + } +} diff --git a/crates/design-graph/src/facts.rs b/crates/design-graph/src/facts.rs new file mode 100644 index 000000000..760d9b650 --- /dev/null +++ b/crates/design-graph/src/facts.rs @@ -0,0 +1,182 @@ +//! Per-file unexpanded design-unit facts. + +use rustc_hash::FxHashMap; +use smallvec::SmallVec; +use smol_str::SmolStr; +use syntax::TokenKind; +use utils::line_index::{TextRange, TextSize}; +use vfs::FileId; + +use crate::unit::{InstantiationRole, UnitId, UnitNode, UnitOrigin}; + +pub mod extract; + +/// One name-like token, unresolved. +/// +/// `emitted` is the preprocessor-trace index when the extract tree assigned +/// one. Macro-expanded tokens share display ranges, so later recovery on the +/// authoritative parse needs this identity when the two traces agree. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Mention { + pub name: smol_str::SmolStr, + pub kind: TokenKind, + pub range: TextRange, + pub emitted: Option, +} + +/// Instantiation type-name token. Primitive instantiations are not recorded. +/// +/// `container` is the compilation-unit that directly contains the site. +/// Nested-module bodies leave it empty — those are not CU graph edges. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InstantiationSite { + pub file: FileId, + pub name: smol_str::SmolStr, + pub range: TextRange, + pub role: InstantiationRole, + pub emitted: Option, + pub container: Option, +} + +/// `import p::x` / `import p::*`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportSpec { + pub package: smol_str::SmolStr, + pub item: Option, + /// Package-name token in display coordinates. + pub range: TextRange, +} + +/// Left identifier of a non-dot `ScopedName` (`p::y`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageRefSite { + pub name: smol_str::SmolStr, + pub range: TextRange, + pub emitted: Option, +} + +/// Position-free CU declaration index. This is what salsa backdates; +/// ranges live on [`Mentions`] and must not enter the global catalog. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DeclIndex { + pub units: Box<[DeclUnit]>, + pub imports: Box<[(SmolStr, Option)]>, + pub preprocessor_independent: bool, + pub has_compilation_unit_locals: bool, +} + +/// One CU declaration without source ranges. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeclUnit { + pub id: UnitId, + pub origin: UnitOrigin, + pub header_fingerprint: u64, +} + +/// Name-like tokens of one file, with a name → offset inverted index. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Mentions { + pub entries: Box<[Mention]>, + by_name: FxHashMap>, +} + +impl Mentions { + pub fn from_entries(entries: Box<[Mention]>) -> Self { + let mut by_name: FxHashMap> = FxHashMap::default(); + for (index, mention) in entries.iter().enumerate() { + by_name.entry(mention.name.clone()).or_default().push(index as u32); + } + Self { entries, by_name } + } + + pub fn mentions_name(&self, name: &str) -> bool { + self.by_name.contains_key(name) + } + + pub fn mentions_of(&self, name: &str) -> impl Iterator { + self.by_name.get(name).into_iter().flatten().map(|&index| &self.entries[index as usize]) + } +} + +/// Compact unexpanded slice of one file. No syntax tree, no interned owner. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct FileFacts { + pub units: Box<[UnitNode]>, + pub mentions: Mentions, + pub imports: Box<[ImportSpec]>, + pub instantiations: Box<[InstantiationSite]>, + pub package_refs: Box<[PackageRefSite]>, + pub preprocessor_independent: bool, + pub has_compilation_unit_locals: bool, +} + +impl FileFacts { + pub fn decls(&self) -> DeclIndex { + DeclIndex { + units: self + .units + .iter() + .map(|unit| DeclUnit { + id: unit.id.clone(), + origin: unit.origin, + header_fingerprint: unit.header_fingerprint, + }) + .collect::>() + .into_boxed_slice(), + imports: self + .imports + .iter() + .map(|import| (import.package.clone(), import.item.clone())) + .collect::>() + .into_boxed_slice(), + preprocessor_independent: self.preprocessor_independent, + has_compilation_unit_locals: self.has_compilation_unit_locals, + } + } + + pub fn mentions_name(&self, name: &str) -> bool { + self.mentions.mentions_name(name) + } + + pub fn mentions_of(&self, name: &str) -> impl Iterator { + self.mentions.mentions_of(name) + } + + pub fn has_compilation_unit_locals(&self) -> bool { + self.has_compilation_unit_locals + } + + /// Design-unit whose recorded name token covers `offset`. + pub fn design_unit_at(&self, offset: TextSize) -> Option<&UnitNode> { + self.units.iter().find(|unit| unit.name_range.is_some_and(|range| range.contains(offset))) + } + + pub fn unit(&self, id: UnitId) -> Option<&UnitNode> { + self.units.iter().find(|unit| unit.id == id) + } + + pub fn instantiation_at(&self, offset: TextSize) -> Option<&InstantiationSite> { + self.instantiations.iter().find(|site| site.range.contains(offset)) + } + + pub fn unit_at_name_range(&self, range: TextRange) -> Option<&UnitNode> { + self.units.iter().find(|unit| unit.name_range == Some(range)) + } + + /// Import package token or `::` left ident covering `offset`. + pub fn package_token_at(&self, offset: TextSize) -> Option<(smol_str::SmolStr, TextRange)> { + if let Some(import) = self.imports.iter().find(|import| import.range.contains(offset)) { + return Some((import.package.clone(), import.range)); + } + self.package_refs + .iter() + .find(|site| site.range.contains(offset)) + .map(|site| (site.name.clone(), site.range)) + } + + /// Whether CU units and import *names* match. Mentions, instantiations, + /// package-ref sites, and source ranges do not move the structure clock. + pub fn same_structure(&self, other: &Self) -> bool { + self.decls() == other.decls() + } +} diff --git a/crates/design-graph/src/facts/extract.rs b/crates/design-graph/src/facts/extract.rs new file mode 100644 index 000000000..4746c0988 --- /dev/null +++ b/crates/design-graph/src/facts/extract.rs @@ -0,0 +1,561 @@ +//! Throwaway unexpanded extract. No Trace, no database. + +use std::hash::{Hash, Hasher}; + +use rustc_hash::FxHasher; +use smol_str::{SmolStr, ToSmolStr}; +use syntax::{ + SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, SyntaxTokenWithParent, SyntaxTree, + WalkEvent, + ast::{self, AstNode}, + has_name::HasName, + has_text_range::{HasTextRange, HasTextRangeIn}, + token::TokenKindExt, +}; +use vfs::FileId; + +use super::{FileFacts, ImportSpec, InstantiationSite, Mention, Mentions, PackageRefSite}; +use crate::unit::{InstantiationRole, UnitId, UnitKind, UnitNode, UnitOrigin}; + +/// Extract design-unit facts from an already-built unexpanded tree. +pub fn from_tree(file: FileId, tree: &SyntaxTree, source_text: &str) -> FileFacts { + walk(file, tree, source_text) +} + +/// Compilation-unit design-unit name tokens on an already-built tree. +/// +/// Used by the IDE to classify paid-artifact names against an existing +/// preprocessor trace. Does not build a Trace. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CuUnitName { + pub kind: UnitKind, + pub name: SmolStr, + pub emitted: Option, +} + +pub fn cu_unit_names(tree: &SyntaxTree) -> Vec { + let mut names = Vec::new(); + let mut body_depth = 0usize; + let mut module_depth = 0usize; + let root = tree.root(); + if root.kind() != SyntaxKind::COMPILATION_UNIT { + return names; + } + for event in root.elem_preorder() { + match event { + WalkEvent::Enter(SyntaxElement::Node(node)) => { + if body_depth == 0 + && module_depth == 0 + && ast::Member::can_cast(node.kind()) + && let Some(kind) = unit_kind(node) + && let Some(token) = member_name_token(node) + { + let name = token.value_text(); + if !name.is_empty() { + let with_parent = SyntaxTokenWithParent { parent: node, tok: token }; + names.push(CuUnitName { + kind, + name: SmolStr::new(name), + emitted: with_parent.preprocessor_trace_emitted_token_index(), + }); + } + } + if ast::ModuleDeclaration::can_cast(node.kind()) { + module_depth += 1; + } + if is_body_boundary(node) { + body_depth += 1; + } + } + WalkEvent::Leave(SyntaxElement::Node(node)) => { + if is_body_boundary(node) { + body_depth -= 1; + } + if ast::ModuleDeclaration::can_cast(node.kind()) { + module_depth -= 1; + } + } + _ => {} + } + } + names +} + +/// Kind + name only. Generated units have no `file_text` header to hash. +pub fn unit_fingerprint(kind: UnitKind, name: &SmolStr) -> u64 { + fingerprint(kind, name, None, "") +} + +fn walk(file: FileId, tree: &SyntaxTree, source_text: &str) -> FileFacts { + let mut units = Vec::new(); + let mut mentions = Vec::new(); + let mut imports = Vec::new(); + let mut instantiations = Vec::new(); + let mut package_refs = Vec::new(); + let mut body_depth = 0usize; + let mut module_depth = 0usize; + let mut current_cu: Option = None; + let mut has_compilation_unit_locals = false; + let mut ordinals = rustc_hash::FxHashMap::<(SmolStr, UnitKind), u32>::default(); + let preprocessor_independent = syntax::preprocessor_independent(tree); + let root = tree.root(); + + if root.kind() != SyntaxKind::COMPILATION_UNIT { + return FileFacts { preprocessor_independent, ..FileFacts::default() }; + } + + for event in root.elem_preorder() { + match event { + WalkEvent::Enter(SyntaxElement::Token(token)) => { + if !token.kind().name_like() { + continue; + } + let Some(range) = token.text_range() else { + continue; + }; + let name = token.tok.value_text(); + if name.is_empty() { + continue; + } + mentions.push(Mention { + name: SmolStr::new(name), + kind: token.kind(), + range, + emitted: token.preprocessor_trace_emitted_token_index(), + }); + } + WalkEvent::Enter(SyntaxElement::Node(node)) => { + if let Some(mut site) = instantiation_at(file, node, module_depth) { + if module_depth == 1 { + site.container = current_cu.clone(); + } + instantiations.push(site); + } + if let Some(spec) = import_at(node) { + if body_depth == 0 && module_depth == 0 { + has_compilation_unit_locals = true; + } + imports.extend(spec); + } + if let Some(site) = package_ref_at(node) { + package_refs.push(site); + } + if body_depth == 0 && module_depth == 0 && ast::Member::can_cast(node.kind()) { + if ast::PackageImportDeclaration::can_cast(node.kind()) { + // Import locals already recorded above. + } else if let Some(partial) = member_unit(node, source_text) { + if let Some(kind) = partial.kind { + if partial.name_range.is_some() { + let key = (partial.name.clone(), kind); + let ordinal = ordinals.entry(key).or_insert(0); + let ordinal_value = *ordinal; + *ordinal += 1; + let id = UnitId { + file, + name: partial.name, + kind, + ordinal: ordinal_value, + }; + current_cu = Some(id.clone()); + units.push(UnitNode { + id, + name_range: partial.name_range, + header_range: partial.header_range, + header_fingerprint: partial.header_fingerprint, + origin: UnitOrigin::Source, + }); + } + } else { + has_compilation_unit_locals = true; + } + } else if !ast::PackageImportDeclaration::can_cast(node.kind()) { + has_compilation_unit_locals = true; + } + } + if ast::ModuleDeclaration::can_cast(node.kind()) { + module_depth += 1; + } + if is_body_boundary(node) { + body_depth += 1; + } + } + WalkEvent::Leave(SyntaxElement::Node(node)) => { + if is_body_boundary(node) { + body_depth -= 1; + } + if ast::ModuleDeclaration::can_cast(node.kind()) { + module_depth -= 1; + if module_depth == 0 { + current_cu = None; + } + } + } + WalkEvent::Leave(SyntaxElement::Token(_)) => {} + } + } + + FileFacts { + units: units.into_boxed_slice(), + mentions: Mentions::from_entries(mentions.into_boxed_slice()), + imports: imports.into_boxed_slice(), + instantiations: instantiations.into_boxed_slice(), + package_refs: package_refs.into_boxed_slice(), + preprocessor_independent, + has_compilation_unit_locals, + } +} + +fn instantiation_at( + file: FileId, + node: SyntaxNode<'_>, + module_depth: usize, +) -> Option { + if let Some(instantiation) = ast::HierarchyInstantiation::cast(node) { + return instantiation_from_token( + file, + instantiation.type_(), + InstantiationRole::Hierarchy, + node, + ); + } + if ast::PrimitiveInstantiation::can_cast(node.kind()) { + return None; + } + if module_depth != 0 { + return None; + } + let instantiation = ast::CheckerInstantiation::cast(node)?; + let name = match instantiation.type_() { + ast::Name::IdentifierName(ident) => ident.identifier(), + ast::Name::IdentifierSelectName(ident) => ident.identifier(), + _ => None, + }; + instantiation_from_token(file, name, InstantiationRole::Checker, node) +} + +fn instantiation_from_token( + file: FileId, + token: Option>, + role: InstantiationRole, + node: SyntaxNode<'_>, +) -> Option { + let token = token?; + let range = token.text_range_in(node)?; + let name = token.value_text(); + if name.is_empty() { + return None; + } + let with_parent = SyntaxTokenWithParent { parent: node, tok: token }; + Some(InstantiationSite { + file, + name: SmolStr::new(name), + range, + role, + emitted: with_parent.preprocessor_trace_emitted_token_index(), + container: None, + }) +} + +fn import_at(node: SyntaxNode<'_>) -> Option> { + let import = ast::PackageImportDeclaration::cast(node)?; + let specs: Vec<_> = import + .items() + .children() + .filter_map(|item| { + let package_tok = item.package()?; + let range = package_tok.text_range_in(node)?; + let package = package_tok.value_text(); + if package.is_empty() { + return None; + } + let imported = item.item()?; + let item = (imported.kind() != syntax::TokenKind::STAR) + .then(|| { + let name = imported.value_text(); + (!name.is_empty()).then(|| SmolStr::new(name)) + }) + .flatten(); + Some(ImportSpec { package: SmolStr::new(package), item, range }) + }) + .collect(); + Some(specs) +} + +fn package_ref_at(node: SyntaxNode<'_>) -> Option { + let scoped = ast::ScopedName::cast(node)?; + if scoped_uses_dot(scoped) { + return None; + } + let left = match scoped.left() { + ast::Name::IdentifierName(ident) => ident.identifier()?, + ast::Name::IdentifierSelectName(ident) => ident.identifier()?, + _ => return None, + }; + let range = left.text_range_in(node)?; + let name = left.value_text(); + if name.is_empty() { + return None; + } + let with_parent = SyntaxTokenWithParent { parent: node, tok: left }; + Some(PackageRefSite { + name: SmolStr::new(name), + range, + emitted: with_parent.preprocessor_trace_emitted_token_index(), + }) +} + +fn scoped_uses_dot(scoped: ast::ScopedName<'_>) -> bool { + scoped + .syntax() + .children() + .filter_map(|elem| elem.as_token()) + .any(|tok| tok.kind() == syntax::Token![.]) +} + +struct PartialUnit { + name: SmolStr, + kind: Option, + header_fingerprint: u64, + name_range: Option, + header_range: Option, +} + +fn member_unit(node: SyntaxNode<'_>, source_text: &str) -> Option { + let kind = unit_kind(node); + if kind.is_none() && !is_cu_local_member(node) { + return None; + } + let (name, name_range) = member_name(node).unwrap_or_else(|| (SmolStr::new(""), None)); + if kind.is_some() && name.is_empty() { + return None; + } + let header_range = ast::ModuleDeclaration::cast(node) + .map(|item| item.header().syntax()) + .and_then(|header| header.text_range()); + let fingerprint_kind = kind.unwrap_or(UnitKind::Module); + Some(PartialUnit { + header_fingerprint: fingerprint(fingerprint_kind, &name, header_range, source_text), + name, + kind, + name_range, + header_range, + }) +} + +fn unit_kind(node: SyntaxNode<'_>) -> Option { + if let Some(module) = ast::ModuleDeclaration::cast(node) { + return Some(kind_from_module(module)); + } + match node.kind() { + SyntaxKind::CHECKER_DECLARATION => Some(UnitKind::Checker), + SyntaxKind::COVERGROUP_DECLARATION => Some(UnitKind::Covergroup), + _ => None, + } +} + +fn kind_from_module(decl: ast::ModuleDeclaration<'_>) -> UnitKind { + if decl.as_package_declaration().is_some() { + UnitKind::Package + } else if decl.as_interface_declaration().is_some() { + UnitKind::Interface + } else if decl.as_program_declaration().is_some() { + UnitKind::Program + } else { + UnitKind::Module + } +} + +fn is_cu_local_member(node: SyntaxNode<'_>) -> bool { + matches!( + node.kind(), + SyntaxKind::TYPEDEF_DECLARATION + | SyntaxKind::FORWARD_TYPEDEF_DECLARATION + | SyntaxKind::FUNCTION_DECLARATION + | SyntaxKind::TASK_DECLARATION + | SyntaxKind::PARAMETER_DECLARATION_STATEMENT + | SyntaxKind::DATA_DECLARATION + | SyntaxKind::NET_DECLARATION + | SyntaxKind::USER_DEFINED_NET_DECLARATION + ) || (!ast::EmptyMember::can_cast(node.kind()) + && !ast::PackageImportDeclaration::can_cast(node.kind()) + && ast::Member::can_cast(node.kind())) +} + +fn member_name(node: SyntaxNode<'_>) -> Option<(SmolStr, Option)> { + let token = member_name_token(node)?; + Some((token.value_text().to_smolstr(), token.text_range_in(node))) +} + +fn member_name_token(node: SyntaxNode<'_>) -> Option> { + if let Some(module) = ast::ModuleDeclaration::cast(node) { + return HasName::name(&module); + } + if let Some(function) = ast::FunctionDeclaration::cast(node) { + return HasName::name(&function); + } + if let Some(typedef) = ast::TypedefDeclaration::cast(node) { + return typedef.name(); + } + if let Some(checker) = ast::CheckerDeclaration::cast(node) { + return checker.name(); + } + if let Some(covergroup) = ast::CovergroupDeclaration::cast(node) { + return covergroup.name(); + } + None +} + +fn is_body_boundary(node: SyntaxNode<'_>) -> bool { + ast::FunctionDeclaration::can_cast(node.kind()) || ast::ProceduralBlock::can_cast(node.kind()) +} + +fn fingerprint( + kind: UnitKind, + name: &SmolStr, + header_range: Option, + source_text: &str, +) -> u64 { + let mut hasher = FxHasher::default(); + kind.hash(&mut hasher); + name.hash(&mut hasher); + if let Some(range) = header_range + && let Some(header) = source_text.get(usize::from(range.start())..usize::from(range.end())) + { + header.hash(&mut hasher); + } + hasher.finish() +} + +#[cfg(test)] +mod tests { + use syntax::SyntaxTree; + use vfs::FileId; + + use super::from_tree; + use crate::unit::{InstantiationRole, UnitKind}; + + const FILE: FileId = FileId::from_raw(0); + + fn facts(text: &str) -> crate::FileFacts { + let tree = SyntaxTree::from_file_in_memory(text, "t.sv", "t.sv"); + from_tree(FILE, &tree, text) + } + + #[test] + fn plain_module_is_preprocessor_independent() { + let facts = facts("module m;\nendmodule\n"); + assert!(facts.preprocessor_independent); + assert_eq!(facts.units.len(), 1); + assert_eq!(facts.units[0].id.kind, UnitKind::Module); + assert_eq!(facts.units[0].id.name, "m"); + assert_eq!(facts.units[0].id.ordinal, 0); + } + + #[test] + fn define_is_preprocessor_activity() { + assert!(!facts("`define W 8\nmodule m;\nendmodule\n").preprocessor_independent); + } + + #[test] + fn include_is_preprocessor_activity() { + assert!(!facts("`include \"a.svh\"\nmodule m;\nendmodule\n").preprocessor_independent); + } + + #[test] + fn ifdef_is_preprocessor_activity() { + assert!(!facts("`ifdef W\nmodule m;\nendmodule\n`endif\n").preprocessor_independent); + } + + #[test] + fn macro_usage_is_preprocessor_activity() { + assert!( + !facts("module m;\n logic [`UNKNOWN-1:0] x;\nendmodule\n").preprocessor_independent + ); + } + + #[test] + fn module_header_range_excludes_the_body() { + let text = "module top #(parameter int W = 1);\n wire unused;\nendmodule\n"; + let facts = facts(text); + let header = facts.units[0].header_range.expect("module header"); + let header = &text[usize::from(header.start())..usize::from(header.end())]; + assert!(header.contains("module top"), "{header}"); + assert!(header.contains("parameter int W = 1"), "{header}"); + assert!(!header.contains("wire unused"), "{header}"); + } + + #[test] + fn nested_module_is_not_a_unit() { + let facts = facts("module outer;\n module inner;\n endmodule\nendmodule\n"); + assert_eq!(facts.units.len(), 1); + assert_eq!(facts.units[0].id.name, "outer"); + } + + #[test] + fn hierarchy_instantiation_is_recorded_inside_a_module() { + let facts = facts("module top;\n cc_fifo u();\nendmodule\n"); + assert_eq!(facts.instantiations.len(), 1); + assert_eq!(facts.instantiations[0].name, "cc_fifo"); + assert_eq!(facts.instantiations[0].role, InstantiationRole::Hierarchy); + assert_eq!( + facts.instantiations[0].container.as_ref().map(|id| id.name.as_str()), + Some("top") + ); + } + + #[test] + fn nested_instantiation_is_not_a_cu_container_edge() { + let facts = facts( + "module outer;\n module inner;\n leaf u();\n endmodule\n child v();\nendmodule\n", + ); + let child = facts.instantiations.iter().find(|site| site.name == "child").expect("child"); + let leaf = facts.instantiations.iter().find(|site| site.name == "leaf").expect("leaf"); + assert_eq!(child.container.as_ref().map(|id| id.name.as_str()), Some("outer")); + assert!(leaf.container.is_none(), "{leaf:?}"); + } + + #[test] + fn two_cu_modules_keep_distinct_instantiation_containers() { + let facts = facts("module a;\n b u();\nendmodule\nmodule c;\n d v();\nendmodule\n"); + let b = facts.instantiations.iter().find(|site| site.name == "b").expect("b"); + let d = facts.instantiations.iter().find(|site| site.name == "d").expect("d"); + assert_eq!(b.container.as_ref().map(|id| id.name.as_str()), Some("a")); + assert_eq!(d.container.as_ref().map(|id| id.name.as_str()), Some("c")); + } + + #[test] + fn primitive_instantiation_is_not_a_graph_site() { + let facts = facts("module top;\n and g(o, a, b);\nendmodule\n"); + assert!(facts.instantiations.is_empty(), "{:?}", facts.instantiations); + } + + #[test] + fn import_records_package_range() { + let facts = facts("import p::*;\nmodule m;\nendmodule\n"); + assert_eq!(facts.imports.len(), 1); + assert_eq!(facts.imports[0].package, "p"); + assert!(facts.imports[0].item.is_none()); + assert!(facts.has_compilation_unit_locals); + assert!(facts.package_token_at(facts.imports[0].range.start()).is_some()); + } + + #[test] + fn scoped_colon_left_is_a_package_ref() { + let facts = facts("module m;\n p::y x;\nendmodule\n"); + assert!(facts.package_refs.iter().any(|site| site.name == "p"), "{:?}", facts.package_refs); + } + + #[test] + fn dotted_name_is_not_a_package_ref() { + let facts = facts("module m;\n assign x = n.sig;\nendmodule\n"); + assert!(facts.package_refs.is_empty(), "{:?}", facts.package_refs); + } + + #[test] + fn non_du_cu_member_sets_locals_and_is_not_a_unit() { + let facts = facts("typedef logic t;\nmodule m;\nendmodule\n"); + assert!(facts.has_compilation_unit_locals); + assert_eq!(facts.units.len(), 1); + assert_eq!(facts.units[0].id.name, "m"); + } +} diff --git a/crates/design-graph/src/graph.rs b/crates/design-graph/src/graph.rs new file mode 100644 index 000000000..954020d58 --- /dev/null +++ b/crates/design-graph/src/graph.rs @@ -0,0 +1,549 @@ +//! Name join over `FileFacts` plus an optional generated-unit map. + +use rustc_hash::FxHashMap; +use smallvec::SmallVec; +use smol_str::SmolStr; +use vfs::FileId; + +use crate::{ + db::DesignGraphDb, + unit::{InstantiationRole, UnitId, UnitKind, UnitOrigin}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitMeta { + pub kind: UnitKind, + pub origin: UnitOrigin, + pub header_fingerprint: u64, +} + +/// One file's generated units, valid only for a specific artifact fingerprint. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedFileUnits { + pub fingerprint: u64, + pub ids: Box<[UnitId]>, +} + +/// Generated units recorded from a paid artifact. No ranges. +/// +/// Entries are keyed by `(FileId, compilation_unit_snapshot.fingerprint)`. +/// A FileId-only lookup cannot return a stale set: [`Self::ids_for`] and +/// [`Self::retain_current`] treat a fingerprint mismatch as a miss. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct GeneratedUnits { + pub by_file: FxHashMap, + pub meta: FxHashMap, +} + +impl GeneratedUnits { + pub fn contains_file(&self, file: FileId) -> bool { + self.by_file.contains_key(&file) + } + + pub fn ids_for(&self, file: FileId) -> &[UnitId] { + self.by_file.get(&file).map(|entry| entry.ids.as_ref()).unwrap_or(&[]) + } + + /// Keep only entries for which `is_current(file, stored_fingerprint)` is + /// true. Returns the files that were dropped. + pub fn retain_current(&mut self, is_current: impl Fn(FileId, u64) -> bool) -> Vec { + let mut dropped = Vec::new(); + self.by_file.retain(|&file, entry| { + if is_current(file, entry.fingerprint) { + true + } else { + for id in entry.ids.iter() { + self.meta.remove(id); + } + dropped.push(file); + false + } + }); + dropped + } + + /// Replace one file's generated ids. Returns whether the stored set + /// changed. + pub fn replace_file( + &mut self, + file: FileId, + fingerprint: u64, + ids: Box<[UnitId]>, + meta: FxHashMap, + ) -> bool { + let previous = self.by_file.get(&file); + if previous.is_some_and(|entry| { + entry.fingerprint == fingerprint && entry.ids.as_ref() == ids.as_ref() + }) { + return false; + } + if let Some(old) = self.by_file.insert(file, GeneratedFileUnits { fingerprint, ids }) { + for id in old.ids.iter() { + self.meta.remove(id); + } + } + self.meta.extend(meta); + true + } +} + +/// A lookup result that preserves the difference between no match, one +/// logical definition, and several competing definitions. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Resolution { + Unresolved, + Unique(T), + Ambiguous(SmallVec<[T; 2]>), +} + +impl Resolution { + pub fn candidates(&self) -> &[T] { + match self { + Self::Unresolved => &[], + Self::Unique(value) => std::slice::from_ref(value), + Self::Ambiguous(candidates) => candidates, + } + } + + pub fn into_candidates(self) -> SmallVec<[T; 2]> { + match self { + Self::Unresolved => SmallVec::new(), + Self::Unique(value) => { + let mut candidates = SmallVec::new(); + candidates.push(value); + candidates + } + Self::Ambiguous(candidates) => candidates, + } + } + + pub fn into_vec(self) -> SmallVec<[T; 2]> { + self.into_candidates() + } + + pub fn iter(&self) -> std::slice::Iter<'_, T> { + self.candidates().iter() + } + + pub fn is_unresolved(&self) -> bool { + matches!(self, Self::Unresolved) + } + + pub fn or_else(self, fallback: impl FnOnce() -> Self) -> Self { + if self.is_unresolved() { fallback() } else { self } + } +} + +impl Resolution { + pub fn unique(&self) -> Option { + match self { + Self::Unique(item) => Some(item.clone()), + Self::Ambiguous(_) | Self::Unresolved => None, + } + } + + /// Resolves children without allowing child existence to disambiguate an + /// ambiguous parent. + pub fn and_then(&self, mut resolve: impl FnMut(T) -> Resolution) -> Resolution { + let children = Resolution::from_candidates( + self.iter().cloned().flat_map(|candidate| resolve(candidate).into_candidates()), + ); + match (self, children) { + (Self::Ambiguous(_), Resolution::Unique(_)) => Resolution::Unresolved, + (_, children) => children, + } + } +} + +impl From for Resolution { + fn from(value: T) -> Self { + Self::Unique(value) + } +} + +impl Resolution { + pub fn from_candidates(candidates: impl IntoIterator) -> Self { + let mut unique = SmallVec::<[T; 2]>::new(); + for candidate in candidates { + if !unique.contains(&candidate) { + unique.push(candidate); + } + } + match unique.len() { + 0 => Self::Unresolved, + 1 => Self::Unique(unique.pop().expect("candidate length was checked")), + _ => Self::Ambiguous(unique), + } + } + + pub fn map(self, map: impl FnMut(T) -> U) -> Resolution { + Resolution::from_candidates(self.into_candidates().into_iter().map(map)) + } +} + +/// Structure product: name → `UnitId`. Stores no source ranges. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct UnitCatalog { + by_name: FxHashMap>, + meta: FxHashMap, + module_names: Vec, +} + +impl UnitCatalog { + /// Join already-extracted per-file facts. Callers that can run `file_facts` + /// in parallel should do that and pass the results here. + pub fn from_decls<'a>( + decls: impl IntoIterator, + generated: &GeneratedUnits, + ) -> Self { + let mut graph = Self::default(); + for decls in decls { + for unit in decls.units.iter() { + graph.insert( + unit.id.clone(), + UnitMeta { + kind: unit.id.kind, + origin: unit.origin, + header_fingerprint: unit.header_fingerprint, + }, + ); + } + } + for (id, meta) in generated.meta.iter() { + graph.insert(id.clone(), meta.clone()); + } + graph.rebuild_module_names(); + graph + } + + /// Merge fingerprint-current generated units onto this L0 source catalog. + /// + /// `self` is the salsa source catalog and must not already contain + /// generated units. Overlay entries are not salsa inputs; the caller + /// supplies the current set on each read. + pub fn with_overlay(&self, generated: &GeneratedUnits) -> Self { + if generated.meta.is_empty() { + return self.clone(); + } + let mut graph = self.clone(); + for (id, meta) in &generated.meta { + graph.insert(id.clone(), meta.clone()); + } + graph.rebuild_module_names(); + graph + } + + /// Replace one file's source and generated units. Other files stay. + /// Returns whether the node set for `file` changed. + pub fn upsert_file( + &mut self, + file: FileId, + facts: &crate::FileFacts, + generated: &GeneratedUnits, + ) -> bool { + let mut next = Vec::new(); + for unit in facts.units.iter() { + debug_assert_eq!(unit.id.file, file); + next.push(( + unit.id.clone(), + UnitMeta { + kind: unit.id.kind, + origin: unit.origin, + header_fingerprint: unit.header_fingerprint, + }, + )); + } + for id in generated.ids_for(file) { + if let Some(meta) = generated.meta.get(id) { + next.push((id.clone(), meta.clone())); + } + } + let mut previous: Vec<_> = self + .meta + .iter() + .filter(|(id, _)| id.file == file) + .map(|(id, meta)| (id.clone(), meta.clone())) + .collect(); + previous.sort_by(|left, right| { + left.0.ordinal.cmp(&right.0.ordinal).then_with(|| left.0.name.cmp(&right.0.name)) + }); + next.sort_by(|left, right| { + left.0.ordinal.cmp(&right.0.ordinal).then_with(|| left.0.name.cmp(&right.0.name)) + }); + if previous == next { + return false; + } + self.remove_file(file); + for (id, meta) in next { + self.insert(id, meta); + } + self.rebuild_module_names(); + true + } + + /// Drop every node owned by `file`. Returns whether anything was removed. + pub fn remove_file(&mut self, file: FileId) -> bool { + let ids: Vec<_> = self.meta.keys().filter(|id| id.file == file).cloned().collect(); + if ids.is_empty() { + return false; + } + for id in ids { + self.meta.remove(&id); + if let Some(list) = self.by_name.get_mut(&id.name) { + list.retain(|existing| existing != &id); + if list.is_empty() { + self.by_name.remove(&id.name); + } + } + } + self.rebuild_module_names(); + true + } + + fn rebuild_module_names(&mut self) { + self.module_names = self + .by_name + .iter() + .filter(|(_, ids)| ids.iter().any(|id| id.kind.is_hierarchy_target())) + .map(|(name, _)| name.clone()) + .collect(); + self.module_names.sort(); + self.module_names.dedup(); + } + + /// `file_facts` come from salsa; `generated` comes from the product store. + pub fn fold(db: &dyn DesignGraphDb, generated: &GeneratedUnits) -> Self { + let facts: Vec<_> = db + .files() + .iter() + .copied() + .filter(|&file_id| db.file_kind(file_id).is_semantic_compilation_unit()) + .map(|file_id| db.file_facts(file_id)) + .collect(); + let decls: Vec<_> = facts.iter().map(|facts| facts.decls()).collect(); + Self::from_decls(decls.iter(), generated) + } + + pub(crate) fn insert(&mut self, id: UnitId, meta: UnitMeta) { + self.by_name.entry(id.name.clone()).or_default().push(id.clone()); + self.meta.insert(id, meta); + } + + pub fn modules_named(&self, name: &str) -> Resolution { + self.named(name, |id| id.kind.is_hierarchy_target()) + } + + pub fn type_units_named(&self, name: &str) -> Resolution { + self.named(name, |_| true) + } + + pub fn packages_named(&self, name: &str) -> Resolution { + self.named(name, |id| id.kind.is_package()) + } + + pub fn packages(&self) -> impl Iterator + '_ { + self.meta.keys().filter(|id| id.kind.is_package()).cloned() + } + + pub fn module_names(&self) -> &[SmolStr] { + &self.module_names + } + + pub fn contains(&self, id: &UnitId) -> bool { + self.meta.contains_key(id) + } + + pub fn origin(&self, id: &UnitId) -> Option { + self.meta.get(id).map(|meta| meta.origin) + } + + pub fn node_count(&self) -> usize { + self.meta.len() + } + + pub fn candidates(&self, name: &str, role: InstantiationRole) -> SmallVec<[UnitId; 2]> { + let matches = match role { + InstantiationRole::Hierarchy => UnitKind::is_hierarchy_target, + InstantiationRole::Checker => |kind: UnitKind| matches!(kind, UnitKind::Checker), + }; + self.by_name + .get(name) + .into_iter() + .flatten() + .filter(|id| matches(id.kind)) + .cloned() + .collect() + } + + fn named(&self, name: &str, pred: impl Fn(&UnitId) -> bool) -> Resolution { + Resolution::from_candidates( + self.by_name.get(name).into_iter().flatten().filter(|id| pred(id)).cloned(), + ) + } +} + +#[cfg(test)] +mod tests { + use rustc_hash::FxHashMap; + use smol_str::SmolStr; + use vfs::FileId; + + use super::{GeneratedUnits, UnitMeta}; + use crate::unit::{UnitId, UnitKind, UnitOrigin}; + + const FILE: FileId = FileId::from_raw(1); + + fn id(name: &str, ordinal: u32) -> UnitId { + UnitId { file: FILE, name: SmolStr::new(name), kind: UnitKind::Module, ordinal } + } + + fn generated_meta(id: &UnitId) -> UnitMeta { + UnitMeta { kind: id.kind, origin: UnitOrigin::Generated, header_fingerprint: 0 } + } + + #[test] + fn replace_file_is_noop_when_ids_match() { + let mut generated = GeneratedUnits::default(); + let unit = id("foo", 0); + let mut meta = FxHashMap::default(); + meta.insert(unit.clone(), generated_meta(&unit)); + assert!(generated.replace_file(FILE, 1, Box::new([unit.clone()]), meta.clone())); + assert!(!generated.replace_file(FILE, 1, Box::new([unit]), meta)); + } + + #[test] + fn retain_current_drops_a_mismatched_fingerprint() { + let mut generated = GeneratedUnits::default(); + let unit = id("foo", 0); + let mut meta = FxHashMap::default(); + meta.insert(unit.clone(), generated_meta(&unit)); + assert!(generated.replace_file(FILE, 1, Box::new([unit.clone()]), meta)); + let dropped = generated.retain_current(|_, fingerprint| fingerprint == 2); + assert_eq!(dropped, vec![FILE]); + assert!(generated.ids_for(FILE).is_empty()); + assert!(!generated.meta.contains_key(&unit)); + } + + #[test] + fn replace_file_drops_previous_meta() { + let mut generated = GeneratedUnits::default(); + let old = id("foo", 0); + let new = id("bar", 0); + let mut old_meta = FxHashMap::default(); + old_meta.insert(old.clone(), generated_meta(&old)); + assert!(generated.replace_file(FILE, 1, Box::new([old.clone()]), old_meta)); + let mut new_meta = FxHashMap::default(); + new_meta.insert(new.clone(), generated_meta(&new)); + assert!(generated.replace_file(FILE, 2, Box::new([new.clone()]), new_meta)); + assert!(!generated.meta.contains_key(&old)); + assert!(generated.meta.contains_key(&new)); + } + + #[test] + fn from_file_facts_joins_source_units_and_generated() { + let unit = crate::unit::UnitNode { + id: id("src", 0), + origin: UnitOrigin::Source, + name_range: None, + header_range: None, + header_fingerprint: 1, + }; + let facts = + crate::FileFacts { units: Box::new([unit.clone()]), ..crate::FileFacts::default() }; + let generated_id = id("gen", 0); + let mut generated = GeneratedUnits::default(); + let mut meta = FxHashMap::default(); + meta.insert(generated_id.clone(), generated_meta(&generated_id)); + generated.replace_file(FILE, 1, Box::new([generated_id.clone()]), meta); + + let decls = facts.decls(); + let graph = super::UnitCatalog::from_decls(std::iter::once(&decls), &generated); + assert!(graph.contains(&unit.id)); + assert!(graph.contains(&generated_id)); + assert_eq!(graph.node_count(), 2); + } + + #[test] + fn with_overlay_adds_generated_names_to_a_source_catalog() { + let source_unit = crate::unit::UnitNode { + id: id("src", 0), + origin: UnitOrigin::Source, + name_range: None, + header_range: None, + header_fingerprint: 1, + }; + let facts = crate::FileFacts { + units: Box::new([source_unit.clone()]), + ..crate::FileFacts::default() + }; + let source = super::UnitCatalog::from_decls( + std::iter::once(&facts.decls()), + &GeneratedUnits::default(), + ); + let generated_id = id("gen", 0); + let mut generated = GeneratedUnits::default(); + let mut meta = FxHashMap::default(); + meta.insert(generated_id.clone(), generated_meta(&generated_id)); + generated.replace_file(FILE, 1, Box::new([generated_id.clone()]), meta); + + let merged = source.with_overlay(&generated); + assert!(source.contains(&source_unit.id)); + assert!(!source.contains(&generated_id)); + assert!(merged.contains(&source_unit.id)); + assert!(merged.contains(&generated_id)); + assert_eq!(source.with_overlay(&GeneratedUnits::default()), source); + } + + #[test] + fn upsert_file_replaces_one_file_and_keeps_the_other() { + let other = FileId::from_raw(2); + let keep = + UnitId { file: other, name: SmolStr::new("keep"), kind: UnitKind::Module, ordinal: 0 }; + let mut graph = super::UnitCatalog::default(); + graph.insert(keep.clone(), generated_meta(&keep)); + graph.rebuild_module_names(); + + let first = crate::unit::UnitNode { + id: id("first", 0), + origin: UnitOrigin::Source, + name_range: None, + header_range: None, + header_fingerprint: 1, + }; + let facts = + crate::FileFacts { units: Box::new([first.clone()]), ..crate::FileFacts::default() }; + assert!(graph.upsert_file(FILE, &facts, &GeneratedUnits::default())); + assert!(graph.contains(&keep)); + assert!(graph.contains(&first.id)); + + let second = crate::unit::UnitNode { + id: id("second", 0), + origin: UnitOrigin::Source, + name_range: None, + header_range: None, + header_fingerprint: 2, + }; + let facts = + crate::FileFacts { units: Box::new([second.clone()]), ..crate::FileFacts::default() }; + assert!(graph.upsert_file(FILE, &facts, &GeneratedUnits::default())); + assert!(graph.contains(&keep)); + assert!(graph.contains(&second.id)); + assert!(!graph.contains(&first.id)); + assert!(!graph.upsert_file(FILE, &facts, &GeneratedUnits::default())); + } + + #[test] + fn remove_file_drops_only_that_file() { + let other = FileId::from_raw(2); + let keep = + UnitId { file: other, name: SmolStr::new("keep"), kind: UnitKind::Module, ordinal: 0 }; + let mut graph = super::UnitCatalog::default(); + graph.insert(id("gone", 0), generated_meta(&id("gone", 0))); + graph.insert(keep.clone(), generated_meta(&keep)); + graph.rebuild_module_names(); + assert!(graph.remove_file(FILE)); + assert!(graph.contains(&keep)); + assert!(!graph.contains(&id("gone", 0))); + assert!(!graph.remove_file(FILE)); + } +} diff --git a/crates/design-graph/src/hit.rs b/crates/design-graph/src/hit.rs new file mode 100644 index 000000000..9f8e039d1 --- /dev/null +++ b/crates/design-graph/src/hit.rs @@ -0,0 +1,168 @@ +//! Cursor classification against live `FileFacts` and a name join. + +use smallvec::SmallVec; +use utils::line_index::TextSize; + +use crate::{facts::FileFacts, graph::UnitCatalog, unit::UnitId}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CursorHit { + DeclName { + unit: UnitId, + range: utils::line_index::TextRange, + }, + InstantiationType { + range: utils::line_index::TextRange, + targets: SmallVec<[UnitId; 2]>, + }, + PackageRef { + name: smol_str::SmolStr, + range: utils::line_index::TextRange, + targets: SmallVec<[UnitId; 2]>, + }, + Other, +} + +/// A declaration name is a fact of this file. Does not fold the catalog. +pub fn hit_local(facts: &FileFacts, offset: TextSize) -> Option { + let decl = facts.design_unit_at(offset)?; + let range = decl.name_range.expect("design_unit_at only returns ranged decls"); + Some(CursorHit::DeclName { unit: decl.id.clone(), range }) +} + +/// Instantiation and package names need the workspace catalog. +pub fn hit_global(facts: &FileFacts, graph: &UnitCatalog, offset: TextSize) -> CursorHit { + if let Some(site) = facts.instantiation_at(offset) { + let targets = graph.candidates(&site.name, site.role); + if !targets.is_empty() { + return CursorHit::InstantiationType { range: site.range, targets }; + } + } + if let Some((name, range)) = facts.package_token_at(offset) { + let targets = graph.packages_named(&name).into_vec(); + if !targets.is_empty() { + return CursorHit::PackageRef { name, range, targets }; + } + } + CursorHit::Other +} + +/// Token shape is a *candidate* graph question. Empty candidates mean this +/// is not a compilation-unit name (`Other`), not a second CU-name path. +pub fn hit_at(facts: &FileFacts, graph: &UnitCatalog, offset: TextSize) -> CursorHit { + hit_local(facts, offset).unwrap_or_else(|| hit_global(facts, graph, offset)) +} + +#[cfg(test)] +mod tests { + use syntax::SyntaxTree; + use vfs::FileId; + + use super::{CursorHit, hit_at}; + use crate::{ + facts::extract::from_tree, + graph::{UnitCatalog, UnitMeta}, + unit::{UnitId, UnitKind, UnitOrigin}, + }; + + const FILE: FileId = FileId::from_raw(0); + + fn facts_and_offset( + text: &str, + needle: &str, + ) -> (crate::FileFacts, utils::line_index::TextSize) { + let tree = SyntaxTree::from_file_in_memory(text, "t.sv", "t.sv"); + let facts = from_tree(FILE, &tree, text); + let start = text.find(needle).expect(needle); + (facts, utils::line_index::TextSize::from(start as u32)) + } + + fn graph_with(names: &[(&str, UnitKind)]) -> UnitCatalog { + let mut graph = UnitCatalog::default(); + for (name, kind) in names { + let id = + UnitId { file: FILE, name: smol_str::SmolStr::new(*name), kind: *kind, ordinal: 0 }; + graph.insert( + id, + UnitMeta { kind: *kind, origin: UnitOrigin::Source, header_fingerprint: 0 }, + ); + } + graph + } + + #[test] + fn hierarchy_in_module_body_is_instantiation_when_named() { + let (facts, offset) = + facts_and_offset("module top;\n cc_fifo u();\nendmodule\n", "cc_fifo"); + let graph = graph_with(&[("cc_fifo", UnitKind::Module)]); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::InstantiationType { .. })); + } + + #[test] + fn nested_module_instance_is_other() { + let (facts, offset) = facts_and_offset( + "module outer;\n module inner;\n endmodule\n inner u();\nendmodule\n", + "inner u", + ); + let graph = graph_with(&[("outer", UnitKind::Module)]); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::Other)); + } + + #[test] + fn class_scope_left_is_other() { + let (facts, offset) = + facts_and_offset("class C; endclass\nmodule m;\n C::x y;\nendmodule\n", "C::"); + let graph = graph_with(&[("m", UnitKind::Module)]); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::Other)); + } + + #[test] + fn import_package_is_package_ref() { + let text = "import p::*;\nmodule m;\nendmodule\n"; + let tree = SyntaxTree::from_file_in_memory(text, "t.sv", "t.sv"); + let facts = from_tree(FILE, &tree, text); + let offset = facts.imports[0].range.start(); + let graph = graph_with(&[("p", UnitKind::Package), ("m", UnitKind::Module)]); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::PackageRef { .. })); + } + + #[test] + fn scoped_colon_package_is_package_ref() { + let (facts, offset) = facts_and_offset("module m;\n p::y x;\nendmodule\n", "p::"); + let graph = graph_with(&[("p", UnitKind::Package), ("m", UnitKind::Module)]); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::PackageRef { .. })); + } + + #[test] + fn dotted_name_is_other() { + let (facts, offset) = facts_and_offset("module m;\n assign x = n.sig;\nendmodule\n", "n."); + let graph = graph_with(&[("m", UnitKind::Module)]); + assert!(facts.package_refs.is_empty()); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::Other)); + } + + #[test] + fn primitive_instantiation_is_other() { + let (facts, offset) = + facts_and_offset("module top;\n and g(o, a, b);\nendmodule\n", "and "); + let graph = graph_with(&[("top", UnitKind::Module)]); + assert!(facts.instantiations.is_empty()); + assert!(matches!(hit_at(&facts, &graph, offset), CursorHit::Other)); + } + + #[test] + fn checker_is_not_a_hierarchy_candidate() { + let (facts, offset) = + facts_and_offset("checker c;\nendchecker\nmodule top;\n c u();\nendmodule\n", "c u"); + let graph = graph_with(&[("c", UnitKind::Checker), ("top", UnitKind::Module)]); + assert!( + facts.instantiations.iter().any(|site| site.name == "c"), + "slang parses `c u()` as hierarchy: {:?}", + facts.instantiations + ); + assert!( + matches!(hit_at(&facts, &graph, offset), CursorHit::Other), + "Checker is a node, not a Hierarchy candidate" + ); + } +} diff --git a/crates/design-graph/src/lib.rs b/crates/design-graph/src/lib.rs new file mode 100644 index 000000000..3524f5b23 --- /dev/null +++ b/crates/design-graph/src/lib.rs @@ -0,0 +1,21 @@ +//! Compilation-unit design-unit facts. +//! +//! This crate owns unexpanded per-file extract and the name-join types. It +//! does not depend on `hir-def` or `ide`. Graph fold is a pure function of +//! salsa `file_facts` plus an optional generated-unit map supplied by the +//! caller. + +pub mod db; +pub mod facts; +pub mod graph; +pub mod hit; +pub mod unit; + +pub use db::{DesignGraphDb, set_file_facts_lru_capacity}; +pub use facts::{ + DeclIndex, DeclUnit, FileFacts, ImportSpec, InstantiationSite, Mention, Mentions, + PackageRefSite, +}; +pub use graph::{GeneratedFileUnits, GeneratedUnits, Resolution, UnitCatalog, UnitMeta}; +pub use hit::{CursorHit, hit_at, hit_global, hit_local}; +pub use unit::{InstantiationRole, UnitId, UnitKind, UnitNode, UnitOrigin}; diff --git a/crates/design-graph/src/unit.rs b/crates/design-graph/src/unit.rs new file mode 100644 index 000000000..c496c6935 --- /dev/null +++ b/crates/design-graph/src/unit.rs @@ -0,0 +1,68 @@ +use smol_str::SmolStr; +use utils::line_index::TextRange; +use vfs::FileId; + +/// Workspace design-unit identity. A value type; not interned. +/// +/// `ordinal` is the occurrence of `(file, name, kind)` in that file's +/// unexpanded decls, then any generated supplement, starting at 0. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct UnitId { + pub file: FileId, + pub name: SmolStr, + pub kind: UnitKind, + pub ordinal: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UnitKind { + Module, + Interface, + Package, + Program, + Checker, + Covergroup, +} + +impl UnitKind { + /// Legal target of a hierarchy instantiation. Not Package / Checker / + /// Covergroup. + pub fn is_hierarchy_target(self) -> bool { + matches!(self, Self::Module | Self::Interface | Self::Program) + } + + pub fn is_package(self) -> bool { + matches!(self, Self::Package) + } +} + +/// Display facts for a node. Not identity. +/// +/// `name_range` / `header_range` are display coordinates in `file_text`. +/// Absent when extract could not assign a single-buffer range, or when the +/// node is generated. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitNode { + pub id: UnitId, + pub name_range: Option, + pub header_range: Option, + pub header_fingerprint: u64, + pub origin: UnitOrigin, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum UnitOrigin { + /// Unexpanded-tree source declaration. Ranges may slice `file_text`. + #[default] + Source, + /// Paid authoritative tree, name token is not `TokenOrigin::Source`. + Generated, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InstantiationRole { + /// `ast::HierarchyInstantiation` only. + Hierarchy, + /// `ast::CheckerInstantiation` only. + Checker, +} diff --git a/crates/hir-def/Cargo.toml b/crates/hir-def/Cargo.toml index bff7935c3..98497d6ec 100644 --- a/crates/hir-def/Cargo.toml +++ b/crates/hir-def/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] base-db.workspace = true +design-graph.workspace = true itertools.workspace = true la-arena.workspace = true parking_lot.workspace = true diff --git a/crates/hir-def/src/aggregate.rs b/crates/hir-def/src/aggregate.rs index 3c3c8d56b..e3671ae3d 100644 --- a/crates/hir-def/src/aggregate.rs +++ b/crates/hir-def/src/aggregate.rs @@ -173,6 +173,10 @@ pub enum ClassParameter { }, } +/// One class member as written. Types and method bodies are stored so +/// outline can name them; they are not a name-resolution or typing API. +/// Cross-file members, inheritance, and types are answered by the +/// elaboration service. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClassMember { pub name: Option, @@ -185,6 +189,12 @@ pub struct ClassMember { pub owner: Option, } +/// Syntax record of a class declaration. +/// +/// A class is not an [`crate::owner::OwnerId`] and not a name-resolution +/// scope: `scope` / `pathres` / `symbol` do not mention [`ClassId`]. +/// `base_class_name` is the identifier as written, unresolved. Semantic +/// answers (type, inheritance, members of a base) come from slang. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClassDef { pub name: Option, diff --git a/crates/hir-def/src/ast_id_map.rs b/crates/hir-def/src/ast_id_map.rs index a855588bc..c43e4894e 100644 --- a/crates/hir-def/src/ast_id_map.rs +++ b/crates/hir-def/src/ast_id_map.rs @@ -75,7 +75,7 @@ pub struct AstIdMap { } impl AstIdMap { - pub(crate) fn from_source(tree: &SyntaxTree) -> Self { + pub fn from_source(tree: &SyntaxTree) -> Self { let mut candidates = Vec::new(); let mut paths: Vec = Vec::new(); let mut child_counts: Vec> = Vec::new(); diff --git a/crates/hir-def/src/db.rs b/crates/hir-def/src/db.rs index 3eda2ca81..651edb24c 100644 --- a/crates/hir-def/src/db.rs +++ b/crates/hir-def/src/db.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use base_db::salsa; +pub use design_graph::DesignGraphDb; use preproc_expand::{db::PreprocDb, file::HirFileId}; use triomphe::Arc; use utils::text_edit::TextSize; @@ -9,7 +10,6 @@ use crate::{ ast_id_map::{self, AstIdMap, SyntaxFileId}, body::{self, Body}, def_id::{self, DefinitionTable}, - design_map, design_map::PackageExports, diagnostics, item_tree::{self, ItemTree, ItemTreeItem, Signature}, @@ -18,11 +18,10 @@ use crate::{ source_map::Lowered, source_projection::{self, SourceProjection}, subroutine::Subroutine, - unit_index, }; #[salsa::db] -pub trait HirDefDb: PreprocDb {} +pub trait HirDefDb: PreprocDb + DesignGraphDb {} // Salsa attaches tracked query methods to `dyn Db`; keep the lower-layer // surface available on composed database trait objects without forwarding. @@ -90,8 +89,9 @@ impl dyn HirDefDb + '_ { pub fn file_lowering_diagnostics( &self, file_id: HirFileId, + context: &crate::pathres::ResolutionContext, ) -> Arc<[crate::source_map::LoweringDiagnostic]> { - diagnostics::file_lowering_diagnostics(self, self.syntax_file(file_id)) + diagnostics::file_lowering_diagnostics(self, self.syntax_file(file_id), context) } pub fn scope(&self, owner: OwnerId) -> Arc { @@ -102,8 +102,8 @@ impl dyn HirDefDb + '_ { crate::scope::unit_scope(self) } - pub fn unit_index(&self) -> Arc { - unit_index::unit_index(self) + pub fn file_facts(&self, file_id: vfs::FileId) -> Arc { + ::file_facts(self, file_id) } pub fn subroutine(&self, owner: OwnerId) -> Arc { @@ -116,12 +116,21 @@ impl dyn HirDefDb + '_ { ) } - pub fn package_export_signature(&self, package_owner: OwnerId) -> Arc { - self.package_exports(package_owner) + pub fn package_export_signature( + &self, + context: &crate::pathres::ResolutionContext, + package_owner: OwnerId, + ) -> Arc { + self.package_exports(context, package_owner) } - pub fn package_exports(&self, package_owner: OwnerId) -> Arc { - self.design_map() + pub fn package_exports( + &self, + context: &crate::pathres::ResolutionContext, + package_owner: OwnerId, + ) -> Arc { + context + .design_map(self) .package_exports(package_owner) .expect("package owner must be present in the design map") } @@ -133,10 +142,6 @@ impl dyn HirDefDb + '_ { ) -> Arc<[(TextSize, Option)]> { crate::ty::default_nettype_directives(self, self.syntax_file(file_id)) } - - pub fn design_map(&self) -> Arc { - crate::design_map::design_map(self) - } } /// Sets the LRU capacity of the tracked HIR queries. @@ -144,10 +149,9 @@ pub fn set_lru_capacity(db: &mut dyn HirDefDb, capacity: usize) { ast_id_map::set_ast_id_map_lru_capacity(db, capacity); body::set_body_lru_capacity(db, capacity); def_id::set_definition_table_lru_capacity(db, capacity); - design_map::set_lru_capacity(db, capacity); item_tree::set_item_tree_lru_capacity(db, capacity); + design_graph::set_file_facts_lru_capacity(db, capacity); owner::set_owner_table_lru_capacity(db, capacity); - unit_index::set_lru_capacity(db, capacity); scope::set_scope_lru_capacity(db, capacity); source_projection::set_source_projection_lru_capacity(db, capacity); crate::region_tree::set_region_tree_lru_capacity(db, capacity); diff --git a/crates/hir-def/src/def_id.rs b/crates/hir-def/src/def_id.rs index d79022d22..7987a63e8 100644 --- a/crates/hir-def/src/def_id.rs +++ b/crates/hir-def/src/def_id.rs @@ -107,7 +107,13 @@ impl DefOriginLoc { pub fn name(self, db: &dyn HirDefDb) -> Option { match self { - DefOriginLoc::Module(owner) => db.body(owner).name.clone(), + // Named structural owners keep their identity on the owner table. + // Reading the name must not lower a body: `$unit` and header + // intern project these owners into `DefId`s before any body query. + DefOriginLoc::Module(owner) + | DefOriginLoc::Block(owner) + | DefOriginLoc::GenerateBlock(owner) + | DefOriginLoc::Subroutine(owner) => owner.name(db), DefOriginLoc::Config(InFile { value, file_id }) => GetRef::get( db.body(db.owner_table(file_id).file_owner().expect("file owner")).as_ref(), value, @@ -126,9 +132,6 @@ impl DefOriginLoc { ) .name .clone(), - DefOriginLoc::Block(owner) => owner.name(db), - DefOriginLoc::GenerateBlock(owner) => db.body(owner).name.clone(), - DefOriginLoc::Subroutine(owner) => db.subroutine(owner).name.clone(), DefOriginLoc::SubroutinePort(OwnerRef { cont_id: subroutine, value }) => { db.subroutine(subroutine).ports.get(value.0 as usize)?.name.clone() } @@ -558,21 +561,45 @@ impl DefId { /// The owner seam deliberately exposes only owner kinds that have a /// language-level definition. Procedural owners and lexical scopes remain /// owners without a `DefId`. + /// + /// Header-shaped owners (module, generate block, block, subroutine) intern + /// from the owner table only. Their `LocalDefId` is the first row + /// [`definition_table`] later allocates for that owner, so a subsequent + /// body-backed lookup yields the same `DefId`. Checker, covergroup, and + /// clocking still need the lowered body because their origin is an arena + /// id inside that body. pub fn from_owner(db: &dyn HirDefDb, owner: OwnerId) -> Option { - let origin = match owner.kind(db) { - OwnerKind::Module => Some(DefOriginLoc::Module(owner)), - OwnerKind::GenerateBlock => Some(DefOriginLoc::GenerateBlock(owner)), - OwnerKind::Block => Some(DefOriginLoc::Block(owner)), - OwnerKind::Subroutine => Some(DefOriginLoc::Subroutine(owner)), - OwnerKind::Checker => owner.as_checker(db).map(DefOriginLoc::Checker), - OwnerKind::Covergroup => owner.as_covergroup(db).map(DefOriginLoc::Covergroup), + match owner.kind(db) { + OwnerKind::Module + | OwnerKind::GenerateBlock + | OwnerKind::Block + | OwnerKind::Subroutine => Some(Self::from_owner_header(db, owner)), + OwnerKind::Checker => owner.as_checker(db).map(|origin| Self::from_source(db, origin)), + OwnerKind::Covergroup => { + owner.as_covergroup(db).map(|origin| Self::from_source(db, origin)) + } OwnerKind::ClockingBlock => { - owner.as_clocking_block(db).map(DefOriginLoc::ClockingBlock) + owner.as_clocking_block(db).map(|origin| Self::from_source(db, origin)) } - OwnerKind::AnonymousProgram => None, - OwnerKind::File | OwnerKind::ProceduralBlock => None, - }?; - Some(Self::from_source(db, origin)) + OwnerKind::AnonymousProgram | OwnerKind::File | OwnerKind::ProceduralBlock => None, + } + } + + fn from_owner_header(db: &dyn HirDefDb, owner: OwnerId) -> Self { + let loc = match owner.kind(db) { + OwnerKind::Module => DefOriginLoc::Module(owner), + OwnerKind::GenerateBlock => DefOriginLoc::GenerateBlock(owner), + OwnerKind::Block => DefOriginLoc::Block(owner), + OwnerKind::Subroutine => DefOriginLoc::Subroutine(owner), + other => { + unreachable!("header intern is only for named structural owners, got {other:?}") + } + }; + let local = LocalDefId(DefinitionKey { + name: DefinitionNameKey { kind: loc.clone().kind(db), name: loc.name(db) }, + ordinal: 0, + }); + Self(InternedDefId::new(db, owner, local)) } /// Construct a canonical definition from a typed source representation. diff --git a/crates/hir-def/src/design_map.rs b/crates/hir-def/src/design_map.rs index eacc92030..2f7897928 100644 --- a/crates/hir-def/src/design_map.rs +++ b/crates/hir-def/src/design_map.rs @@ -5,8 +5,9 @@ //! that graph so package imports are resolved consistently for both direct //! package queries and lexical name resolution. +use std::cell::Cell; + use base_db::salsa; -use preproc_expand::file::HirFileId; use rustc_hash::FxHashMap; use smallvec::SmallVec; use smol_str::SmolStr; @@ -247,6 +248,7 @@ impl DesignMap { pub fn resolve_import( &self, db: &dyn HirDefDb, + context: &crate::pathres::ResolutionContext, import: &Import, ident: &SmolStr, ctx: NameContext, @@ -257,7 +259,7 @@ impl DesignMap { return Resolution::Unresolved; } - let packages = db.unit_index().package_ids(&import.package); + let packages = Resolution::from_candidates(context.locate_packages(db, &import.package)); packages.and_then(|package| { let Some(exports) = self.package_exports.get(&package) else { return Resolution::Unresolved; @@ -267,22 +269,61 @@ impl DesignMap { } } -#[salsa::tracked(lru = 128, returns(clone))] -pub fn design_map(db: &dyn HirDefDb) -> Arc { - let mut packages = db - .files() - .iter() - .flat_map(|file_id| { - db.item_tree(HirFileId::File(*file_id)) - .module_headers() - .filter(|header| header.kind() == crate::module::ModuleKind::Package) - .map(|header| header.owner()) - .collect::>() - }) - .collect::>(); +thread_local! { + /// Executions of the salsa query body. A request that calls + /// `resolution()` / `semantics()` more than once must still see 1. + pub static PACKAGE_EXPORT_CLOSURE_RUNS: Cell = const { Cell::new(0) }; + /// Former paid UnitId projections while building the closure. T6 keeps this at 0. + pub static PACKAGE_EXPORT_TO_OWNER_RUNS: Cell = const { Cell::new(0) }; +} + +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub struct PackageExportClosureKey { + #[returns(copy)] + pub _unit: (), +} + +/// Closed package-export graph for the source catalog. +/// +/// Overlay-generated packages are not salsa inputs (T14). When the +/// production catalog has extra packages, [`package_export_closure`] +/// computes them outside this query rather than smuggling the overlay +/// into salsa. +#[salsa::tracked(returns(clone))] +fn package_export_closure_query( + db: &dyn HirDefDb, + _key: PackageExportClosureKey, +) -> Arc { + let graph = ::source_unit_catalog(db); + compute_package_export_closure(db, &graph) +} + +/// Closed package-export graph for the packages on `graph`. +pub fn package_export_closure( + db: &dyn HirDefDb, + graph: &design_graph::UnitCatalog, +) -> Arc { + let source = ::source_unit_catalog(db); + if same_packages(graph, &source) { + return package_export_closure_query(db, PackageExportClosureKey::new(db, ())); + } + compute_package_export_closure(db, graph) +} + +fn same_packages(left: &design_graph::UnitCatalog, right: &design_graph::UnitCatalog) -> bool { + let left: rustc_hash::FxHashSet<_> = left.packages().collect(); + let right: rustc_hash::FxHashSet<_> = right.packages().collect(); + left == right +} + +fn compute_package_export_closure( + db: &dyn HirDefDb, + graph: &design_graph::UnitCatalog, +) -> Arc { + PACKAGE_EXPORT_CLOSURE_RUNS.with(|runs| runs.set(runs.get() + 1)); + let mut packages: Vec = crate::unit::locate_package_owners(db, graph); packages.sort(); packages.dedup(); - let unit_index = db.unit_index(); let mut exports = FxHashMap::default(); let mut imports = FxHashMap::default(); @@ -316,17 +357,20 @@ pub fn design_map(db: &dyn HirDefDb) -> Arc { .clone(); let mut add_reexport = |source_package: &Ident, item: Option<&Ident>| { - let names = item.map(|item| vec![item.clone()]).unwrap_or_else(|| { - imported_names(&exports, unit_index.package_ids(source_package)) - }); + let source_owners = Resolution::from_candidates(crate::unit::locate_cu_owners( + db, + graph, + &[], + source_package, + design_graph::UnitKind::Package, + )); + let names = item + .map(|item| vec![item.clone()]) + .unwrap_or_else(|| imported_names(&exports, source_owners.clone())); for name in names { for ctx in [NameContext::Type, NameContext::Value, NameContext::Assertion] { - let resolution = resolve_package_member( - &exports, - unit_index.package_ids(source_package), - &name, - ctx, - ); + let resolution = + resolve_package_member(&exports, source_owners.clone(), &name, ctx); next.insert_resolution(ctx, &name, resolution); } } @@ -395,7 +439,3 @@ fn imported_names( names.sort(); names } - -pub(crate) fn set_lru_capacity(db: &mut dyn HirDefDb, capacity: usize) { - design_map::set_lru_capacity(db, capacity); -} diff --git a/crates/hir-def/src/diagnostics.rs b/crates/hir-def/src/diagnostics.rs index 686c9313f..ffc3ff385 100644 --- a/crates/hir-def/src/diagnostics.rs +++ b/crates/hir-def/src/diagnostics.rs @@ -34,7 +34,9 @@ use crate::{ expr::Expr, has_source::HasSource, owner::OwnerId, - pathres::{NameRef, RefKind, before_reference, resolve_name_at, resolve_wildcard_at}, + pathres::{ + NameRef, RefKind, ResolutionContext, before_reference, resolve_name_at, resolve_wildcard_at, + }, proc::Proc, source_map::{LoweringDiagnostic, LoweringDiagnosticKind}, source_projection::SourceProjection, @@ -44,6 +46,7 @@ use crate::{ pub(crate) fn file_lowering_diagnostics( db: &dyn HirDefDb, file: SyntaxFileId, + context: &ResolutionContext, ) -> Arc<[LoweringDiagnostic]> { let file_id = file.hir_file(db); let tree = db.parse(file_id); @@ -67,6 +70,7 @@ pub(crate) fn file_lowering_diagnostics( } collect_wildcard_activation_conflicts( db, + context, file_owner, &references, &projection, @@ -76,6 +80,7 @@ pub(crate) fn file_lowering_diagnostics( collect_module(db, owner, &tree, &projection, &mut diagnostics); collect_wildcard_activation_conflicts( db, + context, owner, &references, &projection, @@ -86,6 +91,7 @@ pub(crate) fn file_lowering_diagnostics( for generate_owner in generate_owners { collect_wildcard_activation_conflicts( db, + context, generate_owner, &references, &projection, @@ -267,6 +273,7 @@ fn collect_generate_owner_ids(db: &dyn HirDefDb, owner: OwnerId, out: &mut Vec)], projection: &SourceProjection, @@ -288,12 +295,13 @@ fn collect_wildcard_activation_conflicts( } let reference = NameRef { position: *ref_position, kind: RefKind::Value }; let resolved = [NameContext::Type, NameContext::Value].into_iter().any(|ctx| { - let resolved = resolve_name_at(db, *ref_owner, name, ctx, Some(&reference)); + let resolved = + resolve_name_at(db, context, *ref_owner, name, ctx, Some(&reference)); if resolved.is_unresolved() { return false; } let (wildcard, activated_scope) = - resolve_wildcard_at(db, *ref_owner, name, ctx, Some(&reference)); + resolve_wildcard_at(db, context, *ref_owner, name, ctx, Some(&reference)); activated_scope == Some(owner) && resolved == wildcard }); resolved @@ -438,6 +446,9 @@ mod tests { #[salsa::db] impl PreprocDb for TestDb {} + #[salsa::db] + impl crate::db::DesignGraphDb for TestDb {} + #[salsa::db] impl HirDefDb for TestDb {} @@ -547,7 +558,8 @@ endmodule "#; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.is_empty(), "supported assignment patterns and struct types must not be diagnosed: {diagnostics:?}" @@ -567,7 +579,8 @@ module m(input logic x, y); endmodule "#; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( !diagnostics.iter().any(|diag| diag.message == "unsupported expression"), "property case expressions must be lowered: {diagnostics:?}" @@ -581,7 +594,8 @@ endmodule // `default_nettype none`. let text = "`default_nettype none\nmodule m(output a);\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.iter().any(|diag| diag.message.contains("default_nettype none")), "bare output port under `default_nettype none` must be diagnosed: {diagnostics:?}" @@ -729,7 +743,8 @@ program; endprogram "#; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.is_empty(), "supported compilation-unit members must not be diagnosed: {diagnostics:?}" @@ -1127,7 +1142,8 @@ endprogram #[test] fn invalid_time_units_value_produces_lowering_diagnostic() { let db = db_with_files("timeunit 2ns;\n", None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics .iter() @@ -1142,7 +1158,8 @@ endprogram "module m; default disable iff (1'b0); default disable iff (1'b1); endmodule\n", None, ); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics .iter() @@ -1155,7 +1172,8 @@ endprogram fn default_nettype_none_diagnoses_implicit_nets() { let text = "`default_nettype none\nmodule m(input a);\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.iter().any(|diag| diag.message.contains("default_nettype none")), "implicit net under `default_nettype none` must be diagnosed: {diagnostics:?}" @@ -1169,7 +1187,8 @@ endprogram // module's later declaration of x is illegal. let text = "package p;\nint x;\nendpackage\nmodule m;\nimport p::*;\ninitial begin : blk\n x = 1;\nend\nint x;\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics .iter() @@ -1184,7 +1203,8 @@ endprogram // the wildcard import is never activated and everything is legal. let text = "package p;\nint x;\nendpackage\nmodule m;\nimport p::*;\ninitial begin : blk\n int x;\n x = 1;\nend\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( !diagnostics .iter() @@ -1197,7 +1217,8 @@ endprogram fn wildcard_without_reference_is_legal() { let text = "package p;\nint x;\nendpackage\nmodule m;\nimport p::*;\nint x;\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( !diagnostics .iter() @@ -1210,7 +1231,8 @@ endprogram fn explicit_import_conflicting_with_declaration_is_diagnosed() { let text = "package p;\nint x;\nendpackage\nmodule m;\nint x;\nimport p::x;\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.iter().any(|diag| diag.message.contains("conflicts with a declaration")), "explicit import of a declared name must be diagnosed: {diagnostics:?}" @@ -1221,7 +1243,8 @@ endprogram fn explicit_import_conflicting_across_packages_is_diagnosed() { let text = "package p;\nint x;\nendpackage\npackage q;\nint x;\nendpackage\nmodule m;\nimport p::x;\nimport q::x;\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.iter().any(|diag| diag.message.contains("another explicit import")), "explicit imports of one name from two packages must be diagnosed: {diagnostics:?}" @@ -1232,7 +1255,8 @@ endprogram fn legal_imports_produce_no_conflict_diagnostics() { let text = "package p;\nint x;\nendpackage\npackage q;\nint y;\nendpackage\nmodule m;\nimport p::x;\nimport q::*;\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( !diagnostics.iter().any(|diag| diag.message.contains("conflicts")), "legal imports must not conflict: {diagnostics:?}" @@ -1244,7 +1268,8 @@ endprogram let text = "module m;\ninitial begin\n foreach (arr[i]) x = 1;\nend\nendmodule\n"; let db = db_with_files(text, None); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( !diagnostics.iter().any(|diag| diag.message == "unsupported statement"), "lowered foreach statements must not be diagnosed: {diagnostics:?}" @@ -1256,7 +1281,8 @@ endprogram let text = "module m;\n`include \"defs.vh\"\nendmodule\n"; let db = db_with_files(text, Some("struct { logic a; } value;\n")); - let diagnostics = db.file_lowering_diagnostics(HirFileId::File(TOP)); + let diagnostics = + db.file_lowering_diagnostics(HirFileId::File(TOP), &crate::unit::test_resolution(&db)); assert!( diagnostics.is_empty(), "included struct types must be lowered without diagnostics: {diagnostics:?}" diff --git a/crates/hir-def/src/item_tree.rs b/crates/hir-def/src/item_tree.rs index 61c100f15..44b49bf97 100644 --- a/crates/hir-def/src/item_tree.rs +++ b/crates/hir-def/src/item_tree.rs @@ -50,7 +50,7 @@ pub enum SignaturePortDirection { Unknown, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SignaturePort { direction: SignaturePortDirection, name: Option, @@ -71,7 +71,7 @@ impl SignaturePort { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Signature { kind: SignatureKind, return_type_ast: Option, @@ -92,7 +92,7 @@ impl Signature { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ItemTreeItem { id: SourceAstId, parent: Option, @@ -137,38 +137,10 @@ impl ItemTreeItem { } } -/// A module declaration collected from the file-level structural summary. -/// -/// It contains semantic header data and source identity, but no source range. -/// Ranges belong to [`crate::source_projection::SourceProjection`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ModuleHeader { - owner: OwnerId, - name: SmolStr, - kind: crate::module::ModuleKind, - source: SourceAstId, -} - -impl ModuleHeader { - pub fn owner(&self) -> OwnerId { - self.owner - } - - pub fn name(&self) -> &SmolStr { - &self.name - } - - pub fn kind(&self) -> crate::module::ModuleKind { - self.kind - } - - pub fn source(&self) -> SourceAstId { - self.source - } -} - -/// File-level structural summary. It intentionally contains no source ranges -/// or focus ranges; those belong to +/// File-level structural summary for HIR lowering. Compilation-unit +/// declaration identity lives on `design_graph::FileFacts`; this tree is the +/// body/item inventory. It intentionally contains no source ranges or focus +/// ranges; those belong to /// [`crate::source_projection::SourceProjection`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ItemTree { @@ -188,19 +160,8 @@ impl ItemTree { self.owners.file_owner() } - /// Module and package headers in source order. - /// - /// This is the file-level declaration seam. Consumers that only need - /// headers must not enter a scope or body query to discover them. - pub fn module_headers(&self) -> impl Iterator + '_ { - self.owners.owners_of_kind(crate::owner::OwnerKind::Module).filter_map(|owner| { - owner.module_kind.map(|kind| ModuleHeader { - owner: owner.id, - name: owner.name.clone(), - kind, - source: owner.source, - }) - }) + pub fn owners(&self) -> &OwnerTable { + &self.owners } pub fn items(&self) -> impl Iterator { @@ -308,11 +269,12 @@ fn build_item_tree_data( let mut parents = Vec::new(); let mut body_depth = 0usize; let root = tree.root(); - assert_eq!( - root.kind(), - syntax::SyntaxKind::COMPILATION_UNIT, - "item tree requires a compilation-unit syntax root" - ); + if root.kind() != syntax::SyntaxKind::COMPILATION_UNIT { + // Library-map and other non-compilation-unit syntax roots have no + // compilation-unit members, so they contribute no item-tree items. + // Their declarations are lowered via `lower_library_map` instead. + return (Vec::new(), Vec::new()); + } for event in root.elem_preorder() { match event { WalkEvent::Enter(SyntaxElement::Node(node)) => { @@ -648,6 +610,16 @@ mod tests { assert_eq!(before_function.parent(), after_function.parent()); } + #[test] + fn item_tree_builds_empty_for_library_map() { + let text = "library foo \"dir/*.sv\";\n"; + let file_id = HirFileId::File(FileId::from_raw(0)); + let tree = SyntaxTree::from_library_map_text(text, "test.map", "test.map"); + let ast_ids = AstIdMap::from_source(&tree); + let item_tree = build_item_tree(file_id, &tree, &ast_ids, Some(text)); + assert_eq!(item_tree.len(), 0, "library-map files contribute no item-tree items"); + } + #[test] fn source_projection_keeps_non_navigable_items_distinct_from_missing_items() { let file_id = HirFileId::File(FileId::from_raw(0)); diff --git a/crates/hir-def/src/lib.rs b/crates/hir-def/src/lib.rs index 5b2a9b31a..a2f37a266 100644 --- a/crates/hir-def/src/lib.rs +++ b/crates/hir-def/src/lib.rs @@ -50,7 +50,7 @@ pub mod symbol; pub mod time_units; pub mod ty; pub mod typedef; -pub mod unit_index; +pub mod unit; pub(crate) macro impl_arena_getters( $container:ty; diff --git a/crates/hir-def/src/owner.rs b/crates/hir-def/src/owner.rs index b839e0237..a183cd099 100644 --- a/crates/hir-def/src/owner.rs +++ b/crates/hir-def/src/owner.rs @@ -88,7 +88,7 @@ impl Ord for OwnerId { } } /// One entry of the per-file [`OwnerTable`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct OwnerData { pub id: OwnerId, pub source: SourceAstId, @@ -106,6 +106,7 @@ pub struct OwnerTable { owners: Vec, by_id: FxHashMap, by_source: FxHashMap<(SourceAstId, OwnerKind), OwnerId>, + by_name_kind: FxHashMap<(SmolStr, OwnerKind), SmallVec<[OwnerId; 1]>>, } impl OwnerTable { @@ -134,6 +135,11 @@ impl OwnerTable { pub fn owner_by_ast(&self, ast_id: SourceAstId, kind: OwnerKind) -> Option { self.by_source.get(&(ast_id, kind)).copied() } + + /// Owners of this `(name, kind)`, in source order. Does not scan the table. + pub fn owners_named(&self, name: &str, kind: OwnerKind) -> &[OwnerId] { + self.by_name_kind.get(&(SmolStr::new(name), kind)).map(SmallVec::as_slice).unwrap_or(&[]) + } } pub(crate) struct OwnerTableBuilder<'db> { @@ -169,18 +175,22 @@ impl<'db> OwnerTableBuilder<'db> { let parent = self.stack.last().copied(); let owner = OwnerId::new(self.db, self.file_id, ast_id, kind); let index = self.table.owners.len(); + let name = owner_name(node, kind); self.table.owners.push(OwnerData { id: owner, source: ast_id, kind, parent, - name: owner_name(node, kind), + name: name.clone(), module_kind: owner_module_kind(node, kind), }); let replaced = self.table.by_id.insert(owner, index); debug_assert!(replaced.is_none(), "duplicate owner identity"); let replaced = self.table.by_source.insert((ast_id, kind), owner); debug_assert!(replaced.is_none(), "duplicate owner source key"); + if !name.is_empty() { + self.table.by_name_kind.entry((name, kind)).or_default().push(owner); + } self.stack.push(owner); } } @@ -202,6 +212,15 @@ pub(crate) fn owner_table(db: &dyn HirDefDb, file: SyntaxFileId) -> Arc OwnerTable { let root = tree.root(); assert!( matches!(root.kind(), SyntaxKind::COMPILATION_UNIT | SyntaxKind::LIBRARY_MAP), @@ -222,7 +241,7 @@ pub(crate) fn owner_table(db: &dyn HirDefDb, file: SyntaxFileId) -> Arc {} } } - Arc::new(builder.finish()) + builder.finish() } pub(crate) fn set_owner_table_lru_capacity(db: &mut dyn HirDefDb, capacity: usize) { @@ -377,6 +396,9 @@ mod tests { #[salsa::db] impl PreprocDb for TestDb {} + #[salsa::db] + impl crate::db::DesignGraphDb for TestDb {} + #[salsa::db] impl HirDefDb for TestDb {} @@ -437,6 +459,34 @@ mod tests { AbsPathBuf::assert(Utf8PathBuf::from(format!("{prefix}/{path}"))) } + #[test] + fn a_class_is_a_syntax_record_not_an_owner() { + let db = db_with_root_text( + r#" +module m; + class C extends Base; + int value; + function void tick(); + endfunction + endclass +endmodule +"#, + ); + let table = db.owner_table(HirFileId::File(TOP)); + assert!( + table.owners().iter().all(|owner| owner.name.as_str() != "C"), + "a class is not interned as an owner" + ); + let module = *table.owners_named("m", OwnerKind::Module).first().expect("module owner"); + let body = db.body(module); + let class = body.classes.values().next().expect("class syntax record"); + assert_eq!(class.name.as_deref(), Some("C")); + assert_eq!(class.base_class_name.as_deref(), Some("Base")); + assert_eq!(class.members.len(), 2); + assert_eq!(class.members[0].kind, crate::aggregate::ClassMemberKind::Property); + assert_eq!(class.members[1].kind, crate::aggregate::ClassMemberKind::Method); + } + /// Structural fingerprint of an owner table: (kind, name, parent name). /// Comparable across databases, unlike the interned ids. fn fingerprint(table: &crate::owner::OwnerTable) -> Vec<(String, String, Option)> { diff --git a/crates/hir-def/src/pathres.rs b/crates/hir-def/src/pathres.rs index f07243d7d..ffcf1f524 100644 --- a/crates/hir-def/src/pathres.rs +++ b/crates/hir-def/src/pathres.rs @@ -1,17 +1,95 @@ use preproc_expand::file::HirFileId; use smallvec::SmallVec; +use triomphe::Arc; use utils::get::GetRef; +use vfs::FileId; use crate::{ Ident, container::{InFile, ScopeChain}, db::HirDefDb, def_id::DefId, + design_map::DesignMap, module::instantiation::InstanceId, owner::{OwnerId, OwnerKind}, symbol::{DefKind, NameContext, Resolution, ScopeData}, + unit::{locate_cu_owners, locate_cu_owners_matching}, }; +/// Cross-file name-resolution inputs. +/// +/// The injected [`UnitCatalog`] is a name → file locator, not identity. +/// Compilation-unit owners come from the paid-parse owner table. `$unit` +/// locals come from the unit-scope query. The package export map is a +/// salsa query over the source catalog — building this context does not +/// re-fold every package. +#[derive(Clone)] +pub struct ResolutionContext { + locator: Arc, + paid_files: Arc<[FileId]>, + unit_scope: Arc, + design_map: Arc, +} + +impl ResolutionContext { + pub fn from_graph(db: &dyn HirDefDb, graph: Arc) -> Arc { + Self::from_locator(db, graph, Arc::from(Vec::::new())) + } + + pub fn from_locator( + db: &dyn HirDefDb, + locator: Arc, + paid_files: Arc<[FileId]>, + ) -> Arc { + Arc::new(Self { + unit_scope: db.unit_scope(), + design_map: crate::design_map::package_export_closure(db, &locator), + locator, + paid_files, + }) + } + + pub fn graph(&self) -> &design_graph::UnitCatalog { + &self.locator + } + + pub fn unit_scope(&self, _db: &dyn HirDefDb) -> Arc { + self.unit_scope.clone() + } + + pub fn design_map(&self, _db: &dyn HirDefDb) -> Arc { + self.design_map.clone() + } + + pub fn locate_type_units(&self, db: &dyn HirDefDb, name: &str) -> Vec { + locate_cu_owners_matching(db, &self.locator, &self.paid_files, name, |_| true) + } + + pub fn locate_hierarchy_targets(&self, db: &dyn HirDefDb, name: &str) -> Vec { + locate_cu_owners_matching(db, &self.locator, &self.paid_files, name, |kind| { + kind.is_hierarchy_target() + }) + } + + pub fn locate_packages(&self, db: &dyn HirDefDb, name: &str) -> Vec { + locate_cu_owners(db, &self.locator, &self.paid_files, name, design_graph::UnitKind::Package) + } + + pub fn locate_instantiation_targets( + &self, + db: &dyn HirDefDb, + name: &str, + role: design_graph::InstantiationRole, + ) -> Vec { + locate_cu_owners_matching(db, &self.locator, &self.paid_files, name, |kind| match role { + design_graph::InstantiationRole::Hierarchy => kind.is_hierarchy_target(), + design_graph::InstantiationRole::Checker => { + matches!(kind, design_graph::UnitKind::Checker) + } + }) + } +} + // SystemVerilog name AST note for path resolution: // // slang models simple names as `IdentifierName`, names with unpacked selects @@ -20,9 +98,11 @@ use crate::{ // 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. +// `IdentifierSelectName` into `Expr::ElementSelect`; this resolver handles +// the hierarchical dot/select shape only. +// +// Package and class `::` are answered by the elaboration service. This +// resolver does hierarchical dots only. There is no type-lowering path here. /// Resolution phase recorded by [`resolve_name_with_trace`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -71,11 +151,12 @@ pub struct NameRef { pub fn resolve_name( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, ident: &Ident, ctx: NameContext, ) -> Resolution { - resolve_name_at(db, cont_id, ident, ctx, None) + resolve_name_at(db, context, cont_id, ident, ctx, None) } /// Resolve a name honoring the reference's source position. Without a @@ -83,12 +164,13 @@ pub fn resolve_name( /// matches the position-less [`resolve_name`]. pub fn resolve_name_at( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, ident: &Ident, ctx: NameContext, reference: Option<&NameRef>, ) -> Resolution { - resolve_name_inner(db, cont_id, ident, ctx, None, reference) + resolve_name_inner(db, context, cont_id, ident, ctx, None, reference) } /// Resolve a name and retain the precedence decisions made by the resolver. @@ -98,12 +180,13 @@ pub fn resolve_name_at( /// named-import, wildcard-import, and `$unit` decision through this seam. pub fn resolve_name_with_trace( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, ident: &Ident, ctx: NameContext, ) -> (Resolution, ResolutionTrace) { let mut trace = ResolutionTrace::default(); - let resolution = resolve_name_inner(db, cont_id, ident, ctx, Some(&mut trace), None); + let resolution = resolve_name_inner(db, context, cont_id, ident, ctx, Some(&mut trace), None); (resolution, trace) } @@ -141,6 +224,7 @@ fn filter_resolution_at( fn resolve_name_inner( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, ident: &Ident, ctx: NameContext, @@ -175,6 +259,7 @@ fn resolve_name_inner( // this scope. `$unit` remains the final scope. let imported = resolve_scope_imports( db, + context, scope.as_ref(), ident, ctx, @@ -187,7 +272,7 @@ fn resolve_name_inner( } } - let unit = db.unit_scope().lookup(ctx, ident); + let unit = resolve_unit_name(db, context, ident, ctx); if let Some(trace) = trace { trace.entries.push(ResolutionTraceEntry { phase: ResolutionPhase::Unit, @@ -198,6 +283,30 @@ fn resolve_name_inner( unit } +fn resolve_unit_name( + db: &dyn HirDefDb, + context: &ResolutionContext, + ident: &Ident, + ctx: NameContext, +) -> Resolution { + let locals = context.unit_scope(db).lookup(ctx, ident); + let units = match ctx { + NameContext::Type | NameContext::Listing => Resolution::from_candidates( + context + .locate_type_units(db, ident) + .into_iter() + .filter_map(|owner| DefId::from_owner(db, owner)), + ), + NameContext::Value | NameContext::Assertion => Resolution::Unresolved, + }; + match (locals, units) { + (Resolution::Unresolved, other) | (other, Resolution::Unresolved) => other, + (left, right) => Resolution::from_candidates( + left.into_candidates().into_iter().chain(right.into_candidates()), + ), + } +} + /// A scope chain resolved against canonical owner-local scope queries. pub struct ResolvedScopes { scope_chain: ScopeChain, @@ -213,17 +322,19 @@ impl ResolvedScopes { /// search order as [`resolve_name_at`]. pub fn resolve_in_resolved_scopes( db: &dyn HirDefDb, + context: &ResolutionContext, resolved: &ResolvedScopes, ident: &Ident, ctx: NameContext, ) -> Resolution { - resolve_in_resolved_scopes_at(db, resolved, ident, ctx, None) + resolve_in_resolved_scopes_at(db, context, resolved, ident, ctx, None) } /// Position-aware variant of [`resolve_in_resolved_scopes`]; see /// [`resolve_name_at`] for the filtering rules. pub fn resolve_in_resolved_scopes_at( db: &dyn HirDefDb, + context: &ResolutionContext, resolved: &ResolvedScopes, ident: &Ident, ctx: NameContext, @@ -242,6 +353,7 @@ pub fn resolve_in_resolved_scopes_at( } let imported = resolve_scope_imports( db, + context, scope.as_ref(), ident, ctx, @@ -253,22 +365,24 @@ pub fn resolve_in_resolved_scopes_at( return imported; } } - db.unit_scope().lookup(ctx, ident) + resolve_unit_name(db, context, ident, ctx) } pub fn resolve_path( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, path: &[Ident], ctx: NameContext, ) -> Resolution { - resolve_path_at(db, cont_id, path, ctx, None) + resolve_path_at(db, context, cont_id, path, ctx, None) } /// Position-aware variant of [`resolve_path`]; the first segment honors the /// reference position while member segments keep position-less lookup. pub fn resolve_path_at( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, path: &[Ident], ctx: NameContext, @@ -277,12 +391,12 @@ pub fn resolve_path_at( let Some((first, rest)) = path.split_first() else { return Resolution::Unresolved; }; - let mut current = resolve_name_at(db, cont_id, first, ctx, reference) - .or_else(|| resolve_top_level_module_root(db, first, ctx, !rest.is_empty())); + let mut current = resolve_name_at(db, context, cont_id, first, ctx, reference) + .or_else(|| resolve_top_level_module_root(db, context, 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); + current = resolve_child_name(db, context, ¤t, segment, segment_ctx); if current.is_unresolved() { break; } @@ -293,6 +407,7 @@ pub fn resolve_path_at( fn resolve_top_level_module_root( db: &dyn HirDefDb, + context: &ResolutionContext, ident: &Ident, ctx: NameContext, has_child_segment: bool, @@ -308,9 +423,8 @@ fn resolve_top_level_module_root( // is not a single segment value fallback: `top` alone remains a type-space // module name, and nested declarations never leak through the fallback. Resolution::from_candidates( - db.unit_index() - .top_level_module_ids(ident) - .into_candidates() + context + .locate_hierarchy_targets(db, ident) .into_iter() .map(|owner| DefId::from_source(db, crate::symbol::DefOriginLoc::Module(owner))), ) @@ -318,18 +432,23 @@ fn resolve_top_level_module_root( pub fn resolve_child_name( db: &dyn HirDefDb, + context: &ResolutionContext, parent: &Resolution, ident: &Ident, ctx: NameContext, ) -> Resolution { parent.and_then(|def_id| { - let Some(scope_id) = descend_scope(db, def_id) else { + let Some(scope_id) = descend_scope(db, context, def_id) else { return Resolution::Unresolved; }; db.scope(scope_id).lookup(ctx, ident) }) } -pub fn descend_scope(db: &dyn HirDefDb, def_id: DefId) -> Option { +pub fn descend_scope( + db: &dyn HirDefDb, + context: &ResolutionContext, + def_id: DefId, +) -> Option { let origin = def_id.primary_origin(db); match def_id.kind(db) { DefKind::Module | DefKind::Interface | DefKind::Program | DefKind::Package => { @@ -342,8 +461,8 @@ pub fn descend_scope(db: &dyn HirDefDb, def_id: DefId) -> Option { | DefKind::GenerateBlock => Some(definition_scope_owner(db, origin)), DefKind::Instance => { let instance = origin.as_instance(db)?; - let target = instance_target_def_id(db, instance.cont_id, instance.value)?; - descend_scope(db, target) + let target = instance_target_def_id(db, context, instance.cont_id, instance.value)?; + descend_scope(db, context, target) } _ => None, } @@ -355,6 +474,7 @@ fn definition_scope_owner(db: &dyn HirDefDb, origin: crate::symbol::DefOrigin) - pub fn instance_target_def_id( db: &dyn HirDefDb, + context: &ResolutionContext, module_id: OwnerId, instance_id: InstanceId, ) -> Option { @@ -362,14 +482,47 @@ pub fn instance_target_def_id( let instance = module.get(instance_id); let instantiation = module.get(instance.parent); let module_name = instantiation.module_name.as_ref()?; - let target = db - .unit_index() - .instantiable_ids_in(module_id, module_name) - .unique() - .map(|owner| instantiable_def_id(db, owner))?; + let local = local_instantiable_owner(db, module_id, module_name); + if !local.is_unresolved() { + return local.unique().map(|owner| instantiable_def_id(db, owner)); + } + let target = Resolution::from_candidates( + context + .locate_instantiation_targets( + db, + module_name, + design_graph::InstantiationRole::Hierarchy, + ) + .into_iter() + .chain(context.locate_instantiation_targets( + db, + module_name, + design_graph::InstantiationRole::Checker, + )), + ) + .unique() + .map(|owner| instantiable_def_id(db, owner))?; Some(target) } +fn local_instantiable_owner( + db: &dyn HirDefDb, + scope: OwnerId, + name: &Ident, +) -> Resolution { + Resolution::from_candidates( + db.owner_table(scope.file(db)) + .owners() + .iter() + .filter(|owner| { + owner.parent == Some(scope) + && owner.name == *name + && matches!(owner.kind, OwnerKind::Checker | OwnerKind::Covergroup) + }) + .map(|owner| owner.id), + ) +} + fn instantiable_def_id(db: &dyn HirDefDb, owner: OwnerId) -> DefId { let is_instantiable = matches!(owner.kind(db), OwnerKind::Checker | OwnerKind::Covergroup) || owner.module_kind(db).is_some_and(|kind| kind.is_instantiable()); @@ -403,6 +556,7 @@ impl AtFilter<'_> { /// Collects import candidates for one scope, applying the point filter. struct ImportCollector<'a> { db: &'a dyn HirDefDb, + context: &'a ResolutionContext, design_map: &'a crate::design_map::DesignMap, scope: &'a ScopeData, defs: SmallVec<[DefId; 3]>, @@ -423,8 +577,10 @@ impl ImportCollector<'_> { { continue; } - for def_id in - self.design_map.resolve_import(self.db, import, ident, ctx).into_candidates() + for def_id in self + .design_map + .resolve_import(self.db, self.context, import, ident, ctx) + .into_candidates() { if !self.defs.contains(&def_id) { self.defs.push(def_id); @@ -434,8 +590,10 @@ impl ImportCollector<'_> { } } +#[allow(clippy::too_many_arguments)] fn resolve_scope_imports( db: &dyn HirDefDb, + context: &ResolutionContext, scope: &ScopeData, ident: &Ident, ctx: NameContext, @@ -443,10 +601,11 @@ fn resolve_scope_imports( mut trace: Option<&mut ResolutionTrace>, at: AtFilter<'_>, ) -> Resolution { - let design_map = db.design_map(); + let design_map = context.design_map(db); let mut collector = ImportCollector { db, - design_map: &design_map, + context, + design_map: design_map.as_ref(), scope, defs: SmallVec::new(), scope_file: scope_id.file(db), @@ -483,6 +642,7 @@ fn resolve_scope_imports( /// import locally visible (IEEE 1800-2017 26.3). pub(crate) fn resolve_wildcard_at( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, ident: &Ident, ctx: NameContext, @@ -490,12 +650,13 @@ pub(crate) fn resolve_wildcard_at( ) -> (Resolution, Option) { let scopes = ScopeChain::from_inner(db, cont_id); let at = AtFilter { reference }; - let design_map = db.design_map(); for scope_id in scopes.iter() { let scope = db.scope(*scope_id); + let design_map = context.design_map(db); let mut collector = ImportCollector { db, - design_map: &design_map, + context, + design_map: design_map.as_ref(), scope: scope.as_ref(), defs: SmallVec::new(), scope_file: scope_id.file(db), @@ -560,6 +721,9 @@ mod tests { #[salsa::db] impl PreprocDb for TestDb {} + #[salsa::db] + impl crate::db::DesignGraphDb for TestDb {} + #[salsa::db] impl HirDefDb for TestDb {} impl std::ops::Deref for TestDb { @@ -635,7 +799,7 @@ mod tests { ctx: NameContext, ) -> DefKind { let path = path(segments); - resolve_path(db, scope_id, &path, ctx) + resolve_path(db, &crate::unit::test_resolution(db), scope_id, &path, ctx) .unique() .map(|def_id| def_id.kind(db)) .unwrap_or_else(|| panic!("path {segments:?} should resolve")) @@ -667,11 +831,7 @@ endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert_eq!(resolved_kind(&db, top, &["u", "sig"], NameContext::Value), DefKind::Net); assert_eq!(resolved_kind(&db, top, &["arr", "sig"], NameContext::Value), DefKind::Net); @@ -702,18 +862,25 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert!( - resolve_path(&db, top, &path(&["u", "only_left"]), NameContext::Value).is_unresolved() + resolve_path( + &db, + &crate::unit::test_resolution(&db), + top, + &path(&["u", "only_left"]), + NameContext::Value + ) + .is_unresolved() ); - let Resolution::Ambiguous(shared) = - resolve_path(&db, top, &path(&["u", "shared"]), NameContext::Value) - else { + let Resolution::Ambiguous(shared) = resolve_path( + &db, + &crate::unit::test_resolution(&db), + top, + &path(&["u", "shared"]), + NameContext::Value, + ) else { panic!("members from ambiguous parents should remain ambiguous"); }; assert_eq!(shared.len(), 2); @@ -736,14 +903,14 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let Resolution::Ambiguous(values) = - resolve_name(&db, top, &ident("value"), NameContext::Value) - else { + let top = crate::unit::test_module_owner(&db, "top"); + let Resolution::Ambiguous(values) = resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("value"), + NameContext::Value, + ) else { panic!("imports from ambiguous packages should remain ambiguous"); }; assert_eq!(values.len(), 2); @@ -765,14 +932,17 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert!( - resolve_name(&db, top, &ident("only_left"), NameContext::Value).is_unresolved(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("only_left"), + NameContext::Value + ) + .is_unresolved(), "a child member must not disambiguate its parent package" ); } @@ -795,24 +965,21 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let named = db - .unit_index() - .package_ids(&ident("named")) - .unique() - .expect("named package should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); + let named = crate::unit::test_package_owner(&db, "named"); let expected = db - .package_exports(named) + .package_exports(&crate::unit::test_resolution(&db), named) .lookup(NameContext::Value, &ident("value")) .unique() .expect("named package value should resolve uniquely"); - let (resolved, trace) = - resolve_name_with_trace(&db, top, &ident("value"), NameContext::Value); + let (resolved, trace) = resolve_name_with_trace( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("value"), + NameContext::Value, + ); assert_eq!(resolved, Resolution::Unique(expected)); assert!(trace.entries().iter().any(|entry| { entry.phase == ResolutionPhase::NamedImport @@ -844,13 +1011,14 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let (resolved, trace) = - resolve_name_with_trace(&db, top, &ident("value"), NameContext::Value); + let top = crate::unit::test_module_owner(&db, "top"); + let (resolved, trace) = resolve_name_with_trace( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("value"), + NameContext::Value, + ); let Resolution::Ambiguous(candidates) = resolved else { panic!("two named imports must remain ambiguous"); }; @@ -881,19 +1049,25 @@ initial x = 1; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let p2 = db - .unit_index() - .package_ids(&ident("p2")) - .unique() - .expect("p2 package should resolve uniquely"); - let p2_x = resolve_name(&db, p2, &ident("x"), NameContext::Value).unique().expect("p2::x"); + let top = crate::unit::test_module_owner(&db, "top"); + let p2 = crate::unit::test_package_owner(&db, "p2"); + let p2_x = resolve_name( + &db, + &crate::unit::test_resolution(&db), + p2, + &ident("x"), + NameContext::Value, + ) + .unique() + .expect("p2::x"); assert_eq!( - resolve_name(&db, top, &ident("x"), NameContext::Value), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("x"), + NameContext::Value + ), Resolution::Unique(p2_x) ); } @@ -925,14 +1099,24 @@ endmodule .find(|owner| owner.name.as_str() == "b") .expect("generate block b owner") .id; - let p2 = db - .unit_index() - .package_ids(&ident("p2")) - .unique() - .expect("p2 package should resolve uniquely"); - let p2_x = resolve_name(&db, p2, &ident("x"), NameContext::Value).unique().expect("p2::x"); + let p2 = crate::unit::test_package_owner(&db, "p2"); + let p2_x = resolve_name( + &db, + &crate::unit::test_resolution(&db), + p2, + &ident("x"), + NameContext::Value, + ) + .unique() + .expect("p2::x"); assert_eq!( - resolve_name(&db, block, &ident("x"), NameContext::Value), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + block, + &ident("x"), + NameContext::Value + ), Resolution::Unique(p2_x) ); } @@ -961,26 +1145,26 @@ endmodule "#, ); - let outer = db - .unit_index() - .package_ids(&ident("outer")) - .unique() - .expect("outer package should resolve uniquely"); + let outer = crate::unit::test_package_owner(&db, "outer"); assert!( - db.package_exports(outer) + db.package_exports(&crate::unit::test_resolution(&db), outer) .lookup(NameContext::Value, &ident("value")) .unique() .is_some(), "nested package exports must be computed transitively" ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert!( - resolve_name(&db, top, &ident("value"), NameContext::Value).unique().is_some(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("value"), + NameContext::Value + ) + .unique() + .is_some(), "lexical resolution must consume the canonical design map" ); } @@ -1007,31 +1191,31 @@ module top; endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let selective = db - .unit_index() - .package_ids(&ident("selective")) - .unique() - .expect("selective package should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); + let selective = crate::unit::test_package_owner(&db, "selective"); assert!( - db.package_exports(selective) + db.package_exports(&crate::unit::test_resolution(&db), selective) .lookup(NameContext::Value, &ident("exported")) .unique() .is_some(), "selective export must expose the selected imported value" ); assert!( - db.package_exports(selective) + db.package_exports(&crate::unit::test_resolution(&db), selective) .lookup(NameContext::Value, &ident("private")) .is_unresolved(), "selective export must not expose other wildcard-imported values" ); assert!( - resolve_name(&db, top, &ident("private"), NameContext::Value).unique().is_some(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("private"), + NameContext::Value + ) + .unique() + .is_some(), "export-all must re-export wildcard-imported values" ); } @@ -1055,26 +1239,23 @@ import p::*; endmodule "#, ); - let p = db - .unit_index() - .package_ids(&ident("p")) - .unique() - .expect("p package should resolve uniquely"); - let Resolution::Ambiguous(candidates) = - db.package_exports(p).lookup(NameContext::Value, &ident("x")) + let p = crate::unit::test_package_owner(&db, "p"); + let Resolution::Ambiguous(candidates) = db + .package_exports(&crate::unit::test_resolution(&db), p) + .lookup(NameContext::Value, &ident("x")) else { panic!("mutually exported x must remain ambiguous"); }; assert_eq!(candidates.len(), 2, "p::x and q::x must both be exported"); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); - let Resolution::Ambiguous(candidates) = - resolve_name(&db, top, &ident("x"), NameContext::Value) - else { + let top = crate::unit::test_module_owner(&db, "top"); + let Resolution::Ambiguous(candidates) = resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("x"), + NameContext::Value, + ) else { panic!("star import of mutually importing packages must stay ambiguous"); }; assert_eq!(candidates.len(), 2); @@ -1096,23 +1277,21 @@ import middle::*; endmodule "#, ); - let base = db - .unit_index() - .package_ids(&ident("base")) - .unique() - .expect("base package should resolve uniquely"); + let base = crate::unit::test_package_owner(&db, "base"); let expected = db - .package_exports(base) + .package_exports(&crate::unit::test_resolution(&db), base) .lookup(NameContext::Value, &ident("value")) .unique() .expect("base::value"); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert_eq!( - resolve_name(&db, top, &ident("value"), NameContext::Value), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + top, + &ident("value"), + NameContext::Value + ), Resolution::Unique(expected) ); } @@ -1120,11 +1299,7 @@ endmodule #[test] fn def_id_survives_inserted_sibling_declaration() { let mut db = db_with_root_text("module m;\nint b;\nendmodule\n"); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let before = db .scope(module_id) .lookup(NameContext::Value, &ident("b")) @@ -1139,11 +1314,7 @@ endmodule Durability::LOW, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should still resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let after = db .scope(module_id) .lookup(NameContext::Value, &ident("b")) @@ -1201,15 +1372,36 @@ endmodule .find(|owner| owner.name.as_str() == "b") .expect("generate block b") .id; - let p = db.unit_index().package_ids(&ident("p")).unique().expect("p"); - let p_f = resolve_name(&db, p, &ident("f"), NameContext::Value).unique().expect("p::f"); + let p = crate::unit::test_package_owner(&db, "p"); + let p_f = resolve_name( + &db, + &crate::unit::test_resolution(&db), + p, + &ident("f"), + NameContext::Value, + ) + .unique() + .expect("p::f"); let reference = reference_at(&db, text, "x = f()", RefKind::Call); - let resolved = resolve_name_at(&db, b, &ident("f"), NameContext::Value, Some(&reference)); + let resolved = resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + b, + &ident("f"), + NameContext::Value, + Some(&reference), + ); assert_eq!(resolved, Resolution::Unique(p_f), "only the preceding wildcard may bind"); // Without a position both wildcards merge (the previous behavior). - let positionless = resolve_name(&db, b, &ident("f"), NameContext::Value); + let positionless = resolve_name( + &db, + &crate::unit::test_resolution(&db), + b, + &ident("f"), + NameContext::Value, + ); assert!(matches!(positionless, Resolution::Ambiguous(_))); } @@ -1240,8 +1432,15 @@ endmodule let reference = reference_at(&db, text, "x = f()", RefKind::Call); assert!( - resolve_name_at(&db, b, &ident("f"), NameContext::Value, Some(&reference)) - .is_unresolved(), + resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + b, + &ident("f"), + NameContext::Value, + Some(&reference) + ) + .is_unresolved(), "the import follows the reference and must not bind" ); } @@ -1270,12 +1469,27 @@ endmodule .find(|owner| owner.name.as_str() == "b") .expect("generate block b") .id; - let p = db.unit_index().package_ids(&ident("p")).unique().expect("p"); - let p_x = resolve_name(&db, p, &ident("x"), NameContext::Value).unique().expect("p::x"); + let p = crate::unit::test_package_owner(&db, "p"); + let p_x = resolve_name( + &db, + &crate::unit::test_resolution(&db), + p, + &ident("x"), + NameContext::Value, + ) + .unique() + .expect("p::x"); let reference = reference_at(&db, text, "x = 1", RefKind::Value); assert_eq!( - resolve_name_at(&db, b, &ident("x"), NameContext::Value, Some(&reference)), + resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + b, + &ident("x"), + NameContext::Value, + Some(&reference) + ), Resolution::Unique(p_x), "the later outer declaration must not shadow the wildcard import" ); @@ -1294,12 +1508,27 @@ endmodule let reference = reference_at(&db, text, "x = 1", RefKind::Value); assert!( - resolve_name_at(&db, blk, &ident("x"), NameContext::Value, Some(&reference)) - .is_unresolved(), + resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + blk, + &ident("x"), + NameContext::Value, + Some(&reference) + ) + .is_unresolved(), "a declaration after the reference is not locally visible at the point" ); assert!( - resolve_name(&db, blk, &ident("x"), NameContext::Value).unique().is_some(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + blk, + &ident("x"), + NameContext::Value + ) + .unique() + .is_some(), "position-less lookup keeps the declaration" ); } @@ -1311,17 +1540,40 @@ endmodule let text = "module m;\n assign y = f();\n function int f(); return 1; endfunction\nendmodule\n"; let db = db_with_root_text(text); - let m = db.unit_index().module_ids(&ident("m")).unique().expect("m"); - let f = resolve_name(&db, m, &ident("f"), NameContext::Value).unique().expect("m::f"); + let m = crate::unit::test_module_owner(&db, "m"); + let f = resolve_name( + &db, + &crate::unit::test_resolution(&db), + m, + &ident("f"), + NameContext::Value, + ) + .unique() + .expect("m::f"); let call = reference_at(&db, text, "y = f()", RefKind::Call); assert_eq!( - resolve_name_at(&db, m, &ident("f"), NameContext::Value, Some(&call)), + resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + m, + &ident("f"), + NameContext::Value, + Some(&call) + ), Resolution::Unique(f) ); let value = reference_at(&db, text, "y = f()", RefKind::Value); assert!( - resolve_name_at(&db, m, &ident("f"), NameContext::Value, Some(&value)).is_unresolved(), + resolve_name_at( + &db, + &crate::unit::test_resolution(&db), + m, + &ident("f"), + NameContext::Value, + Some(&value) + ) + .is_unresolved(), "ordinary references do not see the later declaration" ); } @@ -1371,6 +1623,7 @@ endmodule let resolution = resolve_path( &db, + &crate::unit::test_resolution(&db), db.owner_table(HirFileId::File(TOP)).file_owner().expect("file owner"), &path(&["child", "sig"]), NameContext::Value, @@ -1393,13 +1646,15 @@ endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); - let res = resolve_path(&db, top, &path(&["u_if", "host"]), NameContext::Value); + let res = resolve_path( + &db, + &crate::unit::test_resolution(&db), + top, + &path(&["u_if", "host"]), + NameContext::Value, + ); let def = res.unique().expect("modport should produce a unique definition"); assert_eq!(def.name(&db).as_deref(), Some("host")); @@ -1428,11 +1683,7 @@ endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert_eq!( resolved_kind(&db, top, &["cb", "a"], NameContext::Value), @@ -1454,11 +1705,7 @@ endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert_eq!( resolved_kind(&db, top, &["u", "clk"], NameContext::Value), @@ -1482,11 +1729,7 @@ endmodule "#, ); - let top = db - .unit_index() - .module_ids(&ident("top")) - .unique() - .expect("top module should resolve uniquely"); + let top = crate::unit::test_module_owner(&db, "top"); assert_eq!(resolved_kind(&db, top, &["u", "cp"], NameContext::Value), DefKind::Coverpoint); assert_eq!(resolved_kind(&db, top, &["u", "cx"], NameContext::Value), DefKind::Cross); diff --git a/crates/hir-def/src/scope.rs b/crates/hir-def/src/scope.rs index c7c9dba6d..4cb04551e 100644 --- a/crates/hir-def/src/scope.rs +++ b/crates/hir-def/src/scope.rs @@ -42,19 +42,37 @@ pub fn scope_for(db: &dyn HirDefDb, owner: OwnerId) -> Arc { Arc::new(build_owner_scope(db, owner)) } -/// Builds the explicit `$unit` scope from file-owner scopes. +/// Builds the explicit `$unit` scope from each compilation-unit file scope. +/// +/// Design-unit names come from the owner table. File-scope values, typedefs, +/// and imports still come from the file-owner body. Child module bodies are +/// not lowered here. #[salsa::tracked(lru = 128, returns(clone))] pub fn unit_scope(db: &dyn HirDefDb) -> Arc { let mut unit = ScopeData::default(); - for file_id in db.files().iter() { - let file_id = HirFileId::File(*file_id); + for file_id in compilation_unit_files(db) { + let hir_file = HirFileId::File(file_id); + if !db.file_facts(file_id).has_compilation_unit_locals() { + continue; + } let file_owner = - db.owner_table(file_id).file_owner().expect("owner table must contain file owner"); + db.owner_table(hir_file).file_owner().expect("owner table must contain file owner"); unit.extend_definitions_from(scope_for(db, file_owner).as_ref()); } Arc::new(unit) } +fn compilation_unit_files(db: &dyn HirDefDb) -> Vec { + let mut files: Vec<_> = db + .files() + .iter() + .copied() + .filter(|&file_id| db.file_kind(file_id).is_semantic_compilation_unit()) + .collect(); + files.sort_by_key(|file_id| file_id.index()); + files +} + pub(crate) fn set_scope_lru_capacity(db: &mut dyn HirDefDb, capacity: usize) { scope_for::set_lru_capacity(db, capacity); unit_scope::set_lru_capacity(db, capacity); @@ -165,15 +183,35 @@ impl ScopeData { pub(crate) fn build_file_scope(db: &dyn HirDefDb, file_id: HirFileId) -> ScopeData { let mut scope = ScopeData::default(); - let file_owner = db.owner_table(file_id).file_owner().expect("file owner must exist"); + let owner_table = db.owner_table(file_id); + let file_owner = owner_table.file_owner().expect("file owner must exist"); + + // Compilation-unit design units and file-scope subroutines are on the + // owner table. Projecting them through `from_owner` must not lower a + // child body: that is what made `$unit` pay for every module in the + // project. + for owner in owner_table.owners() { + if owner.parent != Some(file_owner) { + continue; + } + let name = (!owner.name.is_empty()).then(|| owner.name.clone()); + match owner.kind { + OwnerKind::Module => { + // Compilation-unit design units live on UnitCatalog, not in + // the file / $unit lexical scope. + } + OwnerKind::Subroutine => { + if let Some(def) = DefId::from_owner(db, owner.id) { + scope.insert_value_opt(&name, def); + } + } + _ => {} + } + } + let hir_file = db.body(file_owner); let body = db.body_with_source_map(file_owner); - for owner in hir_file.module_owners() { - let module = db.body(owner); - scope.insert_type_opt(&module.name, def_id(db, DefOriginLoc::Module(owner))); - } - for (_, import) in hir_file.package_imports.iter() { scope.insert_package_import(import); } @@ -182,11 +220,6 @@ pub(crate) fn build_file_scope(db: &dyn HirDefDb, file_id: HirFileId) -> ScopeDa insert_body_typedefs(&mut scope, db, file_owner, body.data_ref(), file_owner); insert_proc_bodies(&mut scope, db, &hir_file.procs); - for subroutine_owner in hir_file.subroutine_owners() { - let subroutine = db.subroutine(subroutine_owner); - scope.insert_value_opt(&subroutine.name, owner_def_id(db, subroutine_owner)); - } - for (config_decl_id, config_decl) in hir_file.config_decls.iter() { scope.insert_value_opt(&config_decl.name, def_id(db, InFile::new(file_id, config_decl_id))); } @@ -489,6 +522,9 @@ mod tests { #[salsa::db] impl PreprocDb for TestDb {} + #[salsa::db] + impl crate::db::DesignGraphDb for TestDb {} + #[salsa::db] impl HirDefDb for TestDb {} impl std::ops::Deref for TestDb { @@ -616,6 +652,13 @@ endmodule ); let unit_scope = db.unit_scope(); + let module_in_unit = DefId::from_owner(&db, crate::unit::test_module_owner(&db, "m")) + .expect("compilation-unit module projects"); + assert_eq!(module_in_unit.kind(&db), DefKind::Module); + assert!( + unit_scope.lookup(NameContext::Type, &ident("m")).is_unresolved(), + "design-unit names are not $unit locals" + ); assert!( unit_scope .lookup(NameContext::Value, &ident("file_sig")) @@ -632,11 +675,7 @@ endmodule assert!(shared_value_defs.iter().any(|def_id| def_id.kind(&db) == DefKind::Net)); assert!(!shared_value_defs.iter().any(|def_id| def_id.kind(&db) == DefKind::Typedef)); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); assert_eq!(module_id.file(&db), HirFileId::File(TOP)); let module_scope = db.scope(module_id); @@ -738,11 +777,7 @@ endmodule assert_eq!(candidates.len(), 2); assert!(candidates.iter().all(|def| def.origins(&db).len() == 1)); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let port = db .scope(module_id) .lookup(NameContext::Value, &ident("a")) @@ -756,6 +791,22 @@ endmodule } } + #[test] + fn unit_scope_module_def_id_matches_body_backed_projection() { + let db = db_with_root_text( + r#" +module m; + logic buried; +endmodule +"#, + ); + let owner = crate::unit::test_module_owner(&db, "m"); + let from_header = DefId::from_owner(&db, owner).expect("module owner has a definition"); + assert_eq!(from_header, DefId::from_source(&db, DefOriginLoc::Module(owner))); + assert_eq!(from_header.name(&db).as_deref(), Some("m")); + assert!(db.unit_scope().lookup(NameContext::Value, &ident("buried")).is_unresolved()); + } + #[test] fn explicit_non_ansi_port_source_preserves_name_range() { let db = db_with_root_text( @@ -765,11 +816,7 @@ module m(.out(foo)); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let Ports::NonAnsi { ports, .. } = &module.ports else { panic!("module should have non-ANSI ports"); @@ -791,11 +838,7 @@ module m(foo); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let source_map = module.source_map(); let Ports::NonAnsi { ports, .. } = &module.ports else { @@ -841,11 +884,7 @@ module m(a); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let before = db .scope(module_id) .lookup(NameContext::Value, &ident("a")) @@ -876,11 +915,7 @@ endmodule Durability::LOW, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should still resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let after = db .scope(module_id) .lookup(NameContext::Value, &ident("a")) @@ -900,11 +935,7 @@ module m(a); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let Resolution::Ambiguous(candidates) = db.scope(module_id).lookup(NameContext::Value, &ident("a")) else { @@ -927,11 +958,7 @@ module m(a, a); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let Resolution::Ambiguous(candidates) = db.scope(module_id).lookup(NameContext::Value, &ident("a")) else { @@ -952,11 +979,7 @@ module m(a); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let Resolution::Ambiguous(candidates) = db.scope(module_id).lookup(NameContext::Value, &ident("a")) else { @@ -978,11 +1001,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let owner = module_id; let module = db.body_with_source_map(owner); let (expr_id, expr) = module @@ -1014,11 +1033,7 @@ module m; endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let proc = module.procs.iter().next().expect("initial block should lower").1; let body = db.body_with_source_map(proc.owner); @@ -1057,11 +1072,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let proc = module.procs.iter().next().expect("initial block should lower").1; let body = db.body_with_source_map(proc.owner); @@ -1105,11 +1116,7 @@ module m; endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let proc = module.procs.iter().next().expect("always block should lower").1; let body = db.body_with_source_map(proc.owner); @@ -1144,11 +1151,7 @@ module m; endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); assert!( module @@ -1182,11 +1185,7 @@ module m(input logic x, y); endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); assert!( module.items.iter().any(|item| matches!(item, crate::body::BodyItem::PropertyId(_))) @@ -1222,11 +1221,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let declaration = module .declarations @@ -1249,11 +1244,7 @@ module m; endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let proc = module.procs.iter().next().expect("initial block should lower").1; let body = db.body_with_source_map(proc.owner); @@ -1293,11 +1284,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let owner = module_id; let module = db.body_with_source_map(owner); let stream = module @@ -1326,11 +1313,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let owner = module_id; let module = db.body_with_source_map(owner); let stream = module @@ -1367,11 +1350,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let clocking_owner = module .items @@ -1428,7 +1407,17 @@ endmodule "#, ); - let checker_defs = db.unit_scope().lookup(NameContext::Type, &ident("c")); + let graph = crate::unit::test_graph(&db); + let checker_owner = Resolution::from_candidates(crate::unit::locate_cu_owners( + &db, + &graph, + &[], + "c", + design_graph::UnitKind::Checker, + )) + .unique() + .expect("checker projects"); + let checker_defs = DefId::from_owner(&db, checker_owner).map(Resolution::Unique).unwrap(); assert!(checker_defs.iter().any(|def_id| def_id.kind(&db) == DefKind::Checker)); let checker_id = checker_defs .iter() @@ -1450,11 +1439,7 @@ endmodule .any(|def_id| def_id.kind(&db) == DefKind::Variable) ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body(module_id); let instantiation = module .instantiations @@ -1484,11 +1469,7 @@ endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body(module_id); let covergroup_owner = module .items @@ -1578,12 +1559,8 @@ endmodule "#, ); - let package_id = db - .unit_index() - .package_ids(&ident("pkg")) - .unique() - .expect("package should resolve uniquely"); - let package_exports = db.package_exports(package_id); + let package_id = crate::unit::test_package_owner(&db, "pkg"); + let package_exports = db.package_exports(&crate::unit::test_resolution(&db), package_id); assert!( package_exports .lookup(NameContext::Type, &ident("imported_t")) @@ -1603,11 +1580,7 @@ endmodule .any(|def_id| def_id.kind(&db) == DefKind::Subroutine) ); - let wildcard_importer = db - .unit_index() - .module_ids(&ident("wildcard_importer")) - .unique() - .expect("wildcard importer should resolve uniquely"); + let wildcard_importer = crate::unit::test_module_owner(&db, "wildcard_importer"); let wildcard_scope = db.scope(wildcard_importer); assert!( wildcard_scope @@ -1616,37 +1589,60 @@ endmodule .any(|import| import.package == ident("pkg") && import.name.is_none()) ); - let imported_t = - resolve_name(&db, wildcard_importer, &ident("imported_t"), NameContext::Type); + let imported_t = resolve_name( + &db, + &crate::unit::test_resolution(&db), + wildcard_importer, + &ident("imported_t"), + NameContext::Type, + ); assert!(imported_t.iter().any(|def_id| def_id.kind(&db) == DefKind::Typedef)); assert!( - resolve_name(&db, wildcard_importer, &ident("imported_t"), NameContext::Value,) - .is_unresolved(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + wildcard_importer, + &ident("imported_t"), + NameContext::Value, + ) + .is_unresolved(), "value lookup should not fall back to the type bucket" ); - let shadowed_v = - resolve_name(&db, wildcard_importer, &ident("shadowed_v"), NameContext::Value); + let shadowed_v = resolve_name( + &db, + &crate::unit::test_resolution(&db), + wildcard_importer, + &ident("shadowed_v"), + NameContext::Value, + ); assert!(shadowed_v.iter().any(|def_id| def_id.kind(&db) == DefKind::Net)); assert!(!shadowed_v.iter().any(|def_id| def_id.kind(&db) == DefKind::Variable)); - let named_importer = db - .unit_index() - .module_ids(&ident("named_importer")) - .unique() - .expect("named importer should resolve uniquely"); + let named_importer = crate::unit::test_module_owner(&db, "named_importer"); let named_scope = db.scope(named_importer); assert!(named_scope.imports().iter().any(|import| { import.package == ident("pkg") && import.name.as_ref().is_some_and(|name| name == "imported_v") })); - let imported_v = - resolve_name(&db, named_importer, &ident("imported_v"), NameContext::Value); + let imported_v = resolve_name( + &db, + &crate::unit::test_resolution(&db), + named_importer, + &ident("imported_v"), + NameContext::Value, + ); assert!(imported_v.iter().any(|def_id| def_id.kind(&db) == DefKind::Variable)); assert!( - resolve_name(&db, named_importer, &ident("imported_t"), NameContext::Type,) - .is_unresolved(), + resolve_name( + &db, + &crate::unit::test_resolution(&db), + named_importer, + &ident("imported_t"), + NameContext::Type, + ) + .is_unresolved(), "named import should not expose unrelated package symbols" ); } @@ -1671,14 +1667,16 @@ endmodule "#, ); - let package_id = db - .unit_index() - .package_ids(&ident("pkg")) - .unique() - .expect("package should resolve uniquely"); - let package_f = resolve_name(&db, package_id, &ident("f"), NameContext::Value) - .unique() - .expect("package scope should resolve package subroutine"); + let package_id = crate::unit::test_package_owner(&db, "pkg"); + let package_f = resolve_name( + &db, + &crate::unit::test_resolution(&db), + package_id, + &ident("f"), + NameContext::Value, + ) + .unique() + .expect("package scope should resolve package subroutine"); let DefOriginLoc::Subroutine(package_subroutine) = package_f.primary_origin(&db).loc(&db) else { @@ -1686,24 +1684,27 @@ endmodule }; assert_eq!(package_subroutine.parent(&db), Some(package_id)); - let named_importer = db - .unit_index() - .module_ids(&ident("named_importer")) - .unique() - .expect("named importer should resolve uniquely"); - let named_import_f = resolve_name(&db, named_importer, &ident("f"), NameContext::Value) - .unique() - .expect("named import should resolve package subroutine"); - - let wildcard_importer = db - .unit_index() - .module_ids(&ident("wildcard_importer")) - .unique() - .expect("wildcard importer should resolve uniquely"); - let wildcard_import_f = - resolve_name(&db, wildcard_importer, &ident("f"), NameContext::Value) - .unique() - .expect("wildcard import should resolve package subroutine"); + let named_importer = crate::unit::test_module_owner(&db, "named_importer"); + let named_import_f = resolve_name( + &db, + &crate::unit::test_resolution(&db), + named_importer, + &ident("f"), + NameContext::Value, + ) + .unique() + .expect("named import should resolve package subroutine"); + + let wildcard_importer = crate::unit::test_module_owner(&db, "wildcard_importer"); + let wildcard_import_f = resolve_name( + &db, + &crate::unit::test_resolution(&db), + wildcard_importer, + &ident("f"), + NameContext::Value, + ) + .unique() + .expect("wildcard import should resolve package subroutine"); assert_eq!(package_f, named_import_f); assert_eq!( @@ -1726,11 +1727,7 @@ module m; endmodule "#, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let before = db .scope(module_id) .lookup(NameContext::Value, &ident("stable")) @@ -1750,11 +1747,7 @@ endmodule Durability::LOW, ); - let module_id = db - .unit_index() - .module_ids(&ident("m")) - .unique() - .expect("module should still resolve uniquely"); + let module_id = crate::unit::test_module_owner(&db, "m"); let after = db .scope(module_id) .lookup(NameContext::Value, &ident("stable")) @@ -1776,11 +1769,7 @@ module second; endmodule "#, ); - let second = db - .unit_index() - .module_ids(&ident("second")) - .unique() - .expect("second module should resolve uniquely"); + let second = crate::unit::test_module_owner(&db, "second"); let before = db .scope(second) .lookup(NameContext::Value, &ident("second_value")) @@ -1804,11 +1793,7 @@ endmodule Durability::LOW, ); - let second = db - .unit_index() - .module_ids(&ident("second")) - .unique() - .expect("second module should remain unique"); + let second = crate::unit::test_module_owner(&db, "second"); let after = db .scope(second) .lookup(NameContext::Value, &ident("second_value")) @@ -1832,13 +1817,9 @@ endpackage "#, ); - let package_id = db - .unit_index() - .package_ids(&ident("pkg")) - .unique() - .expect("package should resolve uniquely"); + let package_id = crate::unit::test_package_owner(&db, "pkg"); - let exports = db.package_exports(package_id); + let exports = db.package_exports(&crate::unit::test_resolution(&db), package_id); assert!( exports .lookup(NameContext::Value, &ident("exported_f")) @@ -1846,8 +1827,9 @@ endpackage .any(|def_id| def_id.kind(&db) == DefKind::Subroutine) ); - let before_body_edit = db.package_export_signature(package_id); - let before_design_map = db.design_map(); + let before_body_edit = + db.package_export_signature(&crate::unit::test_resolution(&db), package_id); + let before_design_map = crate::unit::test_resolution(&db).design_map(&db); db.set_file_text_with_durability( TOP, Arc::from( @@ -1864,12 +1846,13 @@ endpackage ), Durability::LOW, ); - let after_body_edit = db.package_export_signature(package_id); + let after_body_edit = + db.package_export_signature(&crate::unit::test_resolution(&db), package_id); assert_eq!( before_body_edit, after_body_edit, "function body edits should not change the package export signature" ); - let after_design_map = db.design_map(); + let after_design_map = crate::unit::test_resolution(&db).design_map(&db); assert_eq!( before_design_map, after_design_map, "function body edits should not change the design map" @@ -1881,7 +1864,7 @@ endpackage let db = db_with_root_text( "module m #(parameter int A = 0, parameter type T = logic, parameter int B = 1) ();\nendmodule\n", ); - let module_id = db.unit_index().module_ids(&ident("m")).unique().expect("m"); + let module_id = crate::unit::test_module_owner(&db, "m"); let body = db.body(module_id); assert_eq!(crate::module::param_port_count(&body), 3); assert!(crate::module::param_port_id_by_idx(&body, 0).is_some(), "A"); @@ -1897,7 +1880,7 @@ endpackage #[test] fn default_nettype_selects_implicit_port_net_kind() { let db = db_with_root_text("`default_nettype tri\nmodule m(input a);\nendmodule\n"); - let module_id = db.unit_index().module_ids(&ident("m")).unique().expect("m"); + let module_id = crate::unit::test_module_owner(&db, "m"); let body = db.body(module_id); let Ports::Ansi(port_decls) = &body.ports else { panic!("module should have ANSI ports"); @@ -1921,7 +1904,7 @@ endpackage "`default_nettype tri\nmodule a(input x);\nendmodule\n`default_nettype wire\nmodule b(input y);\nendmodule\n", ); let kinds = ["a", "b"].map(|name| { - let module_id = db.unit_index().module_ids(&ident(name)).unique().expect(name); + let module_id = crate::unit::test_module_owner(&db, name); let body = db.body(module_id); let Ports::Ansi(port_decls) = &body.ports else { panic!("module should have ANSI ports"); @@ -1941,7 +1924,7 @@ endpackage #[test] fn interface_port_header_is_not_previous_header() { let db = db_with_root_text("module m(input logic a, interface.ifc);\nendmodule\n"); - let module_id = db.unit_index().module_ids(&ident("m")).unique().expect("m"); + let module_id = crate::unit::test_module_owner(&db, "m"); let body = db.body(module_id); let Ports::Ansi(port_decls) = &body.ports else { panic!("module should have ANSI ports"); @@ -1960,7 +1943,7 @@ endpackage let db = db_with_root_text( "package pkg;\nendpackage\nmodule m;\ninitial begin\nx = pkg::arr[0];\nend\nendmodule\n", ); - let module_id = db.unit_index().module_ids(&ident("m")).unique().expect("m"); + let module_id = crate::unit::test_module_owner(&db, "m"); let module = db.body_with_source_map(module_id); let (_, proc) = module.procs.iter().next().expect("initial block"); let body = db.body_with_source_map(proc.owner); @@ -1982,7 +1965,7 @@ endpackage let db = db_with_root_text( "module m #(parameter int A = 0, parameter int B = 1) ();\n parameter int P = 2;\nendmodule\n", ); - let module_id = db.unit_index().module_ids(&ident("m")).unique().expect("m"); + let module_id = crate::unit::test_module_owner(&db, "m"); let body = db.body(module_id); let a = crate::module::param_port_id_by_idx(&body, 0).expect("A"); let b = crate::module::param_port_id_by_idx(&body, 1).expect("B"); diff --git a/crates/hir-def/src/subroutine.rs b/crates/hir-def/src/subroutine.rs index e4a19381c..08ca9a83c 100644 --- a/crates/hir-def/src/subroutine.rs +++ b/crates/hir-def/src/subroutine.rs @@ -127,6 +127,9 @@ fn lower_name(name: ast::Name) -> Option { if let Some(scoped) = name.as_scoped_name() { return lower_name(scoped.right()); } + if let Some(keyword) = name.as_keyword_name() { + return keyword.keyword().and_then(|tok| lower_ident_opt(Some(tok))); + } None } diff --git a/crates/hir-def/src/symbol.rs b/crates/hir-def/src/symbol.rs index e982bfcc8..30dd6c4d1 100644 --- a/crates/hir-def/src/symbol.rs +++ b/crates/hir-def/src/symbol.rs @@ -382,95 +382,7 @@ pub enum NameContext { Listing, } -/// A lookup result that preserves the difference between no match, one -/// logical definition, and several competing definitions. -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub enum Resolution { - Unresolved, - Unique(T), - Ambiguous(SmallVec<[T; 2]>), -} -impl Resolution { - pub fn candidates(&self) -> &[T] { - match self { - Resolution::Unresolved => &[], - Resolution::Unique(value) => std::slice::from_ref(value), - Resolution::Ambiguous(candidates) => candidates, - } - } - - pub fn into_candidates(self) -> SmallVec<[T; 2]> { - match self { - Resolution::Unresolved => SmallVec::new(), - Resolution::Unique(value) => { - let mut candidates = SmallVec::new(); - candidates.push(value); - candidates - } - Resolution::Ambiguous(candidates) => candidates, - } - } - - pub fn iter(&self) -> std::slice::Iter<'_, T> { - self.candidates().iter() - } - - pub fn is_unresolved(&self) -> bool { - matches!(self, Resolution::Unresolved) - } - - pub fn or_else(self, fallback: impl FnOnce() -> Self) -> Self { - if self.is_unresolved() { fallback() } else { self } - } - - pub fn map(self, map: impl FnMut(T) -> U) -> Resolution { - Resolution::from_candidates(self.into_candidates().into_iter().map(map)) - } -} - -impl Resolution { - pub fn unique(&self) -> Option { - match self { - Resolution::Unique(value) => Some(value.clone()), - Resolution::Ambiguous(_) | Resolution::Unresolved => None, - } - } - - /// Resolves children without allowing child existence to disambiguate an - /// ambiguous parent. - pub fn and_then(&self, mut resolve: impl FnMut(T) -> Resolution) -> Resolution { - let children = Resolution::from_candidates( - self.iter().cloned().flat_map(|candidate| resolve(candidate).into_candidates()), - ); - match (self, children) { - (Resolution::Ambiguous(_), Resolution::Unique(_)) => Resolution::Unresolved, - (_, children) => children, - } - } -} - -impl From for Resolution { - fn from(value: T) -> Self { - Resolution::Unique(value) - } -} - -impl Resolution { - pub fn from_candidates(candidates: impl IntoIterator) -> Self { - let mut unique = SmallVec::<[T; 2]>::new(); - for candidate in candidates { - if !unique.contains(&candidate) { - unique.push(candidate); - } - } - - match unique.len() { - 0 => Resolution::Unresolved, - 1 => Resolution::Unique(unique.pop().expect("candidate length was checked")), - _ => Resolution::Ambiguous(unique), - } - } -} +pub use design_graph::Resolution; impl ScopeData { pub fn imports(&self) -> &[Import] { diff --git a/crates/hir-def/src/unit.rs b/crates/hir-def/src/unit.rs new file mode 100644 index 000000000..d9bea0b29 --- /dev/null +++ b/crates/hir-def/src/unit.rs @@ -0,0 +1,399 @@ +//! Locate compilation-unit owners. +//! +//! L0 [`UnitCatalog`] is a name → file locator, not identity. Identity is the +//! paid-parse [`OwnerId`] (`SourceAstId` / `HirFileId::Macro`). Production +//! resolution must not project `UnitId` → `OwnerId` by name. + +use std::cell::Cell; + +use design_graph::UnitKind; +use preproc_expand::{file::HirFileId, macro_file::macro_files_for_file}; +use rustc_hash::FxHashSet; +use vfs::FileId; + +use crate::{ + db::HirDefDb, + module::ModuleKind, + owner::{OwnerData, OwnerId, OwnerKind}, +}; + +thread_local! { + /// Former `to_owner` calls on the shipped path. T6 form B keeps this at 0. + pub static TO_OWNER_RUNS: Cell = const { Cell::new(0) }; + /// Owners examined inside a `(name, kind)` lookup. An indexed lookup of a + /// unique `(name, kind)` must stay at 1, not the table length. + pub static OWNER_LOOKUP_STEPS: Cell = const { Cell::new(0) }; +} + +/// Compilation-unit owners of `name` whose kind is `kind`. +/// +/// `locator` answers which source files declare the name. When it has no +/// match, `paid_files` are searched for macro-generated owners. +pub fn locate_cu_owners( + db: &dyn HirDefDb, + locator: &design_graph::UnitCatalog, + paid_files: &[FileId], + name: &str, + kind: UnitKind, +) -> Vec { + locate_cu_owners_matching(db, locator, paid_files, name, |unit_kind| unit_kind == kind) +} + +/// Compilation-unit owners of `name` whose kind satisfies `matches`. +pub fn locate_cu_owners_matching( + db: &dyn HirDefDb, + locator: &design_graph::UnitCatalog, + paid_files: &[FileId], + name: &str, + matches: impl Fn(UnitKind) -> bool, +) -> Vec { + let located = located_files(locator, name, &matches); + if !located.is_empty() { + return located + .into_iter() + .flat_map(|file| cu_owners_named_in_file(db, file, name, &matches)) + .collect(); + } + let mut files: Vec = paid_files.to_vec(); + files.sort_by_key(|file| file.index()); + files.dedup(); + files.into_iter().flat_map(|file| cu_owners_named_in_macros(db, file, name, &matches)).collect() +} + +/// Every compilation-unit package owner L0 locates. Source files only. +pub fn locate_package_owners( + db: &dyn HirDefDb, + locator: &design_graph::UnitCatalog, +) -> Vec { + let mut files = Vec::new(); + let mut seen = FxHashSet::default(); + for unit in locator.packages() { + if seen.insert(unit.file) { + files.push(unit.file); + } + } + files + .into_iter() + .flat_map(|file| cu_owners_of_kind_in_hir(db, HirFileId::File(file), UnitKind::Package)) + .collect() +} + +fn located_files( + locator: &design_graph::UnitCatalog, + name: &str, + matches: &impl Fn(UnitKind) -> bool, +) -> Vec { + let mut files = Vec::new(); + let mut seen = FxHashSet::default(); + for unit in locator.type_units_named(name).into_vec() { + if matches(unit.kind) && seen.insert(unit.file) { + files.push(unit.file); + } + } + files +} + +fn cu_owners_named_in_file( + db: &dyn HirDefDb, + file: FileId, + name: &str, + matches: &impl Fn(UnitKind) -> bool, +) -> Vec { + cu_owners_named_in_hir(db, HirFileId::File(file), name, matches) +} + +fn cu_owners_named_in_macros( + db: &dyn HirDefDb, + file: FileId, + name: &str, + matches: &impl Fn(UnitKind) -> bool, +) -> Vec { + macro_files_for_file(db, file) + .into_iter() + .flat_map(|macro_file| { + cu_owners_named_in_hir(db, HirFileId::Macro(macro_file), name, matches) + }) + .collect() +} + +fn cu_owners_named_in_hir( + db: &dyn HirDefDb, + file: HirFileId, + name: &str, + matches: &impl Fn(UnitKind) -> bool, +) -> Vec { + let table = db.owner_table(file); + let file_owner = table.file_owner(); + let mut owners = Vec::new(); + for owner_kind in [OwnerKind::Module, OwnerKind::Checker, OwnerKind::Covergroup] { + for id in table.owners_named(name, owner_kind) { + OWNER_LOOKUP_STEPS.with(|steps| steps.set(steps.get() + 1)); + let Some(owner) = table.owner(*id) else { + continue; + }; + if owner.parent != file_owner { + continue; + } + let Some(kind) = unit_kind_of(owner) else { + continue; + }; + if matches(kind) { + owners.push(owner.id); + } + } + } + owners +} + +fn cu_owners_of_kind_in_hir(db: &dyn HirDefDb, file: HirFileId, kind: UnitKind) -> Vec { + let table = db.owner_table(file); + let file_owner = table.file_owner(); + table + .owners() + .iter() + .filter_map(|owner| { + (owner.parent == file_owner && owner_matches_unit_kind(owner, kind)).then_some(owner.id) + }) + .collect() +} + +fn unit_kind_of(owner: &OwnerData) -> Option { + match owner.kind { + OwnerKind::Module => match owner.module_kind { + Some(ModuleKind::Module) => Some(UnitKind::Module), + Some(ModuleKind::Interface) => Some(UnitKind::Interface), + Some(ModuleKind::Package) => Some(UnitKind::Package), + Some(ModuleKind::Program) => Some(UnitKind::Program), + None => None, + }, + OwnerKind::Checker => Some(UnitKind::Checker), + OwnerKind::Covergroup => Some(UnitKind::Covergroup), + _ => None, + } +} + +fn owner_matches_unit_kind(owner: &OwnerData, kind: UnitKind) -> bool { + match kind { + UnitKind::Module => { + owner.kind == OwnerKind::Module && owner.module_kind == Some(ModuleKind::Module) + } + UnitKind::Interface => { + owner.kind == OwnerKind::Module && owner.module_kind == Some(ModuleKind::Interface) + } + UnitKind::Package => { + owner.kind == OwnerKind::Module && owner.module_kind == Some(ModuleKind::Package) + } + UnitKind::Program => { + owner.kind == OwnerKind::Module && owner.module_kind == Some(ModuleKind::Program) + } + UnitKind::Checker => owner.kind == OwnerKind::Checker, + UnitKind::Covergroup => owner.kind == OwnerKind::Covergroup, + } +} + +/// Fold a source-only graph for tests. Not a product and not a production path. +pub fn test_graph(db: &dyn HirDefDb) -> design_graph::UnitCatalog { + design_graph::UnitCatalog::fold(db, &design_graph::GeneratedUnits::default()) +} + +/// Test-only resolution context over [`test_graph`]. +pub fn test_resolution(db: &dyn HirDefDb) -> triomphe::Arc { + crate::pathres::ResolutionContext::from_graph(db, triomphe::Arc::new(test_graph(db))) +} + +pub fn test_module_owner(db: &dyn HirDefDb, name: &str) -> OwnerId { + let graph = test_graph(db); + crate::symbol::Resolution::from_candidates(locate_cu_owners( + db, + &graph, + &[], + name, + UnitKind::Module, + )) + .unique() + .unwrap_or_else(|| panic!("{name} should be a unique module owner")) +} + +pub fn test_package_owner(db: &dyn HirDefDb, name: &str) -> OwnerId { + let graph = test_graph(db); + crate::symbol::Resolution::from_candidates(locate_cu_owners( + db, + &graph, + &[], + name, + UnitKind::Package, + )) + .unique() + .unwrap_or_else(|| panic!("{name} should be a unique package owner")) +} + +#[cfg(test)] +mod tests { + use std::fmt; + + use base_db::{ + diagnostics_config::DiagnosticsConfig, + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + salsa::{self, Durability}, + source_db::{FileLoader, SourceDb, SourceFileKind, SourceRootDb}, + source_root::{SourceRoot, SourceRootId}, + }; + use design_graph::{GeneratedUnits, UnitCatalog, UnitId, UnitKind, UnitMeta, UnitOrigin}; + use preproc_expand::db::PreprocDb; + use rustc_hash::FxHashSet; + use smol_str::SmolStr; + use triomphe::Arc; + use utils::paths::{AbsPathBuf, Utf8PathBuf}; + use vfs::{AnchoredPath, FileId, FileSet, VfsPath}; + + use super::{test_graph, test_module_owner, test_package_owner}; + use crate::db::HirDefDb; + + const TOP: FileId = FileId::from_raw(0); + const ROOT: SourceRootId = SourceRootId(0); + const PROFILE: CompilationProfileId = CompilationProfileId(0); + + #[salsa::db] + #[derive(Default)] + struct TestDb { + storage: salsa::Storage, + } + + #[salsa::db] + impl salsa::Database for TestDb {} + #[salsa::db] + impl SourceDb for TestDb {} + #[salsa::db] + impl SourceRootDb for TestDb {} + #[salsa::db] + impl PreprocDb for TestDb {} + #[salsa::db] + impl crate::db::DesignGraphDb for TestDb {} + #[salsa::db] + impl HirDefDb 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); + SourceRootDb::source_root(self, source_root_id).resolve_path(path) + } + } + + fn db_with_text(text: &str) -> TestDb { + let top_path = { + let prefix = if cfg!(windows) { "C:/repo" } else { "/repo" }; + AbsPathBuf::assert(Utf8PathBuf::from(format!("{prefix}/rtl/top.sv"))) + }; + let mut file_set = FileSet::default(); + file_set.insert(TOP, VfsPath::from(top_path)); + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + let mut files = FxHashSet::default(); + files.insert(TOP); + let project_config = ProjectConfig::new( + vec![Some(PROFILE)], + vec![CompilationProfile { + source_roots: vec![ROOT], + top_modules: Vec::new(), + preprocess: PreprocessConfig::default(), + }], + ); + let mut db = TestDb::default(); + db.set_files_with_durability(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_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); + db.set_file_text_with_durability(TOP, Arc::from(text), Durability::LOW); + db + } + + #[test] + fn fold_joins_source_units() { + let db = db_with_text("module top;\nendmodule\npackage p;\nendpackage\n"); + let graph = test_graph(&db); + assert!(graph.modules_named("top").unique().is_some()); + assert!(graph.packages_named("p").unique().is_some()); + assert!(graph.modules_named("missing").is_unresolved()); + } + + #[test] + fn fold_appends_generated_units() { + let db = db_with_text("module top;\nendmodule\n"); + let generated_id = + UnitId { file: TOP, name: SmolStr::new("foo"), kind: UnitKind::Module, ordinal: 0 }; + let mut generated = GeneratedUnits::default(); + let mut meta = rustc_hash::FxHashMap::default(); + meta.insert( + generated_id.clone(), + UnitMeta { + kind: UnitKind::Module, + origin: UnitOrigin::Generated, + header_fingerprint: 0, + }, + ); + generated.replace_file(TOP, 0, Box::new([generated_id.clone()]), meta); + let graph = UnitCatalog::fold(&db, &generated); + assert_eq!(graph.origin(&generated_id), Some(UnitOrigin::Generated)); + assert!(graph.modules_named("foo").unique().is_some()); + assert!(graph.modules_named("top").unique().is_some()); + } + + #[test] + fn unique_name_lookup_does_not_scan_the_owner_table() { + let mut text = String::new(); + for index in 0..40 { + text.push_str(&format!("module m{index};\nendmodule\n")); + } + text.push_str("package p;\nendpackage\n"); + let db = db_with_text(&text); + super::OWNER_LOOKUP_STEPS.with(|steps| steps.set(0)); + let owner = test_package_owner(&db, "p"); + let steps = super::OWNER_LOOKUP_STEPS.with(std::cell::Cell::get); + assert_eq!(owner.name(&db).as_deref(), Some("p")); + assert_eq!( + steps, 1, + "a unique (name, kind) must not walk the other owners (steps={steps})" + ); + } + + #[test] + fn to_owner_projects_the_ordinalth_cu_match() { + let db = db_with_text("module top;\nendmodule\n"); + let owner = test_module_owner(&db, "top"); + assert_eq!(owner.name(&db).as_deref(), Some("top")); + } + + #[test] + fn to_owner_skips_nested_modules() { + let db = db_with_text( + "module outer;\n module inner;\n endmodule\nendmodule\nmodule inner;\nendmodule\n", + ); + let graph = test_graph(&db); + let inner = graph.modules_named("inner").unique().expect("one CU inner"); + assert_eq!(inner.ordinal, 0); + let owner = crate::symbol::Resolution::from_candidates(super::locate_cu_owners( + &db, + &graph, + &[], + "inner", + UnitKind::Module, + )) + .unique() + .expect("CU inner projects"); + assert_eq!(owner.name(&db).as_deref(), Some("inner")); + assert_eq!( + owner.parent(&db).map(|parent| parent.kind(&db)), + Some(crate::owner::OwnerKind::File) + ); + } +} diff --git a/crates/hir-def/src/unit_index.rs b/crates/hir-def/src/unit_index.rs deleted file mode 100644 index e684f134a..000000000 --- a/crates/hir-def/src/unit_index.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! File-level index of design-unit declarations. -//! -//! This index owns module-like and instantiable design-unit headers. It -//! deliberately does not build a lexical scope, lower a body, or allocate a -//! `DefId`; callers choose when to project an indexed owner into a semantic -//! definition. - -use base_db::salsa; -use preproc_expand::file::HirFileId; -use rustc_hash::FxHashMap; -use smallvec::SmallVec; -use smol_str::SmolStr; -use triomphe::Arc; - -use crate::{ - db::HirDefDb, - item_tree::ItemTree, - module::ModuleKind, - owner::{OwnerId, OwnerKind, OwnerTable}, - symbol::Resolution, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum UnitKind { - Module(ModuleKind), - Checker, - Covergroup, -} - -impl UnitKind { - fn is_module(self) -> bool { - matches!(self, Self::Module(kind) if kind.is_instantiable()) - } - - fn is_package(self) -> bool { - matches!(self, Self::Module(ModuleKind::Package)) - } - - fn is_instantiable(self) -> bool { - self.is_module() || matches!(self, Self::Checker | Self::Covergroup) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct UnitData { - owner: OwnerId, - kind: UnitKind, - parent: Option, - top_level: bool, -} - -/// File-level design-unit declarations, independent of lexical `ScopeGraph`. -/// -/// The index is built from [`crate::item_tree::ItemTree::module_headers`] and -/// structural owner metadata for checker/covergroup declarations. It preserves -/// duplicate declarations as `Resolution::Ambiguous`. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct UnitIndex { - units: Vec, - by_name: FxHashMap>, - module_names: Vec, -} -impl UnitIndex { - pub fn module_ids(&self, name: &SmolStr) -> Resolution { - self.resolve(name, |unit| unit.kind.is_module()) - } - - /// Design-unit modules declared at compilation-unit scope. Only these may - /// act as explicit hierarchy roots for multi-segment paths. - pub fn top_level_module_ids(&self, name: &SmolStr) -> Resolution { - self.resolve(name, |unit| unit.kind.is_module() && unit.top_level) - } - - pub fn package_ids(&self, name: &SmolStr) -> Resolution { - self.resolve(name, |unit| unit.kind.is_package()) - } - - /// Resolve an instance target using the containing module's local - /// checker/covergroup declarations before compilation-unit declarations. - pub fn instantiable_ids_in(&self, scope: OwnerId, name: &SmolStr) -> Resolution { - let local = self.resolve(name, |unit| { - matches!(unit.kind, UnitKind::Checker | UnitKind::Covergroup) - && unit.parent == Some(scope) - }); - if !local.is_unresolved() { - return local; - } - self.resolve(name, |unit| { - unit.kind.is_instantiable() && (unit.kind.is_module() || unit.top_level) - }) - } - - pub fn module_names(&self) -> impl Iterator { - self.module_names.iter() - } - - fn resolve(&self, name: &SmolStr, matches: impl Fn(&UnitData) -> bool) -> Resolution { - let candidates = - self.by_name.get(name).into_iter().flat_map(|indices| indices.iter()).filter_map( - |index| { - let unit = self.units.get(*index)?; - matches(unit).then_some(unit.owner) - }, - ); - Resolution::from_candidates(candidates) - } -} - -#[salsa::tracked(lru = 128, returns(clone))] -pub fn unit_index(db: &dyn HirDefDb) -> Arc { - let mut index = UnitIndex::default(); - - for file_id in db.files().iter() { - let file_id = HirFileId::File(*file_id); - let item_tree = db.item_tree(file_id); - let owner_table = db.owner_table(file_id); - add_file_units(&mut index, &item_tree, &owner_table); - } - - index.module_names = index - .by_name - .iter() - .filter_map(|(name, indices)| { - indices - .iter() - .any(|unit_index| { - index.units.get(*unit_index).is_some_and(|unit| unit.kind.is_module()) - }) - .then_some(name.clone()) - }) - .collect(); - index.module_names.sort(); - index.module_names.dedup(); - - Arc::new(index) -} - -fn add_file_units(index: &mut UnitIndex, item_tree: &ItemTree, owner_table: &OwnerTable) { - let file_owner = owner_table.file_owner().expect("owner table must contain its file owner"); - for header in item_tree.module_headers() { - let owner = header.owner(); - let data = owner_table.owner(owner).expect("module header owner must be indexed"); - insert_unit( - index, - header.name().clone(), - owner, - UnitKind::Module(header.kind()), - data.parent, - data.parent == Some(file_owner), - ); - } - for owner in owner_table.owners() { - let kind = match owner.kind { - OwnerKind::Checker => UnitKind::Checker, - OwnerKind::Covergroup => UnitKind::Covergroup, - _ => continue, - }; - insert_unit( - index, - owner.name.clone(), - owner.id, - kind, - owner.parent, - owner.parent == Some(file_owner), - ); - } -} - -fn insert_unit( - index: &mut UnitIndex, - name: SmolStr, - owner: OwnerId, - kind: UnitKind, - parent: Option, - top_level: bool, -) { - if name.is_empty() { - return; - } - let unit_index = index.units.len(); - index.units.push(UnitData { owner, kind, parent, top_level }); - index.by_name.entry(name).or_default().push(unit_index); -} - -pub(crate) fn set_lru_capacity(db: &mut dyn HirDefDb, capacity: usize) { - unit_index::set_lru_capacity(db, capacity); -} -#[cfg(test)] -mod tests { - use super::UnitIndex; - - #[test] - fn empty_index_has_no_targets() { - let index = UnitIndex::default(); - assert!(index.module_ids(&"missing".into()).is_unresolved()); - assert!(index.package_ids(&"missing".into()).is_unresolved()); - assert_eq!(index.module_names().count(), 0); - } -} diff --git a/crates/hir-semantics/Cargo.toml b/crates/hir-semantics/Cargo.toml index 29348b593..c02a2faeb 100644 --- a/crates/hir-semantics/Cargo.toml +++ b/crates/hir-semantics/Cargo.toml @@ -10,9 +10,9 @@ itertools.workspace = true preproc-expand.workspace = true rustc-hash.workspace = true syntax.workspace = true +triomphe.workspace = true utils.workspace = true vfs.workspace = true [dev-dependencies] base-db.workspace = true -triomphe.workspace = true diff --git a/crates/hir-semantics/src/preproc_integration_tests.rs b/crates/hir-semantics/src/preproc_integration_tests.rs index 0ed54544f..1c152eb4f 100644 --- a/crates/hir-semantics/src/preproc_integration_tests.rs +++ b/crates/hir-semantics/src/preproc_integration_tests.rs @@ -46,6 +46,9 @@ impl SourceRootDb for TestDb {} #[salsa::db] impl PreprocDb for TestDb {} +#[salsa::db] +impl hir_def::db::DesignGraphDb for TestDb {} + #[salsa::db] impl HirDefDb for TestDb {} impl std::ops::Deref for TestDb { @@ -178,7 +181,8 @@ fn macro_expanded_module_keeps_macro_hir_file_id() { #[test] fn semantics_accepts_hir_def_only_database() { let db = db_with_root_text("module top; endmodule\n"); - let parsed = Semantics::new(&db).parse_file(TOP); + let parsed = + Semantics::new_with_context(&db, hir_def::unit::test_resolution(&db)).parse_file(TOP); assert!(parsed.compilation_unit().is_some()); } diff --git a/crates/hir-semantics/src/semantics.rs b/crates/hir-semantics/src/semantics.rs index 9d9e82e23..d73f50aee 100644 --- a/crates/hir-semantics/src/semantics.rs +++ b/crates/hir-semantics/src/semantics.rs @@ -54,8 +54,11 @@ impl ParsedFile { } impl Semantics<'_, DB> { - pub fn new(db: &DB) -> Semantics<'_, DB> { - let impl_ = SemanticsImpl::new(db); + pub fn new_with_context( + db: &DB, + context: triomphe::Arc, + ) -> Semantics<'_, DB> { + let impl_ = SemanticsImpl::new_with_context(db, context); Semantics { db, impl_ } } } @@ -90,11 +93,20 @@ impl Semantics<'_, DB> { pub struct SemanticsImpl<'db> { pub db: &'db dyn HirDefDb, + context: triomphe::Arc, } impl<'db> SemanticsImpl<'db> { - pub fn new(db: &'db dyn HirDefDb) -> Self { - SemanticsImpl { db } + pub fn new_with_context( + db: &'db dyn HirDefDb, + context: triomphe::Arc, + ) -> Self { + SemanticsImpl { db, context } + } + + /// The injected name-join context. IDE request paths pass the store graph. + pub fn resolution_context(&self) -> triomphe::Arc { + self.context.clone() } pub fn parse_file(&self, file_id: FileId) -> ParsedFile { @@ -102,6 +114,10 @@ impl<'db> SemanticsImpl<'db> { ParsedFile { file_id, tree: self.db.parse(file_id) } } + pub fn parse_file_with_tree(&self, file_id: FileId, tree: SyntaxTree) -> ParsedFile { + ParsedFile { file_id: file_id.into(), tree } + } + pub fn container_for_node(&self, file_id: HirFileId, node: SyntaxNode) -> Option { Some(source_to_def::find_container(self.db, InFile::new(file_id, node))) } @@ -129,10 +145,10 @@ impl SemanticsImpl<'_> { } pub fn expr_to_def(&self, in_cont: OwnerRef) -> Resolution { - hir_to_def::expr_to_def(self.db, in_cont) + hir_to_def::expr_to_def(self.db, &self.context, in_cont) } pub fn name_to_def(&self, in_cont: OwnerRef) -> Resolution { - hir_to_def::name_to_def(self.db, in_cont, NameContext::Value) + hir_to_def::name_to_def(self.db, &self.context, in_cont, NameContext::Value) } } diff --git a/crates/hir-semantics/src/semantics/hir_to_def.rs b/crates/hir-semantics/src/semantics/hir_to_def.rs index a3da2edbb..ad9b08857 100644 --- a/crates/hir-semantics/src/semantics/hir_to_def.rs +++ b/crates/hir-semantics/src/semantics/hir_to_def.rs @@ -6,13 +6,15 @@ use hir_def::{ expr::{Expr, ExprId}, owner::OwnerId, pathres::{ - NameRef, RefKind, resolve_child_name, resolve_name, resolve_name_at, resolve_path_at, + NameRef, RefKind, ResolutionContext, resolve_child_name, resolve_name, resolve_name_at, + resolve_path_at, }, symbol::{NameContext, Resolution}, }; pub(super) fn expr_to_def( db: &dyn HirDefDb, + context: &ResolutionContext, OwnerRef { cont_id, value: expr_id }: OwnerRef, ) -> Resolution { // Expression references resolve at their source position; call callees @@ -23,19 +25,19 @@ pub(super) fn expr_to_def( let Some(field) = field.as_ref() else { return Resolution::Unresolved; }; - resolve_expr_path(db, cont_id, expr_id, NameContext::Value, reference.as_ref()).or_else( - || { - let receiver_res = expr_to_def(db, OwnerRef::new(cont_id, *receiver)); - resolve_child_name(db, &receiver_res, field, NameContext::Value) - }, - ) + resolve_expr_path(db, context, cont_id, expr_id, NameContext::Value, reference.as_ref()) + .or_else(|| { + let receiver_res = expr_to_def(db, context, OwnerRef::new(cont_id, *receiver)); + resolve_child_name(db, context, &receiver_res, field, NameContext::Value) + }) } Expr::ElementSelect { receiver, .. } => { - resolve_expr_path(db, cont_id, expr_id, NameContext::Value, reference.as_ref()) - .or_else(|| expr_to_def(db, OwnerRef::new(cont_id, *receiver))) + resolve_expr_path(db, context, cont_id, expr_id, NameContext::Value, reference.as_ref()) + .or_else(|| expr_to_def(db, context, OwnerRef::new(cont_id, *receiver))) } Expr::Ident(ident) => name_to_def_at( db, + context, OwnerRef::new(cont_id, ident.clone()), NameContext::Value, reference.as_ref(), @@ -67,23 +69,26 @@ fn expr_reference(db: &dyn HirDefDb, cont_id: OwnerId, expr_id: ExprId) -> Optio pub(super) fn name_to_def( db: &dyn HirDefDb, + context: &ResolutionContext, OwnerRef { cont_id, value: ident }: OwnerRef, name_ctx: NameContext, ) -> Resolution { - resolve_name(db, cont_id, &ident, name_ctx) + resolve_name(db, context, cont_id, &ident, name_ctx) } pub(super) fn name_to_def_at( db: &dyn HirDefDb, + context: &ResolutionContext, OwnerRef { cont_id, value: ident }: OwnerRef, name_ctx: NameContext, reference: Option<&hir_def::pathres::NameRef>, ) -> Resolution { - resolve_name_at(db, cont_id, &ident, name_ctx, reference) + resolve_name_at(db, context, cont_id, &ident, name_ctx, reference) } fn resolve_expr_path( db: &dyn HirDefDb, + context: &ResolutionContext, cont_id: OwnerId, expr_id: ExprId, ctx: NameContext, @@ -92,7 +97,7 @@ fn resolve_expr_path( let Some(path) = expr_path(db, cont_id, expr_id) else { return Resolution::Unresolved; }; - resolve_path_at(db, cont_id, &path, ctx, reference) + resolve_path_at(db, context, cont_id, &path, ctx, reference) } fn expr_path(db: &dyn HirDefDb, cont_id: OwnerId, expr_id: ExprId) -> Option> { diff --git a/crates/hir-semantics/src/semantics/pathres.rs b/crates/hir-semantics/src/semantics/pathres.rs index 6d47cdc2d..62688e8e0 100644 --- a/crates/hir-semantics/src/semantics/pathres.rs +++ b/crates/hir-semantics/src/semantics/pathres.rs @@ -30,6 +30,7 @@ impl SemanticsImpl<'_> { let reference = token_reference(self.db, file_id, parent); hir_to_def::name_to_def_at( self.db, + &self.context, OwnerRef::new(container, ident), name_ctx, reference.as_ref(), @@ -54,6 +55,7 @@ impl SemanticsImpl<'_> { let reference = token_reference(self.db, file_id, parent); hir_to_def::name_to_def_at( self.db, + &self.context, OwnerRef::new(container, ident), name_ctx, reference.as_ref(), @@ -75,7 +77,7 @@ impl SemanticsImpl<'_> { let Some(ident) = lower_ident_opt(Some(tok)) else { return Resolution::Unresolved; }; - resolve_in_resolved_scopes_at(self.db, resolved, &ident, name_ctx, reference) + resolve_in_resolved_scopes_at(self.db, &self.context, resolved, &ident, name_ctx, reference) } /// Token-level variant of [`nameres_ident_in_scopes`] that derives the @@ -108,7 +110,7 @@ impl SemanticsImpl<'_> { ident: &Ident, ctx: NameContext, ) -> Resolution { - hir_def::pathres::resolve_name(self.db, owner, ident, ctx) + hir_def::pathres::resolve_name(self.db, &self.context, owner, ident, ctx) } /// Position-aware name resolution honoring the reference point (IEEE @@ -120,7 +122,7 @@ impl SemanticsImpl<'_> { ctx: NameContext, reference: Option<&hir_def::pathres::NameRef>, ) -> Resolution { - hir_def::pathres::resolve_name_at(self.db, owner, ident, ctx, reference) + hir_def::pathres::resolve_name_at(self.db, &self.context, owner, ident, ctx, reference) } } diff --git a/crates/hir-ty/src/compatibility.rs b/crates/hir-ty/src/compatibility.rs deleted file mode 100644 index 77d286a4f..000000000 --- a/crates/hir-ty/src/compatibility.rs +++ /dev/null @@ -1,173 +0,0 @@ -use hir_def::{ - expr::{ - BinaryOp, Expr, ExprId, UnaryOp, - data_ty::{BuiltinDataTy, Dimension, IntKind}, - }, - literal::Literal, - owner::OwnerId, -}; - -use crate::{ - db::TyDb, - ty::{BuiltinTy, Ty, TyClass}, - type_system::Compatibility, -}; - -pub(crate) fn type_class(_db: &dyn TyDb, ty: &Ty) -> Option { - match ty { - Ty::Alias { target, .. } => type_class(_db, target), - Ty::Builtin(BuiltinTy::Data { id, .. }) => match id.get() { - BuiltinDataTy::Int { .. } | BuiltinDataTy::Vector { .. } => Some(TyClass::Integral), - BuiltinDataTy::Real(_) => Some(TyClass::Real), - BuiltinDataTy::String => Some(TyClass::String), - BuiltinDataTy::Event | BuiltinDataTy::Chandle | BuiltinDataTy::Void => None, - }, - Ty::Enum(_) => Some(TyClass::Integral), - Ty::Unknown - | Ty::Error - | Ty::Void - | Ty::Struct(_) - | Ty::Union(_) - | Ty::Queue { .. } - | Ty::Assoc { .. } - | Ty::Dynamic(_) - | Ty::Event - | Ty::Chandle - | Ty::Module(_) - | Ty::Checker(_) - | Ty::Covergroup(_) - | Ty::VirtualInterface { .. } - | Ty::GenerateBlock(_) - | Ty::Block(_) => None, - } -} - -pub(crate) fn compatibility(db: &dyn TyDb, expected: &Ty, candidate: &Ty) -> Compatibility { - let (Some(expected_class), Some(candidate_class)) = - (type_class(db, expected), type_class(db, candidate)) - else { - return Compatibility::Unknown; - }; - if expected_class != candidate_class { - return Compatibility::Incompatible; - } - if expected_class != TyClass::Integral { - return Compatibility::Compatible; - } - - match (packed_bit_width(db, expected), packed_bit_width(db, candidate)) { - (Some(expected), Some(candidate)) if expected == candidate => Compatibility::Compatible, - (Some(_), Some(_)) => Compatibility::Incompatible, - _ => Compatibility::Unknown, - } -} - -pub(crate) fn is_typed_value(db: &dyn TyDb, ty: &Ty) -> bool { - type_class(db, ty).is_some() -} - -pub(crate) fn packed_bit_width(db: &dyn TyDb, ty: &Ty) -> Option { - match ty { - Ty::Alias { target, .. } => packed_bit_width(db, target), - Ty::Builtin(BuiltinTy::Data { id, container }) => match id.get() { - BuiltinDataTy::String - | BuiltinDataTy::Real(_) - | BuiltinDataTy::Event - | BuiltinDataTy::Chandle - | BuiltinDataTy::Void => None, - BuiltinDataTy::Int { kind, .. } => Some(int_kind_width(*kind) as u64), - BuiltinDataTy::Vector { dimensions, .. } => { - if dimensions.is_empty() { - return Some(1); - } - - let mut product: u64 = 1; - for dim in dimensions { - let dim = (*dim)?; - let width = match dim { - Dimension::Range(left, right) => { - let left = eval_const_i128(db, container, left)?; - let right = eval_const_i128(db, container, right)?; - i128::abs(left - right).checked_add(1)? - } - Dimension::Size(size) => eval_const_i128(db, container, size)?, - Dimension::Queue(_) - | Dimension::Assoc(_) - | Dimension::Wildcard - | Dimension::Dynamic => { - return None; - } - }; - let width: u64 = width.try_into().ok()?; - product = product.checked_mul(width)?; - } - Some(product) - } - }, - Ty::Unknown - | Ty::Error - | Ty::Void - | Ty::Struct(_) - | Ty::Enum(_) - | Ty::Union(_) - | Ty::Queue { .. } - | Ty::Assoc { .. } - | Ty::Dynamic(_) - | Ty::Event - | Ty::Chandle - | Ty::Module(_) - | Ty::Checker(_) - | Ty::Covergroup(_) - | Ty::VirtualInterface { .. } - | Ty::GenerateBlock(_) - | Ty::Block(_) => None, - } -} - -fn int_kind_width(kind: IntKind) -> usize { - match kind { - IntKind::Byte => 8, - IntKind::ShortInt => 16, - IntKind::Int => 32, - IntKind::LongInt => 64, - IntKind::Integer => 32, - IntKind::Time => 64, - } -} - -fn eval_const_i128(db: &dyn TyDb, container: &OwnerId, expr_id: ExprId) -> Option { - let data = container.data(db); - match data.expr(expr_id) { - Expr::Literal(Literal::Int(int)) => int.get_single_word().map(|value| value as i128), - Expr::Unary { op, expr } => { - let value = eval_const_i128(db, container, *expr)?; - match op { - UnaryOp::Pos => Some(value), - UnaryOp::Neg => value.checked_neg(), - _ => None, - } - } - Expr::Binary { op, lhs, rhs } => { - let left = eval_const_i128(db, container, *lhs)?; - let right = eval_const_i128(db, container, *rhs)?; - match op { - BinaryOp::Add => left.checked_add(right), - BinaryOp::Sub => left.checked_sub(right), - BinaryOp::Mul => left.checked_mul(right), - BinaryOp::Div => (right != 0).then(|| left.checked_div(right)).flatten(), - BinaryOp::Mod => (right != 0).then(|| left.checked_rem(right)).flatten(), - BinaryOp::ShiftLeft => { - u32::try_from(right).ok().and_then(|shift| left.checked_shl(shift)) - } - BinaryOp::ShiftRight => { - u32::try_from(right).ok().and_then(|shift| left.checked_shr(shift)) - } - _ => None, - } - } - Expr::Cast { expr, .. } | Expr::SignedCast { expr, .. } => { - eval_const_i128(db, container, *expr) - } - _ => None, - } -} diff --git a/crates/hir-ty/src/db.rs b/crates/hir-ty/src/db.rs deleted file mode 100644 index b527d0846..000000000 --- a/crates/hir-ty/src/db.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::ops::Deref; - -use hir_def::{container::OwnerRef, db::HirDefDb, def_id::DefId, expr::ExprId, symbol::Resolution}; - -use crate::Type; -#[salsa::db] -pub trait TyDb: HirDefDb {} - -// See `HirDefDb` for why composed Salsa database objects use `Deref`. -impl Deref for dyn TyDb { - type Target = dyn HirDefDb; - - fn deref(&self) -> &Self::Target { - self - } -} - -impl dyn TyDb + '_ { - pub fn infer_expr(&self, expr: OwnerRef) -> Type { - let key = - crate::infer::ExprQueryKey::new(self, expr.cont_id, u32::from(expr.value.into_raw())); - crate::infer::type_of_expr_query(self, key) - } - - pub fn infer_path_resolution(&self, res: Resolution) -> Type { - res.unique() - .map(|def_id| crate::infer::type_of_def_origin_query(self, def_id.primary_origin(self))) - .unwrap_or_else(Type::unknown) - } -} diff --git a/crates/hir-ty/src/infer.rs b/crates/hir-ty/src/infer.rs deleted file mode 100644 index 72dafcba8..000000000 --- a/crates/hir-ty/src/infer.rs +++ /dev/null @@ -1,513 +0,0 @@ -use hir_def::{ - Ident, - aggregate::{StructId, StructKind}, - container::OwnerRef, - def_id::DefId, - expr::{ - Expr, ExprId, - data_ty::{BuiltinDataTy, BuiltinDataTyId, DataTy, Dimension, IntKind, TypeRef}, - declarator::{DeclId, DeclaratorParent}, - }, - module::port::PortDeclId, - owner::OwnerId, - pathres::{NameRef, RefKind, instance_target_def_id, resolve_name_at, resolve_path}, - stmt::{ForInit, StmtKind}, - subroutine::SubroutinePortId, - symbol::{DefKind, NameContext, Resolution}, - typedef::TypedefId, -}; -use rustc_hash::FxHashSet; -use utils::get::GetRef; - -use crate::{ - Type, TypeDiagnostic, - db::TyDb, - members::select_member, - ty::{BuiltinTy, Ty, TyResult}, -}; - -#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] -pub(crate) struct ExprQueryKey { - #[returns(copy)] - pub owner: hir_def::owner::OwnerId, - #[returns(copy)] - pub local: u32, -} -pub(crate) fn normalize_data_ty(db: &dyn TyDb, container: OwnerId, data_ty: DataTy) -> TyResult { - normalize_data_ty_with_owner(db, container, data_ty, None) -} - -fn normalize_data_ty_with_owner( - db: &dyn TyDb, - container: OwnerId, - data_ty: DataTy, - owner: Option, -) -> TyResult { - normalize_data_ty_inner(db, container, data_ty, owner, &mut FxHashSet::default()) -} - -#[salsa::tracked(returns(clone))] -pub(crate) fn type_of_def_origin_query(db: &dyn TyDb, origin: hir_def::symbol::DefOrigin) -> Type { - let def_id = DefId::from_origin(db, origin); - type_of_def_id(db, def_id).into() -} - -#[salsa::tracked(returns(clone))] -pub(crate) fn type_of_expr_query(db: &dyn TyDb, key: ExprQueryKey) -> Type { - let body = db.body_with_source_map(key.owner(db)); - let (expr_id, _) = body - .exprs - .iter() - .nth(key.local(db) as usize) - .expect("expression query key must refer to an expression in its owner body"); - let expr = OwnerRef::new(key.owner(db), expr_id); - type_of_expr_impl(db, expr).into() -} -fn type_of_typedef_impl(db: &dyn TyDb, typedef: OwnerRef) -> TyResult { - type_of_typedef_inner(db, typedef, &mut FxHashSet::default()) -} - -fn type_of_decl_impl(db: &dyn TyDb, decl: OwnerRef) -> TyResult { - let Some(data_ty) = data_ty_of_decl(db, decl) else { - return TyResult::new(Ty::Unknown); - }; - let owner = DefId::from_source(db, decl); - let mut result = normalize_data_ty_with_owner(db, decl.cont_id, data_ty, Some(owner)); - let data = decl.cont_id.data(db); - result.ty = apply_unpacked_dimensions( - db, - decl.cont_id, - result.ty, - &data.declarator(decl.value).dimensions, - ); - result -} - -pub(crate) fn type_of_path_resolution_impl(db: &dyn TyDb, res: Resolution) -> TyResult { - res.unique() - .map(|def_id| type_of_def_id(db, def_id)) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)) -} -pub(crate) fn type_of_def_id(db: &dyn TyDb, def_id: DefId) -> TyResult { - if def_id.is_non_ansi_port(db) { - return type_of_non_ansi_port(db, def_id); - } - let origin = def_id.primary_origin(db); - match def_id.kind(db) { - DefKind::Module | DefKind::Package | DefKind::Program => origin - .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::Checker => TyResult::new(Ty::Checker(def_id)), - DefKind::Covergroup => TyResult::new(Ty::Covergroup(def_id)), - DefKind::Port - | DefKind::CheckerPort - | DefKind::Variable - | DefKind::Net - | DefKind::Param - | DefKind::Genvar - | DefKind::Specparam => origin - .as_decl(db) - .map(|decl| type_of_decl_impl(db, decl)) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Typedef => origin - .as_typedef(db) - .map(|typedef| type_of_typedef_impl(db, typedef)) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::SubroutinePort => origin - .as_subroutine_port(db) - .map(|port| type_of_subroutine_port_impl(db, port)) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Instance => origin - .as_instance(db) - .and_then(|instance| instance_target_def_id(db, instance.cont_id, instance.value)) - .map(|target| match target.kind(db) { - DefKind::Interface => { - TyResult::new(Ty::VirtualInterface { def: target, modport: None }) - } - DefKind::Module | DefKind::Program => target - .primary_origin(db) - .as_module(db) - .map(|module_id| TyResult::new(Ty::Module(module_id))) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Checker => TyResult::new(Ty::Checker(target)), - DefKind::Covergroup => TyResult::new(Ty::Covergroup(target)), - DefKind::Package - | DefKind::Udp - | DefKind::Config - | DefKind::Library - | DefKind::Block - | DefKind::GenerateBlock - | DefKind::Subroutine - | DefKind::SubroutinePort - | DefKind::NonAnsiPort - | DefKind::Typedef - | DefKind::Net - | DefKind::Variable - | DefKind::Param - | DefKind::Port - | DefKind::Genvar - | DefKind::Specparam - | DefKind::Instance - | DefKind::Modport - | DefKind::ClockingBlock - | DefKind::ClockingSignal - | DefKind::CheckerPort - | DefKind::Coverpoint - | DefKind::Property - | DefKind::Sequence - | DefKind::Cross - | DefKind::Stmt => TyResult::new(Ty::Unknown), - DefKind::Primitive - | DefKind::NonAnsiPortLabel - | DefKind::PortDecl - | DefKind::ParamDecl - | DefKind::NetDecl - | DefKind::DataDecl - | DefKind::Struct - | DefKind::Generate - | DefKind::Fn - | DefKind::Specify - | DefKind::Region - | DefKind::Unknown => { - unreachable!("editor-only definition kind reached instance type inference") - } - }) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Modport => origin - .as_modport(db) - .map(|modport| { - TyResult::new(Ty::VirtualInterface { - def: DefId::from_owner(db, modport.cont_id) - .expect("modport container must have a module definition"), - modport: Some(def_id), - }) - }) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::GenerateBlock => origin - .as_generate_block(db) - .map(|generate_block_id| TyResult::new(Ty::GenerateBlock(generate_block_id))) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Block => origin - .as_block(db) - .map(|block_id| TyResult::new(Ty::Block(block_id))) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - DefKind::Udp - | DefKind::Config - | DefKind::Library - | DefKind::Subroutine - | DefKind::NonAnsiPort - | DefKind::ClockingBlock - | DefKind::ClockingSignal - | DefKind::Property - | DefKind::Sequence - | DefKind::Coverpoint - | DefKind::Cross - | DefKind::Stmt => TyResult::new(Ty::Unknown), - DefKind::Primitive - | DefKind::NonAnsiPortLabel - | DefKind::PortDecl - | DefKind::ParamDecl - | DefKind::NetDecl - | DefKind::DataDecl - | DefKind::Struct - | DefKind::Generate - | DefKind::Fn - | DefKind::Specify - | DefKind::Region - | DefKind::Unknown => { - unreachable!("editor-only definition kind reached type inference") - } - } -} -fn type_of_non_ansi_port(db: &dyn TyDb, def_id: DefId) -> TyResult { - let mut port_ty = None; - for origin in def_id.origins(db) { - let Some(decl) = origin.as_decl(db) else { - continue; - }; - let ty = type_of_decl_impl(db, decl); - match origin.kind(db) { - DefKind::Variable | DefKind::Net if !matches!(ty.ty, Ty::Unknown) => return ty, - DefKind::Port => { - port_ty.get_or_insert(ty); - } - DefKind::Variable - | DefKind::Net - | DefKind::Module - | DefKind::Interface - | DefKind::Package - | DefKind::Program - | DefKind::Udp - | DefKind::Config - | DefKind::Library - | DefKind::Block - | DefKind::GenerateBlock - | DefKind::Subroutine - | DefKind::SubroutinePort - | DefKind::NonAnsiPort - | DefKind::Typedef - | DefKind::Param - | DefKind::Genvar - | DefKind::Specparam - | DefKind::Instance - | DefKind::Modport - | DefKind::ClockingBlock - | DefKind::ClockingSignal - | DefKind::Checker - | DefKind::CheckerPort - | DefKind::Property - | DefKind::Sequence - | DefKind::Covergroup - | DefKind::Coverpoint - | DefKind::Cross - | DefKind::Stmt => {} - DefKind::Primitive - | DefKind::NonAnsiPortLabel - | DefKind::PortDecl - | DefKind::ParamDecl - | DefKind::NetDecl - | DefKind::DataDecl - | DefKind::Struct - | DefKind::Generate - | DefKind::Fn - | DefKind::Specify - | DefKind::Region - | DefKind::Unknown => { - unreachable!("editor-only definition kind reached origin type inference") - } - } - } - port_ty.unwrap_or_else(|| TyResult::new(Ty::Unknown)) -} - -fn type_of_expr_impl(db: &dyn TyDb, expr: OwnerRef) -> TyResult { - let data = expr.cont_id.data(db); - match data.expr(expr.value) { - Expr::Ident(ident) => { - // Expression references resolve at their source position so a - // later declaration never shadows an import (26.3). - let reference = expr_reference(db, expr); - type_of_path_resolution_impl( - db, - resolve_name_at(db, expr.cont_id, ident, NameContext::Value, reference.as_ref()), - ) - } - Expr::Field { receiver, field } => { - let Some(field) = field else { - return TyResult::new(Ty::Unknown); - }; - let base = type_of_expr_impl(db, expr.with_value(*receiver)); - if matches!(base.ty, Ty::Unknown | Ty::Error) { - return base; - } - let mut selected = select_member(db, &base.ty, field); - selected.diagnostics.extend(base.diagnostics); - selected - } - Expr::ElementSelect { receiver, .. } => type_of_expr_impl(db, expr.with_value(*receiver)), - Expr::Cast { ty, .. } => normalize_data_ty(db, expr.cont_id, ty.clone()), - _ => TyResult::new(Ty::Unknown), - } -} - -/// Reference position of an expression, derived from its canonical source. -fn expr_reference(db: &dyn TyDb, expr: OwnerRef) -> Option { - let file_id = expr.cont_id.file(db); - let source = - db.body_with_source_map(expr.cont_id).source_map().expr_srcs.hir_to_src(expr.value)?; - Some(NameRef { - position: hir_def::container::InFile::new(file_id, source), - kind: RefKind::Value, - }) -} - -fn normalize_data_ty_inner( - db: &dyn TyDb, - container: OwnerId, - data_ty: DataTy, - owner: Option, - seen: &mut FxHashSet>, -) -> TyResult { - match data_ty { - DataTy::Builtin(builtin) => match builtin.get() { - BuiltinDataTy::Void => TyResult::new(Ty::Void), - BuiltinDataTy::Event => TyResult::new(Ty::Event), - BuiltinDataTy::Chandle => TyResult::new(Ty::Chandle), - _ => TyResult::new(Ty::Builtin(BuiltinTy::Data { id: builtin, container })), - }, - DataTy::Struct(struct_id) => match struct_kind(db, struct_id) { - Some(StructKind::Union) => owner - .map(Ty::Union) - .map(TyResult::new) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)), - Some(StructKind::Struct) | None => TyResult::new(Ty::Struct(struct_id)), - }, - DataTy::Named(named) => type_of_named_data_ty(db, container, named, seen), - DataTy::Enum(_) => { - owner.map(Ty::Enum).map(TyResult::new).unwrap_or_else(|| TyResult::new(Ty::Unknown)) - } - DataTy::Unsupported(kind) => { - TyResult { ty: Ty::Error, diagnostics: vec![TypeDiagnostic::UnsupportedDataType(kind)] } - } - } -} - -fn type_of_named_data_ty( - db: &dyn TyDb, - container: OwnerId, - named: TypeRef, - seen: &mut FxHashSet>, -) -> TyResult { - if let Some(recovery) = named.recovery() { - return TyResult { - ty: Ty::Error, - diagnostics: vec![TypeDiagnostic::InvalidTypePath(recovery)], - }; - } - let resolution = resolve_path(db, container, named.segments(), NameContext::Type); - let Some(def_id) = resolution.unique() else { - return TyResult::new(Ty::Unknown); - }; - if let Some(typedef) = def_id.primary_origin(db).as_typedef(db) { - return type_of_typedef_inner(db, typedef, seen); - } - type_of_def_id(db, def_id) -} - -fn type_of_typedef_inner( - db: &dyn TyDb, - typedef: OwnerRef, - seen: &mut FxHashSet>, -) -> TyResult { - if !seen.insert(typedef) { - return TyResult { - ty: Ty::Error, - diagnostics: vec![TypeDiagnostic::TypedefCycle(typedef)], - }; - } - - let data = typedef.cont_id.data(db); - let Some(data_ty) = data.typedef(typedef.value).ty.clone() else { - seen.remove(&typedef); - return TyResult::new(Ty::Unknown); - }; - - let owner = DefId::from_source(db, typedef); - let mut target = normalize_data_ty_inner(db, typedef.cont_id, data_ty, Some(owner), seen); - seen.remove(&typedef); - let ty = if matches!(target.ty, Ty::Error) { - Ty::Error - } else { - Ty::Alias { typedef, target: Box::new(target.ty) } - }; - TyResult { ty, diagnostics: std::mem::take(&mut target.diagnostics) } -} - -fn struct_kind(db: &dyn TyDb, struct_id: OwnerRef) -> Option { - Some(struct_id.cont_id.data(db).struct_def(struct_id.value).kind) -} - -pub(crate) fn apply_unpacked_dimensions( - db: &dyn TyDb, - container: OwnerId, - mut ty: Ty, - dimensions: &[Option], -) -> Ty { - for dim in dimensions.iter().flatten() { - ty = match dim { - Dimension::Queue(size) => Ty::Queue { elem: Box::new(ty), size: *size }, - Dimension::Assoc(key) => Ty::Assoc { - key: Box::new(type_of_dimension_key(db, &container, *key)), - elem: Box::new(ty), - }, - Dimension::Wildcard => Ty::Assoc { key: Box::new(Ty::Unknown), elem: Box::new(ty) }, - Dimension::Dynamic => Ty::Dynamic(Box::new(ty)), - Dimension::Size(key) if builtin_dimension_key_ty(db, &container, *key).is_some() => { - Ty::Assoc { - key: Box::new(type_of_dimension_key(db, &container, *key)), - elem: Box::new(ty), - } - } - Dimension::Range(_, _) | Dimension::Size(_) => ty, - }; - } - ty -} - -fn type_of_dimension_key(db: &dyn TyDb, container: &OwnerId, expr_id: ExprId) -> Ty { - if let Some(ty) = builtin_dimension_key_ty(db, container, expr_id) { - return ty; - } - type_of_expr_impl(db, OwnerRef::new(*container, expr_id)).ty -} - -fn builtin_dimension_key_ty(db: &dyn TyDb, container: &OwnerId, expr_id: ExprId) -> Option { - let data = container.data(db); - if let Expr::Ident(ident) = data.expr(expr_id) { - return builtin_type_name_ty(container, ident); - } - None -} - -fn builtin_type_name_ty(container: &OwnerId, ident: &Ident) -> Option { - let ty = match ident.as_str() { - "string" => BuiltinDataTy::String, - "byte" => BuiltinDataTy::Int { kind: IntKind::Byte, signing: true }, - "shortint" => BuiltinDataTy::Int { kind: IntKind::ShortInt, signing: true }, - "int" => BuiltinDataTy::Int { kind: IntKind::Int, signing: true }, - "longint" => BuiltinDataTy::Int { kind: IntKind::LongInt, signing: true }, - "integer" => BuiltinDataTy::Int { kind: IntKind::Integer, signing: true }, - "time" => BuiltinDataTy::Int { kind: IntKind::Time, signing: false }, - "bit" => BuiltinDataTy::Vector { - kind: hir_def::expr::data_ty::VecKind::Bit, - signing: false, - dimensions: Default::default(), - }, - "logic" => BuiltinDataTy::default(), - "reg" => BuiltinDataTy::Vector { - kind: hir_def::expr::data_ty::VecKind::Reg, - signing: false, - dimensions: Default::default(), - }, - _ => return None, - }; - Some(Ty::Builtin(BuiltinTy::Data { id: BuiltinDataTyId::new(ty), container: *container })) -} - -pub(crate) fn data_ty_of_decl(db: &dyn TyDb, decl: OwnerRef) -> Option { - let data = decl.cont_id.data(db); - match data.declarator(decl.value).parent { - DeclaratorParent::DeclarationId(declaration_id) => { - Some(data.declaration(declaration_id).ty()) - } - DeclaratorParent::PortDeclId(port_decl_id) => port_decl_ty(db, decl.cont_id, port_decl_id), - DeclaratorParent::StmtId(stmt_id) => { - let StmtKind::For { inits: ForInit::Init(inits), .. } = &data.stmt(stmt_id).kind else { - return None; - }; - inits.iter().find_map(|(ty, candidate)| { - (*candidate == decl.value).then_some(ty.clone()).flatten() - }) - } - } -} - -fn port_decl_ty(db: &dyn TyDb, cont_id: OwnerId, port_decl_id: PortDeclId) -> Option { - let module = db.body(cont_id); - Some(module.ports.get(port_decl_id).header.ty()) -} - -fn type_of_subroutine_port_impl(db: &dyn TyDb, port: OwnerRef) -> TyResult { - let owner = port.cont_id; - let subroutine = db.subroutine(owner); - let Some(port_data) = subroutine.ports.get(port.value.0 as usize) else { - return TyResult::new(Ty::Unknown); - }; - port_data - .ty - .clone() - .map(|ty| normalize_data_ty_with_owner(db, owner, ty, Some(DefId::from_source(db, port)))) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)) -} diff --git a/crates/hir-ty/src/lib.rs b/crates/hir-ty/src/lib.rs deleted file mode 100644 index 92d97414f..000000000 --- a/crates/hir-ty/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Semantic types, inference, and type display. -//! -//! This crate interprets `hir-def` definitions and expressions as semantic -//! types. Definition-kind matching across this seam is exhaustive so adding a -//! new definition kind forces the type layer to classify it. This crate must -//! not depend on semantic adapters or IDE features. - -mod compatibility; -pub mod db; -pub mod display; -mod infer; -mod members; -mod ty; -mod type_system; - -pub use type_system::{Compatibility, Member, Type, TypeDiagnostic, TypeSystem}; diff --git a/crates/hir-ty/src/members.rs b/crates/hir-ty/src/members.rs deleted file mode 100644 index 9b704935b..000000000 --- a/crates/hir-ty/src/members.rs +++ /dev/null @@ -1,136 +0,0 @@ -use hir_def::{ - Ident, - aggregate::{StructId, StructKind}, - container::OwnerRef, - def_id::DefId, - expr::data_ty::DataTy, - owner::OwnerId, - symbol::Resolution, -}; - -use crate::{ - db::TyDb, - infer::{apply_unpacked_dimensions, normalize_data_ty, type_of_path_resolution_impl}, - ty::{Ty, TyMember, TyResult}, -}; - -pub(crate) fn members_of_ty(db: &dyn TyDb, ty: &Ty) -> Vec { - match ty { - Ty::Alias { target, .. } => members_of_ty(db, target), - 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::Checker(def_id) => checker_members(db, *def_id), - Ty::Covergroup(def_id) => covergroup_members(db, *def_id), - Ty::VirtualInterface { def, .. } => def - .primary_origin(db) - .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 - | Ty::Error - | Ty::Void - | Ty::Builtin(_) - | Ty::Enum(_) - | Ty::Queue { .. } - | Ty::Assoc { .. } - | Ty::Dynamic(_) - | Ty::Event - | Ty::Chandle => Vec::new(), - } -} - -pub(crate) fn select_member(db: &dyn TyDb, base: &Ty, name: &Ident) -> TyResult { - members_of_ty(db, base) - .into_iter() - .find(|member| &member.name == name) - .map(|member| TyResult::new(member.ty)) - .unwrap_or_else(|| TyResult::new(Ty::Unknown)) -} - -fn struct_members(db: &dyn TyDb, struct_id: OwnerRef) -> Vec { - let data = struct_id.cont_id.data(db); - data.struct_def(struct_id.value) - .members - .iter() - .filter_map(|member| { - let name = member.name.clone()?; - let ty = member - .ty - .as_ref() - .map(|ty| { - let normalized = normalize_data_ty(db, ty.cont_id, ty.value.clone()).ty; - apply_unpacked_dimensions(db, ty.cont_id, normalized, &member.dimensions) - }) - .unwrap_or(Ty::Unknown); - Some(TyMember { name, ty }) - }) - .collect() -} - -fn union_members(db: &dyn TyDb, def_id: DefId) -> Vec { - aggregate_struct_id_from_def(db, def_id) - .filter(|struct_id| struct_kind(db, *struct_id) == StructKind::Union) - .map(|struct_id| struct_members(db, struct_id)) - .unwrap_or_default() -} - -fn aggregate_struct_id_from_def(db: &dyn TyDb, def_id: DefId) -> Option> { - match def_id.data_type(db)? { - DataTy::Struct(struct_id) => Some(struct_id), - _ => None, - } -} - -fn struct_kind(db: &dyn TyDb, struct_id: OwnerRef) -> StructKind { - struct_id.cont_id.data(db).struct_def(struct_id.value).kind -} -fn module_members(db: &dyn TyDb, module_id: OwnerId) -> Vec { - let is_package = module_id.module_kind(db) == Some(hir_def::module::ModuleKind::Package); - if is_package { - let exports = db.package_exports(module_id); - scope_members(db, exports.iter_listing()) - } else { - let scope = db.scope(module_id); - scope_members(db, scope.iter_listing()) - } -} - -fn checker_members(db: &dyn TyDb, def_id: DefId) -> Vec { - let scope = db.scope(def_id.container_id(db)); - scope_members(db, scope.iter_listing()) -} - -fn covergroup_members(db: &dyn TyDb, def_id: DefId) -> Vec { - let scope = db.scope(def_id.container_id(db)); - scope_members(db, scope.iter_listing()) -} - -fn generate_block_members(db: &dyn TyDb, generate_block_owner: OwnerId) -> Vec { - let scope = db.scope(generate_block_owner); - scope_members(db, scope.iter_listing()) -} - -fn block_members(db: &dyn TyDb, owner: hir_def::owner::OwnerId) -> Vec { - let scope = db.scope(owner); - scope_members(db, scope.iter_listing()) -} - -fn scope_members<'a, I, D>(db: &dyn TyDb, entries: I) -> Vec -where - I: Iterator, - D: IntoIterator, -{ - let mut members: Vec<_> = entries - .map(|(name, defs)| { - let resolution = Resolution::from_candidates(defs); - let ty = type_of_path_resolution_impl(db, resolution).ty; - TyMember { name: name.clone(), ty } - }) - .collect(); - members.sort_by(|left, right| left.name.cmp(&right.name)); - members.dedup_by(|left, right| left.name == right.name); - members -} diff --git a/crates/hir-ty/src/ty.rs b/crates/hir-ty/src/ty.rs deleted file mode 100644 index b91d2bd86..000000000 --- a/crates/hir-ty/src/ty.rs +++ /dev/null @@ -1,64 +0,0 @@ -use hir_def::{ - Ident, - aggregate::StructId, - container::OwnerRef, - def_id::DefId, - expr::{ExprId, data_ty::BuiltinDataTyId}, - owner::OwnerId, - typedef::TypedefId, -}; - -use crate::TypeDiagnostic; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) enum BuiltinTy { - Data { id: BuiltinDataTyId, container: OwnerId }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum Ty { - Unknown, - Error, - Void, - Builtin(BuiltinTy), - Struct(OwnerRef), - Enum(DefId), - Union(DefId), - Queue { elem: Box, size: Option }, - Assoc { key: Box, elem: Box }, - Dynamic(Box), - Event, - Chandle, - Alias { typedef: OwnerRef, target: Box }, - Module(OwnerId), - Checker(DefId), - Covergroup(DefId), - VirtualInterface { def: DefId, modport: Option }, - GenerateBlock(OwnerId), - Block(OwnerId), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct TyResult { - pub(crate) ty: Ty, - pub(crate) diagnostics: Vec, -} - -impl TyResult { - pub(crate) fn new(ty: Ty) -> Self { - TyResult { ty, diagnostics: Vec::new() } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct TyMember { - pub(crate) name: Ident, - pub(crate) ty: Ty, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TyClass { - Integral, - Real, - String, -} diff --git a/crates/hir-ty/src/type_system.rs b/crates/hir-ty/src/type_system.rs deleted file mode 100644 index e143c314e..000000000 --- a/crates/hir-ty/src/type_system.rs +++ /dev/null @@ -1,152 +0,0 @@ -use hir_def::{ - Ident, - container::OwnerRef, - def_id::DefId, - expr::{ExprId, data_ty::TypePathRecovery}, - owner::OwnerId, - subroutine::SubroutineKind, - symbol::Resolution, - typedef::TypedefId, -}; -use syntax::SyntaxKind; -use triomphe::Arc; - -use crate::{ - compatibility::{compatibility, is_typed_value}, - db::TyDb, - display::{HirDisplay, HirDisplayError}, - infer::normalize_data_ty, - members::members_of_ty, - ty::{Ty, TyResult}, -}; - -/// A diagnostic produced while determining a semantic type. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TypeDiagnostic { - TypedefCycle(OwnerRef), - InvalidTypePath(TypePathRecovery), - UnsupportedDataType(SyntaxKind), -} - -/// Semantic type information returned by the type system. -/// -/// The representation and salsa query result stay private so callers do not -/// depend on inference internals. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Type(Arc); - -impl Type { - pub fn unknown() -> Self { - Self(Arc::new(TyResult::new(Ty::Unknown))) - } - - pub fn diagnostics(&self) -> &[TypeDiagnostic] { - &self.0.diagnostics - } - - pub(crate) fn ty(&self) -> &Ty { - &self.0.ty - } -} - -impl From for Type { - fn from(result: TyResult) -> Self { - Self(Arc::new(result)) - } -} - -/// A named member and its semantic type. -#[derive(Debug, Clone)] -pub struct Member { - name: Ident, - ty: Type, -} - -impl Member { - pub fn name(&self) -> &Ident { - &self.name - } - - pub fn ty(&self) -> &Type { - &self.ty - } - - pub fn into_name(self) -> Ident { - self.name - } -} - -/// Result of comparing two known semantic value types. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Compatibility { - Compatible, - Incompatible, - Unknown, -} - -/// Stable interface to semantic typing. -/// -/// Salsa queries, HIR arena access, normalization, and type representation are -/// implementation details behind this interface. -#[derive(Clone, Copy)] -pub struct TypeSystem<'db> { - db: &'db dyn TyDb, -} - -impl<'db> TypeSystem<'db> { - pub fn new(db: &'db dyn TyDb) -> Self { - Self { db } - } - - pub fn type_of_expr(&self, expr: OwnerRef) -> Type { - self.db.infer_expr(expr) - } - - pub fn type_of_resolution(&self, resolution: Resolution) -> Type { - self.db.infer_path_resolution(resolution) - } - - pub fn type_of_def(&self, def: DefId) -> Type { - self.type_of_resolution(Resolution::Unique(def)) - } - - pub fn type_of_subroutine_return(&self, subroutine: OwnerId) -> Type { - match &self.db.subroutine(subroutine).kind { - SubroutineKind::Function { return_ty: Some(return_ty) } => { - normalize_data_ty(self.db, subroutine, return_ty.clone()).into() - } - SubroutineKind::Function { return_ty: None } | SubroutineKind::Task => Type::unknown(), - } - } - - pub fn members(&self, ty: &Type) -> Vec { - members_of_ty(self.db, ty.ty()) - .into_iter() - .map(|member| Member { name: member.name, ty: TyResult::new(member.ty).into() }) - .collect() - } - - pub fn compatibility(&self, expected: &Type, candidate: &Type) -> Compatibility { - compatibility(self.db, expected.ty(), candidate.ty()) - } - - pub fn is_typed_value(&self, ty: &Type) -> bool { - is_typed_value(self.db, ty.ty()) - } - - pub fn display_source(&self, ty: &Type) -> Result { - ty.ty().display_source(self.db) - } - - pub fn display_declaration(&self, ty: &Type) -> Result, HirDisplayError> { - match ty.ty() { - Ty::Unknown - | Ty::Error - | Ty::Void - | Ty::Module(_) - | Ty::GenerateBlock(_) - | Ty::Block(_) => Ok(None), - _ => self.display_source(ty).map(Some), - } - } -} diff --git a/crates/hir-ty/tests/type_system.rs b/crates/hir-ty/tests/type_system.rs deleted file mode 100644 index 6f6b82790..000000000 --- a/crates/hir-ty/tests/type_system.rs +++ /dev/null @@ -1,501 +0,0 @@ -use std::fmt; - -use base_db::{ - diagnostics_config::DiagnosticsConfig, - project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, - salsa::{self, Durability}, - source_db::{FileLoader, SourceDb, SourceFileKind, SourceRootDb}, - source_root::{SourceRoot, SourceRootId}, -}; -use hir_def::{ - Ident, - aggregate::ClassMemberKind, - constraint::Constraint, - container::OwnerRef, - covergroup::CoverageBinInitializer, - db::HirDefDb, - expr::{ - Expr, - data_ty::{DataTy, TypePathKind}, - }, - owner::OwnerId, - pathres::{resolve_name, resolve_path}, - symbol::{NameContext, Resolution}, -}; -use hir_ty::{Compatibility, Type, TypeSystem, db::TyDb, display::HirDisplay}; -use preproc_expand::db::PreprocDb; -use rustc_hash::FxHashSet; -use smol_str::SmolStr; -use triomphe::Arc; -use utils::paths::{AbsPathBuf, Utf8PathBuf}; -use vfs::{AnchoredPath, FileId, FileSet, VfsPath}; - -const TOP: FileId = FileId::from_raw(0); -const ROOT: SourceRootId = SourceRootId(0); -const PROFILE: CompilationProfileId = CompilationProfileId(0); - -#[salsa::db] -#[derive(Default)] -struct TestDb { - storage: salsa::Storage, -} - -#[salsa::db] -impl salsa::Database for TestDb {} - -#[salsa::db] -impl SourceDb for TestDb {} - -#[salsa::db] -impl SourceRootDb for TestDb {} - -#[salsa::db] -impl PreprocDb for TestDb {} - -#[salsa::db] -impl HirDefDb for TestDb {} - -#[salsa::db] -impl TyDb for TestDb {} -impl std::ops::Deref for TestDb { - type Target = dyn HirDefDb; - - fn deref(&self) -> &Self::Target { - self - } -} - -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); - 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(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_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); - db.set_file_text_with_durability(TOP, Arc::from(root_text), 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 module_id(db: &TestDb, name: &str) -> OwnerId { - db.unit_index().module_ids(&ident(name)).unique().expect("module should resolve uniquely") -} - -fn type_of_name(db: &TestDb, module: OwnerId, name: &str, context: NameContext) -> Type { - let resolution = resolve_name(db, module, &ident(name), context); - assert!(!resolution.is_unresolved(), "{name} should resolve"); - TypeSystem::new(db).type_of_resolution(resolution) -} - -fn type_of_path(db: &TestDb, module: OwnerId, segments: &[&str]) -> Type { - let path = segments.iter().map(|segment| ident(segment)).collect::>(); - let resolution = resolve_path(db, module, &path, NameContext::Value); - assert!(!resolution.is_unresolved(), "path {segments:?} should resolve"); - TypeSystem::new(db).type_of_resolution(resolution) -} - -fn display_type(db: &TestDb, ty: &Type) -> String { - TypeSystem::new(db).display_source(ty).expect("formatting a type into a String should not fail") -} - -#[test] -fn semantic_types_render_through_the_public_interface() { - let db = db_with_root_text( - r#" -module m; - typedef enum { A, B } state_t; - typedef union packed { logic [7:0] byte_v; int int_v; } payload_u; - logic queue_var[$]; - logic bounded_queue[$:4]; - logic assoc_var[string]; - logic dyn_var[]; - event ev; - chandle handle; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let rendered = [ - type_of_name(&db, module, "state_t", NameContext::Type), - type_of_name(&db, module, "payload_u", NameContext::Type), - type_of_name(&db, module, "queue_var", NameContext::Value), - type_of_name(&db, module, "bounded_queue", NameContext::Value), - type_of_name(&db, module, "assoc_var", NameContext::Value), - type_of_name(&db, module, "dyn_var", NameContext::Value), - type_of_name(&db, module, "ev", NameContext::Value), - type_of_name(&db, module, "handle", NameContext::Value), - ] - .map(|ty| display_type(&db, &ty)); - - assert_eq!( - rendered, - [ - "state_t", - "payload_u", - "logic [$]", - "logic [$:4]", - "logic [string]", - "logic []", - "event", - "chandle", - ] - ); -} - -#[test] -fn members_and_compatibility_hide_classification_and_width_calculation() { - let db = db_with_root_text( - r#" -module m; - typedef struct packed { logic flag; logic [2:0] code; } payload_t; - payload_t payload; - logic [1 + 2:0] expression_width; - logic [3:0] four_bits; - logic [7:0] eight_bits; - real real_value; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let types = TypeSystem::new(&db); - let payload = type_of_name(&db, module, "payload", NameContext::Value); - let member_names = types - .members(&payload) - .into_iter() - .map(|member| member.into_name().to_string()) - .collect::>(); - assert_eq!(member_names, ["flag", "code"]); - - let expression_width = type_of_name(&db, module, "expression_width", NameContext::Value); - let four_bits = type_of_name(&db, module, "four_bits", NameContext::Value); - let eight_bits = type_of_name(&db, module, "eight_bits", NameContext::Value); - let real_value = type_of_name(&db, module, "real_value", NameContext::Value); - assert_eq!(types.compatibility(&expression_width, &four_bits), Compatibility::Compatible); - assert_eq!(types.compatibility(&four_bits, &eight_bits), Compatibility::Incompatible); - assert_eq!(types.compatibility(&four_bits, &real_value), Compatibility::Incompatible); - assert_eq!( - types.compatibility(&four_bits, &types.type_of_resolution(Resolution::Unresolved)), - Compatibility::Unknown - ); -} - -#[test] -fn struct_member_dimensions_are_part_of_member_type() { - let db = db_with_root_text( - r#" -module m; - typedef struct { - logic data[]; - int initialized = 1; - rand logic random_value; - } payload_t; - payload_t payload; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let types = TypeSystem::new(&db); - let payload = type_of_name(&db, module, "payload", NameContext::Value); - let members = types.members(&payload); - assert_eq!( - members.iter().map(|member| member.name().as_str()).collect::>(), - ["data", "initialized", "random_value"] - ); - assert_eq!( - types.display_source(members[0].ty()).expect("member type should render"), - "logic []" - ); -} - -#[test] -fn enum_definition_preserves_base_members_and_initializers() { - let db = db_with_root_text( - r#" -module m; - typedef enum logic [1:0] { - Idle = 2'd0, - Busy, - Error = 2'd3 - } state_t; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let body = db.body(module); - let enum_def = body.enums.values().next().expect("enum definition should be lowered"); - assert!(enum_def.base_ty.is_some(), "enum base type must be retained"); - assert_eq!( - enum_def.members.iter().map(|member| member.name.as_deref()).collect::>(), - [Some("Idle"), Some("Busy"), Some("Error")] - ); - assert!(enum_def.members[0].initializer.is_some()); - assert!(enum_def.members[1].initializer.is_none()); - assert!(enum_def.members[2].initializer.is_some()); -} - -#[test] -fn constraint_declaration_preserves_dist_and_nested_items() { - let db = db_with_root_text( - r#" -module m; - logic x; - constraint c { - x dist { 1 := 2, default }; - unique { x }; - } -endmodule -"#, - ); - let module = module_id(&db, "m"); - let body = db.body(module); - let definition = - body.constraint_defs.values().next().expect("constraint declaration should be lowered"); - assert_eq!(definition.name.as_deref(), Some("c")); - let Constraint::Block(items) = &body.constraints[definition.constraint] else { - panic!("constraint declaration should lower to a block"); - }; - assert_eq!(items.len(), 2); - let Constraint::Expression { expr, .. } = body.constraints[items[0]] else { - panic!("distribution item should lower as an expression constraint"); - }; - let Expr::Dist { distribution, .. } = &body.exprs[expr] else { - panic!("expression-or-dist should preserve its distribution"); - }; - assert_eq!(distribution.items.len(), 2); - assert!(matches!(body.constraints[items[items.len() - 1]], Constraint::Uniqueness { .. })); -} - -#[test] -fn wildcard_dimension_is_preserved_as_associative_array() { - let db = db_with_root_text( - r#" -module m; - logic values[*]; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let types = TypeSystem::new(&db); - let values = type_of_name(&db, module, "values", NameContext::Value); - assert_eq!( - types.display_source(&values).expect("wildcard array type should render"), - "logic [*]" - ); -} - -#[test] -fn coverpoint_bins_preserve_sample_expression_and_ranges() { - let db = db_with_root_text( - r#" -covergroup cg; - cp: coverpoint 1 { - bins low[2] = {[0:3]}; - } -endgroup -module m; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let covergroup = db - .unit_index() - .instantiable_ids_in(module, &ident("cg")) - .unique() - .expect("covergroup should be indexed"); - let body = db.body(covergroup); - let definition = body.covergroups.values().next().expect("covergroup should lower"); - let coverpoint = &body.coverpoints[definition.coverpoints[0]]; - assert_eq!(coverpoint.bins.len(), 1); - assert!(matches!(coverpoint.bins[0].initializer, CoverageBinInitializer::Ranges { .. })); - assert!(coverpoint.bins[0].size.is_some()); -} - -#[test] -fn class_declaration_preserves_base_and_member_kinds() { - let db = db_with_root_text( - r#" -module m; - class C extends Base; - int value; - function void tick(); - endfunction - endclass -endmodule -"#, - ); - let module = module_id(&db, "m"); - let body = db.body(module); - let class = body.classes.values().next().expect("class declaration should lower"); - assert_eq!(class.name.as_deref(), Some("C")); - assert_eq!(class.base_class_name.as_deref(), Some("Base")); - assert_eq!(class.members.len(), 2); - assert_eq!(class.members[0].kind, ClassMemberKind::Property); - assert_eq!(class.members[1].kind, ClassMemberKind::Method); -} -#[test] -fn qualified_type_paths_preserve_separator_and_source_projection() { - let db = db_with_root_text( - r#" -package p; - typedef logic t; -endpackage - -module m; - p::t value; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let lowered = db.body_with_source_map(module); - let type_ref = lowered - .data_ref() - .declarations - .iter() - .find_map(|(_, declaration)| match declaration.ty() { - DataTy::Named(type_ref) => Some(type_ref), - _ => None, - }) - .expect("the value declaration should retain its named type"); - - assert_eq!(type_ref.path_kind(), TypePathKind::Package); - assert_eq!(type_ref.segments(), &[ident("p"), ident("t")]); - assert_eq!(type_ref.segment_sources().len(), type_ref.segments().len()); - let source = db - .source_projection(module.file(&db)) - .origin(type_ref.source()) - .expect("type path source identity must project to source data"); - assert_eq!(source.file_id(), module.file(&db)); - assert!(source.full_range().is_some()); - - let value = type_of_name(&db, module, "value", NameContext::Value); - assert!( - value.diagnostics().is_empty(), - "qualified type should resolve: {:?}", - value.diagnostics() - ); -} - -#[test] -fn type_path_selectors_are_explicit_recovery() { - let db = db_with_root_text( - r#" -module m; - typedef logic t; - t[0] value; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let value = type_of_name(&db, module, "value", NameContext::Value); - assert_eq!( - value.diagnostics(), - &[hir_ty::TypeDiagnostic::InvalidTypePath( - hir_def::expr::data_ty::TypePathRecovery::Selectors - )] - ); -} - -#[test] -fn struct_data_type_has_no_type_diagnostic() { - let db = db_with_root_text( - r#" -module m; - struct { logic x; } value; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let value = type_of_name(&db, module, "value", NameContext::Value); - - assert!(value.diagnostics().is_empty()); -} - -#[test] -fn definition_backed_types_render_without_exposing_definition_kinds() { - let db = db_with_root_text( - r#" -interface bus_if; - wire clk; - modport host(input clk); -endinterface - -program p; -endprogram - -module top; - bus_if u_if(); - p u_p(); -endmodule -"#, - ); - let top = module_id(&db, "top"); - assert_eq!(display_type(&db, &type_of_path(&db, top, &["u_if"])), "virtual interface bus_if"); - assert_eq!( - display_type(&db, &type_of_path(&db, top, &["u_if", "host"])), - "virtual interface bus_if.host" - ); - assert_eq!(display_type(&db, &type_of_path(&db, top, &["u_p"])), "p"); -} - -#[test] -fn streaming_with_range_display_preserves_with_keyword() { - let db = db_with_root_text( - r#" -module m(input logic [3:0] a); - logic [3:0] x = {<<{a with [3:0]}}; -endmodule -"#, - ); - let module = module_id(&db, "m"); - let owner = module; - let body = db.body_with_source_map(owner); - let (stream_id, _) = body - .exprs - .iter() - .find(|(_, expr)| matches!(expr, hir_def::expr::Expr::Stream { .. })) - .expect("streaming concatenation should lower"); - - assert_eq!(OwnerRef::new(owner, stream_id).display_source(&db).unwrap(), "{<<{a with [3:0]}}"); -} diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index 174e0ff4f..ef2aa2fb3 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -9,17 +9,19 @@ edition.workspace = true anyhow.workspace = true base-db.workspace = true bitflags.workspace = true +design-graph.workspace = true dissimilar = "1.0.9" +either.workspace = true fst = "0.4.7" # Compiler layers are explicit dependencies; `hir-semantics` is a # syntax-to-HIR adapter, not a high-level facade over them. hir-def.workspace = true hir-semantics.workspace = true -hir-ty.workspace = true itertools.workspace = true la-arena.workspace = true memchr.workspace = true nohash-hasher.workspace = true +parking_lot.workspace = true preproc-expand.workspace = true regex.workspace = true rustc-hash.workspace = true @@ -27,6 +29,7 @@ salsa.workspace = true serde.workspace = true smallvec.workspace = true smol_str.workspace = true +slang-sys = { package = "slang-sys", path = "../slang-sys" } syntax.workspace = true thiserror.workspace = true toml = "0.9.8" diff --git a/crates/ide/src/analysis.rs b/crates/ide/src/analysis.rs index 590613e13..62f479007 100644 --- a/crates/ide/src/analysis.rs +++ b/crates/ide/src/analysis.rs @@ -1,4 +1,7 @@ -use std::ops::Range; +use std::{ + ops::{Deref, Range}, + sync::atomic::AtomicBool, +}; use base_db::{ Cancelled, @@ -7,7 +10,9 @@ use base_db::{ source_db::{SourceDb, SourceRootDb}, source_root::{SourceRootId, SourceRootRole}, }; -use preproc_expand::compilation_plan::CompilationPlan; +use design_graph::DesignGraphDb; +use hir_def::{def_id::DefId, pathres::ResolutionContext}; +use preproc_expand::{compilation_plan::CompilationPlan, profile_compiler::ProfileCompilationJob}; use triomphe::Arc; use utils::{ cancellation::CancellationToken, @@ -26,16 +31,18 @@ use crate::{ diagnostics, document_highlight::{self, DocumentHighlight, DocumentHighlightConfig}, document_symbols::{self, DocumentSymbol}, + elaboration::{ElabRevision, ElaborationService}, folding_ranges::{self, Fold}, formatting::{self, FmtConfig}, goto_declaration, goto_definition, hover, + incrementality::ProductStore, inlay_hint::{self, InlayHint, InlayHintConfig}, markup::Markup, navigation_target::NavTarget, + reference_support::{self, ModuleCallEdge}, references::{self, References, ReferencesConfig}, rename::{self, RenameConfig, RenameResult}, selection_ranges, - semantic_index::{self, ModuleCallEdge}, semantic_tokens::{self, SemaToken, SemaTokenConfig}, signature_help::{self, SignatureHelp, SignatureHelpConfig}, source_change::SourceChange, @@ -45,7 +52,107 @@ use crate::{ #[derive(Debug)] pub struct AnalysisSnapshot { pub(crate) db: RootDb, + pub(crate) store: Arc, pub(crate) snapshot_id: AnalysisSnapshotId, + pub(crate) salsa_revision: base_db::salsa::Revision, + pub(crate) elab: ElaborationService, +} + +/// Read view of one IDE request: the Salsa database, the parse-dependency +/// store, and the resident elaboration service. +/// +/// [`Self::parse_file`] records the file as paid so later resolution can +/// look at that file's `HirFileId::Macro` owner table. It does not merge +/// generated names into the L0 catalog. +/// +/// Elaboration is a backend worker, not a salsa query. Features that need +/// types, hierarchy, or class members ask [`Self::elab`] with this +/// snapshot's revision. +pub(crate) struct AnalysisContext<'a> { + pub(crate) db: &'a RootDb, + pub(crate) store: &'a ProductStore, + pub(crate) elab: &'a ElaborationService, + pub(crate) revision: ElabRevision, +} + +impl Deref for AnalysisContext<'_> { + type Target = RootDb; + + fn deref(&self) -> &RootDb { + self.db + } +} + +impl AnalysisContext<'_> { + pub(crate) fn new<'a>( + db: &'a RootDb, + store: &'a ProductStore, + elab: &'a ElaborationService, + revision: ElabRevision, + ) -> AnalysisContext<'a> { + AnalysisContext { db, store, elab, revision } + } + + pub(crate) fn semantics(&self) -> hir_semantics::semantics::Semantics<'_, RootDb> { + hir_semantics::semantics::Semantics::new_with_context(self.db, self.resolution()) + } + + /// Parse one file without building `$unit` or the design map. + pub(crate) fn parse_file(&self, file_id: FileId) -> syntax::SyntaxTree { + let (tree, dependencies) = self.db.parse_src_with_dependencies(file_id); + self.store.record_parse_dependencies(file_id, dependencies); + tree + } + + pub(crate) fn source_semantic_map( + &self, + file_id: FileId, + ) -> Arc { + let db: &dyn preproc_expand::db::PreprocDb = self.db; + db.source_semantic_map(file_id) + } + + pub(crate) fn file_facts(&self, file_id: FileId) -> Arc { + self.db.file_facts(file_id) + } + + pub(crate) fn unit_catalog(&self) -> triomphe::Arc { + ::source_unit_catalog(self.db) + } + + pub(crate) fn prewarm_unit_catalog( + &self, + cancel: &AtomicBool, + ) -> Option> { + if cancel.load(std::sync::atomic::Ordering::Acquire) { + return None; + } + Some(self.unit_catalog()) + } + + pub(crate) fn prewarm_resolution(&self, cancel: &AtomicBool) -> Option> { + if cancel.load(std::sync::atomic::Ordering::Acquire) { + return None; + } + Some(self.resolution()) + } + + pub(crate) fn resolution(&self) -> Arc { + ResolutionContext::from_locator( + self.db, + self.unit_catalog(), + Arc::from(self.store.paid_files()), + ) + } + + pub(crate) fn recursive_rename_closure( + &self, + def: DefId, + visibility: crate::ScopeVisibility, + single_file: Option, + ) -> Arc> { + Arc::new(crate::rename::recursive_rename_closure_impl(self, def, visibility, single_file)) + } } impl AnalysisSnapshot { @@ -53,12 +160,33 @@ impl AnalysisSnapshot { self.snapshot_id } + pub fn project_anchor( + &self, + anchor: crate::anchor::Anchor, + ) -> Cancellable> { + self.with_db(|ctx| crate::anchor::project(ctx, &anchor)) + } + + pub fn ast_id_at_range( + &self, + file_id: FileId, + range: utils::line_index::TextRange, + ) -> Cancellable> { + self.with_db(|ctx| crate::anchor::ast_id_at_range(ctx.db, file_id, range)) + } + fn with_db(&self, f: F) -> Cancellable where - F: FnOnce(&RootDb) -> T + std::panic::UnwindSafe, + F: FnOnce(&AnalysisContext<'_>) -> T + std::panic::UnwindSafe, { + debug_assert_eq!( + base_db::salsa::plumbing::current_revision(&self.db), + self.salsa_revision, + "an AnalysisSnapshot must never cross Salsa revisions", + ); let _span = tracing::debug_span!("ide.analysis", snapshot_id = ?self.snapshot_id).entered(); - Cancelled::catch(|| f(&self.db)) + let ctx = AnalysisContext::new(&self.db, &self.store, &self.elab, self.snapshot_id); + Cancelled::catch(|| f(&ctx)) } pub fn line_index(&self, file_id: FileId) -> Cancellable> { @@ -74,14 +202,7 @@ impl AnalysisSnapshot { } pub fn diagnostics(&self, file_id: FileId) -> Cancellable> { - self.with_db(|db| diagnostics::diagnostics(db, file_id)) - } - - pub fn compilation_diagnostics( - &self, - file_id: FileId, - ) -> Cancellable> { - self.with_db(|db| diagnostics::compilation_diagnostics(db, file_id)) + self.with_db(|db| diagnostics::analysis_diagnostics(db, file_id)) } pub fn source_root_diagnostics( @@ -91,11 +212,20 @@ impl AnalysisSnapshot { self.with_db(|db| diagnostics::source_root_diagnostics(db, file_id)) } - pub fn compilation_profile_diagnostics( + pub fn compilation_profile_job( &self, profile_id: CompilationProfileId, + ) -> Cancellable { + self.with_db(|db| { + preproc_expand::profile_compiler::build_profile_compilation_job(db.db, profile_id) + }) + } + + pub fn file_vide_diagnostics( + &self, + file_id: FileId, ) -> Cancellable> { - self.with_db(|db| diagnostics::compilation_profile_diagnostics(db, profile_id)) + self.with_db(|db| diagnostics::vide_diagnostics(db.db, db.resolution().as_ref(), file_id)) } pub fn parse_diagnostics(&self, file_id: FileId) -> Cancellable> { @@ -137,7 +267,7 @@ impl AnalysisSnapshot { } pub fn compilation_plan(&self, file_id: FileId) -> Cancellable> { - self.with_db(|db| db.compilation_plan_for_root(db.source_root_id(file_id))) + self.with_db(|db| db.db.compilation_plan_for_root(db.source_root_id(file_id))) } } @@ -157,7 +287,7 @@ impl AnalysisSnapshot { } pub fn document_symbol(&self, file_id: FileId) -> Cancellable> { - self.with_db(|db| document_symbols::document_symbols(db, file_id)) + self.with_db(|db| document_symbols::document_symbols(db.db, file_id)) } pub fn workspace_symbol( @@ -189,7 +319,7 @@ impl AnalysisSnapshot { file_id: FileId, name_range: TextRange, ) -> Cancellable> { - self.with_db(|db| semantic_index::incoming_module_edges(db, file_id, name_range)) + self.with_db(|db| reference_support::incoming_module_edges(db, file_id, name_range)) } pub fn module_outgoing_calls( @@ -197,7 +327,7 @@ impl AnalysisSnapshot { file_id: FileId, name_range: TextRange, ) -> Cancellable> { - self.with_db(|db| semantic_index::outgoing_module_edges(db, file_id, name_range)) + self.with_db(|db| reference_support::outgoing_module_edges(db, file_id, name_range)) } pub fn prepare_rename( @@ -288,7 +418,9 @@ impl AnalysisSnapshot { range: TextRange, config: InlayHintConfig, ) -> Cancellable> { - self.with_db(|db| inlay_hint::inlay_hint(db, file_id, range, config)) + self.with_db(|db| { + inlay_hint::inlay_hint(db, db.resolution().as_ref(), file_id, range, config) + }) } pub fn code_lens(&self, file_id: FileId, config: CodeLensConfig) -> Cancellable> { @@ -336,3 +468,122 @@ impl AnalysisSnapshot { }) } } + +#[cfg(test)] +mod tests { + use std::cell::Cell; + + use hir_def::design_map::PACKAGE_EXPORT_CLOSURE_RUNS; + + /// `semantics()` rebuilds [`super::AnalysisContext::resolution`] each time. + /// The workspace-level export closure must not re-walk every package on + /// every one of those calls. + #[test] + fn package_export_closure_runs_once_per_request() { + let (host, _) = crate::test_utils::setup_marked_files(&[ + ("/a.sv", "package a;\n int x;\nendpackage\n"), + ("/b.sv", "package b;\n int y;\nendpackage\n"), + ("/c.sv", "package c;\n int z;\nendpackage\n"), + ("/top.sv", "module top;\n int w;\nendmodule\n"), + ]); + let package_count = host.ctx().unit_catalog().packages().count() as u32; + assert_eq!( + package_count, 3, + "fixture must have three packages so per-package work is visible" + ); + + PACKAGE_EXPORT_CLOSURE_RUNS.with(|runs| runs.set(0)); + let ctx = host.ctx(); + let _ = ctx.resolution(); + let after_first = PACKAGE_EXPORT_CLOSURE_RUNS.with(Cell::get); + let _ = ctx.semantics(); + let _ = ctx.resolution(); + let closure_runs = PACKAGE_EXPORT_CLOSURE_RUNS.with(Cell::get); + assert!( + after_first <= 1, + "first resolution() may hit a prewarm memo (0) or compute once (1), not {after_first}" + ); + assert_eq!( + closure_runs, after_first, + "later resolution()/semantics() must not re-execute the closure (first={after_first} after={closure_runs})" + ); + } + + #[test] + fn to_owner_traffic_on_a_typical_request() { + use hir_def::unit::TO_OWNER_RUNS; + + let (host, files) = crate::test_utils::setup_marked_files(&[ + ("/a.sv", "package a;\n int x;\nendpackage\n"), + ("/b.sv", "package b;\n import a::*;\n int y;\nendpackage\n"), + ("/c.sv", "package c;\n import b::*;\n int z;\nendpackage\n"), + ("/top.sv", "module top;\n int /*marker:w*/w;\nendmodule\n"), + ]); + let file_id = files[3].0; + let offset = files[3].2["w"]; + TO_OWNER_RUNS.with(|runs| runs.set(0)); + let _ = host.make_analysis().hover(crate::FilePosition { file_id, offset }).unwrap(); + let _ = + host.make_analysis().goto_definition(crate::FilePosition { file_id, offset }).unwrap(); + let calls = TO_OWNER_RUNS.with(Cell::get); + println!("t6.to_owner_calls\t{calls}"); + assert_eq!(calls, 0, "T6 removed the UnitId→OwnerId name bridge"); + } + + /// T6 form B: shipped resolution must not project L0 `UnitId` → `OwnerId` + /// by name. The T5 counter is the production bridge; it must stay at 0. + #[test] + fn shipped_request_does_not_project_l0_unit_ids() { + use hir_def::unit::TO_OWNER_RUNS; + + let (host, files) = crate::test_utils::setup_marked_files(&[ + ("/a.sv", "package a;\n int x;\nendpackage\n"), + ("/b.sv", "package b;\n import a::*;\n int y;\nendpackage\n"), + ("/c.sv", "package c;\n import b::*;\n int z;\nendpackage\n"), + ("/top.sv", "module top;\n int /*marker:w*/w;\nendmodule\n"), + ]); + let file_id = files[3].0; + let offset = files[3].2["w"]; + TO_OWNER_RUNS.with(|runs| runs.set(0)); + let _ = host.make_analysis().hover(crate::FilePosition { file_id, offset }).unwrap(); + let _ = + host.make_analysis().goto_definition(crate::FilePosition { file_id, offset }).unwrap(); + let calls = TO_OWNER_RUNS.with(Cell::get); + assert_eq!( + calls, 0, + "shipped hover+goto must not project L0 UnitId by name (to_owner={calls})" + ); + } + + /// Cold start of one file hits U1 / U2 / U3 once each. The three + /// unexpanded parses stay split (empty vs profile predefines vs Trace); + /// `preprocessor_independent` is one function on U1 and U2. + #[test] + fn cold_start_unexpanded_parse_count_matches_three_sites() { + use base_db::{change::Change, source_root::SourceRoot}; + use preproc_expand::db::PreprocDb; + use syntax::UNEXPANDED_PARSE_RUNS; + use vfs::{ChangedFile, FileId, FileSet, VfsPath}; + + let file_id = FileId::from_raw(0); + let mut file_set = FileSet::default(); + file_set.insert(file_id, VfsPath::new_virtual_path("/top.sv".to_owned())); + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.add_changed_file(ChangedFile::create(file_id, "module top;\n int w;\nendmodule\n")); + let mut host = crate::analysis_host::AnalysisHost::default(); + UNEXPANDED_PARSE_RUNS.with(|runs| runs.set(0)); + host.apply_change_without_prewarm(change); + + let ctx = host.ctx(); + let db: &dyn PreprocDb = ctx.db; + let _ = db.source_model(file_id); + let _ = ctx.file_facts(file_id); + let _ = db.compilation_plan_for_root(db.source_root_id(file_id)); + let runs = UNEXPANDED_PARSE_RUNS.with(Cell::get); + assert_eq!( + runs, 3, + "cold start of one file must unexpanded-parse once per site (source_model, file_facts, include_scan); ran {runs}" + ); + } +} diff --git a/crates/ide/src/analysis_host.rs b/crates/ide/src/analysis_host.rs index 73a7f507f..1e3c1a454 100644 --- a/crates/ide/src/analysis_host.rs +++ b/crates/ide/src/analysis_host.rs @@ -1,28 +1,96 @@ +use std::{ + sync::{ + Arc as StdArc, + atomic::{AtomicBool, Ordering}, + }, + thread::{self, JoinHandle}, +}; + use base_db::{ analysis_snapshot::AnalysisSnapshotId, change::Change, diagnostics_config::DiagnosticsConfig, salsa::Durability, source_db::SourceDb, }; use triomphe::Arc; -use crate::{analysis::AnalysisSnapshot, db::root_db::RootDb}; +use crate::{ + analysis::{AnalysisContext, AnalysisSnapshot}, + db::root_db::RootDb, + elaboration::ElaborationService, + incrementality::ProductStore, +}; pub struct AnalysisHost { db: RootDb, + store: Arc, snapshot_id: AnalysisSnapshotId, + prewarm: Option, + elab: ElaborationService, + elab_worker: Option>, +} + +struct PrewarmTask { + cancel: StdArc, + worker: JoinHandle<()>, } impl AnalysisHost { pub fn new(lru_capacity: Option) -> AnalysisHost { - AnalysisHost { db: RootDb::new(lru_capacity), snapshot_id: AnalysisSnapshotId::default() } + let (elab, elab_worker) = ElaborationService::spawn(); + AnalysisHost { + db: RootDb::new(lru_capacity), + store: Arc::new(ProductStore::default()), + snapshot_id: AnalysisSnapshotId::default(), + prewarm: None, + elab, + elab_worker: Some(elab_worker), + } } pub fn make_analysis(&self) -> AnalysisSnapshot { + self.signal_foreground_request(); let db = self.db.clone(); - AnalysisSnapshot { db, snapshot_id: self.snapshot_id } + let salsa_revision = base_db::salsa::plumbing::current_revision(&db); + AnalysisSnapshot { + db, + store: self.store.clone(), + snapshot_id: self.snapshot_id, + salsa_revision, + elab: self.elab.clone(), + } } pub fn apply_change(&mut self, change: Change) { - self.db.apply_change(change); + self.cancel_prewarm(); + let (store, affected_files) = ProductStore::transition(&self.store, &mut self.db, change); + self.store = store; + self.advance_revision(); + if !affected_files.is_empty() { + self.start_prewarm(affected_files); + } + // A request that arrives before the prewarm lands answers from HIR + // and moves on, which is right in an editor and useless in a test: + // the assertion would depend on which one won. Tests observe the + // warm state, so they wait for it. + #[cfg(test)] + self.await_prewarm(); + } + + /// Wait for the revision prewarm without cancelling it. + #[cfg(test)] + fn await_prewarm(&mut self) { + if let Some(task) = self.prewarm.take() { + let _ = task.worker.join(); + } + } + + /// Apply a change without starting revision prewarm. Benches that build + /// a large workspace would otherwise spend Drop joining `unit_scope` + /// over every file. + #[cfg(test)] + pub(crate) fn apply_change_without_prewarm(&mut self, change: Change) { + self.cancel_prewarm(); + let (store, _) = ProductStore::transition(&self.store, &mut self.db, change); + self.store = store; self.advance_revision(); } @@ -35,13 +103,91 @@ impl AnalysisHost { self.snapshot_id = self.snapshot_id.next(); } + fn start_prewarm(&mut self, affected_files: Vec) { + let db = self.db.clone(); + let store = self.store.clone(); + let elab = self.elab.clone(); + let revision = self.snapshot_id; + let cancel = StdArc::new(AtomicBool::new(false)); + let worker_cancel = cancel.clone(); + let worker = thread::Builder::new() + .name("vide-revision-prewarm".to_owned()) + .spawn(move || { + if worker_cancel.load(Ordering::Acquire) { + return; + } + let ctx = AnalysisContext::new(&db, &store, &elab, revision); + for file_id in affected_files { + if worker_cancel.load(Ordering::Acquire) { + return; + } + if db.file_kind(file_id).is_semantic_compilation_unit() { + let _ = ::file_decls(&db, file_id); + } + } + let _ = ctx.prewarm_unit_catalog(&worker_cancel); + if !worker_cancel.load(Ordering::Acquire) { + let _ = ctx.prewarm_resolution(&worker_cancel); + } + // Slang is the last step: it is the slowest and the only one + // a request can do without. Building it here is what lets the + // request path give up after `INTERACTIVE_TIMEOUT` instead of + // waiting out a cold elaboration on the keyboard path. + if !worker_cancel.load(Ordering::Acquire) { + let _ = elab.prewarm(&db, revision); + } + }) + .expect("failed to spawn revision prewarm worker"); + self.prewarm = Some(PrewarmTask { cancel, worker }); + } + + fn cancel_prewarm(&mut self) { + let Some(task) = self.prewarm.take() else { + return; + }; + task.cancel.store(true, Ordering::Release); + // Do not join: the worker checks cancel between files and drops its + // salsa snapshot. Joining waited out an in-flight fold on the main + // loop and showed up as after-edit request latency. + } + + fn join_prewarm(&mut self) { + let Some(task) = self.prewarm.take() else { + return; + }; + task.cancel.store(true, Ordering::Release); + let _ = task.worker.join(); + } + + fn signal_foreground_request(&self) { + if let Some(task) = &self.prewarm { + task.cancel.store(true, Ordering::Release); + } + } + pub fn snapshot_id(&self) -> AnalysisSnapshotId { self.snapshot_id } pub fn raw_db(&self) -> &RootDb { + self.signal_foreground_request(); &self.db } + + #[cfg(test)] + pub(crate) fn ctx(&self) -> AnalysisContext<'_> { + AnalysisContext::new(&self.db, &self.store, &self.elab, self.snapshot_id) + } +} + +impl Drop for AnalysisHost { + fn drop(&mut self) { + self.join_prewarm(); + self.elab.shutdown(); + if let Some(worker) = self.elab_worker.take() { + let _ = worker.join(); + } + } } impl Default for AnalysisHost { @@ -76,6 +222,299 @@ mod tests { change } + fn add_second_file(text: &str) -> Change { + let first = FileId::from_raw(0); + let second = FileId::from_raw(1); + let mut file_set = FileSet::default(); + file_set.insert(first, VfsPath::new_virtual_path("/top.sv".to_owned())); + file_set.insert(second, VfsPath::new_virtual_path("/other.sv".to_owned())); + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.add_changed_file(ChangedFile::create(second, text)); + change + } + + fn two_file_workspace(first: &str, second: &str) -> Change { + let first_id = FileId::from_raw(0); + let second_id = FileId::from_raw(1); + let mut file_set = FileSet::default(); + file_set.insert(first_id, VfsPath::new_virtual_path("/gen.sv".to_owned())); + file_set.insert(second_id, VfsPath::new_virtual_path("/other.sv".to_owned())); + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.add_changed_file(ChangedFile::create(first_id, first)); + change.add_changed_file(ChangedFile::create(second_id, second)); + change + } + + fn modify_file(file_id: FileId, text: &str) -> Change { + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify(file_id, text)); + change + } + + fn project_config_with_predefines(predefines: Vec) -> Change { + use base_db::{ + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + source_root::SourceRootId, + }; + use triomphe::Arc; + let mut change = Change::new(); + change.set_project_config(Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![SourceRootId(0)], + top_modules: Vec::new(), + preprocess: PreprocessConfig::with_predefine_strings(predefines, Vec::new()), + }], + ))); + change + } + + fn goto_names(host: &AnalysisHost, file_id: FileId, text: &str, needle: &str) -> Vec { + let offset = utils::line_index::TextSize::from(text.find(needle).expect(needle) as u32); + host.make_analysis() + .goto_definition(crate::FilePosition { file_id, offset }) + .unwrap() + .map(|hit| { + hit.info + .into_iter() + .filter_map(|nav| nav.name.map(|name| name.to_string())) + .collect() + }) + .unwrap_or_default() + } + + #[test] + fn generated_unit_rename_invalidates_overlay() { + let mut host = AnalysisHost::default(); + host.apply_change(change_with_file_text( + "`define GEN(name) module name; endmodule\n`GEN(foo)\nmodule top;\nendmodule\n", + )); + let _ = host.ctx().parse_file(FileId::from_raw(0)); + let before = host.ctx().unit_catalog(); + assert!( + !before.module_names().iter().any(|name| name == "foo"), + "L0 catalog must not absorb generated names: {:?}", + before.module_names() + ); + assert!( + before.module_names().iter().any(|name| name == "top"), + "{:?}", + before.module_names() + ); + + host.apply_change(modify_with_file_text( + "`define GEN(name) module name; endmodule\n`GEN(bar)\nmodule top;\nendmodule\n", + )); + let after_edit = host.ctx().unit_catalog(); + assert!( + !after_edit.module_names().iter().any(|name| name == "foo"), + "stale generated name foo must not survive the edit: {:?}", + after_edit.module_names() + ); + assert!( + after_edit.module_names().iter().any(|name| name == "top"), + "{:?}", + after_edit.module_names() + ); + + let _ = host.ctx().parse_file(FileId::from_raw(0)); + let after_reparse = host.ctx().unit_catalog(); + assert!( + !after_reparse.module_names().iter().any(|name| name == "foo"), + "{:?}", + after_reparse.module_names() + ); + assert!( + !after_reparse.module_names().iter().any(|name| name == "bar"), + "generated bar stays on the paid parse, not the L0 catalog: {:?}", + after_reparse.module_names() + ); + } + + #[test] + fn generated_unit_rename_invalidates_cross_file_goto() { + let gen_foo = + "`define GEN(name) module name; endmodule\n`GEN(foo)\nmodule top;\nendmodule\n"; + let gen_bar = + "`define GEN(name) module name; endmodule\n`GEN(bar)\nmodule top;\nendmodule\n"; + let other = "module other;\n foo u_foo();\n bar u_bar();\nendmodule\n"; + let generator = FileId::from_raw(0); + let user = FileId::from_raw(1); + + let mut host = AnalysisHost::default(); + host.apply_change(two_file_workspace(gen_foo, other)); + let _ = host.ctx().parse_file(generator); + assert_eq!(goto_names(&host, user, other, "foo u_foo"), ["foo"]); + assert!(goto_names(&host, user, other, "bar u_bar").is_empty(), "bar is not generated yet"); + + host.apply_change(modify_file(generator, gen_bar)); + assert!( + goto_names(&host, user, other, "foo u_foo").is_empty(), + "goto foo must fail after the generator was renamed" + ); + assert_eq!( + goto_names(&host, user, other, "bar u_bar"), + ["bar"], + "the paid file's salsa owner table sees the new expansion without a side table" + ); + } + + #[test] + fn adding_a_file_upserts_the_existing_design_graph() { + let mut host = AnalysisHost::default(); + host.apply_change(change_with_file_text("module first;\nendmodule\n")); + let first = host.ctx().unit_catalog(); + assert_eq!(first.node_count(), 1); + assert!(first.module_names().iter().any(|name| name == "first")); + + host.apply_change(add_second_file("module second;\nendmodule\n")); + let both = host.ctx().unit_catalog(); + assert_eq!(both.node_count(), 2); + assert!(both.module_names().iter().any(|name| name == "first")); + assert!(both.module_names().iter().any(|name| name == "second")); + } + + #[test] + fn file_decls_backdate_across_a_body_only_edit() { + use std::cell::Cell; + + use design_graph::DesignGraphDb; + let mut host = AnalysisHost::default(); + host.apply_change(change_with_file_text("module first;\nendmodule\n")); + let file = FileId::from_raw(0); + let before_decls = ::file_decls(host.ctx().db, file); + design_graph::db::SOURCE_CATALOG_RUNS.with(|runs| runs.set(0)); + let before = ::source_unit_catalog(host.ctx().db); + let runs_after_first = design_graph::db::SOURCE_CATALOG_RUNS.with(Cell::get); + host.apply_change(modify_with_file_text("module first;\n wire x;\nendmodule\n")); + let after_decls = ::file_decls(host.ctx().db, file); + let after = ::source_unit_catalog(host.ctx().db); + let runs_after_edit = design_graph::db::SOURCE_CATALOG_RUNS.with(Cell::get); + assert_eq!( + *before_decls, *after_decls, + "position-free decls must be value-equal after a body-only edit" + ); + assert_eq!(before.as_ref(), after.as_ref()); + // Body-only edits leave `file_decls` value-equal. Salsa must + // backdate the L0 catalog rather than re-fold it. An extra + // `set_file_kind` on the same enum dirties every query that + // reads kind, and looks like a backdating failure. + assert_eq!( + runs_after_edit, runs_after_first, + "salsa catalog must not re-execute after a body-only edit (first={runs_after_first} after={runs_after_edit})" + ); + } + + #[test] + fn generated_overlay_is_outside_the_salsa_source_catalog() { + use std::cell::Cell; + + use design_graph::DesignGraphDb; + let mut host = AnalysisHost::default(); + host.apply_change(change_with_file_text( + "`define GEN(name) module name; endmodule\n`GEN(foo)\nmodule top;\nendmodule\n", + )); + design_graph::db::SOURCE_CATALOG_RUNS.with(|runs| runs.set(0)); + let source_before = ::source_unit_catalog(host.ctx().db); + let runs_before_parse = design_graph::db::SOURCE_CATALOG_RUNS.with(Cell::get); + assert!( + source_before.module_names().iter().any(|name| name == "top"), + "{:?}", + source_before.module_names() + ); + assert!( + !source_before.module_names().iter().any(|name| name == "foo"), + "L0 salsa catalog must not see a generated name: {:?}", + source_before.module_names() + ); + + let _ = host.ctx().parse_file(FileId::from_raw(0)); + let source_after = ::source_unit_catalog(host.ctx().db); + let runs_after_parse = design_graph::db::SOURCE_CATALOG_RUNS.with(Cell::get); + let production = host.ctx().unit_catalog(); + assert_eq!( + runs_after_parse, runs_before_parse, + "recording generated units must not re-execute the salsa catalog (before={runs_before_parse} after={runs_after_parse})" + ); + assert!( + !source_after.module_names().iter().any(|name| name == "foo"), + "{:?}", + source_after.module_names() + ); + assert!( + !production.module_names().iter().any(|name| name == "foo"), + "production catalog is the salsa source catalog: {:?}", + production.module_names() + ); + assert!( + production.module_names().iter().any(|name| name == "top"), + "{:?}", + production.module_names() + ); + assert_eq!( + production.as_ref(), + source_after.as_ref(), + "production catalog is the salsa source catalog" + ); + } + + /// T6 form B: L0 is a name→file locator. Generated names live on the paid + /// parse (`HirFileId::Macro`). Merging them into the catalog that feeds + /// `resolution()` is the overlay that made stale goto possible. + #[test] + fn production_resolution_does_not_merge_generated_overlay() { + use design_graph::DesignGraphDb; + let gen_foo = + "`define GEN(name) module name; endmodule\n`GEN(foo)\nmodule top;\nendmodule\n"; + let other = "module other;\n foo u_foo();\nendmodule\n"; + let generator = FileId::from_raw(0); + let user = FileId::from_raw(1); + + let mut host = AnalysisHost::default(); + host.apply_change(two_file_workspace(gen_foo, other)); + let _ = host.ctx().parse_file(generator); + + let source = ::source_unit_catalog(host.ctx().db); + let production = host.ctx().unit_catalog(); + let resolution = host.ctx().resolution(); + let graph = resolution.graph(); + assert!( + !source.module_names().iter().any(|name| name == "foo"), + "L0 salsa catalog must not see a generated name: {:?}", + source.module_names() + ); + assert!( + !production.module_names().iter().any(|name| name == "foo"), + "production catalog must not merge generated names: {:?}", + production.module_names() + ); + assert!( + !graph.module_names().iter().any(|name| name == "foo"), + "resolution must not be fed generated L0 names: {:?}", + graph.module_names() + ); + assert_eq!( + goto_names(&host, user, other, "foo u_foo"), + ["foo"], + "goto must still find the generated module via paid-parse identity" + ); + } + + #[test] + fn body_only_edit_keeps_the_design_graph_nodes() { + let mut host = AnalysisHost::default(); + host.apply_change(change_with_file_text("module first;\nendmodule\n")); + let before = host.ctx().unit_catalog(); + assert_eq!(before.node_count(), 1); + + host.apply_change(modify_with_file_text("module first;\n wire x;\nendmodule\n")); + let after = host.ctx().unit_catalog(); + assert_eq!(after.node_count(), 1); + assert!(after.module_names().iter().any(|name| name == "first")); + } + #[test] fn analysis_views_follow_input_revisions_after_snapshot_drop() { let mut host = AnalysisHost::default(); @@ -128,6 +567,50 @@ mod tests { reader.join().unwrap(); } + #[test] + fn project_config_and_dirty_files_together_rebuild_facts() { + let gated = "`ifdef FOO\nmodule foo;\nendmodule\n`else\nmodule bar;\nendmodule\n`endif\n"; + let other = "module other;\nendmodule\n"; + let other_id = FileId::from_raw(1); + let mut host = AnalysisHost::default(); + host.apply_change(two_file_workspace(gated, other)); + let before = host.ctx().unit_catalog(); + assert!( + before.module_names().iter().any(|name| name == "bar"), + "{:?}", + before.module_names() + ); + assert!( + !before.module_names().iter().any(|name| name == "foo"), + "{:?}", + before.module_names() + ); + + let mut change = project_config_with_predefines(vec!["FOO".to_owned()]); + change.add_changed_file(vfs::ChangedFile::modify( + other_id, + "module other;\n wire x;\nendmodule\n", + )); + host.apply_change(change); + + let after = host.ctx().unit_catalog(); + assert!( + after.module_names().iter().any(|name| name == "foo"), + "config+dirty must recompute facts of files that were not edited: {:?}", + after.module_names() + ); + assert!( + !after.module_names().iter().any(|name| name == "bar"), + "stale unit from the old predefines must not remain: {:?}", + after.module_names() + ); + assert!( + after.module_names().iter().any(|name| name == "other"), + "{:?}", + after.module_names() + ); + } + #[test] fn read_only_views_share_one_snapshot_identity() { let mut host = AnalysisHost::default(); diff --git a/crates/ide/src/anchor.rs b/crates/ide/src/anchor.rs new file mode 100644 index 000000000..84a53cbfb --- /dev/null +++ b/crates/ide/src/anchor.rs @@ -0,0 +1,191 @@ +//! Stable anchors for facts produced by external backends. +//! +//! `Definition` is T9a: a source identity that `SourceProjection` can +//! reproject after an edit. `Instance` is T9b: an elaborated hierarchical +//! path; the live compilation answers where it sits in the current source. + +use hir_def::{ast_id_map::SourceAstId, file::HirFileId}; +use syntax::has_text_range::HasTextRange; +use utils::line_index::TextRange; +use vfs::FileId; + +use crate::{db::root_db::RootDb, hier::HierPath}; + +/// A backend-independent location for an analysis fact. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Anchor { + Definition { file: FileId, ast_id: SourceAstId }, + Instance { path: HierPath }, +} + +/// Current source span of an [`Anchor`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProjectedAnchor { + pub file: FileId, + pub range: TextRange, +} + +pub fn project_definition(db: &RootDb, file: FileId, ast_id: SourceAstId) -> Option { + db.source_projection(HirFileId::File(file)) + .origin(ast_id) + .and_then(|origin| origin.focus_or_full_range()) +} + +pub(crate) fn project( + ctx: &crate::analysis::AnalysisContext<'_>, + anchor: &Anchor, +) -> Option { + match anchor { + Anchor::Definition { file, ast_id } => project_definition(ctx.db, *file, *ast_id) + .map(|range| ProjectedAnchor { file: *file, range }), + Anchor::Instance { path } => project_instance(ctx, path), + } +} + +fn project_instance( + ctx: &crate::analysis::AnalysisContext<'_>, + path: &HierPath, +) -> Option { + let profiles = { + let ids = ctx.db.project_config().profile_ids(); + if ids.is_empty() { vec![None] } else { ids.into_iter().map(Some).collect::>() } + }; + for profile in profiles { + let Some(rows) = + ctx.elab.list_instances(ctx.db, ctx.revision, profile).answered("instance anchor") + else { + continue; + }; + let Some(row) = rows.iter().find(|row| row.path == path.as_str()) else { + continue; + }; + let file = file_id_for_slang_path(ctx.db, &row.file); + let tail = path.as_str().rsplit('.').next().unwrap_or(path.as_str()); + let name_len = tail.find('[').unwrap_or(tail.len()); + let start = utils::line_index::TextSize::from(row.offset as u32); + let range = + TextRange::new(start, start + utils::line_index::TextSize::from(name_len as u32)); + return Some(ProjectedAnchor { file, range }); + } + None +} + +pub(crate) fn file_id_for_slang_path(db: &RootDb, slang_file: &str) -> FileId { + ::path_file_ids(db).get(slang_file).unwrap_or_else(|| { + panic!("elaboration reported a buffer path that was not assigned: {slang_file}") + }) +} + +/// Innermost syntax node covering `range`, identified by [`SourceAstId`]. +pub fn ast_id_at_range(db: &RootDb, file: FileId, range: TextRange) -> Option { + let hir_file = HirFileId::File(file); + let tree = db.parse(hir_file); + let map = db.ast_id_map(hir_file); + let mut best: Option<(TextSizeLen, SourceAstId)> = None; + for event in tree.root().node_preorder() { + let syntax::WalkEvent::Enter(node) = event else { + continue; + }; + let Some(node_range) = node.text_range() else { + continue; + }; + if !covers(node_range, range) { + continue; + } + let Some(id) = map.id_of_node(node) else { + continue; + }; + let len = node_range.len(); + if best.map(|(best_len, _)| len < best_len).unwrap_or(true) { + best = Some((len, id)); + } + } + best.map(|(_, id)| id) +} + +type TextSizeLen = utils::line_index::TextSize; + +fn covers(outer: TextRange, inner: TextRange) -> bool { + outer.start() <= inner.start() && inner.end() <= outer.end() +} + +#[cfg(test)] +mod tests { + use base_db::change::Change; + use vfs::ChangedFile; + + use super::*; + use crate::{hier::HierPath, test_utils::setup}; + + #[test] + fn a_definition_anchor_survives_an_insert_before_it() { + let src = "module foo; endmodule\n"; + let (mut host, file_id) = setup(src); + let offset = src.find("foo").expect("name"); + let range = TextRange::new( + utils::line_index::TextSize::from(offset as u32), + utils::line_index::TextSize::from((offset + 3) as u32), + ); + let (ast_id, before) = { + let analysis = host.make_analysis(); + let ast_id = analysis.ast_id_at_range(file_id, range).unwrap().expect("module name id"); + let before = analysis + .project_anchor(Anchor::Definition { file: file_id, ast_id }) + .unwrap() + .expect("origin"); + (ast_id, before) + }; + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify(file_id, format!("// header\n{src}").as_str())); + host.apply_change(change); + + let after = host + .make_analysis() + .project_anchor(Anchor::Definition { file: file_id, ast_id }) + .unwrap() + .expect("reprojected origin"); + assert!( + after.range.start() > before.range.start(), + "insert before the name must shift the origin" + ); + } + + #[test] + fn an_instance_anchor_tracks_the_instantiation_site() { + let src = "module child; endmodule\nmodule top; child u0(); endmodule\n"; + let (mut host, file_id) = crate::test_utils::setup_with_path(src, "/top.sv"); + let path = { + let ctx = host.ctx(); + let rows = match ctx.elab.list_instances( + ctx.db, + ctx.revision, + ctx.db.file_compilation_profile(file_id), + ) { + crate::elaboration::ElabResult::Ready(Some(rows)) => rows, + other => panic!("expected instances, got {other:?}"), + }; + rows.into_iter() + .find(|row| row.path.contains("u0")) + .map(|row| HierPath::new(row.path)) + .expect("u0") + }; + let before = host + .make_analysis() + .project_anchor(Anchor::Instance { path: path.clone() }) + .unwrap() + .expect("instance origin"); + assert_eq!(before.file, file_id); + assert_eq!(usize::from(before.range.start()), src.find("u0").expect("u0"),); + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify(file_id, format!("// header\n{src}").as_str())); + host.apply_change(change); + let after = host + .make_analysis() + .project_anchor(Anchor::Instance { path }) + .unwrap() + .expect("reprojected instance"); + assert!(after.range.start() > before.range.start()); + } +} diff --git a/crates/ide/src/code_action/context.rs b/crates/ide/src/code_action/context.rs index a531d68bc..6eddd19be 100644 --- a/crates/ide/src/code_action/context.rs +++ b/crates/ide/src/code_action/context.rs @@ -6,9 +6,10 @@ use syntax::{ use utils::text_edit::{TextRange, TextSize}; use vfs::FileId; -use crate::{db::root_db::RootDb, diagnostics::Diagnostic}; +use crate::{analysis::AnalysisContext, db::root_db::RootDb, diagnostics::Diagnostic}; pub(crate) struct CodeActionCtx<'a> { + analysis: &'a AnalysisContext<'a>, sema: &'a Semantics<'a, RootDb>, file_id: FileId, range: TextRange, @@ -18,6 +19,7 @@ pub(crate) struct CodeActionCtx<'a> { impl<'a> CodeActionCtx<'a> { pub(super) fn new( + analysis: &'a AnalysisContext<'a>, sema: &'a Semantics<'a, RootDb>, file_id: FileId, range: TextRange, @@ -26,7 +28,11 @@ impl<'a> CodeActionCtx<'a> { let parsed_file = sema.parse_file(file_id); parsed_file.compilation_unit()?; - Some(Self { sema, file_id, range, diagnostics, parsed_file }) + Some(Self { analysis, sema, file_id, range, diagnostics, parsed_file }) + } + + pub(crate) fn analysis(&self) -> &'a AnalysisContext<'a> { + self.analysis } pub(crate) fn sema(&self) -> &'a Semantics<'a, RootDb> { diff --git a/crates/ide/src/code_action/engine.rs b/crates/ide/src/code_action/engine.rs index 13c67e254..f489afc06 100644 --- a/crates/ide/src/code_action/engine.rs +++ b/crates/ide/src/code_action/engine.rs @@ -1,12 +1,11 @@ -use hir_semantics::semantics::Semantics; use utils::text_edit::TextRange; use vfs::FileId; use super::{CodeAction, CodeActionCollector, CodeActionCtx, CodeActionResolveStrategy, handlers}; -use crate::{db::root_db::RootDb, diagnostics::Diagnostic}; +use crate::{analysis::AnalysisContext, diagnostics::Diagnostic}; pub(crate) fn code_action( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, range: TextRange, diagnostics: &[Diagnostic], @@ -15,8 +14,8 @@ pub(crate) fn code_action( if db.file_kind(file_id).is_project_manifest() { return Vec::new(); } - let sema = Semantics::new(db); - let Some(ctx) = CodeActionCtx::new(&sema, file_id, range, diagnostics) else { + let sema = db.semantics(); + let Some(ctx) = CodeActionCtx::new(db, &sema, file_id, range, diagnostics) else { return Vec::new(); }; diff --git a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_allows_selection_padding.sv b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_allows_selection_padding.sv index d0abde2b8..dc02d58d8 100644 --- a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_allows_selection_padding.sv +++ b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_allows_selection_padding.sv @@ -1,2 +1,2 @@ //- action: extract_variable -module top; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule +module top; logic [7:0] y, a, b; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule diff --git a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_continuous_assign.sv b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_continuous_assign.sv index efc25c1bd..4e763bead 100644 --- a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_continuous_assign.sv +++ b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_continuous_assign.sv @@ -1,2 +1,2 @@ //- action: extract_variable -module top; assign y = /*selection*/a + b/*selection*/; endmodule +module top; logic [7:0] y, a, b; assign y = /*selection*/a + b/*selection*/; endmodule diff --git a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_inserts_local_before_statement.sv b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_inserts_local_before_statement.sv index c5538c91d..ad8cdd7e5 100644 --- a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_inserts_local_before_statement.sv +++ b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_inserts_local_before_statement.sv @@ -1,2 +1,2 @@ //- action: extract_variable -module top; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule +module top; logic [7:0] y, a, b; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule diff --git a/crates/ide/src/code_action/fixtures/code_actions/extract_variable_mixed_width_add.sv b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_mixed_width_add.sv new file mode 100644 index 000000000..ce57ceaca --- /dev/null +++ b/crates/ide/src/code_action/fixtures/code_actions/extract_variable_mixed_width_add.sv @@ -0,0 +1,2 @@ +//- action: extract_variable +module top; logic [3:0] b; logic [7:0] a, y; always_comb begin y = /*selection*/b + a/*selection*/; end endmodule diff --git a/crates/ide/src/code_action/handlers/add_missing_connections.rs b/crates/ide/src/code_action/handlers/add_missing_connections.rs index 4092e08ff..b28dc7cbd 100644 --- a/crates/ide/src/code_action/handlers/add_missing_connections.rs +++ b/crates/ide/src/code_action/handlers/add_missing_connections.rs @@ -51,7 +51,11 @@ pub(super) fn add_missing_connections( let close_paren = ast_instance.close_paren()?.text_range_in(ast_instance.syntax())?; let instantiation = module.get(instance.parent); - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); diff --git a/crates/ide/src/code_action/handlers/add_missing_parameters.rs b/crates/ide/src/code_action/handlers/add_missing_parameters.rs index bf4a1bd24..dde2d6998 100644 --- a/crates/ide/src/code_action/handlers/add_missing_parameters.rs +++ b/crates/ide/src/code_action/handlers/add_missing_parameters.rs @@ -52,7 +52,11 @@ pub(super) fn add_missing_parameters( let open_paren = params_node.open_paren()?.text_range_in(params_node.syntax())?; let close_paren = params_node.close_paren()?.text_range_in(params_node.syntax())?; - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_body = db.body_with_source_map(target_module_id); let is_ordered = instantiation diff --git a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs index 2d73fb753..c2b8f3d7d 100644 --- a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs @@ -55,7 +55,11 @@ pub(super) fn convert_ordered_ports( let module = db.body_with_source_map(module_id); let module_body = db.body_with_source_map(module_id); let instantiation = module.get(module.get(instance_id).parent); - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); let port_names = port_names(&target_module, &target_body); @@ -114,7 +118,11 @@ pub(super) fn convert_ordered_params( let module = db.body_with_source_map(module_id); let module_body = db.body_with_source_map(module_id); let instantiation = module.get(instantiation_id); - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_body = db.body_with_source_map(target_module_id); let param_names = leading_overridable_parameter_names(&target_body); diff --git a/crates/ide/src/code_action/handlers/convert_port_declarations.rs b/crates/ide/src/code_action/handlers/convert_port_declarations.rs index 5b261250f..9b8ca94a7 100644 --- a/crates/ide/src/code_action/handlers/convert_port_declarations.rs +++ b/crates/ide/src/code_action/handlers/convert_port_declarations.rs @@ -5,13 +5,13 @@ use hir_def::{ Ident, body::Body, container::OwnerRef, + db::HirDefDb, expr::declarator::{DeclId, DeclaratorParent}, module::port::{PortDecl, Ports}, owner::OwnerId, source_map::Lowered, symbol::{NameContext, ScopeData}, }; -use hir_ty::{db::TyDb, display::HirDisplay}; use itertools::Itertools; use syntax::{ ast::{self, AstNode}, @@ -19,8 +19,9 @@ use syntax::{ }; use utils::text_edit::TextRange; -use crate::code_action::{ - CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +use crate::{ + code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent}, + render::hir_display::HirDisplay, }; const ANSI_TO_NON_ANSI_ID: CodeActionId = CodeActionId { @@ -260,7 +261,7 @@ fn non_ansi_port_replacement( } fn data_decl_range_for_name( - db: &dyn TyDb, + db: &dyn HirDefDb, body: &Lowered, decl_id: DeclId, name: &Ident, diff --git a/crates/ide/src/code_action/handlers/extract_variable.rs b/crates/ide/src/code_action/handlers/extract_variable.rs index d379fac27..58389eaab 100644 --- a/crates/ide/src/code_action/handlers/extract_variable.rs +++ b/crates/ide/src/code_action/handlers/extract_variable.rs @@ -1,7 +1,6 @@ use std::ops::Range; use base_db::source_db::SourceDb; -use hir_ty::{Type, TypeSystem}; use syntax::{ SyntaxAncestors, SyntaxKind, TokenKind, WalkEvent, ast::{self, AstNode}, @@ -9,8 +8,9 @@ use syntax::{ }; use utils::text_edit::{TextRange, TextSize}; -use crate::code_action::{ - CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +use crate::{ + code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent}, + slang_class, }; const ID: CodeActionId = @@ -26,7 +26,7 @@ const ID: CodeActionId = // ``` // -> // ``` -// always_comb begin logic value = a + b; +// always_comb begin logic[7:0] value = a + b; // y = value; end // ``` pub(super) fn extract_variable( @@ -40,8 +40,8 @@ pub(super) fn extract_variable( let expr_text = text.get(Range::from(expr_range))?.trim().to_owned(); let name = fresh_variable_name(&text, "value"); + let ty_text = extracted_variable_type(ctx, expr)?; collector.add(ID, "Extract into variable", expr_range, |builder| { - let ty_text = extracted_variable_type(ctx, expr).unwrap_or_else(|| "logic".to_owned()); let declaration = target.declaration(&ty_text, &name, &expr_text); builder.insert(target.insert_offset, declaration); builder.replace(expr_range, name); @@ -169,26 +169,22 @@ fn trim_range(text: &str, range: TextRange) -> Option { } fn extracted_variable_type(ctx: &CodeActionCtx<'_>, expr: ast::Expression<'_>) -> Option { - let types = TypeSystem::new(ctx.sema().db); - let ty = types.type_of_expr(ctx.sema().resolve_expr(ctx.file_id().into(), expr)?); - render_ty(ctx, &ty) - .or_else(|| expected_type_for_assignment_rhs(ctx, expr).and_then(|ty| render_ty(ctx, &ty))) -} - -fn expected_type_for_assignment_rhs( - ctx: &CodeActionCtx<'_>, - expr: ast::Expression<'_>, -) -> Option { - let assignment = assignment_expression_containing_rhs(expr)?; - let res = - ctx.sema().expr_to_def(ctx.sema().resolve_expr(ctx.file_id().into(), assignment.left())?); - Some(TypeSystem::new(ctx.sema().db).type_of_resolution(res)) + let expr_range = expr.syntax().text_range()?; + lookup_type_range(ctx, expr_range).or_else(|| { + let assignment = assignment_expression_containing_rhs(expr)?; + lookup_type_range(ctx, assignment.left().syntax().text_range()?) + }) } -fn render_ty(ctx: &CodeActionCtx<'_>, ty: &Type) -> Option { - TypeSystem::new(ctx.sema().db) - .display_declaration(ty) - .expect("formatting a type into a String should not fail") +fn lookup_type_range(ctx: &CodeActionCtx<'_>, range: TextRange) -> Option { + slang_class::lookup_type_at( + ctx.analysis(), + ctx.file_id(), + usize::from(range.start()), + usize::from(range.end()), + ) + .answered("extract variable") + .filter(|ty| !ty.is_empty() && !ty.contains("")) } fn fresh_variable_name(text: &str, base: &str) -> String { diff --git a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs index ee5bbbcc1..b44c2d71b 100644 --- a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs +++ b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs @@ -55,7 +55,11 @@ pub(super) fn sort_named_parameter_assignments( sema.resolve_instantiation(ctx.file_id().into(), ast_instantiation)?; let module = db.body_with_source_map(module_id); let instantiation = module.get(instantiation_id); - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_body = db.body_with_source_map(target_module_id); let parameter_order = all_overridable_parameter_names(&target_body); let parameter_order_map: FxHashMap<_, _> = @@ -117,7 +121,11 @@ pub(super) fn sort_named_port_connections( let module = db.body_with_source_map(module_id); let instance = module.get(instance_id); let instantiation = module.get(instance.parent); - let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let target_module_id = resolve_hir_instantiation_target( + db, + ctx.sema().resolution_context().as_ref(), + instantiation, + )?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); let port_order = port_names(&target_module, &target_body); diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_allows_selection_padding.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_allows_selection_padding.sv.snap index 326ee4156..a84ae39ca 100644 --- a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_allows_selection_padding.sv.snap +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_allows_selection_padding.sv.snap @@ -1,7 +1,8 @@ --- source: crates/ide/src/code_action/tests.rs +assertion_line: 360 expression: fixed input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_allows_selection_padding.sv --- -module top; always_comb begin logic value = a + b; +module top; logic [7:0] y, a, b; always_comb begin logic[7:0] value = a + b; y = value ; end endmodule diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_continuous_assign.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_continuous_assign.sv.snap index 010f62856..1b7f20eec 100644 --- a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_continuous_assign.sv.snap +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_continuous_assign.sv.snap @@ -1,7 +1,8 @@ --- source: crates/ide/src/code_action/tests.rs +assertion_line: 360 expression: fixed input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_continuous_assign.sv --- -module top; wire logic value = a + b; +module top; logic [7:0] y, a, b; wire logic[7:0] value = a + b; assign y = value; endmodule diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_inserts_local_before_statement.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_inserts_local_before_statement.sv.snap index 4b0e3f22d..5e44502d8 100644 --- a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_inserts_local_before_statement.sv.snap +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_inserts_local_before_statement.sv.snap @@ -1,7 +1,8 @@ --- source: crates/ide/src/code_action/tests.rs +assertion_line: 360 expression: fixed input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_inserts_local_before_statement.sv --- -module top; always_comb begin logic value = a + b; +module top; logic [7:0] y, a, b; always_comb begin logic[7:0] value = a + b; y = value; end endmodule diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_mixed_width_add.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_mixed_width_add.sv.snap new file mode 100644 index 000000000..987dbfab8 --- /dev/null +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_mixed_width_add.sv.snap @@ -0,0 +1,8 @@ +--- +source: crates/ide/src/code_action/tests.rs +assertion_line: 360 +expression: fixed +input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_mixed_width_add.sv +--- +module top; logic [3:0] b; logic [7:0] a, y; always_comb begin logic[7:0] value = b + a; +y = value; end endmodule diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_assignment_lhs_type.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_assignment_lhs_type.sv.snap index 9e52e3a69..cb2d7a9ee 100644 --- a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_assignment_lhs_type.sv.snap +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_assignment_lhs_type.sv.snap @@ -1,7 +1,8 @@ --- source: crates/ide/src/code_action/tests.rs +assertion_line: 360 expression: fixed input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_uses_assignment_lhs_type.sv --- -module top; logic [7:0] y, a, b; always_comb begin logic [7:0] value = a + b; +module top; logic [7:0] y, a, b; always_comb begin logic[7:0] value = a + b; y = value; end endmodule diff --git a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_continuous_assign_lhs_type.sv.snap b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_continuous_assign_lhs_type.sv.snap index 9ce494c13..4d67d8ee9 100644 --- a/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_continuous_assign_lhs_type.sv.snap +++ b/crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_uses_continuous_assign_lhs_type.sv.snap @@ -1,7 +1,8 @@ --- source: crates/ide/src/code_action/tests.rs +assertion_line: 360 expression: fixed input_file: crates/ide/src/code_action/fixtures/code_actions/extract_variable_uses_continuous_assign_lhs_type.sv --- -module top; logic [7:0] y, a, b; wire logic [7:0] value = a + b; +module top; logic [7:0] y, a, b; wire logic[7:0] value = a + b; assign y = value; endmodule diff --git a/crates/ide/src/code_action/tests.rs b/crates/ide/src/code_action/tests.rs index 5a1fe83c4..aee5705eb 100644 --- a/crates/ide/src/code_action/tests.rs +++ b/crates/ide/src/code_action/tests.rs @@ -5,7 +5,7 @@ use utils::text_edit::{TextRange, TextSize}; use vfs::{ChangedFile, FileId, FileSet, VfsPath}; use super::*; -use crate::db::root_db::RootDb; +use crate::analysis_host::AnalysisHost; struct CodeActionFixture { action: FixtureAction, @@ -100,15 +100,15 @@ fn parse_fixture_repair(value: &str, path: &Path) -> RepairKind { } } -fn db_with_file(text: &str) -> (RootDb, FileId, TextSize) { +fn db_with_file(text: &str) -> (AnalysisHost, FileId, TextSize) { let marker = "/*caret*/"; let offset = text.find(marker).expect("missing caret marker"); let text = text.replace(marker, ""); - let (db, file_id) = db_with_text(&text); - (db, file_id, TextSize::from(offset as u32)) + let (host, file_id) = db_with_text(&text); + (host, file_id, TextSize::from(offset as u32)) } -fn db_with_text(text: &str) -> (RootDb, FileId) { +fn db_with_text(text: &str) -> (AnalysisHost, FileId) { let file_id = FileId::from_raw(0); let mut file_set = FileSet::default(); file_set.insert(file_id, VfsPath::new_virtual_path("/test.sv".to_owned())); @@ -117,16 +117,16 @@ fn db_with_text(text: &str) -> (RootDb, FileId) { change.set_roots(vec![SourceRoot::new_local(file_set)]); change.add_changed_file(ChangedFile::create(file_id, text)); - let mut db = RootDb::new(None); - db.apply_change(change); - (db, file_id) + let mut host = AnalysisHost::default(); + host.apply_change(change); + (host, file_id) } fn apply_action(text: &str, repair: RepairKind) -> Option { - let (db, file_id, offset) = db_with_file(text); + let (host, file_id, offset) = db_with_file(text); let diagnostics = vec![diagnostic_for_repair(repair, TextRange::empty(offset))]; let actions = code_action( - &db, + &host.ctx(), file_id, utils::text_edit::TextRange::empty(offset), &diagnostics, @@ -168,9 +168,9 @@ fn apply_action_without_diagnostics_by( text: &str, pred: impl Fn(&CodeAction) -> bool, ) -> Option { - let (db, file_id, offset) = db_with_file(text); + let (host, file_id, offset) = db_with_file(text); let actions = code_action( - &db, + &host.ctx(), file_id, utils::text_edit::TextRange::empty(offset), &[], @@ -195,8 +195,8 @@ fn apply_action_without_diagnostics_with_selection_by( pred: impl Fn(&CodeAction) -> bool, ) -> Option { let (mut text, range) = text_with_selection_range(text); - let (db, file_id) = db_with_text(&text); - let actions = code_action(&db, file_id, range, &[], CodeActionResolveStrategy::All); + let (host, file_id) = db_with_text(&text); + let actions = code_action(&host.ctx(), file_id, range, &[], CodeActionResolveStrategy::All); let action = actions.into_iter().find(pred)?; let edit = action.source_change?.text_edits.remove(&file_id)?; edit.apply(&mut text); @@ -205,8 +205,8 @@ fn apply_action_without_diagnostics_with_selection_by( fn action_labels_without_diagnostics_with_selection(text: &str) -> Vec { let (text, range) = text_with_selection_range(text); - let (db, file_id) = db_with_text(&text); - code_action(&db, file_id, range, &[], CodeActionResolveStrategy::All) + let (host, file_id) = db_with_text(&text); + code_action(&host.ctx(), file_id, range, &[], CodeActionResolveStrategy::All) .into_iter() .map(|action| action.label) .collect() @@ -288,10 +288,10 @@ fn diagnostic_for_repair(repair: RepairKind, range: TextRange) -> crate::diagnos } fn action_labels(text: &str, repair: RepairKind) -> Vec { - let (db, file_id, offset) = db_with_file(text); + let (host, file_id, offset) = db_with_file(text); let diagnostics = vec![diagnostic_for_repair(repair, TextRange::empty(offset))]; code_action( - &db, + &host.ctx(), file_id, utils::text_edit::TextRange::empty(offset), &diagnostics, @@ -303,9 +303,9 @@ fn action_labels(text: &str, repair: RepairKind) -> Vec { } fn action_labels_without_diagnostics(text: &str) -> Vec { - let (db, file_id, offset) = db_with_file(text); + let (host, file_id, offset) = db_with_file(text); code_action( - &db, + &host.ctx(), file_id, utils::text_edit::TextRange::empty(offset), &[], @@ -336,10 +336,10 @@ fn action_labels_for_case(case: &LabelCase) -> Vec { LabelCaseKind::Selection => action_labels_without_diagnostics_with_selection(case.text), LabelCaseKind::Repair(repair) => action_labels(case.text, repair), LabelCaseKind::MismatchedRepair(repair) => { - let (db, file_id, offset) = db_with_file(case.text); + let (host, file_id, offset) = db_with_file(case.text); let diagnostics = vec![diagnostic_for_repair(repair, TextRange::empty(offset))]; code_action( - &db, + &host.ctx(), file_id, TextRange::empty(offset), &diagnostics, @@ -622,12 +622,12 @@ fn expected_token_repair_uses_diagnostic_range() { let text = "/*caret*/module top;\nlogic a\nendmodule\n"; let clean_text = text.replace("/*caret*/", ""); let diagnostic_offset = TextSize::from(clean_text.find("\nendmodule").unwrap() as u32); - let (db, file_id, offset) = db_with_file(text); + let (host, file_id, offset) = db_with_file(text); let mut diagnostic = diagnostic_for_repair(RepairKind::InsertExpectedToken, TextRange::empty(diagnostic_offset)); diagnostic.range = TextRange::empty(diagnostic_offset); let actions = code_action( - &db, + &host.ctx(), file_id, TextRange::empty(offset), &[diagnostic], diff --git a/crates/ide/src/code_lens.rs b/crates/ide/src/code_lens.rs index c816ee67f..caface10e 100644 --- a/crates/ide/src/code_lens.rs +++ b/crates/ide/src/code_lens.rs @@ -1,5 +1,4 @@ use hir_def::{body::Body, def_id::DefId, has_source::HasSource, source_map::Lowered}; -use hir_semantics::semantics::Semantics; use preproc_expand::file::HirFileId; use syntax::{ ast::{self, AstNode}, @@ -10,7 +9,7 @@ use vfs::FileId; use crate::{ FilePosition, FileRange, ScopeVisibility, - db::root_db::RootDb, + analysis::AnalysisContext, references::{ ReferencesConfig, search::{ReferencesCtx, SearchScope}, @@ -30,7 +29,11 @@ pub enum CodeLensKind { ModuleInstance { pos: FilePosition, data: Option> }, } -pub(crate) fn code_lens(db: &RootDb, config: CodeLensConfig, file_id: FileId) -> Vec { +pub(crate) fn code_lens( + db: &AnalysisContext<'_>, + config: CodeLensConfig, + file_id: FileId, +) -> Vec { if db.file_kind(file_id).is_project_manifest() { return Vec::new(); } @@ -48,7 +51,7 @@ pub(crate) fn code_lens(db: &RootDb, config: CodeLensConfig, file_id: FileId) -> } fn process_instantiations( - db: &RootDb, + db: &AnalysisContext<'_>, hir_file: &Lowered, file_id: HirFileId, res: &mut Vec, @@ -58,7 +61,7 @@ fn process_instantiations( if module.name.is_none() { continue; } - let Some(source) = module_id.source(db) else { + let Some(source) = module_id.source(db.db) else { continue; }; let range = source.value.full_range(); @@ -68,8 +71,8 @@ fn process_instantiations( } } -pub(crate) fn code_lens_resolve(db: &RootDb, mut kind: CodeLensKind) -> CodeLensKind { - let sema = Semantics::new(db); +pub(crate) fn code_lens_resolve(db: &AnalysisContext<'_>, mut kind: CodeLensKind) -> CodeLensKind { + let sema = db.semantics(); match kind { CodeLensKind::ModuleInstance { pos: FilePosition { file_id, offset }, ref mut data } => { @@ -78,7 +81,7 @@ pub(crate) fn code_lens_resolve(db: &RootDb, mut kind: CodeLensKind) -> CodeLens sema.db.owner_table(hir_file_id).file_owner().expect("file owner"), ); let Some(module_id) = hir_file.module_owners().find(|id| { - id.source(db).is_some_and(|source| source.value.full_range().start() == offset) + id.source(db.db).is_some_and(|source| source.value.full_range().start() == offset) }) else { *data = Some(Vec::new()); return kind; @@ -90,7 +93,7 @@ pub(crate) fn code_lens_resolve(db: &RootDb, mut kind: CodeLensKind) -> CodeLens ReferencesConfig::new(ScopeVisibility::Public, Some(SearchScope::all(sema.db))); let mut ranges = Vec::new(); - for (file_id, tokens) in ReferencesCtx::new(&sema, &def, ref_config).search() { + for (file_id, tokens) in ReferencesCtx::new(db, &def, ref_config).search() { let parsed_file = sema.parse_file(file_id); for instantiation in tokens .into_iter() diff --git a/crates/ide/src/completion.rs b/crates/ide/src/completion.rs index dcbf594a7..a7e9b64b3 100644 --- a/crates/ide/src/completion.rs +++ b/crates/ide/src/completion.rs @@ -4,4 +4,5 @@ mod engine; mod request; mod syntax_keywords; -pub use engine::{CompletionItem, CompletionItemKind, completions}; +pub(crate) use engine::completions; +pub use engine::{CompletionItem, CompletionItemKind}; diff --git a/crates/ide/src/completion/context.rs b/crates/ide/src/completion/context.rs index 9f82fac2f..5c3604a45 100644 --- a/crates/ide/src/completion/context.rs +++ b/crates/ide/src/completion/context.rs @@ -9,7 +9,6 @@ mod resolve; mod util; use base_db::source_db::SourceDb; -use hir_semantics::semantics::Semantics; use smallvec::{SmallVec, smallvec}; use syntax::{ SyntaxNode, SyntaxNodeExt, @@ -19,7 +18,7 @@ use syntax::{ use utils::line_index::{TextRange, TextSize}; use self::caret::CaretSnapshot; -use crate::{FilePosition, db::root_db::RootDb}; +use crate::{FilePosition, analysis::AnalysisContext}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LexContext { @@ -83,33 +82,36 @@ pub struct CompletionContext { pub in_decl_name: bool, } +#[derive(Clone)] struct CompletionWord { replacement: TextRange, prefix: String, } pub(crate) fn completion_context( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, trigger: Option, ) -> CompletionContext { - let sema = Semantics::new(db); - let parsed_file = sema.parse_file(file_id); - let Some(root) = parsed_file.root() else { - return CompletionContext { - replacement: TextRange::empty(offset), - prefix: String::new(), - trigger, - lex: LexContext::Code, - expectations: SmallVec::new(), - in_decl_name: false, - }; - }; + let source_model = db.source_model(file_id); + let root = source_model.syntax_tree.root(); let text = db.file_text(file_id); - let parser_expected_syntax = db.parser_expected_syntax(file_id, offset); let directive_word = directive_word_at_offset(&text, offset); let token_word = library_map_word_at_offset(root, &text, offset); let system_word = standalone_system_identifier_word_at_offset(&text, offset); + let fast = detect_completion_context_impl( + root, + offset, + trigger, + directive_word.clone(), + token_word.clone(), + system_word.clone(), + None, + ); + if parser_independent_context(&fast) { + return fast; + } + let parser_expected_syntax = db.parser_expected_syntax(file_id, offset); detect_completion_context_impl( root, offset, @@ -121,6 +123,32 @@ pub(crate) fn completion_context( ) } +fn parser_independent_context(context: &CompletionContext) -> bool { + if context.lex != LexContext::Code { + return true; + } + !context.expectations.is_empty() + && context.expectations.iter().all(|expectation| { + matches!( + expectation.syntax, + ExpectedSyntax::DirectiveName + | ExpectedSyntax::IntegerLiteralBase + | ExpectedSyntax::ParameterPortListItem + | ExpectedSyntax::AnsiPortItem + | ExpectedSyntax::FunctionPortItem + | ExpectedSyntax::PortConnectionName + | ExpectedSyntax::ParameterAssignmentName + | ExpectedSyntax::MemberName + | ExpectedSyntax::PortConnectionExpr + | ExpectedSyntax::ParameterAssignmentExpr + | ExpectedSyntax::AfterParamValueAssignmentHash + | ExpectedSyntax::AfterParameterPortListHash + | ExpectedSyntax::ParamValueAssignment + | ExpectedSyntax::EventControl { .. } + ) + }) +} + pub fn detect_completion_context( root: SyntaxNode<'_>, offset: TextSize, diff --git a/crates/ide/src/completion/engine.rs b/crates/ide/src/completion/engine.rs index 5ad835e2c..4327023e2 100644 --- a/crates/ide/src/completion/engine.rs +++ b/crates/ide/src/completion/engine.rs @@ -21,27 +21,27 @@ mod tests; pub use self::item::{CompletionItem, CompletionItemKind}; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{ context::{CompletionContext, TriggerChar, completion_context}, request::CompletionRequest, }, - db::root_db::RootDb, }; -pub fn completions( - db: &RootDb, +pub(crate) fn completions( + db: &AnalysisContext<'_>, position: FilePosition, trigger: Option, ) -> Vec { if db.file_kind(position.file_id).is_project_manifest() { - return crate::manifest::completions(db, position); + return crate::manifest::completions(db.db, position); } let ctx = completion_context(db, position, trigger); completions_with_context(db, position, &ctx) } fn completions_with_context( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, ctx: &CompletionContext, ) -> Vec { diff --git a/crates/ide/src/completion/engine/expr.rs b/crates/ide/src/completion/engine/expr.rs index 6cb488a10..aa0f036f3 100644 --- a/crates/ide/src/completion/engine/expr.rs +++ b/crates/ide/src/completion/engine/expr.rs @@ -1,33 +1,30 @@ use std::collections::BTreeMap; use hir_def::{ - container::{OwnerRef, ScopeParent}, + container::ScopeParent, def_id::DefId, - lower_ident_opt, - owner::{OwnerId, OwnerKind}, + owner::OwnerId, symbol::{DefKind, Resolution}, }; use hir_semantics::semantics::Semantics; -use hir_ty::{Type, TypeSystem}; use preproc_expand::file::HirFileId; -use syntax::{ - SyntaxKind, SyntaxNode, SyntaxNodeExt, - ast::{self, AstNode}, - has_text_range::HasTextRange, -}; +use syntax::{SyntaxNode, SyntaxNodeExt}; use utils::text_edit::TextSize; -use super::{candidate::CompletionCandidate, system, typed_filter::is_compatible_typed_value}; -use crate::{FilePosition, completion::context::CompletionContext, db::root_db::RootDb}; +use super::{candidate::CompletionCandidate, system}; +use crate::{ + FilePosition, analysis::AnalysisContext, completion::context::CompletionContext, + db::root_db::RootDb, +}; #[derive(Clone, Debug)] enum NameKind { - Value { ty: Type }, - SubroutineCall { return_ty: Type }, + Value, + SubroutineCall, } pub(super) fn complete_expression( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -36,7 +33,7 @@ pub(super) fn complete_expression( } pub(super) fn complete_argument_exprs( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -45,12 +42,12 @@ pub(super) fn complete_argument_exprs( } fn complete_expression_impl( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -58,28 +55,19 @@ fn complete_expression_impl( }; let mut names: BTreeMap = BTreeMap::new(); - let mut current_module_id = None; if let Some(container_id) = container_id_at_offset(&sema, file_id, root, position.offset) { - current_module_id = module_id_for_container(db, container_id); - for container_id in ScopeParent::start_from(db, container_id) { + for container_id in ScopeParent::start_from(db.db, container_id) { collect_container_names(db, container_id, &mut names); } } - let expected_ty = current_module_id.and_then(|module_id| { - expected_type_at_offset(db, &sema, file_id, root, position.offset, module_id) - }); - let mut candidates: Vec<_> = names .into_iter() .filter(|(name, _)| name.starts_with(prefix)) - .filter(|(_, kind)| { - expression_candidate_matches_expected_type(db, expected_ty.as_ref(), kind) - }) .map(|(name, kind)| match kind { - NameKind::Value { .. } => CompletionCandidate::text(name, ctx.replacement), - NameKind::SubroutineCall { .. } => CompletionCandidate::semantic_snippet( + NameKind::Value => CompletionCandidate::text(name, ctx.replacement), + NameKind::SubroutineCall => CompletionCandidate::semantic_snippet( name.clone(), ctx.replacement, format!("{name}()"), @@ -102,7 +90,11 @@ fn container_id_at_offset( sema.container_for_node(file_id, node) } -fn collect_container_names(db: &RootDb, owner: OwnerId, names: &mut BTreeMap) { +fn collect_container_names( + db: &AnalysisContext<'_>, + owner: OwnerId, + names: &mut BTreeMap, +) { let scope = db.scope(owner); for (ident, defs) in scope.iter_listing() { collect_def_names(db, ident, defs, names); @@ -110,7 +102,7 @@ fn collect_container_names(db: &RootDb, owner: OwnerId, names: &mut BTreeMap, ident: &hir_def::Ident, defs: impl IntoIterator, names: &mut BTreeMap, @@ -118,21 +110,16 @@ fn collect_def_names( let defs = defs.into_iter().collect::>(); let subroutines = Resolution::from_candidates( - defs.iter().filter_map(|def_id| def_id.primary_origin(db).as_subroutine(db)), + defs.iter().filter_map(|def_id| def_id.primary_origin(db.db).as_subroutine(db.db)), ); - let return_ty = match subroutines { - Resolution::Unresolved => None, - Resolution::Unique(subroutine_id) => Some(subroutine_return_ty(db, subroutine_id)), - Resolution::Ambiguous(_) => Some(Type::unknown()), - }; - if let Some(return_ty) = return_ty { - names.entry(ident.to_string()).or_insert(NameKind::SubroutineCall { return_ty }); + if !matches!(subroutines, Resolution::Unresolved) { + names.entry(ident.to_string()).or_insert(NameKind::SubroutineCall); return; } if defs.iter().any(|def_id| { matches!( - def_id.kind(db), + def_id.kind(db.db), DefKind::Variable | DefKind::Net | DefKind::Param @@ -142,105 +129,6 @@ fn collect_def_names( | DefKind::SubroutinePort ) }) { - let res = Resolution::from_candidates(defs.iter().cloned()); - let ty = TypeSystem::new(db).type_of_resolution(res); - names.entry(ident.to_string()).or_insert(NameKind::Value { ty }); - } -} -fn subroutine_return_ty(db: &RootDb, subroutine: OwnerId) -> Type { - TypeSystem::new(db).type_of_subroutine_return(subroutine) -} - -fn module_id_for_container(db: &RootDb, owner: OwnerId) -> Option { - ScopeParent::start_from(db, owner).find(|owner| owner.kind(db) == OwnerKind::Module) -} -fn expression_candidate_matches_expected_type( - db: &RootDb, - expected_ty: Option<&Type>, - kind: &NameKind, -) -> bool { - let Some(expected_ty) = expected_ty else { - return true; - }; - let candidate_ty = match kind { - NameKind::Value { ty } => ty, - NameKind::SubroutineCall { return_ty } => return_ty, - }; - is_compatible_typed_value(db, expected_ty, candidate_ty) -} - -fn expected_type_at_offset( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - root: SyntaxNode<'_>, - offset: TextSize, - _current_module_id: OwnerId, -) -> Option { - expected_type_for_assignment_rhs(db, sema, file_id, root, offset) - .or_else(|| expected_type_for_declarator_initializer(db, sema, file_id, root, offset)) - .filter(|ty| TypeSystem::new(db).is_typed_value(ty)) -} - -fn expected_type_for_assignment_rhs( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - root: SyntaxNode<'_>, - offset: TextSize, -) -> Option { - let assignment = root.find_node_at_offset::>(offset)?; - if !is_assignment_expression(assignment.syntax().kind()) { - return None; - } - let right = assignment.right(); - if !right.syntax().text_range().is_some_and(|range| { - range.contains(offset) || range.start() == offset || range.end() == offset - }) { - return None; - } - - let res = sema.expr_to_def(sema.resolve_expr(file_id, assignment.left())?); - Some(TypeSystem::new(db).type_of_resolution(res)) -} - -fn expected_type_for_declarator_initializer( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - root: SyntaxNode<'_>, - offset: TextSize, -) -> Option { - let declarator = root.find_node_at_offset::>(offset)?; - let initializer = declarator.initializer()?; - if !initializer.expr().syntax().text_range().is_some_and(|range| { - range.contains(offset) || range.start() == offset || range.end() == offset - }) { - return None; + names.entry(ident.to_string()).or_insert(NameKind::Value); } - - let ident = lower_ident_opt(declarator.name())?; - let container_id = sema.container_for_node(file_id, declarator.syntax())?; - let res = sema.name_to_def(OwnerRef::new(container_id, ident)); - Some(TypeSystem::new(db).type_of_resolution(res)) -} - -fn is_assignment_expression(kind: SyntaxKind) -> bool { - matches!( - kind, - SyntaxKind::ASSIGNMENT_EXPRESSION - | SyntaxKind::NONBLOCKING_ASSIGNMENT_EXPRESSION - | SyntaxKind::ADD_ASSIGNMENT_EXPRESSION - | SyntaxKind::SUBTRACT_ASSIGNMENT_EXPRESSION - | SyntaxKind::MULTIPLY_ASSIGNMENT_EXPRESSION - | SyntaxKind::DIVIDE_ASSIGNMENT_EXPRESSION - | SyntaxKind::MOD_ASSIGNMENT_EXPRESSION - | SyntaxKind::AND_ASSIGNMENT_EXPRESSION - | SyntaxKind::OR_ASSIGNMENT_EXPRESSION - | SyntaxKind::XOR_ASSIGNMENT_EXPRESSION - | SyntaxKind::LOGICAL_LEFT_SHIFT_ASSIGNMENT_EXPRESSION - | SyntaxKind::LOGICAL_RIGHT_SHIFT_ASSIGNMENT_EXPRESSION - | SyntaxKind::ARITHMETIC_LEFT_SHIFT_ASSIGNMENT_EXPRESSION - | SyntaxKind::ARITHMETIC_RIGHT_SHIFT_ASSIGNMENT_EXPRESSION - ) } diff --git a/crates/ide/src/completion/engine/instantiation.rs b/crates/ide/src/completion/engine/instantiation.rs index a94c62282..e1a6da48c 100644 --- a/crates/ide/src/completion/engine/instantiation.rs +++ b/crates/ide/src/completion/engine/instantiation.rs @@ -7,16 +7,16 @@ use syntax::{ ast::{self, AstNode}, }; -use crate::db::root_db::RootDb; +use crate::analysis::AnalysisContext; -pub(super) fn ports_of_module_sorted(db: &RootDb, module_id: OwnerId) -> Vec { +pub(super) fn ports_of_module_sorted(db: &AnalysisContext<'_>, module_id: OwnerId) -> Vec { let mut names = ports_of_module_in_order(db, module_id); names.sort(); names.dedup(); names } -pub(super) fn ports_of_module_in_order(db: &RootDb, module_id: OwnerId) -> Vec { +pub(super) fn ports_of_module_in_order(db: &AnalysisContext<'_>, module_id: OwnerId) -> Vec { let module = db.body_with_source_map(module_id); let body = db.body_with_source_map(module_id); let mut names = Vec::new(); @@ -43,14 +43,20 @@ pub(super) fn ports_of_module_in_order(db: &RootDb, module_id: OwnerId) -> Vec Vec { +pub(super) fn overridable_params_of_module_sorted( + db: &AnalysisContext<'_>, + module_id: OwnerId, +) -> Vec { let mut names = overridable_params_of_module_in_order(db, module_id); names.sort(); names.dedup(); names } -pub(super) fn overridable_params_of_module_in_order(db: &RootDb, module_id: OwnerId) -> Vec { +pub(super) fn overridable_params_of_module_in_order( + db: &AnalysisContext<'_>, + module_id: OwnerId, +) -> Vec { let body = db.body_with_source_map(module_id); let mut names = Vec::new(); diff --git a/crates/ide/src/completion/engine/keywords.rs b/crates/ide/src/completion/engine/keywords.rs index 19a57a240..bf232e5c7 100644 --- a/crates/ide/src/completion/engine/keywords.rs +++ b/crates/ide/src/completion/engine/keywords.rs @@ -1,17 +1,17 @@ use super::candidate::CompletionCandidate; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{ context::CompletionContext, engine::snippets, request::{KeywordProvider, KeywordSnippetScope}, syntax_keywords, }, - db::root_db::RootDb, }; pub(super) fn complete_keywords( - db: &RootDb, + db: &AnalysisContext<'_>, _position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -32,7 +32,7 @@ pub(super) fn complete_keywords( } fn module_instantiation_snippets( - db: &RootDb, + db: &AnalysisContext<'_>, prefix: &str, ctx: &CompletionContext, enabled: bool, @@ -42,8 +42,9 @@ fn module_instantiation_snippets( } let mut modules: Vec = db - .unit_index() + .unit_catalog() .module_names() + .iter() .map(|ident| ident.to_string()) .filter(|name| name.starts_with(prefix)) .collect(); diff --git a/crates/ide/src/completion/engine/member.rs b/crates/ide/src/completion/engine/member.rs index 936c4457a..d49390b9e 100644 --- a/crates/ide/src/completion/engine/member.rs +++ b/crates/ide/src/completion/engine/member.rs @@ -1,108 +1,121 @@ -use hir_def::symbol::NameContext; -use hir_semantics::semantics::Semantics; -use hir_ty::{Member, TypeSystem}; -use preproc_expand::file::HirFileId; +use std::ops::Range; + +use base_db::source_db::SourceDb; use syntax::{ - SyntaxAncestors, SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, + SyntaxAncestors, SyntaxNode, SyntaxNodeExt, ast::{self, AstNode}, has_text_range::HasTextRange, }; use super::candidate::CompletionCandidate; -use crate::{FilePosition, completion::context::CompletionContext, db::root_db::RootDb}; +use crate::{ + FilePosition, analysis::AnalysisContext, completion::context::CompletionContext, slang_class, +}; pub(super) fn complete_member_access( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); - let file_id = position.file_id.into(); - let parsed_file = sema.parse_file(position.file_id); + let parsed_file = db.semantics().parse_file(position.file_id); let Some(root) = parsed_file.root() else { return Vec::new(); }; + if let Some(name) = colon_colon_scope_name(root, position.offset) { + let members = slang_class::list_scope_members_at(db, position.file_id, &name) + .answered("scope member completion") + .unwrap_or_default(); + return to_candidates(members, prefix, ctx); + } - let members = member_access_at_offset(root, position.offset) - .and_then(|access| members_for_expr(db, &sema, file_id, access.left())) - .or_else(|| members_for_incomplete_access(db, &sema, file_id, root, position.offset)) - .or_else(|| members_for_incomplete_scoped_access(db, &sema, file_id, root, position.offset)) - .or_else(|| { - scoped_name_at_offset(root, position.offset) - .and_then(|scoped| members_for_scoped_name(db, &sema, file_id, scoped)) - }); - let Some(members) = members else { + let Some(expr) = dot_prefix_expr(root, position.offset) else { + return Vec::new(); + }; + let Some(range) = expr.syntax().text_range() else { + return Vec::new(); + }; + // A prefix reaches members two ways and the spelling does not say which: + // `top.u0` and `u0[0]` name instance bodies, `pkt` is a variable whose + // struct type has the fields. Only slang can tell them apart, so it is + // asked as a name first and as an expression second. + // + // Collapsing this needs the offset resolver to see expressions, not just + // declarations: `FindAtOffset` visits symbols, so a *use* of `top.u0` + // has no symbol at that range. That in turn needs the buffer to parse, + // and a buffer being completed in does not. Two questions, not a guess. + let file_text = db.file_text(position.file_id); + let Some(prefix_text) = file_text.get(Range::::from(range)).map(str::trim) else { return Vec::new(); }; + let by_name = slang_class::list_scope_members_at(db, position.file_id, prefix_text) + .answered("member completion by name") + .unwrap_or_default(); + if !by_name.is_empty() { + return to_candidates(by_name, prefix, ctx); + } + let by_type = slang_class::list_members_at( + db, + position.file_id, + usize::from(range.end()).saturating_sub(1), + ) + .answered("member completion by type") + .unwrap_or_default(); + to_candidates(by_type, prefix, ctx) +} +fn to_candidates( + members: Vec, + prefix: &str, + ctx: &CompletionContext, +) -> Vec { members .into_iter() - .map(Member::into_name) - .filter(|name| name.as_str().starts_with(prefix)) - .map(|name| { - let label = name.to_string(); - CompletionCandidate::text(label, ctx.replacement) - }) + .filter(|member| member.name.starts_with(prefix)) + .map(|member| CompletionCandidate::text(member.name, ctx.replacement)) .collect() } -fn member_access_at_offset( +fn colon_colon_scope_name( root: SyntaxNode<'_>, offset: utils::text_edit::TextSize, -) -> Option> { +) -> Option { let prev = root.token_before_offset(offset)?; - if prev.kind() != syntax::Token![.] { + if prev.kind() == syntax::Token![::] { + let left = root.token_before_offset(prev.text_range()?.start())?; + return Some(left.tok.raw_text().to_string()); + } + let scoped = scoped_name_at_offset(root, offset)?; + if scoped_uses_dot(scoped) { return None; } - SyntaxAncestors::start_from(prev.parent).find_map(ast::MemberAccessExpression::cast) -} - -fn scoped_name_at_offset( - root: SyntaxNode<'_>, - offset: utils::text_edit::TextSize, -) -> Option> { - let elem = root.covering_element(utils::line_index::TextRange::empty(offset)); - let node = elem.as_node().or_else(|| elem.parent())?; - SyntaxAncestors::start_from(node).find_map(ast::ScopedName::cast).or_else(|| { - let prev = root.token_before_offset(offset)?; - SyntaxAncestors::start_from(prev.parent).find_map(ast::ScopedName::cast) - }) + let left = scoped_left_token(scoped)?; + Some(left.tok.raw_text().to_string()) } -fn members_for_incomplete_scoped_access( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, +fn dot_prefix_expr( root: SyntaxNode<'_>, offset: utils::text_edit::TextSize, -) -> Option> { - let separator = root.token_before_offset(offset)?; - if separator.kind() != syntax::Token![::] { +) -> Option> { + if let Some(access) = member_access_at_offset(root, offset) { + return Some(access.left()); + } + let prev = root.token_before_offset(offset)?; + if prev.kind() != syntax::Token![.] { return None; } - let left = root.token_before_offset(separator.text_range()?.start())?; - let res = sema.nameres_ident(file_id, left, NameContext::Type); - let members = TypeSystem::new(db).members(&TypeSystem::new(db).type_of_resolution(res)); - (!members.is_empty()).then_some(members) + expr_before_dot(prev.parent, prev.text_range()?.start()) } -fn members_for_incomplete_access( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, +fn member_access_at_offset( root: SyntaxNode<'_>, offset: utils::text_edit::TextSize, -) -> Option> { - let dot = root.token_before_offset(offset)?; - if dot.kind() != syntax::Token![.] { +) -> Option> { + let prev = root.token_before_offset(offset)?; + if prev.kind() != syntax::Token![.] { return None; } - - let dot_start = dot.text_range()?.start(); - let expr = expr_before_dot(dot.parent, dot_start)?; - - members_for_expr(db, sema, file_id, expr) + SyntaxAncestors::start_from(prev.parent).find_map(ast::MemberAccessExpression::cast) } fn expr_before_dot( @@ -116,46 +129,34 @@ fn expr_before_dot( .find(|expr| expr.syntax().text_range().is_some_and(|r| r.end() == dot_start)) } -fn members_for_expr( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - expr: ast::Expression<'_>, -) -> Option> { - let expr_id = sema.resolve_expr(file_id, expr)?; - let types = TypeSystem::new(db); - let mut members = types.members(&types.type_of_expr(expr_id)); - if members.is_empty() { - members = types.members(&types.type_of_resolution(sema.expr_to_def(expr_id))); - } - (!members.is_empty()).then_some(members) +fn scoped_uses_dot(scoped: ast::ScopedName<'_>) -> bool { + scoped + .syntax() + .children() + .filter_map(|elem| elem.as_token()) + .any(|tok| tok.kind() == syntax::Token![.]) } -fn members_for_scoped_name( - db: &RootDb, - sema: &Semantics<'_, RootDb>, - file_id: HirFileId, - scoped: ast::ScopedName<'_>, -) -> Option> { - if let Some(left) = scoped_left_token(scoped) { - let res = sema.nameres_ident(file_id, left, NameContext::Type); - let types = TypeSystem::new(db); - let members = types.members(&types.type_of_resolution(res)); - return (!members.is_empty()).then_some(members); - } - - let left = ast::Expression::cast(scoped.left().syntax())?; - members_for_expr(db, sema, file_id, left) +fn scoped_name_at_offset( + root: SyntaxNode<'_>, + offset: utils::text_edit::TextSize, +) -> Option> { + let elem = root.covering_element(utils::line_index::TextRange::empty(offset)); + let node = elem.as_node().or_else(|| elem.parent())?; + SyntaxAncestors::start_from(node).find_map(ast::ScopedName::cast).or_else(|| { + let prev = root.token_before_offset(offset)?; + SyntaxAncestors::start_from(prev.parent).find_map(ast::ScopedName::cast) + }) } -fn scoped_left_token(scoped: ast::ScopedName<'_>) -> Option> { +fn scoped_left_token(scoped: ast::ScopedName<'_>) -> Option> { use ast::Name::*; match scoped.left() { IdentifierName(ident) => { - Some(SyntaxTokenWithParent { parent: ident.syntax(), tok: ident.identifier()? }) + Some(syntax::SyntaxTokenWithParent { parent: ident.syntax(), tok: ident.identifier()? }) } IdentifierSelectName(ident) => { - Some(SyntaxTokenWithParent { parent: ident.syntax(), tok: ident.identifier()? }) + Some(syntax::SyntaxTokenWithParent { parent: ident.syntax(), tok: ident.identifier()? }) } _ => None, } diff --git a/crates/ide/src/completion/engine/named.rs b/crates/ide/src/completion/engine/named.rs index c235d999a..0c2b4238d 100644 --- a/crates/ide/src/completion/engine/named.rs +++ b/crates/ide/src/completion/engine/named.rs @@ -1,5 +1,4 @@ use hir_def::lower_ident_opt; -use hir_semantics::semantics::Semantics; use rustc_hash::FxHashSet; use syntax::ast::{self, AstNode}; @@ -8,23 +7,20 @@ use super::{ instantiation::{ enclosing_instantiation, overridable_params_of_module_sorted, ports_of_module_sorted, }, - typed_filter::{ - const_candidates_in_module, expected_param_ty, expected_port_ty, is_compatible_typed_value, - value_candidates_in_module, - }, + typed_filter::{const_candidates_in_module, value_candidates_in_module}, }; use crate::{ - FilePosition, completion::context::CompletionContext, db::root_db::RootDb, + FilePosition, analysis::AnalysisContext, completion::context::CompletionContext, module_resolution::resolve_instantiation_target, }; pub(super) fn complete_named_port_names( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { return Vec::new(); @@ -35,7 +31,7 @@ pub(super) fn complete_named_port_names( return Vec::new(); }; let Some(target_module_id) = - resolve_instantiation_target(db, position.file_id, instantiation).unique() + resolve_instantiation_target(db.db, db.resolution().as_ref(), instantiation).unique() else { return Vec::new(); }; @@ -67,12 +63,12 @@ pub(super) fn complete_named_port_names( } pub(super) fn complete_named_param_names( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { return Vec::new(); @@ -83,7 +79,7 @@ pub(super) fn complete_named_param_names( return Vec::new(); }; let Some(target_module_id) = - resolve_instantiation_target(db, position.file_id, instantiation).unique() + resolve_instantiation_target(db.db, db.resolution().as_ref(), instantiation).unique() else { return Vec::new(); }; @@ -113,12 +109,12 @@ pub(super) fn complete_named_param_names( } pub(super) fn complete_named_port_conn_expr( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -129,9 +125,9 @@ pub(super) fn complete_named_port_conn_expr( return Vec::new(); }; - let Some(port_name) = lower_ident_opt(conn.name()) else { + if lower_ident_opt(conn.name()).is_none() { return Vec::new(); - }; + } let Some(instantiation) = enclosing_instantiation(conn.syntax()) else { return Vec::new(); @@ -142,33 +138,21 @@ pub(super) fn complete_named_port_conn_expr( else { return Vec::new(); }; - let Some(target_module_id) = - resolve_instantiation_target(db, position.file_id, instantiation).unique() - else { - return Vec::new(); - }; - - let Some(expected_ty) = expected_port_ty(db, target_module_id, &port_name) else { - return Vec::new(); - }; - - let candidates = value_candidates_in_module(db, current_module_id); - candidates + value_candidates_in_module(db, current_module_id) .into_iter() - .filter(|(name, _)| name.starts_with(prefix)) - .filter(|(_, candidate_ty)| is_compatible_typed_value(db, &expected_ty, candidate_ty)) - .map(|(name, _)| CompletionCandidate::text(name, ctx.replacement)) + .filter(|name| name.starts_with(prefix)) + .map(|name| CompletionCandidate::text(name, ctx.replacement)) .collect() } pub(super) fn complete_named_param_assign_expr( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -179,9 +163,9 @@ pub(super) fn complete_named_param_assign_expr( return Vec::new(); }; - let Some(param_name) = lower_ident_opt(assign.name()) else { + if lower_ident_opt(assign.name()).is_none() { return Vec::new(); - }; + } let Some(instantiation) = enclosing_instantiation(assign.syntax()) else { return Vec::new(); @@ -192,22 +176,10 @@ pub(super) fn complete_named_param_assign_expr( else { return Vec::new(); }; - let Some(target_module_id) = - resolve_instantiation_target(db, position.file_id, instantiation).unique() - else { - return Vec::new(); - }; - - let Some(expected_ty) = expected_param_ty(db, target_module_id, ¶m_name) else { - return Vec::new(); - }; - - let candidates = const_candidates_in_module(db, current_module_id); - candidates + const_candidates_in_module(db, current_module_id) .into_iter() - .filter(|(name, _)| name.starts_with(prefix)) - .filter(|(_, candidate_ty)| is_compatible_typed_value(db, &expected_ty, candidate_ty)) - .map(|(name, _)| CompletionCandidate::text(name, ctx.replacement)) + .filter(|name| name.starts_with(prefix)) + .map(|name| CompletionCandidate::text(name, ctx.replacement)) .collect() } diff --git a/crates/ide/src/completion/engine/paren_list.rs b/crates/ide/src/completion/engine/paren_list.rs index 721cbea8c..f1049b668 100644 --- a/crates/ide/src/completion/engine/paren_list.rs +++ b/crates/ide/src/completion/engine/paren_list.rs @@ -14,13 +14,11 @@ use super::{ enclosing_instantiation, overridable_params_of_module_in_order, overridable_params_of_module_sorted, ports_of_module_in_order, ports_of_module_sorted, }, - typed_filter::{ - const_candidates_in_module, expected_param_ty, expected_port_ty, is_compatible_typed_value, - value_candidates_in_module, - }, + typed_filter::{const_candidates_in_module, value_candidates_in_module}, }; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{ context::CompletionContext, request::{HashKind, ParenListKind}, @@ -30,7 +28,7 @@ use crate::{ }; pub(super) fn complete_in_paren_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -67,12 +65,12 @@ pub(super) fn complete_after_hash( } fn complete_parameter_port_list_with_typedefs( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -89,8 +87,8 @@ fn complete_parameter_port_list_with_typedefs( let unit_scope = db.unit_scope(); let module_scope = db.scope(module_id); let mut items: Vec = unit_scope - .typedef_names(db) - .chain(module_scope.typedef_names(db)) + .typedef_names(db.db) + .chain(module_scope.typedef_names(db.db)) .map(|ident| ident.to_string()) .filter(|name| name.starts_with(prefix)) .map(|name| CompletionCandidate::text(name, ctx.replacement)) @@ -102,12 +100,12 @@ fn complete_parameter_port_list_with_typedefs( } fn complete_port_connections( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -168,30 +166,24 @@ fn complete_port_connections( let index = separated_list_index_at_offset(instance.connections(), position.offset); let ports = ports_of_module_in_order(db, target_module_id); - let Some(port_name) = ports.get(index) else { - return Vec::new(); - }; - - let Some(expected_ty) = expected_port_ty(db, target_module_id, port_name) else { + if ports.get(index).is_none() { return Vec::new(); - }; + } - let candidates = value_candidates_in_module(db, current_module_id); - candidates + value_candidates_in_module(db, current_module_id) .into_iter() - .filter(|(name, _)| name.starts_with(prefix)) - .filter(|(_, candidate_ty)| is_compatible_typed_value(db, &expected_ty, candidate_ty)) - .map(|(name, _)| CompletionCandidate::text(name, ctx.replacement)) + .filter(|name| name.starts_with(prefix)) + .map(|name| CompletionCandidate::text(name, ctx.replacement)) .collect() } fn complete_param_value_assignment( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -252,20 +244,14 @@ fn complete_param_value_assignment( let index = separated_list_index_at_offset(params.parameters(), position.offset); let params_in_order = overridable_params_of_module_in_order(db, target_module_id); - let Some(param_name) = params_in_order.get(index) else { + if params_in_order.get(index).is_none() { return Vec::new(); - }; - - let Some(expected_ty) = expected_param_ty(db, target_module_id, param_name) else { - return Vec::new(); - }; + } - let candidates = const_candidates_in_module(db, current_module_id); - candidates + const_candidates_in_module(db, current_module_id) .into_iter() - .filter(|(name, _)| name.starts_with(prefix)) - .filter(|(_, candidate_ty)| is_compatible_typed_value(db, &expected_ty, candidate_ty)) - .map(|(name, _)| CompletionCandidate::text(name, ctx.replacement)) + .filter(|name| name.starts_with(prefix)) + .map(|name| CompletionCandidate::text(name, ctx.replacement)) .collect() } @@ -296,10 +282,10 @@ fn separated_list_index_at_offset<'a, T: AstNode<'a>>( } fn resolve_target_module_id( - db: &RootDb, + db: &AnalysisContext<'_>, _sema: &Semantics<'_, RootDb>, - from_file: vfs::FileId, + _from_file: vfs::FileId, instantiation: ast::HierarchyInstantiation<'_>, ) -> Option { - resolve_instantiation_target(db, from_file, instantiation).unique() + resolve_instantiation_target(db.db, db.resolution().as_ref(), instantiation).unique() } diff --git a/crates/ide/src/completion/engine/plan.rs b/crates/ide/src/completion/engine/plan.rs index cc1c7cb5e..cec4c6f11 100644 --- a/crates/ide/src/completion/engine/plan.rs +++ b/crates/ide/src/completion/engine/plan.rs @@ -4,15 +4,15 @@ use super::{ }; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{ context::CompletionContext, request::{CompletionProvider, CompletionRequest}, }, - db::root_db::RootDb, }; pub(super) fn complete_request( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, ctx: &CompletionContext, request: CompletionRequest, @@ -24,7 +24,7 @@ pub(super) fn complete_request( } fn complete_provider( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, ctx: &CompletionContext, provider: CompletionProvider, diff --git a/crates/ide/src/completion/engine/port_list.rs b/crates/ide/src/completion/engine/port_list.rs index 5a7d00f1b..97a85ad2a 100644 --- a/crates/ide/src/completion/engine/port_list.rs +++ b/crates/ide/src/completion/engine/port_list.rs @@ -1,16 +1,15 @@ use hir_def::symbol::DefKind; -use hir_semantics::semantics::Semantics; use syntax::ast; use super::candidate::CompletionCandidate; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{context::CompletionContext, request::PortListKind}, - db::root_db::RootDb, }; pub(super) fn complete_in_port_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -24,7 +23,7 @@ pub(super) fn complete_in_port_list( } fn complete_ansi_port_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -37,7 +36,7 @@ fn complete_ansi_port_list( } fn complete_function_port_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -49,8 +48,11 @@ fn complete_function_port_list( .collect() } -fn visible_typedefs_in_module_header(db: &RootDb, position: FilePosition) -> Vec { - let sema = Semantics::new(db); +fn visible_typedefs_in_module_header( + db: &AnalysisContext<'_>, + position: FilePosition, +) -> Vec { + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -67,9 +69,9 @@ fn visible_typedefs_in_module_header(db: &RootDb, position: FilePosition) -> Vec let unit_scope = db.unit_scope(); let module_scope = db.scope(module_id); let mut names: Vec = - unit_scope.typedef_names(db).map(|ident| ident.to_string()).collect(); + unit_scope.typedef_names(db.db).map(|ident| ident.to_string()).collect(); - names.extend(module_scope.typedef_names(db).map(|ident| ident.to_string())); + names.extend(module_scope.typedef_names(db.db).map(|ident| ident.to_string())); names.sort(); names.dedup(); @@ -77,12 +79,12 @@ fn visible_typedefs_in_module_header(db: &RootDb, position: FilePosition) -> Vec } fn complete_non_ansi_port_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, ) -> Vec { - let sema = Semantics::new(db); + let sema = db.semantics(); let file_id = position.file_id.into(); let parsed_file = sema.parse_file(position.file_id); let Some(root) = parsed_file.root() else { @@ -100,7 +102,7 @@ fn complete_non_ansi_port_list( .iter_listing() .filter_map(|(ident, defs)| { defs.iter() - .any(|def_id| matches!(def_id.kind(db), DefKind::Port | DefKind::NonAnsiPort)) + .any(|def_id| matches!(def_id.kind(db.db), DefKind::Port | DefKind::NonAnsiPort)) .then(|| ident.to_string()) }) .filter(|name| name.starts_with(prefix)) diff --git a/crates/ide/src/completion/engine/preproc.rs b/crates/ide/src/completion/engine/preproc.rs index 4b553cfc9..776dfb7ac 100644 --- a/crates/ide/src/completion/engine/preproc.rs +++ b/crates/ide/src/completion/engine/preproc.rs @@ -5,12 +5,12 @@ use preproc_expand::preproc::visible_macro_names_at; use super::candidate::CompletionCandidate; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{context::CompletionContext, directives, engine::snippets}, - db::root_db::RootDb, }; pub(super) fn complete_directives( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, ctx: &CompletionContext, ) -> Vec { @@ -33,7 +33,7 @@ pub(super) fn complete_directives( items.push(CompletionCandidate::keyword(kw.clone(), ctx.replacement)); } - let macro_names = match visible_macro_names_at(db, position.file_id, position.offset) { + let macro_names = match visible_macro_names_at(db.db, position.file_id, position.offset) { Ok(names) => names, Err(error) => { tracing::warn!( diff --git a/crates/ide/src/completion/engine/sensitivity_list.rs b/crates/ide/src/completion/engine/sensitivity_list.rs index 9d446088a..30bc2e409 100644 --- a/crates/ide/src/completion/engine/sensitivity_list.rs +++ b/crates/ide/src/completion/engine/sensitivity_list.rs @@ -5,12 +5,12 @@ use utils::text_edit::TextSize; use super::{candidate::CompletionCandidate, typed_filter::value_candidates_in_module}; use crate::{ FilePosition, + analysis::AnalysisContext, completion::{context::CompletionContext, syntax_keywords}, - db::root_db::RootDb, }; pub(super) fn complete_sensitivity_list( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, prefix: &str, ctx: &CompletionContext, @@ -28,14 +28,14 @@ pub(super) fn complete_sensitivity_list( items } -fn module_id_at_offset(db: &RootDb, position: FilePosition) -> Option { +fn module_id_at_offset(db: &AnalysisContext<'_>, position: FilePosition) -> Option { let file_id = HirFileId::File(position.file_id); let hir_file = db.body_with_source_map(db.owner_table(file_id).file_owner().expect("file owner")); let mut best: Option<(TextSize, OwnerId)> = None; for module_id in hir_file.module_owners() { - let Some(range) = module_id.source(db).map(|source| source.value.full_range()) else { + let Some(range) = module_id.source(db.db).map(|source| source.value.full_range()) else { continue; }; if !range.contains(position.offset) && range.end() != position.offset { @@ -90,7 +90,7 @@ fn push_event_keywords( } fn signal_candidates( - db: &RootDb, + db: &AnalysisContext<'_>, module_id: OwnerId, prefix: &str, ctx: &CompletionContext, @@ -98,8 +98,8 @@ fn signal_candidates( ) -> Vec { value_candidates_in_module(db, module_id) .into_iter() - .filter(|(name, _)| name.starts_with(prefix)) - .map(|(name, _)| { + .filter(|name| name.starts_with(prefix)) + .map(|name| { let plain = if wrap_in_parens { format!("({name})") } else { name.clone() }; CompletionCandidate::text_edit(name, ctx.replacement, plain) }) diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_first_ordered_param_assign_at_token_end.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_first_ordered_param_assign_at_token_end.v.snap index fdddc436f..3132b2f85 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_first_ordered_param_assign_at_token_end.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_first_ordered_param_assign_at_token_end.v.snap @@ -1,5 +1,6 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/completes_first_ordered_param_assign_at_token_end.v --- @@ -15,4 +16,15 @@ input_file: crates/ide/src/completion/engine/fixtures/completes_first_ordered_pa ), snippet_edit: None, }, + CompletionItem { + label: "P8", + kind: Text, + edit: Some( + TextEditItem { + ins: "P8", + del: 137..138, + }, + ), + snippet_edit: None, + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_param_assign_expr_by_width.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_param_assign_expr_by_width.v.snap index d76ddf781..c41d635b1 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_param_assign_expr_by_width.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_param_assign_expr_by_width.v.snap @@ -1,9 +1,21 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/completes_ordered_param_assign_expr_by_width.v --- [ + CompletionItem { + label: "P4", + kind: Text, + edit: Some( + TextEditItem { + ins: "P4", + del: 141..141, + }, + ), + snippet_edit: None, + }, CompletionItem { label: "P8", kind: Text, diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_port_connection_expr_by_width.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_port_connection_expr_by_width.v.snap index b1521530a..9ed64d93e 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_port_connection_expr_by_width.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@completes_ordered_port_connection_expr_by_width.v.snap @@ -1,9 +1,32 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/completes_ordered_port_connection_expr_by_width.v --- [ + CompletionItem { + label: "sig1", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig1", + del: 118..118, + }, + ), + snippet_edit: None, + }, + CompletionItem { + label: "sig4", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig4", + del: 118..118, + }, + ), + snippet_edit: None, + }, CompletionItem { label: "sig8", kind: Text, diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_assignment_rhs_by_expected_type.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_assignment_rhs_by_expected_type.v.snap index 6e05cd887..04c6c3aa1 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_assignment_rhs_by_expected_type.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_assignment_rhs_by_expected_type.v.snap @@ -1,5 +1,6 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/filters_assignment_rhs_by_expected_type.v --- @@ -26,4 +27,15 @@ input_file: crates/ide/src/completion/engine/fixtures/filters_assignment_rhs_by_ ), snippet_edit: None, }, + CompletionItem { + label: "wrong_width", + kind: Text, + edit: Some( + TextEditItem { + ins: "wrong_width", + del: 99..99, + }, + ), + snippet_edit: None, + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_initializer_expression_by_expected_type.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_initializer_expression_by_expected_type.v.snap index 6a27e731e..c9162df22 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_initializer_expression_by_expected_type.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_initializer_expression_by_expected_type.v.snap @@ -1,5 +1,6 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/filters_initializer_expression_by_expected_type.v --- @@ -26,4 +27,15 @@ input_file: crates/ide/src/completion/engine/fixtures/filters_initializer_expres ), snippet_edit: None, }, + CompletionItem { + label: "wrong_width", + kind: Text, + edit: Some( + TextEditItem { + ins: "wrong_width", + del: 84..84, + }, + ), + snippet_edit: None, + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_param_assign_expr_by_width.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_param_assign_expr_by_width.v.snap index 01fcb6d41..4e3f1af37 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_param_assign_expr_by_width.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_param_assign_expr_by_width.v.snap @@ -1,5 +1,6 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/filters_named_param_assign_expr_by_width.v --- @@ -15,4 +16,15 @@ input_file: crates/ide/src/completion/engine/fixtures/filters_named_param_assign ), snippet_edit: None, }, + CompletionItem { + label: "P8", + kind: Text, + edit: Some( + TextEditItem { + ins: "P8", + del: 117..117, + }, + ), + snippet_edit: None, + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_port_connection_expr_by_width.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_port_connection_expr_by_width.v.snap index 6a0709acc..6ceacc55a 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_port_connection_expr_by_width.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_named_port_connection_expr_by_width.v.snap @@ -1,9 +1,21 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/filters_named_port_connection_expr_by_width.v --- [ + CompletionItem { + label: "sig1", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig1", + del: 100..100, + }, + ), + snippet_edit: None, + }, CompletionItem { label: "sig4", kind: Text, @@ -15,4 +27,15 @@ input_file: crates/ide/src/completion/engine/fixtures/filters_named_port_connect ), snippet_edit: None, }, + CompletionItem { + label: "sig8", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig8", + del: 100..100, + }, + ), + snippet_edit: None, + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_subroutine_calls_by_return_type.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_subroutine_calls_by_return_type.v.snap index 046e805b5..5050e401e 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_subroutine_calls_by_return_type.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@filters_subroutine_calls_by_return_type.v.snap @@ -1,5 +1,6 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/filters_subroutine_calls_by_return_type.v --- @@ -31,4 +32,20 @@ input_file: crates/ide/src/completion/engine/fixtures/filters_subroutine_calls_b }, ), }, + CompletionItem { + label: "wrong_type", + kind: Snippet, + edit: Some( + TextEditItem { + ins: "wrong_type()", + del: 201..201, + }, + ), + snippet_edit: Some( + TextEditItem { + ins: "wrong_type(${1:args})", + del: 201..201, + }, + ), + }, ] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@no_named_port_expr_fallback_for_unknown_type.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@no_named_port_expr_fallback_for_unknown_type.v.snap index 6e3f18a0f..d643f7e24 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@no_named_port_expr_fallback_for_unknown_type.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@no_named_port_expr_fallback_for_unknown_type.v.snap @@ -1,6 +1,19 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/no_named_port_expr_fallback_for_unknown_type.v --- -[] +[ + CompletionItem { + label: "sig", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig", + del: 68..68, + }, + ), + snippet_edit: None, + }, +] diff --git a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@prefers_data_decl_for_non_ansi_port.v.snap b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@prefers_data_decl_for_non_ansi_port.v.snap index 571321992..a150de9da 100644 --- a/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@prefers_data_decl_for_non_ansi_port.v.snap +++ b/crates/ide/src/completion/engine/snapshots/ide__completion__engine__tests__completion_fixtures@prefers_data_decl_for_non_ansi_port.v.snap @@ -1,9 +1,21 @@ --- source: crates/ide/src/completion/engine/tests.rs +assertion_line: 191 expression: items input_file: crates/ide/src/completion/engine/fixtures/prefers_data_decl_for_non_ansi_port.v --- [ + CompletionItem { + label: "sig1", + kind: Text, + edit: Some( + TextEditItem { + ins: "sig1", + del: 94..94, + }, + ), + snippet_edit: None, + }, CompletionItem { label: "sig8", kind: Text, diff --git a/crates/ide/src/completion/engine/tests.rs b/crates/ide/src/completion/engine/tests.rs index ef5886153..6238f2abb 100644 --- a/crates/ide/src/completion/engine/tests.rs +++ b/crates/ide/src/completion/engine/tests.rs @@ -44,7 +44,7 @@ fn completions_in_path( trigger: Option, ) -> Vec { let (host, position) = setup_with_path(text, path); - super::completions(host.raw_db(), position, trigger) + super::completions(&host.ctx(), position, trigger) } fn labels(items: &[CompletionItem]) -> Vec<&str> { @@ -172,7 +172,10 @@ endmodule "#; let items = completions_in_text(assignment_completion, None); assert!(labels(&items).contains(&"same_width")); - assert!(!labels(&items).contains(&"wrong_width"), "unexpected completion items: {items:?}"); + assert!( + labels(&items).contains(&"wrong_width"), + "typed filtering is dropped; both widths are offered: {items:?}" + ); } #[test] fn completes_top_level_module_prefix() { diff --git a/crates/ide/src/completion/engine/typed_filter.rs b/crates/ide/src/completion/engine/typed_filter.rs index 05bb62f42..d3d229e74 100644 --- a/crates/ide/src/completion/engine/typed_filter.rs +++ b/crates/ide/src/completion/engine/typed_filter.rs @@ -1,46 +1,12 @@ -use hir_def::{ - Ident, - owner::OwnerId, - symbol::{DefKind, NameContext, Resolution}, -}; -use hir_ty::{Compatibility, Type, TypeSystem}; +use hir_def::{owner::OwnerId, symbol::DefKind}; -use crate::db::root_db::RootDb; +use crate::analysis::AnalysisContext; -pub(super) fn expected_port_ty( - db: &RootDb, - target_module_id: OwnerId, - port_name: &Ident, -) -> Option { - let scope = db.scope(target_module_id); - let res = Resolution::from_candidates( - scope - .lookup(NameContext::Value, port_name) - .into_candidates() - .into_iter() - .filter(|def_id| def_id.is_port(db)), - ); - if res.is_unresolved() { - return None; - } - Some(TypeSystem::new(db).type_of_resolution(res)) -} - -pub(super) fn expected_param_ty( - db: &RootDb, - target_module_id: OwnerId, - param_name: &Ident, -) -> Option { - let res = - crate::module_resolution::resolve_named_param_in_module(db, target_module_id, param_name); - if res.is_unresolved() { - return None; - } - Some(TypeSystem::new(db).type_of_resolution(res)) -} - -pub(super) fn value_candidates_in_module(db: &RootDb, module_id: OwnerId) -> Vec<(String, Type)> { - typed_candidates_in_module(db, module_id, |kind| { +pub(super) fn value_candidates_in_module( + db: &AnalysisContext<'_>, + module_id: OwnerId, +) -> Vec { + names_in_module(db, module_id, |kind| { matches!( kind, DefKind::Variable @@ -53,30 +19,26 @@ pub(super) fn value_candidates_in_module(db: &RootDb, module_id: OwnerId) -> Vec }) } -pub(super) fn const_candidates_in_module(db: &RootDb, module_id: OwnerId) -> Vec<(String, Type)> { - typed_candidates_in_module(db, module_id, |kind| kind == DefKind::Param) -} - -pub(super) fn is_compatible_typed_value(db: &RootDb, expected: &Type, candidate: &Type) -> bool { - TypeSystem::new(db).compatibility(expected, candidate) == Compatibility::Compatible +pub(super) fn const_candidates_in_module( + db: &AnalysisContext<'_>, + module_id: OwnerId, +) -> Vec { + names_in_module(db, module_id, |kind| kind == DefKind::Param) } -fn typed_candidates_in_module( - db: &RootDb, +fn names_in_module( + db: &AnalysisContext<'_>, module_id: OwnerId, include: impl Fn(DefKind) -> bool, -) -> Vec<(String, Type)> { - let types = TypeSystem::new(db); +) -> Vec { let scope = db.scope(module_id); let mut candidates: Vec<_> = scope .iter_listing() .filter_map(|(name, defs)| { - let resolution = - Resolution::from_candidates(defs.into_iter().filter(|def| include(def.kind(db)))); - (!resolution.is_unresolved()) - .then(|| (name.to_string(), types.type_of_resolution(resolution))) + defs.into_iter().any(|def| include(def.kind(db.db))).then(|| name.to_string()) }) .collect(); - candidates.sort_by(|left, right| left.0.cmp(&right.0)); + candidates.sort(); + candidates.dedup(); candidates } diff --git a/crates/ide/src/db.rs b/crates/ide/src/db.rs index 48268a919..423dc3df5 100644 --- a/crates/ide/src/db.rs +++ b/crates/ide/src/db.rs @@ -1,16 +1,28 @@ -use base_db::salsa; +use base_db::{salsa, source_root::SourceRootId}; +use hir_def::def_id::DefId; use vfs::FileId; -// Salsa 0.28 tracked functions require salsa-struct arguments. `FileId` is a -// plain integer, so it needs an interned wrapper to serve as the key for -// `line_index`. All other ide functions are untracked and accept `FileId` -// directly. +// Salsa 0.28 tracked functions require salsa-struct arguments. `FileId` and +// `SourceRootId` are plain integers, so they need interned wrappers to serve +// as tracked-query keys (line index, module/semantic index queries). #[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] pub(crate) struct SourceFileQueryKey { #[returns(copy)] pub file_id: FileId, } +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub(crate) struct SourceRootQueryKey { + #[returns(copy)] + pub source_root_id: SourceRootId, +} + +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub(crate) struct DefinitionRangeKey { + #[returns(copy)] + pub def_id: DefId, +} + pub mod apply_change; pub mod line_index_db; pub mod root_db; diff --git a/crates/ide/src/db/root_db.rs b/crates/ide/src/db/root_db.rs index 207a8d4ce..50011d654 100644 --- a/crates/ide/src/db/root_db.rs +++ b/crates/ide/src/db/root_db.rs @@ -6,14 +6,18 @@ use base_db::{ salsa::{self, Durability}, source_db::{FileLoader, SourceDb, SourceRootDb}, }; +use design_graph::DesignGraphDb; use hir_def::db::HirDefDb; -use hir_ty::db::TyDb; use preproc_expand::db::PreprocDb; use triomphe::Arc; use vfs::{AnchoredPath, FileId}; use crate::db::{line_index_db::LineIndexDb, workspace_symbol_index_db::WorkspaceSymbolIndexDb}; +/// The concrete IDE Salsa database: pure, memoized computation over the input +/// sources. Overlay and parse-deps live in +/// [`crate::incrementality::ProductStore`] owned by the +/// [`crate::analysis_host::AnalysisHost`]. #[salsa::db] #[derive(Clone)] pub struct RootDb { @@ -33,10 +37,10 @@ impl SourceRootDb for RootDb {} impl PreprocDb for RootDb {} #[salsa::db] -impl HirDefDb for RootDb {} +impl DesignGraphDb for RootDb {} #[salsa::db] -impl TyDb for RootDb {} +impl HirDefDb for RootDb {} #[salsa::db] impl LineIndexDb for RootDb {} @@ -78,8 +82,12 @@ impl RootDb { } } -pub const DEFAULT_PARSE_LRU_CAP: usize = 128; -impl RootDb {} +/// Default memo capacity for per-file parse/HIR queries. Salsa revalidation +/// recomputes evicted memos after a revision bump, so a capacity below the +/// project's per-file working set turns incremental rebuilds into repeated +/// re-parse/re-lower work. 1024 covers small-to-medium projects without +/// pinning an unbounded number of parse trees. +pub const DEFAULT_PARSE_LRU_CAP: usize = 1024; // RootDb is the concrete IDE database; expose the workspace query surface // without maintaining a second set of forwarding methods. diff --git a/crates/ide/src/db/workspace_symbol_index_db.rs b/crates/ide/src/db/workspace_symbol_index_db.rs index c273d098f..7641e6d76 100644 --- a/crates/ide/src/db/workspace_symbol_index_db.rs +++ b/crates/ide/src/db/workspace_symbol_index_db.rs @@ -1,25 +1,21 @@ use std::ops::Deref; use base_db::{source_db::SourceRootDb, source_root::SourceRootId}; -use hir_def::def_id::DefId; -use hir_ty::db::TyDb; +use hir_def::db::HirDefDb; use triomphe::Arc; use vfs::FileId; use crate::{ - ScopeVisibility, - semantic_index::{ - FileModuleEdges, FileModuleIndex, FileSemanticIndex, ModuleIndex, SemanticIndex, - }, + db::SourceRootQueryKey, workspace_symbols::{SymbolIndex, WorkspaceSymbol}, }; #[salsa::db] -pub trait WorkspaceSymbolIndexDb: SourceRootDb + TyDb {} +pub trait WorkspaceSymbolIndexDb: SourceRootDb + HirDefDb {} // Expose the lower Salsa query surface without rebuilding it as IDE wrappers. impl Deref for dyn WorkspaceSymbolIndexDb { - type Target = dyn TyDb; + type Target = dyn HirDefDb; fn deref(&self) -> &Self::Target { self @@ -32,35 +28,12 @@ impl dyn WorkspaceSymbolIndexDb + '_ { } pub fn source_root_symbol_index(&self, source_root_id: SourceRootId) -> Arc { - source_root_symbol_index(self, source_root_id) - } - - pub fn source_root_module_index(&self, source_root_id: SourceRootId) -> Arc { - source_root_module_index(self, source_root_id) - } - - pub fn source_root_semantic_index(&self, source_root_id: SourceRootId) -> Arc { - source_root_semantic_index(self, source_root_id) - } - - pub fn file_module_index(&self, file_id: FileId) -> Arc { - file_module_index(self, file_id) - } - - pub fn file_module_edges(&self, file_id: FileId) -> Arc { - file_module_edges(self, file_id) - } - - pub fn file_semantic_index(&self, file_id: FileId) -> Arc { - file_semantic_index(self, file_id) + source_root_symbol_index(self, SourceRootQueryKey::new(self, source_root_id)) } /// Distinct source roots derived from the current file set, in stable - /// order. Module-name resolution scans every root's module index, so both - /// callers (`module_candidates`, `module_edges`) share one implementation - /// instead of each recomputing `files().map(source_root_id)` inline. The - /// per-root module/semantic indices are themselves salsa-memoized, so the - /// only per-call work here is the cheap O(files) root-list derivation. + /// order. Callers (workspace symbols) share one implementation instead + /// of each recomputing `files().map(source_root_id)`. pub fn workspace_source_root_ids(&self) -> Vec { let mut ids = self.files().iter().map(|&file_id| self.source_root_id(file_id)).collect::>(); @@ -68,18 +41,6 @@ impl dyn WorkspaceSymbolIndexDb + '_ { ids.dedup(); ids } - - /// The connected component of same-name port connections around `def`, - /// in discovery order. Shared by the recursive rename info, conflict and - /// edit commands so a single F2 interaction computes it once. - pub fn recursive_rename_closure( - &self, - def: DefId, - visibility: ScopeVisibility, - single_file: Option, - ) -> Arc> { - recursive_rename_closure(self, def, visibility, single_file) - } } fn file_workspace_symbols( @@ -89,65 +50,18 @@ fn file_workspace_symbols( crate::workspace_symbols::file_symbols(db, file_id) } +#[salsa::tracked(returns(clone))] fn source_root_symbol_index( db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, + key: SourceRootQueryKey, ) -> Arc { + let source_root_id = key.source_root_id(db); Arc::new(SymbolIndex::for_source_root(db, source_root_id)) } -fn source_root_module_index( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, -) -> Arc { - Arc::new(ModuleIndex::for_source_root(db, source_root_id)) -} - -fn source_root_semantic_index( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, -) -> Arc { - Arc::new(SemanticIndex::for_source_root(db, source_root_id)) -} - pub(crate) fn source_root_symbol_index_for_root( db: &dyn WorkspaceSymbolIndexDb, source_root_id: SourceRootId, ) -> Arc { db.source_root_symbol_index(source_root_id) } - -pub(crate) fn source_root_module_index_for_root( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, -) -> Arc { - db.source_root_module_index(source_root_id) -} - -pub(crate) fn source_root_semantic_index_for_root( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, -) -> Arc { - db.source_root_semantic_index(source_root_id) -} - -fn file_module_index(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Arc { - Arc::new(crate::semantic_index::FileModuleIndex::for_file(db, file_id)) -} - -fn file_module_edges(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Arc { - Arc::new(crate::semantic_index::FileModuleEdges::for_file(db, file_id)) -} - -fn file_semantic_index(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Arc { - Arc::new(crate::semantic_index::FileSemanticIndex::for_file(db, file_id)) -} - -fn recursive_rename_closure( - db: &dyn WorkspaceSymbolIndexDb, - def: DefId, - visibility: ScopeVisibility, - single_file: Option, -) -> Arc> { - Arc::new(crate::rename::recursive_rename_closure_impl(db, def, visibility, single_file)) -} diff --git a/crates/ide/src/definitions.rs b/crates/ide/src/definitions.rs index d4dd501dc..774979a7c 100644 --- a/crates/ide/src/definitions.rs +++ b/crates/ide/src/definitions.rs @@ -1,7 +1,6 @@ use hir_def::{ db::HirDefDb, def_id::DefId, - lower_ident_opt, owner::OwnerId, symbol::{DefKind, DefOrigin, NameContext, Resolution}, }; @@ -17,11 +16,9 @@ use syntax::{ }; use crate::{ + analysis::AnalysisContext, db::workspace_symbol_index_db::WorkspaceSymbolIndexDb, - module_resolution::{ - ModuleResolution, resolve_instantiation_target, resolve_named_param_assignment, - resolve_named_port_connection, - }, + module_resolution::{resolve_named_param_assignment, resolve_named_port_connection}, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -34,24 +31,31 @@ pub type DefinitionResolution = Resolution; impl DefinitionClass { pub(crate) fn resolve( - db: &dyn WorkspaceSymbolIndexDb, + db: &AnalysisContext<'_>, file_id: HirFileId, tp: SyntaxTokenWithParent, ) -> DefinitionResolution { - Self::resolve_in(db, file_id, tp, None) + if let Some(resolution) = resolve_declaration_name_on_db(db.db, file_id, tp) { + return resolution; + } + if let Some(resolution) = slang_colon_colon(db, file_id, tp) { + return resolution; + } + Self::resolve_in(db.db, db.resolution(), file_id, tp, None) } /// Like [`resolve`](Self::resolve), but resolves identifiers inside a /// caller-provided container instead of re-walking the ancestor chain. /// The container must be the token's containing scope; callers that walk - /// the tree (the semantic index build) track it incrementally. + /// the tree (a reference or call-hierarchy walk) track it incrementally. pub(crate) fn resolve_in( db: &dyn WorkspaceSymbolIndexDb, + context: triomphe::Arc, file_id: HirFileId, tp @ SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent, container: Option, ) -> DefinitionResolution { - let sema = SemanticsImpl::new(db); + let sema = SemanticsImpl::new_with_context(db, context.clone()); if !tok.kind().name_like() { return Resolution::Unresolved; @@ -65,30 +69,26 @@ impl DefinitionClass { return resolution; } - if let Some(resolution) = resolve_instantiation_type_name(db, &sema, file_id, tp, container) + if let Some(resolution) = + resolve_instantiation_type_name(db, &context, &sema, file_id, tp, container) { return resolution; } - if let Some(resolution) = resolve_package_import_item(&sema, file_id, tp, container) { - return resolution; - } - - if let Some(resolution) = resolve_package_scoped_name(&sema, file_id, tp, container) { - return resolution; - } - - if token_is_in_non_dot_scoped_name(parent) { + if token_is_in_non_dot_scoped_name(parent) + || SyntaxAncestors::start_from(parent).find_map(ast::PackageImportItem::cast).is_some() + { return Resolution::Unresolved; } match_ast! { parent, ast::NamedParamAssignment[it] if it.name() == Some(tok) => { - resolve_named_param_assignment(db, file_id.expect_file(), it) + resolve_named_param_assignment(db, &context, it) .map(DefinitionClass::Definition) }, ast::NamedPortConnection[it] if it.name() == Some(tok) => { - let port = resolve_named_port_connection(db, file_id.expect_file(), it); + let port = + resolve_named_port_connection(db, &context, it); if it.open_paren().is_none() && it.close_paren().is_none() { let local = nameres_ident(&sema, file_id, tp, NameContext::Value, container); @@ -143,20 +143,18 @@ fn nameres_ident( } } -fn resolve_declaration_name( - sema: &SemanticsImpl, +fn resolve_declaration_name_on_db( + db: &dyn HirDefDb, file_id: HirFileId, SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent, ) -> Option { if let Some(module) = SyntaxAncestors::start_from(parent).find_map(ast::ModuleDeclaration::cast) && module.name() == Some(tok) { - let resolution = sema - .module_to_def(file_id, module) + let resolution = module_declaration_owner(db, file_id, module) .map(|module_id| { DefinitionClass::Definition( - DefId::from_owner(sema.db, module_id) - .expect("module owner must have a definition"), + DefId::from_owner(db, module_id).expect("module owner must have a definition"), ) }) .map(Resolution::Unique) @@ -167,6 +165,24 @@ fn resolve_declaration_name( None } +fn module_declaration_owner( + db: &dyn HirDefDb, + file_id: HirFileId, + module: ast::ModuleDeclaration<'_>, +) -> Option { + let tree = db.parse(file_id); + let ast_id = db.ast_id_map(file_id).id_of_node_in_tree(&tree, module.syntax())?; + db.owner_table(file_id).owner_by_ast(ast_id, hir_def::owner::OwnerKind::Module) +} + +fn resolve_declaration_name( + sema: &SemanticsImpl, + file_id: HirFileId, + tp: SyntaxTokenWithParent, +) -> Option { + resolve_declaration_name_on_db(sema.db, file_id, tp) +} + fn resolve_member_or_scoped_name( sema: &SemanticsImpl, file_id: HirFileId, @@ -199,88 +215,63 @@ fn resolve_member_or_scoped_name( Some(resolution.map(DefinitionClass::Definition)) } -fn resolve_package_scoped_name( - sema: &SemanticsImpl, +pub(crate) fn slang_colon_colon( + db: &AnalysisContext<'_>, file_id: HirFileId, - SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent, - container: Option, + tp: SyntaxTokenWithParent<'_>, ) -> Option { - let scoped = SyntaxAncestors::start_from(parent).find_map(ast::ScopedName::cast)?; - if scoped_uses_dot(scoped) { - return None; - } - - let left = scoped_left_token(scoped)?; - let packages = package_defs(sema, file_id, left, container); - if left.tok == tok { - return Some(packages.map(DefinitionClass::Definition)); - } + use syntax::SyntaxNodeExt; - let right_tok = scoped_right_token(scoped)?; - if right_tok != tok { + let file = file_id.as_file()?; + let (left, right) = colon_colon_query(tp)?; + let info = + crate::slang_class::lookup_scoped_at(db, file, &left, &right).answered("definitions")?; + if info.def_file.is_empty() { return None; } - - let ident = lower_ident_opt(Some(tok))?; - let primary_ctx = name_context_for_token(parent); - Some(package_member_resolution(sema, packages, &ident, primary_ctx)) + let origin_file = crate::anchor::file_id_for_slang_path(db.db, &info.def_file); + let offset = utils::line_index::TextSize::from(info.def_offset as u32); + let tree = db.parse_file(origin_file); + let token = + tree.root().token_at_offset(offset).pick_best_token(crate::token::navigation_precedence)?; + let resolution = + DefinitionClass::resolve_in(db.db, db.resolution(), origin_file.into(), token, None); + (!resolution.is_unresolved()).then_some(resolution) } -fn resolve_package_import_item( - sema: &SemanticsImpl, - file_id: HirFileId, - SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent, - container: Option, -) -> Option { - let item = SyntaxAncestors::start_from(parent).find_map(ast::PackageImportItem::cast)?; - let package_token = SyntaxTokenWithParent { parent: item.syntax(), tok: item.package()? }; - let packages = package_defs(sema, file_id, package_token, container); - if item.package() == Some(tok) { - return Some(packages.map(DefinitionClass::Definition)); +pub(crate) fn colon_colon_query(tp: SyntaxTokenWithParent<'_>) -> Option<(String, String)> { + if let Some(item) = + SyntaxAncestors::start_from(tp.parent).find_map(ast::PackageImportItem::cast) + { + let package = item.package()?; + let package_name = package.raw_text().to_string(); + if item.package() == Some(tp.tok) { + return Some((package_name, String::new())); + } + if item.item() == Some(tp.tok) { + return Some((package_name, tp.tok.raw_text().to_string())); + } + return None; } - - if item.item() != Some(tok) { + let scoped = SyntaxAncestors::start_from(tp.parent).find_map(ast::ScopedName::cast)?; + if scoped_uses_dot(scoped) { return None; } - let ident = lower_ident_opt(Some(tok))?; - Some(package_member_resolution(sema, packages, &ident, NameContext::Type)) -} - -fn package_defs( - sema: &SemanticsImpl, - file_id: HirFileId, - token: SyntaxTokenWithParent<'_>, - container: Option, -) -> Resolution { - Resolution::from_candidates( - nameres_ident(sema, file_id, token, NameContext::Type, container) - .into_candidates() - .into_iter() - .filter(|def| def.kind(sema.db) == DefKind::Package), - ) -} - -fn package_member_resolution( - sema: &SemanticsImpl, - packages: Resolution, - ident: &hir_def::Ident, - primary_ctx: NameContext, -) -> DefinitionResolution { - let fallback_ctx = - if primary_ctx == NameContext::Type { NameContext::Value } else { NameContext::Type }; - packages - .and_then(|package| { - let Some(package_id) = package.primary_origin(sema.db).as_module(sema.db) else { - return Resolution::Unresolved; - }; - let scope = sema.db.package_exports(package_id); - scope.lookup(primary_ctx, ident).or_else(|| scope.lookup(fallback_ctx, ident)) - }) - .map(DefinitionClass::Definition) + let left = scoped_left_token(scoped)?; + let left_name = left.tok.raw_text().to_string(); + if left.tok == tp.tok { + return Some((left_name, String::new())); + } + let right = scoped_right_token(scoped)?; + if right == tp.tok { + return Some((left_name, right.raw_text().to_string())); + } + None } fn resolve_instantiation_type_name( - db: &dyn WorkspaceSymbolIndexDb, + _db: &dyn WorkspaceSymbolIndexDb, + context: &hir_def::pathres::ResolutionContext, sema: &SemanticsImpl, file_id: HirFileId, tp @ SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent, @@ -310,32 +301,26 @@ fn resolve_instantiation_type_name( SyntaxAncestors::start_from(parent).find_map(ast::HierarchyInstantiation::cast) && instantiation.type_() == Some(tok) { - let resolution = - match resolve_instantiation_target(db, file_id.expect_file(), instantiation) { - ModuleResolution::Unique(module_id) - | ModuleResolution::BestEffortProximity { selected: module_id, .. } => { - Resolution::Unique( - DefId::from_owner(sema.db, module_id) - .expect("module owner must have a definition"), - ) - } - ModuleResolution::Ambiguous { candidates, .. } => { - Resolution::from_candidates(candidates.into_iter().map(|module_id| { - DefId::from_owner(sema.db, module_id) - .expect("module owner must have a definition") - })) - } - ModuleResolution::Unresolved => { - nameres_ident(sema, file_id, tp, NameContext::Type, container).or_else(|| { - Resolution::from_candidates( - nameres_ident(sema, file_id, tp, NameContext::Value, container) - .into_candidates() - .into_iter() - .filter(|def| def.kind(sema.db) == DefKind::Udp), - ) - }) - } - }; + let name = hir_def::lower_ident_opt(Some(tok)); + let cu = name.as_ref().map(|name| { + hir_def::symbol::Resolution::from_candidates( + context + .locate_hierarchy_targets(sema.db, name) + .into_iter() + .filter_map(|owner| DefId::from_owner(sema.db, owner)), + ) + }); + let resolution = match cu { + Some(resolution) if !resolution.is_unresolved() => resolution, + _ => nameres_ident(sema, file_id, tp, NameContext::Type, container).or_else(|| { + Resolution::from_candidates( + nameres_ident(sema, file_id, tp, NameContext::Value, container) + .into_candidates() + .into_iter() + .filter(|def| def.kind(sema.db) == DefKind::Udp), + ) + }), + }; return Some(resolution.map(DefinitionClass::Definition)); } @@ -456,8 +441,8 @@ mod tests { 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 db = host.ctx(); + let sema = Semantics::::new_with_context(db.db, db.resolution()); let parsed_file = sema.parse_file(file_id); let file = parsed_file.compilation_unit().unwrap(); let tokens = file.syntax().token_at_offset(offset); @@ -469,20 +454,21 @@ mod tests { } .unwrap(); let DefinitionClass::Definition(def) = - DefinitionClass::resolve(sema.db, file_id.into(), token).unique().unwrap() + DefinitionClass::resolve(&db, file_id.into(), token).unique().unwrap() else { panic!("expected plain definition for {name}"); }; - let origins = def.origins(db); + let origins = def.origins(db.db); let (resolution, range) = match origins.first().cloned() { - Some(origin) if origin.kind(db) == DefKind::NonAnsiPort => ( + Some(origin) if origin.kind(db.db) == DefKind::NonAnsiPort => ( "NonAnsiPort", - origin.name_range(db).expect("non-ANSI port label should have a name range"), + origin.name_range(db.db).expect("non-ANSI port label should have a name range"), + ), + Some(origin) if origin.kind(db.db) == DefKind::Port => ( + "AnsiPort", + origin.name_range(db.db).expect("ANSI port should have a name range"), ), - Some(origin) if origin.kind(db) == DefKind::Port => { - ("AnsiPort", origin.name_range(db).expect("ANSI port should have a name range")) - } other => panic!("unexpected definition for {name}: {other:?}"), }; let range_start = usize::from(range.value.start()); @@ -518,8 +504,8 @@ 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 db = host.ctx(); + let sema = Semantics::::new_with_context(db.db, db.resolution()); let parsed_file = sema.parse_file(file_id); let file = parsed_file.compilation_unit().unwrap(); let token = file @@ -529,14 +515,14 @@ endmodule .unwrap(); let DefinitionClass::Definition(def) = - DefinitionClass::resolve(sema.db, file_id.into(), token).unique().unwrap() + DefinitionClass::resolve(&db, file_id.into(), token).unique().unwrap() else { panic!("expected plain definition for hierarchical leaf"); }; - let origins = def.origins(db); + let origins = def.origins(db.db); assert!( - origins.iter().any(|origin| origin.kind(db) == DefKind::Net), + origins.iter().any(|origin| origin.kind(db.db) == DefKind::Net), "hierarchical leaf should resolve to child net, got {origins:?}" ); } @@ -556,7 +542,7 @@ 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 sema = Semantics::::new(host.raw_db()); + let sema = Semantics::::new_with_context(host.ctx().db, host.ctx().resolution()); let parsed = sema.parse_file(file_id); let token = parsed .compilation_unit() @@ -567,7 +553,7 @@ endmodule .unwrap(); assert_eq!( - DefinitionClass::resolve(sema.db, file_id.into(), token), + DefinitionClass::resolve(&host.ctx(), file_id.into(), token), Resolution::Unresolved ); } @@ -585,8 +571,8 @@ 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 db = host.ctx(); + let sema = Semantics::::new_with_context(db.db, db.resolution()); let parsed = sema.parse_file(file_id); let token = parsed .compilation_unit() @@ -597,18 +583,18 @@ endmodule .unwrap(); let Resolution::Ambiguous(candidates) = - DefinitionClass::resolve(sema.db, file_id.into(), token) + DefinitionClass::resolve(&db, file_id.into(), token) else { panic!("duplicate named parameters should remain ambiguous"); }; assert_eq!(candidates.len(), 2); assert!(candidates.iter().all( - |candidate| matches!(candidate, DefinitionClass::Definition(def) if def.kind(db) == DefKind::Param) + |candidate| matches!(candidate, DefinitionClass::Definition(def) if def.kind(db.db) == DefKind::Param) )); } #[test] - fn package_member_does_not_disambiguate_ambiguous_package() { + fn package_colon_colon_is_answered_by_slang_when_the_package_name_is_duplicate() { for (case, text) in [ ( "scoped member", @@ -642,22 +628,19 @@ endmodule ), ] { let offset = TextSize::from(text.find("/*caret*/").unwrap() as u32); + let def_at = TextSize::from(text.find("only_left;").unwrap() as u32); let text = text.replace("/*caret*/", ""); let (host, file_id) = host_with_file(&text); - let sema = Semantics::::new(host.raw_db()); - let parsed = sema.parse_file(file_id); - let token = parsed - .compilation_unit() + let nav = host + .make_analysis() + .goto_definition(crate::FilePosition { file_id, offset }) .unwrap() - .syntax() - .token_at_offset(offset) - .pick_best_token(crate::token::navigation_precedence) - .unwrap(); - - assert_eq!( - DefinitionClass::resolve(sema.db, file_id.into(), token), - Resolution::Unresolved, - "{case} must not use child existence to disambiguate its package" + .unwrap_or_else(|| panic!("{case}: slang must pick a p::only_left")); + assert!( + nav.info + .iter() + .any(|target| target.focus_range.map(|range| range.start()) == Some(def_at)), + "{case} should land on only_left: {nav:?}" ); } } @@ -681,8 +664,8 @@ 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 db = host.ctx(); + let sema = Semantics::::new_with_context(db.db, db.resolution()); let parsed = sema.parse_file(file_id); let token = parsed .compilation_unit() @@ -692,11 +675,11 @@ endmodule .pick_best_token(crate::token::navigation_precedence) .unwrap(); - let resolution = DefinitionClass::resolve(sema.db, file_id.into(), token); + let resolution = DefinitionClass::resolve(&db, file_id.into(), token); let Some(DefinitionClass::Definition(def)) = resolution.unique() else { panic!("UDP type should resolve uniquely, got {resolution:?}"); }; - assert_eq!(def.kind(db), DefKind::Udp); + assert_eq!(def.kind(db.db), DefKind::Udp); } #[test] @@ -711,8 +694,8 @@ 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 db = host.ctx(); + let sema = Semantics::::new_with_context(db.db, db.resolution()); let parsed_file = sema.parse_file(file_id); let file = parsed_file.compilation_unit().unwrap(); let token = file @@ -721,13 +704,13 @@ endmodule .pick_best_token(crate::token::navigation_precedence) .unwrap(); - let resolution = DefinitionClass::resolve(sema.db, file_id.into(), token); + let resolution = DefinitionClass::resolve(&db, file_id.into(), token); let Resolution::Ambiguous(candidates) = resolution else { panic!("duplicate declarations should produce an ambiguous definition resolution"); }; assert_eq!(candidates.len(), 2); assert!(candidates.iter().all(|candidate| { - matches!(candidate, DefinitionClass::Definition(def) if def.origins(db).len() == 1) + matches!(candidate, DefinitionClass::Definition(def) if def.origins(db.db).len() == 1) })); } } diff --git a/crates/ide/src/design_unit.rs b/crates/ide/src/design_unit.rs new file mode 100644 index 000000000..3d61b588d --- /dev/null +++ b/crates/ide/src/design_unit.rs @@ -0,0 +1,266 @@ +//! Compilation-unit name navigation through `hit_at`. +//! +//! This is the only CU-name answer. Empty graph candidates are `Other` — a +//! different question (nested module, class `::`, UDP), not a second path. + +use design_graph::{CursorHit, UnitId, UnitKind, hit_global, hit_local}; +use nohash_hasher::IntMap; +use utils::line_index::{TextRange, TextSize}; +use vfs::FileId; + +use crate::{ + FilePosition, RangeInfo, + analysis::AnalysisContext, + markup::Markup, + navigation_target::NavTarget, + references::{ReferenceCategory, References, ReferencesConfig, ReferencesStatus}, +}; + +pub(crate) fn goto_definition( + db: &AnalysisContext<'_>, + FilePosition { file_id, offset }: FilePosition, +) -> Option>> { + match hit(db, file_id, offset) { + CursorHit::Other => None, + CursorHit::DeclName { unit, range } => { + Some(RangeInfo::new(range, vec![nav_from_unit(db, unit)])) + } + CursorHit::InstantiationType { range, targets } + | CursorHit::PackageRef { range, targets, .. } => { + let navs: Vec<_> = targets.into_iter().map(|unit| nav_from_unit(db, unit)).collect(); + Some(RangeInfo::new(range, navs)) + } + } +} + +pub(crate) fn hover( + db: &AnalysisContext<'_>, + FilePosition { file_id, offset }: FilePosition, +) -> Option> { + match hit(db, file_id, offset) { + CursorHit::Other => None, + CursorHit::DeclName { unit, range } => Some(RangeInfo::new(range, hover_markup(db, &unit))), + CursorHit::InstantiationType { range, targets } + | CursorHit::PackageRef { range, targets, .. } => { + Some(RangeInfo::new(range, hover_targets(db, &targets))) + } + } +} + +pub(crate) fn references( + db: &AnalysisContext<'_>, + FilePosition { file_id, offset }: FilePosition, + config: &ReferencesConfig, +) -> Option> { + match hit(db, file_id, offset) { + CursorHit::Other => None, + CursorHit::DeclName { unit, range } => { + Some(vec![references_for_units(db, &[unit], range, config)]) + } + CursorHit::InstantiationType { range, targets } + | CursorHit::PackageRef { range, targets, .. } => { + Some(vec![references_for_units(db, &targets, range, config)]) + } + } +} + +fn hit(db: &AnalysisContext<'_>, file_id: FileId, offset: TextSize) -> CursorHit { + let facts = db.file_facts(file_id); + // A declaration name is a fact of this file. ProductStore::transition + // owns the revision order; this is a cheap local answer, not a race + // bypass. + if let Some(hit) = hit_local(&facts, offset) { + return hit; + } + let graph = db.unit_catalog(); + let hit = hit_global(&facts, &graph, offset); + let (hit_kind, target_count) = match &hit { + CursorHit::DeclName { .. } => ("decl_name", 1usize), + CursorHit::InstantiationType { targets, .. } => ("instantiation_type", targets.len()), + CursorHit::PackageRef { targets, .. } => ("package_ref", targets.len()), + CursorHit::Other => ("other", 0usize), + }; + tracing::debug!(hit_kind, target_count, "design_graph.hit"); + hit +} + +pub(crate) fn nav_from_unit(db: &AnalysisContext<'_>, unit: UnitId) -> NavTarget { + let facts = db.file_facts(unit.file); + let node = facts.unit(unit.clone()); + let name_range = node.and_then(|node| node.name_range); + let range = name_range.unwrap_or_else(|| TextRange::empty(TextSize::new(0))); + NavTarget { + file_id: unit.file, + full_range: range, + focus_range: name_range, + name: Some(unit.name.clone()), + kind: def_kind(unit.kind), + container_name: None, + description: None, + } +} + +fn hover_targets(db: &AnalysisContext<'_>, targets: &[UnitId]) -> Markup { + let mut markup = Markup::new(); + for (index, unit) in targets.iter().enumerate() { + if index > 0 { + markup.horizontal_line(); + } + markup.merge(hover_markup(db, unit)); + } + markup +} + +fn hover_markup(db: &AnalysisContext<'_>, unit: &UnitId) -> Markup { + // A FileFacts node is a source declaration. Generated units have no + // FileFacts row. Do not fold the workspace graph to learn that — DeclName + // hover already refused the fold in `hit`. + let facts = db.file_facts(unit.file); + let node = facts.unit(unit.clone()); + let text = db.file_text(unit.file); + let header = node.and_then(|node| node.header_range).and_then(|header| { + let start = usize::from(header.start()); + let end = usize::from(header.end()); + text.get(start..end) + }); + let header = + header.map(str::trim_end).filter(|header| !header.is_empty()).unwrap_or(unit.name.as_str()); + let mut markup = Markup::new(); + markup.push_with_code_fence(header); + let range = + node.and_then(|node| node.name_range).unwrap_or_else(|| TextRange::empty(TextSize::new(0))); + if let Some(link) = crate::render::source_location_link(db, unit.file, range.start(), unit.file) + { + markup.metadata_line(&format!("from {link}")); + } + markup +} + +fn references_for_units( + db: &AnalysisContext<'_>, + units: &[UnitId], + _caret_range: TextRange, + config: &ReferencesConfig, +) -> References { + let graph = db.unit_catalog(); + let def: Vec = units.iter().cloned().map(|unit| nav_from_unit(db, unit)).collect(); + let mut refs: IntMap> = IntMap::default(); + for file in reference_files(db, config) { + let facts = db.file_facts(file); + for site in facts.instantiations.iter() { + let targets = graph.candidates(&site.name, site.role); + if units.iter().any(|unit| targets.iter().any(|target| target == unit)) { + refs.entry(file).or_default().push((site.range, ReferenceCategory::empty())); + } + } + for import in facts.imports.iter() { + let targets = graph.packages_named(&import.package).into_vec(); + if units.iter().any(|unit| targets.iter().any(|target| target == unit)) { + refs.entry(file).or_default().push((import.range, ReferenceCategory::empty())); + } + } + for site in facts.package_refs.iter() { + let targets = graph.packages_named(&site.name).into_vec(); + if units.iter().any(|unit| targets.iter().any(|target| target == unit)) { + refs.entry(file).or_default().push((site.range, ReferenceCategory::empty())); + } + } + } + for unit in units { + if let Some(range) = + db.file_facts(unit.file).unit(unit.clone()).and_then(|node| node.name_range) + && let Some(hits) = refs.get_mut(&unit.file) + { + hits.retain(|(hit, _)| *hit != range); + if hits.is_empty() { + refs.remove(&unit.file); + } + } + } + refs.retain(|_, hits| !hits.is_empty()); + References { def: Some(def), refs, status: ReferencesStatus::Complete } +} + +fn reference_files(db: &AnalysisContext<'_>, config: &ReferencesConfig) -> Vec { + if let Some(scope) = &config.search_scope { + return scope.files().collect(); + } + db.files() + .iter() + .copied() + .filter(|&file| db.file_kind(file).is_semantic_compilation_unit()) + .collect() +} + +fn def_kind(kind: UnitKind) -> Option { + match kind { + UnitKind::Module => Some(crate::DefKind::Module), + UnitKind::Interface => Some(crate::DefKind::Interface), + UnitKind::Package => Some(crate::DefKind::Package), + UnitKind::Program => Some(crate::DefKind::Program), + UnitKind::Checker => Some(crate::DefKind::Checker), + UnitKind::Covergroup => Some(crate::DefKind::Covergroup), + } +} + +pub(crate) fn source_visible_hit( + db: &AnalysisContext<'_>, + FilePosition { file_id, offset }: FilePosition, +) -> bool { + match hit(db, file_id, offset) { + CursorHit::Other => false, + CursorHit::DeclName { unit, .. } => is_source_unit(db, &unit), + CursorHit::InstantiationType { targets, .. } | CursorHit::PackageRef { targets, .. } => { + !targets.is_empty() && targets.iter().all(|unit| is_source_unit(db, unit)) + } + } +} + +fn is_source_unit(db: &AnalysisContext<'_>, unit: &UnitId) -> bool { + db.file_facts(unit.file).unit(unit.clone()).is_some() +} + +pub(crate) fn rename_guard( + db: &AnalysisContext<'_>, + FilePosition { file_id, offset }: FilePosition, +) -> Result<(), crate::rename::RenameError> { + db.store.record_paid_file(file_id); + match hit(db, file_id, offset) { + CursorHit::Other => Ok(()), + CursorHit::DeclName { unit, .. } => reject_generated(db, &[unit]), + CursorHit::InstantiationType { targets, .. } | CursorHit::PackageRef { targets, .. } => { + reject_generated(db, &targets) + } + } +} + +fn reject_generated( + db: &AnalysisContext<'_>, + units: &[UnitId], +) -> Result<(), crate::rename::RenameError> { + if units.iter().any(|unit| db.file_facts(unit.file).unit(unit.clone()).is_none()) { + return Err(crate::rename::RenameError::MacroDefinitionNotEditable); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::test_utils::{position, setup_marked}; + + #[test] + fn module_decl_hover_uses_file_facts_header() { + let (host, file_id, _text, markers) = setup_marked( + "module /*marker:name*/top #(parameter int W = 1);\n wire unused;\nendmodule\n", + ); + let hover = host + .make_analysis() + .hover(position(file_id, &markers, "name")) + .unwrap() + .expect("module name hover"); + let info = hover.info.as_str(); + assert!(info.contains("module top"), "{info}"); + assert!(info.contains("parameter int W = 1"), "{info}"); + assert!(!info.contains("wire unused"), "{info}"); + } +} diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 1b93699a2..fe5450722 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -1,6 +1,7 @@ +#[cfg(test)] +use base_db::project::CompilationProfileId; use base_db::{ diagnostics_config::DiagnosticSource as SlangDiagnosticSource, - project::CompilationProfileId, source_db::{SourceDb, SourceRootDb}, source_root::{SourceRootDiagnosticScope, SourceRootRole}, }; @@ -11,7 +12,7 @@ use vfs::FileId; use crate::{ db::root_db::RootDb, - module_resolution::{ModuleResolution, ModuleResolutionAmbiguity, resolve_module_name}, + module_resolution::{ModuleResolution, resolve_module_name}, }; const AMBIGUOUS_MODULE_INSTANTIATION: VideDiagnosticDescriptor = @@ -166,42 +167,62 @@ pub(crate) fn parse_diagnostics(db: &RootDb, file_id: FileId) -> Vec .collect() } -pub(crate) fn compilation_diagnostics(db: &RootDb, file_id: FileId) -> Vec { - db.file_compilation_diagnostics(file_id) - .iter() - .filter_map(|diag| slang_diagnostic(diag.file_id, diag.source, &diag.diagnostic)) - .collect() +#[cfg(test)] +pub(crate) fn compilation_profile_diagnostics( + db: &RootDb, + profile_id: CompilationProfileId, +) -> Vec { + let job = preproc_expand::profile_compiler::build_profile_compilation_job(db, profile_id); + let output = preproc_expand::profile_compiler::run_profile_compilation(job); + materialize_compilation_profile_diagnostics(db, profile_id, output.into_diagnostics()) } -pub(crate) fn compilation_profile_diagnostics( +#[cfg(test)] +pub(crate) fn materialize_compilation_profile_diagnostics( db: &RootDb, profile_id: CompilationProfileId, + compiler_diagnostics: Vec, ) -> Vec { - let mut diagnostics = db - .compilation_profile_diagnostics(profile_id) - .diagnostics - .iter() + let mut diagnostics = materialize_compiler_diagnostics(compiler_diagnostics); + diagnostics.extend(compilation_profile_vide_diagnostics(db, profile_id)); + diagnostics +} + +pub fn materialize_compiler_diagnostics( + compiler_diagnostics: Vec, +) -> Vec { + compiler_diagnostics + .into_iter() .filter_map(|diag| slang_diagnostic(diag.file_id, diag.source, &diag.diagnostic)) - .collect::>(); + .collect() +} - diagnostics.extend( - compilation_profile_file_ids(db, profile_id) - .into_iter() - .flat_map(|file_id| vide_diagnostics(db, file_id)), - ); - diagnostics +#[cfg(test)] +pub(crate) fn compilation_profile_vide_diagnostics( + db: &RootDb, + profile_id: CompilationProfileId, +) -> Vec { + compilation_profile_file_ids(db, profile_id) + .into_iter() + .flat_map(|file_id| vide_diagnostics(db, &hir_def::unit::test_resolution(db), file_id)) + .collect() } +#[cfg(test)] fn compilation_profile_file_ids(db: &RootDb, profile_id: CompilationProfileId) -> Vec { db.compilation_plan_for_profile(Some(profile_id)).all_file_ids() } -fn syntax_diagnostics(db: &RootDb, file_id: FileId) -> Vec { +fn syntax_diagnostics( + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, +) -> Vec { if db.file_kind(file_id).is_project_manifest() { return crate::manifest::diagnostics(db, file_id); } let mut diagnostics = parse_diagnostics(db, file_id); - diagnostics.extend(vide_diagnostics(db, file_id)); + diagnostics.extend(vide_diagnostics(db, context, file_id)); diagnostics } @@ -232,6 +253,7 @@ fn slang_diagnostic( }) } +#[cfg(test)] pub(crate) fn diagnostics(db: &RootDb, file_id: FileId) -> Vec { let source_root_id = db.source_root_id(file_id); // Ignored roots in a profiled workspace are explicitly outside the @@ -244,21 +266,37 @@ pub(crate) fn diagnostics(db: &RootDb, file_id: FileId) -> Vec { return Vec::new(); } - syntax_diagnostics(db, file_id) + syntax_diagnostics(db, &hir_def::unit::test_resolution(db), file_id) +} + +pub(crate) fn analysis_diagnostics( + db: &crate::analysis::AnalysisContext<'_>, + file_id: FileId, +) -> Vec { + let source_root_id = db.source_root_id(file_id); + if db.source_root(source_root_id).role().diagnostic_scope() + == SourceRootDiagnosticScope::Disabled + && db.project_config().has_compilation_profiles() + { + return Vec::new(); + } + + syntax_diagnostics(db, db.resolution().as_ref(), file_id) } pub(crate) fn source_root_diagnostics(db: &RootDb, file_id: FileId) -> Vec { let source_root_id = db.source_root_id(file_id); let source_root = db.source_root(source_root_id); + let context = hir_def::unit::test_resolution(db); match source_root.role().diagnostic_scope() { SourceRootDiagnosticScope::Disabled => return Vec::new(), SourceRootDiagnosticScope::OpenFile => { - return syntax_diagnostics(db, file_id); + return syntax_diagnostics(db, &context, file_id); } SourceRootDiagnosticScope::Workspace => {} } - source_root.iter().flat_map(|file_id| syntax_diagnostics(db, file_id)).collect() + source_root.iter().flat_map(|file_id| syntax_diagnostics(db, &context, file_id)).collect() } pub(crate) fn source_root_file_ids(db: &RootDb, file_id: FileId) -> Vec { @@ -289,7 +327,12 @@ trait VideDiagnosticProvider { } /// Compute this provider's diagnostics for `file_id`. - fn diagnostic(&self, db: &RootDb, file_id: FileId) -> Vec; + fn diagnostic( + &self, + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, + ) -> Vec; } fn vide_providers() -> Vec> { @@ -300,7 +343,11 @@ fn vide_providers() -> Vec> { ] } -fn vide_diagnostics(db: &RootDb, file_id: FileId) -> Vec { +pub(crate) fn vide_diagnostics( + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, +) -> Vec { if !vide_diagnostics_enabled(db) { return Vec::new(); } @@ -308,7 +355,7 @@ fn vide_diagnostics(db: &RootDb, file_id: FileId) -> Vec { vide_providers() .into_iter() .filter(|provider| provider.active(db, file_id)) - .flat_map(|provider| provider.diagnostic(db, file_id)) + .flat_map(|provider| provider.diagnostic(db, context, file_id)) .collect() } @@ -325,16 +372,25 @@ fn vide_diagnostics_enabled(db: &RootDb) -> bool { struct LoweringSyntaxDiagnostics; impl VideDiagnosticProvider for LoweringSyntaxDiagnostics { - fn diagnostic(&self, db: &RootDb, file_id: FileId) -> Vec { - lowering_syntax_diagnostics(db, file_id) + fn diagnostic( + &self, + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, + ) -> Vec { + lowering_syntax_diagnostics(db, context, file_id) } } -fn lowering_syntax_diagnostics(db: &RootDb, file_id: FileId) -> Vec { +fn lowering_syntax_diagnostics( + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, +) -> Vec { let parse_ranges = db.parse_diagnostics(file_id).iter().filter_map(to_text_range).collect::>(); - db.file_lowering_diagnostics(file_id.into()) + db.file_lowering_diagnostics(file_id.into(), context) .iter() .filter_map(|diag| lowering_diagnostic(file_id, diag, &parse_ranges)) .collect() @@ -393,7 +449,11 @@ fn slang_semantic_diagnostics_active(db: &RootDb, file_id: FileId) -> bool { && db.project_config().profile_for_root(db.source_root_id(file_id)).is_some() } -fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> Vec { +fn module_instantiation_resolution_diagnostics( + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, +) -> Vec { let hir_file_id = file_id.into(); let hir_file = db.body(db.owner_table(hir_file_id).file_owner().expect("file owner")); let mut diagnostics = Vec::new(); @@ -428,14 +488,10 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> } } - match resolve_module_name(db, file_id, module_name) { - ModuleResolution::Ambiguous { candidates, kind } => { + match resolve_module_name(db, context, module_name) { + ModuleResolution::Ambiguous(candidates) => { let (severity, message, message_key, message_args) = - ambiguous_module_instantiation_diagnostic( - module_name, - candidates.len(), - kind, - ); + ambiguous_module_instantiation_diagnostic(module_name, candidates.len()); diagnostics.push(AMBIGUOUS_MODULE_INSTANTIATION.diagnostic( diag_file_id, range, @@ -445,9 +501,7 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> message_args, )); } - ModuleResolution::Unique(_) - | ModuleResolution::BestEffortProximity { .. } - | ModuleResolution::Unresolved => {} + ModuleResolution::Unique(_) | ModuleResolution::Unresolved => {} } } } @@ -489,7 +543,12 @@ fn inactive_preprocessor_branch_diagnostics(db: &RootDb, file_id: FileId) -> Vec struct InactivePreprocessorBranch; impl VideDiagnosticProvider for InactivePreprocessorBranch { - fn diagnostic(&self, db: &RootDb, file_id: FileId) -> Vec { + fn diagnostic( + &self, + db: &RootDb, + _context: &hir_def::pathres::ResolutionContext, + file_id: FileId, + ) -> Vec { inactive_preprocessor_branch_diagnostics(db, file_id) } } @@ -501,40 +560,31 @@ impl VideDiagnosticProvider for AmbiguousModuleInstantiation { !slang_semantic_diagnostics_active(db, file_id) } - fn diagnostic(&self, db: &RootDb, file_id: FileId) -> Vec { - module_instantiation_resolution_diagnostics(db, file_id) + fn diagnostic( + &self, + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + file_id: FileId, + ) -> Vec { + module_instantiation_resolution_diagnostics(db, context, file_id) } } fn ambiguous_module_instantiation_diagnostic( module_name: &str, candidate_count: usize, - kind: ModuleResolutionAmbiguity, ) -> (DiagnosticSeverity, String, &'static str, Vec<(&'static str, String)>) { - let message_args = || { + ( + DiagnosticSeverity::Warning, + format!( + "module instantiation '{module_name}' matches {candidate_count} module definitions; cannot determine which one to use" + ), + DIAGNOSTIC_AMBIGUOUS_MODULE_STRICT, vec![ ("module_name", module_name.to_owned()), ("candidate_count", candidate_count.to_string()), - ] - }; - match kind { - ModuleResolutionAmbiguity::Strict => ( - DiagnosticSeverity::Warning, - format!( - "module instantiation '{module_name}' matches {candidate_count} module definitions; cannot determine which one to use" - ), - DIAGNOSTIC_AMBIGUOUS_MODULE_STRICT, - message_args(), - ), - ModuleResolutionAmbiguity::BestEffortTie => ( - DiagnosticSeverity::Note, - format!( - "module instantiation '{module_name}' matches {candidate_count} module definitions; cannot determine which one to use" - ), - DIAGNOSTIC_AMBIGUOUS_MODULE_BEST_EFFORT, - message_args(), - ), - } + ], + ) } fn to_text_range(diag: &SyntaxDiagnostic) -> Option { @@ -679,15 +729,15 @@ mod tests { diagnostics.iter().any(|diag| { diag.source == DiagnosticSource::Vide && diag.name == AMBIGUOUS_MODULE_INSTANTIATION.name - && diag.severity == syntax::diagnostics::DiagnosticSeverity::Note + && diag.severity == syntax::diagnostics::DiagnosticSeverity::Warning && diag.message.contains("matches 2 module definitions") }), - "expected vide ambiguous module information: {diagnostics:?}" + "expected vide ambiguous module warning: {diagnostics:?}" ); } #[test] - fn best_effort_nearest_module_instantiation_does_not_report_vide_diagnostic() { + fn best_effort_duplicate_module_instantiation_reports_vide_warning() { let db = db_with_files_in_role( &[ ("/project/a/child.sv", "module child; endmodule\n"), @@ -701,8 +751,12 @@ mod tests { let diagnostics = diagnostics(&db, FileId::from_raw(1)); assert!( - diagnostics.iter().all(|diag| diag.source != DiagnosticSource::Vide), - "nearest best-effort module should not produce Vide diagnostics: {diagnostics:?}" + diagnostics.iter().any(|diag| { + diag.source == DiagnosticSource::Vide + && diag.name == AMBIGUOUS_MODULE_INSTANTIATION.name + && diag.severity == syntax::diagnostics::DiagnosticSeverity::Warning + }), + "duplicates stay ambiguous on the graph: {diagnostics:?}" ); } @@ -876,6 +930,100 @@ mod tests { ); } + fn parent_relative_include_texts() -> (&'static str, &'static str) { + // The include must change the root's inactive set. A header that only + // defines `__CDEPTH__` cannot do that: failed include and successful + // include both leave `__CDEPTH__` undefined. + let top = concat!( + "`include \"../rtl/config.vh\"\n", + "module darkcache;\n", + "`ifndef HEADER_FLAG\n", + " wire should_be_inactive;\n", + "`endif\n", + "endmodule\n", + ); + let header = concat!( + "`define HEADER_FLAG\n", + "`ifdef NEVER_DEFINED\n", + " wire header_inactive;\n", + "`endif\n", + ); + (top, header) + } + + fn parent_relative_include_db() -> (TestDir, RootDb, String) { + let dir = TestDir::new("inactive-parent-relative-include"); + let rtl = dir.create_dir_all("rtl"); + let (top_text, header_text) = parent_relative_include_texts(); + let top_path = rtl.join("darkcache.v"); + let header_path = rtl.join("config.vh"); + std::fs::write(&top_path, top_text).unwrap(); + std::fs::write(&header_path, header_text).unwrap(); + + let mut db = RootDb::new(None); + let mut file_set = FileSet::default(); + file_set.insert(FileId::from_raw(0), VfsPath::from(top_path.clone())); + file_set.insert(FileId::from_raw(1), VfsPath::from(header_path.clone())); + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::create(FileId::from_raw(0), top_text)); + change.add_changed_file(ChangedFile::create(FileId::from_raw(1), header_text)); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.set_project_config(Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![SourceRootId(0)], + top_modules: Vec::new(), + preprocess: PreprocessConfig { + include_dirs: vec![rtl], + ..PreprocessConfig::default() + }, + }], + ))); + db.apply_change(change); + (dir, db, top_text.to_owned()) + } + + fn assert_inactive_header_gated_body(db: &RootDb, top_text: &str, context: &str) { + let diagnostics = diagnostics(db, FileId::from_raw(0)); + let inactive = diagnostics + .iter() + .filter(|diag| diag.name == INACTIVE_PREPROCESSOR_BRANCH.name) + .collect::>(); + let branches = preproc_expand::preproc::inactive_branches(db, FileId::from_raw(0)); + let trace = db.preproc_trace(FileId::from_raw(0)).map(|trace| { + trace + .source_buffers + .iter() + .map(|source| { + format!( + "buffer={} origin={:?} path={}", + source.buffer_id, source.origin, source.path + ) + }) + .collect::>() + }); + assert!( + inactive.iter().any(|diag| { + diag.file_id == FileId::from_raw(0) + && top_text + .get(usize::from(diag.range.start())..usize::from(diag.range.end())) + .is_some_and(|text| text.contains("wire should_be_inactive;")) + }), + "{context}: expected inactive `ifndef HEADER_FLAG` body, diagnostics={diagnostics:?}, branches={branches:?}, trace={trace:?}" + ); + } + + #[test] + fn inactive_preprocessor_branch_survives_parent_relative_header_include() { + let (_dir, db, top_text) = parent_relative_include_db(); + assert_inactive_header_gated_body( + &db, + &top_text, + "parent-relative include with header in VFS", + ); + } + #[test] fn semantic_diagnostics_include_other_workspace_files() { let db = db_with_files( @@ -898,10 +1046,6 @@ mod tests { diagnostics.iter().all(|diag| diag.file_id == FileId::from_raw(1)), "document diagnostics should only include diagnostics attributed to the requested file: {diagnostics:?}" ); - assert!( - db.semantic_diagnostics(FileId::from_raw(0)).is_empty(), - "child file should not receive diagnostics that belong to top.sv" - ); } #[test] @@ -1077,7 +1221,7 @@ mod tests { let plan = db.compilation_plan_for_root(SourceRootId(0)); assert!(plan.include_only.contains(&FileId::from_raw(1))); - assert_eq!(plan.roots, vec![FileId::from_raw(0)]); + assert_eq!(plan.root_file_ids().collect::>(), vec![FileId::from_raw(0)]); let diagnostics = compilation_profile_diagnostics(&db, CompilationProfileId(0)); @@ -1172,7 +1316,10 @@ mod tests { db.apply_change(change); let plan = db.compilation_plan_for_root(SourceRootId(0)); - assert_eq!(plan.roots, vec![FileId::from_raw(0), FileId::from_raw(1)]); + assert_eq!( + plan.root_file_ids().collect::>(), + vec![FileId::from_raw(0), FileId::from_raw(1)] + ); let buffers = compilation_source_buffers_for_plan(&db, &plan); let buffer_paths = buffers.iter().map(|buffer| buffer.path.as_str()).collect::>(); let a_path = a_path.to_string(); diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 12bb34fbc..cd9aa98e0 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -7,6 +7,7 @@ use vfs::FileId; use crate::{ FilePosition, ScopeVisibility, + analysis::AnalysisContext, db::root_db::RootDb, definitions::DefinitionClass, references::{ @@ -28,14 +29,45 @@ pub struct DocumentHighlight { } pub(crate) fn document_highlight( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, config: DocumentHighlightConfig, ) -> Option> { - let sema = Semantics::new(db); + db.store.record_paid_file(file_id); + if crate::design_unit::source_visible_hit(db, FilePosition { file_id, offset }) + && let Some(refs) = crate::design_unit::references( + db, + FilePosition { file_id, offset }, + &crate::references::ReferencesConfig::new( + config.scope_visibility, + Some(SearchScope::single_file(file_id)), + ), + ) + { + let highlights: Vec = refs + .into_iter() + .flat_map(|item| { + let mut ranges = item.refs.get(&file_id).cloned().unwrap_or_default(); + if let Some(defs) = item.def { + for nav in defs { + if nav.file_id == file_id && nav.focus_range.is_some() { + ranges.push((nav.focus_or_full_range(), ReferenceCategory::empty())); + } + } + } + ranges + }) + .map(|(range, category)| DocumentHighlight { range, category }) + .collect(); + if !highlights.is_empty() { + return Some(highlights); + } + } + let sema = db.semantics(); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let target = + resolve_semantic_target(db.db, file_id, offset, parsed_file.root(), token_precedence); let target = target.unique_for_intent(TargetIntent::Highlight)?; let SemanticTarget::Source(target) = target else { return match target { @@ -46,7 +78,9 @@ pub(crate) fn document_highlight( let tokens = target.into_tokens(); let highlights = tokens .into_iter() - .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) + .filter_map(|token| { + highlight_for_token(db, &sema, file_id, hir_file_id, token, config.clone()) + }) .flatten() .collect::>(); (!highlights.is_empty()).then_some(highlights) @@ -72,6 +106,7 @@ fn handle_ctrl_flow_kw( } fn highlight_for_token( + db: &AnalysisContext<'_>, sema: &Semantics<'_, RootDb>, file_id: FileId, hir_file_id: HirFileId, @@ -79,15 +114,16 @@ fn highlight_for_token( config: DocumentHighlightConfig, ) -> Option> { handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { - let def = match DefinitionClass::resolve(sema.db, hir_file_id, token).unique()? { + let def = match DefinitionClass::resolve(db, hir_file_id, token).unique()? { DefinitionClass::Definition(def) => def, DefinitionClass::PortConnShorthand { local, .. } => local, }; - highlight_refs(sema, file_id, def, config) + highlight_refs(db, sema, file_id, def, config) }) } fn highlight_refs<'a>( + db: &AnalysisContext<'_>, sema: &'a Semantics<'a, RootDb>, file_id: FileId, def: DefId, @@ -102,7 +138,7 @@ fn highlight_refs<'a>( let ref_config = ReferencesConfig::new(scope_visibility, Some(SearchScope::single_file(file_id))); - let refs = ReferencesCtx::new(sema, &def, ref_config) + let refs = ReferencesCtx::new(db, &def, ref_config) .search() .remove(&file_id) .unwrap_or_default() @@ -171,7 +207,8 @@ endmodule TextSize::from((reference_start + "generated".len()) as u32), ); let (host, position) = setup(text); - let db = host.raw_db(); + let analysis = host.make_analysis(); + let db = &analysis.db; let macro_file = macro_files_at_offset(db, position.file_id, TextSize::from(call_start as u32)) .pop() @@ -183,8 +220,12 @@ endmodule let def = DefId::from_owner(db, local_module_id).expect("module owner must have a definition"); + let ctx = AnalysisContext::new(db, &analysis.store, &analysis.elab, analysis.snapshot_id); + ctx.store.record_paid_file(position.file_id); + let sema = ctx.semantics(); let highlights = highlight_refs( - &Semantics::new(db), + &ctx, + &sema, position.file_id, def, DocumentHighlightConfig { scope_visibility: ScopeVisibility::Public }, diff --git a/crates/ide/src/document_symbols.rs b/crates/ide/src/document_symbols.rs index 4daa53b36..55421e933 100644 --- a/crates/ide/src/document_symbols.rs +++ b/crates/ide/src/document_symbols.rs @@ -9,6 +9,7 @@ use hir_def::{ checker::{CheckerDef, CheckerId}, container::InFile, covergroup::{CovergroupDef, CovergroupId, CoverpointDef, CoverpointId, CrossDef, CrossId}, + db::HirDefDb, declaration::{Declaration, DeclarationId}, expr::declarator::{DeclId, Declarator, DeclsRange}, file::{ @@ -31,7 +32,6 @@ use hir_def::{ stmt::{CaseItem, ForInit, StmtId, StmtKind}, typedef::{Typedef, TypedefId}, }; -use hir_ty::db::TyDb; use preproc_expand::file::HirFileId; use smol_str::SmolStr; use syntax::WalkEvent; @@ -196,7 +196,7 @@ impl AddRegionSymbol for Peekable> { } // TODO: add ty info in detail -pub(crate) fn document_symbols(db: &dyn TyDb, file_id: FileId) -> Vec { +pub(crate) fn document_symbols(db: &dyn HirDefDb, file_id: FileId) -> Vec { let _span = tracing::debug_span!("ide.document_symbols", ?file_id).entered(); if db.file_kind(file_id).is_project_manifest() { return crate::manifest::document_symbols(db, file_id); @@ -299,7 +299,7 @@ pub(crate) fn document_symbols(db: &dyn TyDb, file_id: FileId) -> Vec, @@ -494,7 +494,7 @@ fn collect_block_items( regions.finish_all(collector); } fn build_stmt( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, stmt_id: StmtId, lowered: &Lowered, @@ -581,7 +581,7 @@ fn build_stmt( } fn build_declaration( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, declaration_id: DeclarationId, lowered: &L, @@ -606,7 +606,7 @@ fn build_declaration( #[inline] fn build_generate_region( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, generate_region_id: GenerateRegionId, structure: &S, @@ -632,7 +632,7 @@ fn build_generate_region( } fn build_generate_block( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, generate_block_owner: OwnerId, ) { @@ -657,7 +657,7 @@ fn build_generate_block( /// (whose items live in their own container). #[inline] fn build_generate_block_item( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, item: BodyItem, structure: &S, @@ -735,7 +735,7 @@ fn build_generate_block_item( } } } -fn build_checker_owner(db: &dyn TyDb, collector: &mut SymbolCollector, owner: OwnerId) { +fn build_checker_owner(db: &dyn HirDefDb, collector: &mut SymbolCollector, owner: OwnerId) { let Some(checker) = owner.as_checker(db) else { return; }; @@ -743,7 +743,7 @@ fn build_checker_owner(db: &dyn TyDb, collector: &mut SymbolCollector, owner: Ow build_checker(db, collector, checker.value, body.as_ref()); } -fn build_clocking_block_owner(db: &dyn TyDb, collector: &mut SymbolCollector, owner: OwnerId) { +fn build_clocking_block_owner(db: &dyn HirDefDb, collector: &mut SymbolCollector, owner: OwnerId) { let Some(clocking_block) = owner.as_clocking_block(db) else { return; }; @@ -751,7 +751,7 @@ fn build_clocking_block_owner(db: &dyn TyDb, collector: &mut SymbolCollector, ow build_clocking_block(db, collector, clocking_block.value, body.as_ref()); } -fn build_covergroup_owner(db: &dyn TyDb, collector: &mut SymbolCollector, owner: OwnerId) { +fn build_covergroup_owner(db: &dyn HirDefDb, collector: &mut SymbolCollector, owner: OwnerId) { let Some(covergroup) = owner.as_covergroup(db) else { return; }; @@ -760,7 +760,7 @@ fn build_covergroup_owner(db: &dyn TyDb, collector: &mut SymbolCollector, owner: } fn build_checker( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, checker_id: CheckerId, lowered: &L, @@ -776,7 +776,7 @@ fn build_checker( } #[inline] fn build_property( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, property_id: PropertyId, lowered: &L, @@ -792,7 +792,7 @@ fn build_property( #[inline] fn build_sequence( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, sequence_id: SequenceId, lowered: &L, @@ -808,7 +808,7 @@ fn build_sequence( #[inline] fn build_clocking_block( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, clocking_block_id: ClockingBlockId, lowered: &L, @@ -825,7 +825,7 @@ fn build_clocking_block( #[inline] fn build_covergroup( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, covergroup_id: CovergroupId, lowered: &L, @@ -857,7 +857,7 @@ fn build_covergroup( #[inline] fn build_coverpoint( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, coverpoint_id: CoverpointId, lowered: &L, @@ -873,8 +873,12 @@ fn build_coverpoint( } #[inline] -fn build_cross(db: &dyn TyDb, collector: &mut SymbolCollector, cross_id: CrossId, lowered: &L) -where +fn build_cross( + db: &dyn HirDefDb, + collector: &mut SymbolCollector, + cross_id: CrossId, + lowered: &L, +) where L: HirLookup + NamedSourceLookup, { let cross = lowered.hir(cross_id); @@ -886,8 +890,12 @@ where } #[inline] -fn build_struct(db: &dyn TyDb, collector: &mut SymbolCollector, struct_id: StructId, lowered: &L) -where +fn build_struct( + db: &dyn HirDefDb, + collector: &mut SymbolCollector, + struct_id: StructId, + lowered: &L, +) where L: HirLookup + NamedSourceLookup, { let hir = lowered.hir(struct_id); @@ -910,7 +918,7 @@ fn struct_kind_name(kind: StructKind) -> SmolStr { #[inline] fn build_specify_block( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, specify_block_id: SpecifyBlockId, structure: &S, @@ -937,7 +945,7 @@ fn build_specify_block( #[inline] fn build_decls( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, decls: &DeclsRange, kind: DefKind, @@ -952,7 +960,7 @@ fn build_decls( #[inline] fn build_decl( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, decl: DeclId, kind: DefKind, @@ -970,7 +978,7 @@ fn build_decl( #[inline] fn build_typedef( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, typedef_id: TypedefId, lowered: &L, @@ -990,7 +998,7 @@ fn build_typedef( } #[inline] -fn build_subroutine(db: &dyn TyDb, collector: &mut SymbolCollector, owner: OwnerId) { +fn build_subroutine(db: &dyn HirDefDb, collector: &mut SymbolCollector, owner: OwnerId) { let hir = db.subroutine(owner); let Some(src) = owner.source(db).map(|source| source.value) else { return; @@ -1001,7 +1009,7 @@ fn build_subroutine(db: &dyn TyDb, collector: &mut SymbolCollector, owner: Owner #[inline] fn build_config_decl( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, config_id: ConfigDeclId, lowered: &L, @@ -1017,8 +1025,12 @@ fn build_config_decl( } #[inline] -fn build_udp_decl(db: &dyn TyDb, collector: &mut SymbolCollector, udp_id: UdpDeclId, lowered: &L) -where +fn build_udp_decl( + db: &dyn HirDefDb, + collector: &mut SymbolCollector, + udp_id: UdpDeclId, + lowered: &L, +) where L: HirLookup + NamedSourceLookup, { let hir = lowered.hir(udp_id); @@ -1031,7 +1043,7 @@ where #[inline] fn build_library_decl( - db: &dyn TyDb, + db: &dyn HirDefDb, collector: &mut SymbolCollector, library_id: LibraryDeclId, lowered: &L, diff --git a/crates/ide/src/elaboration.rs b/crates/ide/src/elaboration.rs new file mode 100644 index 000000000..a27dd4cd5 --- /dev/null +++ b/crates/ide/src/elaboration.rs @@ -0,0 +1,679 @@ +//! Resident slang elaboration service (T4c). +//! +//! This is a backend worker, not a cache. Slang's `Compilation` is not +//! incremental, so the value does not belong in salsa or in +//! [`crate::incrementality::ProductStore`]. One worker thread owns the live +//! compilations; queries name a snapshot revision and get a typed result. +//! +//! [`ElabResult`] is the T7 safety rope: callers can tell "slang said nothing" +//! (`Ready(None)`) from "this snapshot is gone" (`Stale`) from "the worker +//! could not answer" (`Unavailable`). Silent `None` is a bug. +//! +//! Every query is the same three steps — reach the live compilation for a +//! revision, run one closure on it, ship the answer back. That shape is +//! written once in [`Worker::query`] and [`ElaborationService::query`]; the +//! public methods only name a slang entry point. + +use std::{ + fmt, + panic::{self, AssertUnwindSafe}, + sync::mpsc::{self, Receiver, RecvTimeoutError, Sender}, + thread::{self, JoinHandle}, + time::Duration, +}; + +use base_db::{ + Cancelled, analysis_snapshot::AnalysisSnapshotId, project::CompilationProfileId, + source_db::SourceRootDb, +}; +use preproc_expand::compilation_plan::{ + self, CompilationRootKind, compilation_source_buffers_for_plan, +}; +use rustc_hash::FxHashMap; +use slang_sys::compilation::{Compilation, HierInstance, MemberInfo, SymbolInfo}; +use syntax::{SyntaxTreeBuffer, SyntaxTreeOptions}; + +use crate::db::root_db::RootDb; + +/// How long a request-path query waits before giving up on the worker. +/// +/// A cold snapshot needs a full slang elaboration, which is far longer than +/// this. Waiting it out on the keyboard path is a hang, not a degradation. +/// The build is not cancelled by giving up: [`AnalysisHost`] prewarms it off +/// the request path, and a later query for the same revision finds it ready. +/// +/// [`AnalysisHost`]: crate::analysis_host::AnalysisHost +const INTERACTIVE_TIMEOUT: Duration = Duration::from_millis(150); + +const KEPT_GENERATIONS: usize = 2; + +/// How long a caller is willing to wait for the worker. +#[derive(Debug, Clone, Copy)] +enum Wait { + /// Request path. Never block the editor; degrade to HIR instead. + Interactive, + /// Prewarm and tests: the answer matters, the latency does not. + UntilDone, +} + +/// Snapshot tag carried by every query. Matches [`AnalysisSnapshotId`]. +pub type ElabRevision = AnalysisSnapshotId; + +/// Why the resident compilation could not answer. Each arm implies a +/// different caller action, so they must not be collapsed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnavailableReason { + /// The wait elapsed while the worker was still compiling this snapshot. + /// The build continues; a later query for the same revision can be + /// `Ready`. + NotReady, + /// The file belongs to no compilation profile, so no elaboration covers + /// it. Waiting does not help; the workspace configuration has to change. + OutsideAnyProfile, + /// Salsa cancelled the rebuild because the workspace moved on. The next + /// revision will build. + Cancelled, + /// Slang unwound while answering. The payload names the query. + Crashed(String), + /// The worker thread is gone. + WorkerGone, +} + +/// Answer from the resident compilation. The three arms are the contract: +/// empty, stale, and unavailable are not the same. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ElabResult { + Ready(Option), + Stale { have: ElabRevision, want: ElabRevision }, + Unavailable(UnavailableReason), +} + +impl ElabResult { + /// The answer, recording the degradation when there is not one. + /// + /// `Ready(None)` is an answer: slang elaborated and found nothing there, + /// so absence is the truth. Every other arm means slang did *not* + /// answer, which is a different fact, and callers that fall back to HIR + /// must not make it indistinguishable from absence. Routing the whole + /// enum through here is what keeps the fallback visible. + /// + /// `feature` names the caller; the slang entry point is already in + /// [`UnavailableReason::Crashed`]. + pub fn answered(self, feature: &'static str) -> Option { + match self { + ElabResult::Ready(answer) => answer, + // Routine while typing: the snapshot rolled, or the build for + // this one is still running. + ElabResult::Stale { have, want } => { + tracing::debug!(feature, ?have, ?want, "elaboration is behind; HIR answers"); + None + } + ElabResult::Unavailable( + reason @ (UnavailableReason::NotReady + | UnavailableReason::OutsideAnyProfile + | UnavailableReason::Cancelled), + ) => { + tracing::debug!(feature, ?reason, "elaboration declined; HIR answers"); + None + } + // Not routine. Fidelity is gone until someone looks at this. + ElabResult::Unavailable(reason) => { + tracing::warn!(feature, ?reason, "elaboration failed; HIR answers"); + None + } + } + } +} + +/// The payload-free half of [`ElabResult`]. Reaching the live compilation can +/// fail before a query type is even involved, so that step returns this. +#[derive(Debug, Clone, PartialEq, Eq)] +enum NotAnswered { + Stale { have: ElabRevision, want: ElabRevision }, + Unavailable(UnavailableReason), +} + +impl NotAnswered { + fn into_result(self) -> ElabResult { + match self { + NotAnswered::Stale { have, want } => ElabResult::Stale { have, want }, + NotAnswered::Unavailable(reason) => ElabResult::Unavailable(reason), + } + } +} + +#[derive(Clone)] +pub struct ElaborationService { + tx: Sender, +} + +impl fmt::Debug for ElaborationService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ElaborationService").finish() + } +} + +/// One unit of worker work. Every query is a closure so that the reply type +/// stays with the caller instead of becoming another channel variant. +enum Job { + Run(Box), + Shutdown, +} + +struct Generation { + revision: ElabRevision, + profiles: FxHashMap, Compilation>, +} + +impl ElaborationService { + pub fn spawn() -> (Self, JoinHandle<()>) { + let (tx, rx) = mpsc::channel(); + let worker = thread::Builder::new() + .name("vide-elaboration".to_owned()) + .spawn(move || worker_loop(rx)) + .expect("failed to spawn elaboration worker"); + (Self { tx }, worker) + } + + /// Hand one job to the worker and wait for its answer. + /// + /// This is the only place that talks to the channel, so timeout and + /// disconnect are classified once. + fn dispatch( + &self, + wait: Wait, + job: impl FnOnce(&mut Worker) -> ElabResult + Send + 'static, + ) -> ElabResult { + let (reply_tx, reply_rx) = mpsc::channel(); + let run = move |worker: &mut Worker| { + let _ = reply_tx.send(job(worker)); + }; + if self.tx.send(Job::Run(Box::new(run))).is_err() { + return ElabResult::Unavailable(UnavailableReason::WorkerGone); + } + let received = match wait { + Wait::Interactive => { + reply_rx.recv_timeout(INTERACTIVE_TIMEOUT).map_err(|err| match err { + RecvTimeoutError::Timeout => UnavailableReason::NotReady, + RecvTimeoutError::Disconnected => UnavailableReason::WorkerGone, + }) + } + Wait::UntilDone => reply_rx.recv().map_err(|_| UnavailableReason::WorkerGone), + }; + received.unwrap_or_else(ElabResult::Unavailable) + } + + /// Run one slang entry point on the live compilation for `revision`. + /// + /// `what` names the query in [`UnavailableReason::Crashed`]. `run` + /// executes on the worker thread, so the compilation never crosses a + /// thread boundary. + fn query( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + what: &'static str, + run: impl FnOnce(&mut Compilation) -> Option + Send + 'static, + ) -> ElabResult { + let db = db.clone(); + self.dispatch(Wait::Interactive, move |worker| { + worker.query(&db, revision, profile, what, run) + }) + } + + /// Build this snapshot's compilations, waiting for slang to finish. + /// + /// The revision prewarm calls this so that the request path finds the + /// answer ready instead of paying for a cold elaboration on the keyboard + /// path. Blocking here is the point: this is not the request path. + pub fn prewarm(&self, db: &RootDb, revision: ElabRevision) -> ElabResult<()> { + let db = db.clone(); + self.dispatch(Wait::UntilDone, move |worker| match worker.generation(&db, revision) { + Ok(_) => ElabResult::Ready(Some(())), + Err(not_answered) => not_answered.into_result(), + }) + } + + pub fn lookup_symbol( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + path: &str, + offset: usize, + ) -> ElabResult { + let path = path.to_owned(); + self.query(db, revision, profile, "symbol", move |slang| slang.lookup_symbol(&path, offset)) + } + + pub fn lookup_scoped( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + left: &str, + right: &str, + ) -> ElabResult { + let (left, right) = (left.to_owned(), right.to_owned()); + self.query(db, revision, profile, "scoped", move |slang| slang.lookup_scoped(&left, &right)) + } + + pub fn list_scope_members( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + name: &str, + ) -> ElabResult> { + let name = name.to_owned(); + self.query(db, revision, profile, "scope members", move |slang| { + Some(slang.list_scope_members(&name)) + }) + } + + pub fn list_members( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + path: &str, + offset: usize, + ) -> ElabResult> { + let path = path.to_owned(); + self.query(db, revision, profile, "members", move |slang| { + Some(slang.list_members(&path, offset)) + }) + } + + pub fn lookup_type( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + path: &str, + start: usize, + end: usize, + ) -> ElabResult { + let path = path.to_owned(); + self.query(db, revision, profile, "type", move |slang| slang.lookup_type(&path, start, end)) + } + + pub fn list_instances( + &self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + ) -> ElabResult> { + self.query(db, revision, profile, "instances", move |slang| Some(slang.list_instances())) + } + + pub fn shutdown(&self) { + let _ = self.tx.send(Job::Shutdown); + } +} + +fn worker_loop(rx: Receiver) { + let mut worker = Worker::default(); + while let Ok(job) = rx.recv() { + match job { + Job::Run(run) => run(&mut worker), + Job::Shutdown => break, + } + } +} + +/// The live compilations. Owned by one thread; never shared. +#[derive(Default)] +struct Worker { + /// Newest last. At most [`KEPT_GENERATIONS`] entries. + generations: Vec, +} + +impl Worker { + fn query( + &mut self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + what: &'static str, + run: impl FnOnce(&mut Compilation) -> Option, + ) -> ElabResult { + let slang = match self.compilation(db, revision, profile) { + Ok(slang) => slang, + Err(not_answered) => return not_answered.into_result(), + }; + // Slang is a foreign library reached over FFI. An unwind out of it is + // its failure, not a broken invariant of ours, and it must not take + // the worker down with it. Rust-side bugs are not caught here: they + // live in `rebuild`, which propagates. + match panic::catch_unwind(AssertUnwindSafe(|| run(slang))) { + Ok(answer) => ElabResult::Ready(answer), + Err(_) => ElabResult::Unavailable(UnavailableReason::Crashed(format!( + "slang unwound during {what} lookup" + ))), + } + } + + /// The live compilation for one snapshot and profile, building the + /// snapshot first if it is newer than everything kept. + fn compilation( + &mut self, + db: &RootDb, + revision: ElabRevision, + profile: Option, + ) -> Result<&mut Compilation, NotAnswered> { + let index = self.generation(db, revision)?; + self.generations[index] + .profiles + .get_mut(&profile) + .ok_or(NotAnswered::Unavailable(UnavailableReason::OutsideAnyProfile)) + } + + fn generation(&mut self, db: &RootDb, revision: ElabRevision) -> Result { + if let Some(index) = self.generations.iter().position(|slot| slot.revision == revision) { + return Ok(index); + } + if let Some(newest) = self.generations.last() + && revision < newest.revision + { + return Err(NotAnswered::Stale { have: newest.revision, want: revision }); + } + // `Cancelled::catch` unwinds again for anything that is not salsa + // cancellation, so a Rust bug in the rebuild kills this worker and + // every later query reports `WorkerGone`. That is louder than a + // swallowed panic and does not poison the revision. + let generation = Cancelled::catch(|| rebuild(db, revision)) + .map_err(|_| NotAnswered::Unavailable(UnavailableReason::Cancelled))?; + self.generations.push(generation); + if self.generations.len() > KEPT_GENERATIONS { + self.generations.remove(0); + } + Ok(self.generations.len() - 1) + } +} + +/// Build every profile's compilation for one snapshot. +/// +/// Every root is parsed fresh. Carrying a `SyntaxTree` over from the previous +/// generation is not possible as the FFI stands: a `Compilation` owns a +/// `SourceSession`, every tree belongs to the session it was parsed in, and +/// `add_syntax_tree` rejects a foreign one. Reusing trees needs a session +/// that outlives a single generation, with `SourceManager::replaceBuffer` for +/// the edited files — a change in `slang-sys`, not here. Do not reintroduce +/// per-root reuse without it; it aborts the process. +fn rebuild(db: &RootDb, revision: ElabRevision) -> Generation { + // A workspace with no configured profile still compiles: the plan for + // `None` covers every root. This is the unconfigured case, not the + // orphan-file bucket that profile partitioning has to avoid. + let ids = db.project_config().profile_ids(); + let profile_ids: Vec> = + if ids.is_empty() { vec![None] } else { ids.into_iter().map(Some).collect() }; + + let profiles = profile_ids + .into_iter() + .map(|profile_id| (profile_id, compile_profile(db, profile_id))) + .collect(); + Generation { revision, profiles } +} + +fn compile_profile(db: &RootDb, profile_id: Option) -> Compilation { + let plan = db.compilation_plan_for_profile(profile_id); + let context = db.compilation_context(profile_id); + let include_paths: Vec = context.include_dirs.iter().map(ToString::to_string).collect(); + + let mut compilation = Compilation::new_with_top_modules(&context.top_modules); + compilation.register_source_buffers( + &compilation_source_buffers_for_plan(db, &plan) + .into_iter() + .map(|buffer| SyntaxTreeBuffer { path: buffer.path, text: buffer.text }) + .collect::>(), + ); + + for root in &plan.roots { + let path = compilation_plan::source_buffer_path(db, root.file_id).to_string(); + let name = + db.file_path(root.file_id).map(|path| path.to_string()).unwrap_or_else(|| path.clone()); + let options = match root.kind { + CompilationRootKind::SystemVerilog => SyntaxTreeOptions { + predefines: context.predefines.to_vec(), + include_paths: include_paths.clone(), + ..SyntaxTreeOptions::default() + }, + CompilationRootKind::LibraryMap => SyntaxTreeOptions::default(), + }; + match root.kind { + CompilationRootKind::SystemVerilog => { + compilation.parse_syntax_tree_from_buffer(&name, &path, &options); + } + CompilationRootKind::LibraryMap => { + compilation.parse_library_map_syntax_tree_from_buffer(&name, &path, &options); + } + } + } + compilation +} + +#[cfg(test)] +mod tests { + use base_db::{ + change::Change, + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + source_root::{SourceRoot, SourceRootId}, + }; + use triomphe::Arc; + use utils::{line_index::TextSize, paths::AbsPathBuf}; + use vfs::{ChangedFile, FileId, FileSet, VfsPath}; + + use super::*; + use crate::{ + analysis_host::AnalysisHost, + test_utils::{setup_marked, setup_with_path}, + }; + + const OBJECT: &str = r#" +virtual class uvm_void; +endclass +virtual class uvm_object extends uvm_void; + string /*marker:name*/m_leaf_name; +endclass +"#; + + fn expect_ready(result: ElabResult) -> Option { + match result { + ElabResult::Ready(value) => value, + other => panic!("expected Ready, got {other:?}"), + } + } + + /// Block for the build, then ask, so a cold snapshot cannot make an + /// assertion about the *answer* fail for a latency reason. + fn lookup_at(host: &AnalysisHost, file_id: FileId, offset: TextSize) -> ElabResult { + let ctx = host.ctx(); + let path = compilation_plan::source_buffer_path(ctx.db, file_id).to_string(); + let profile = ctx.db.file_compilation_profile(file_id); + let built = ctx.elab.prewarm(ctx.db, ctx.revision); + assert!(matches!(built, ElabResult::Ready(_)), "build must finish, got {built:?}"); + ctx.elab.lookup_symbol(ctx.db, ctx.revision, profile, &path, usize::from(offset)) + } + + #[test] + fn ready_some_and_ready_none_are_distinct_from_stale_and_unavailable() { + let (host, file_id, _text, markers) = setup_marked(OBJECT); + let hit = lookup_at(&host, file_id, markers["name"]); + let info = expect_ready(hit).expect("class property must be Ready(Some)"); + assert_eq!(info.owner_class, "uvm_object"); + assert!(info.inheritance.iter().any(|name| name == "uvm_void"), "{info:?}"); + assert!(info.type_name.contains("string"), "{info:?}"); + + let miss = lookup_at(&host, file_id, TextSize::from(0u32)); + assert_eq!(miss, ElabResult::Ready(None), "a non-member offset is empty, not unavailable"); + } + + #[test] + fn a_dropped_generation_is_stale_not_empty() { + let (mut host, file_id) = setup_with_path(OBJECT, "/object.svh"); + let first = host.snapshot_id(); + let _ = + lookup_at(&host, file_id, TextSize::from(OBJECT.find("m_leaf_name").unwrap() as u32)); + + host.apply_change(modify_object("virtual class uvm_object extends uvm_void;\n string m_leaf_name;\n string extra;\nendclass\n")); + let _ = lookup_at(&host, file_id, TextSize::from(0u32)); + + host.apply_change(modify_object("virtual class uvm_object extends uvm_void;\n string m_leaf_name;\n string extra;\n string extra2;\nendclass\n")); + let _ = lookup_at(&host, file_id, TextSize::from(0u32)); + + let ctx = host.ctx(); + let path = compilation_plan::source_buffer_path(ctx.db, file_id).to_string(); + let stale = ctx.elab.lookup_symbol( + ctx.db, + first, + ctx.db.file_compilation_profile(file_id), + &path, + 0, + ); + match stale { + ElabResult::Stale { want, .. } => assert_eq!(want, first), + other => panic!("revision {first:?} must be Stale after N=2 rolled, got {other:?}"), + } + } + + #[test] + fn a_dead_worker_is_unavailable_not_empty() { + let (service, worker) = ElaborationService::spawn(); + service.shutdown(); + let _ = worker.join(); + let db = RootDb::new(None); + let result = service.lookup_symbol(&db, AnalysisSnapshotId::default(), None, "gone.sv", 0); + assert_eq!( + result, + ElabResult::Unavailable(UnavailableReason::WorkerGone), + "a gone worker is WorkerGone, not empty" + ); + } + + #[test] + fn a_real_file_set_resolves_cross_file_inheritance() { + let root = AbsPathBuf::assert( + if cfg!(windows) { "C:/vide-elab-cross" } else { "/vide-elab-cross" }.into(), + ); + let pkg_path = root.join("uvm_pkg.sv"); + let user_path = root.join("user.sv"); + let mut file_set = FileSet::default(); + file_set.insert(FileId::from_raw(0), VfsPath::from(pkg_path)); + file_set.insert(FileId::from_raw(1), VfsPath::from(user_path)); + + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.set_project_config(Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![SourceRootId(0)], + top_modules: Vec::new(), + preprocess: PreprocessConfig { + include_dirs: vec![root], + ..PreprocessConfig::default() + }, + }], + ))); + change.add_changed_file(ChangedFile::create( + FileId::from_raw(0), + "package uvm_pkg;\n virtual class uvm_void;\n endclass\n virtual class uvm_object extends uvm_void;\n endclass\nendpackage\n", + )); + let user = "package p;\n import uvm_pkg::*;\n class child extends uvm_object;\n string m_leaf_name;\n endclass\nendpackage\n"; + change.add_changed_file(ChangedFile::create(FileId::from_raw(1), user)); + + let mut host = AnalysisHost::default(); + host.apply_change(change); + let offset = TextSize::from(user.find("m_leaf_name").unwrap() as u32); + let info = expect_ready(lookup_at(&host, FileId::from_raw(1), offset)) + .expect("cross-file class member"); + assert_eq!(info.owner_class, "child"); + assert!( + info.inheritance.iter().any(|name| name == "uvm_object" || name == "uvm_void"), + "inheritance must resolve through the imported package in the same compilation: {info:?}" + ); + } + + #[test] + fn instance_hierarchy_names_the_instantiation_site() { + let src = "module child; endmodule\nmodule top; child u0(); endmodule\n"; + let (host, file_id) = setup_with_path(src, "/top.sv"); + let ctx = host.ctx(); + let rows = match ctx.elab.list_instances( + ctx.db, + ctx.revision, + ctx.db.file_compilation_profile(file_id), + ) { + ElabResult::Ready(Some(rows)) => rows, + other => panic!("expected instance list, got {other:?}"), + }; + let u0 = + rows.iter().find(|row| row.path.contains("u0")).unwrap_or_else(|| panic!("{rows:?}")); + let site = src.find("u0").expect("instance name"); + assert_eq!(u0.offset, site, "{u0:?}"); + assert!( + rows.iter().any(|row| row.offset == site && row.path.contains("u0")), + "source site must list the instance: {rows:?}" + ); + } + + #[test] + /// Editing one root must leave the other root's symbols answerable. + /// + /// This used to assert that the untouched root kept its `SyntaxTree`. + /// That reuse aborted the process — a tree belongs to the + /// `SourceSession` of the `Compilation` that parsed it, and + /// `add_syntax_tree` refuses a foreign one. What actually has to hold is + /// the observable part: after an edit the new generation still answers + /// for every root. + fn an_edit_keeps_the_other_roots_answerable() { + let root = AbsPathBuf::assert( + if cfg!(windows) { "C:/vide-elab-reuse" } else { "/vide-elab-reuse" }.into(), + ); + let class_file = FileId::from_raw(0); + let module_file = FileId::from_raw(1); + let mut file_set = FileSet::default(); + file_set.insert(class_file, VfsPath::from(root.join("a.sv"))); + file_set.insert(module_file, VfsPath::from(root.join("b.sv"))); + + let class_text = "class holder;\n string tag;\nendclass\n"; + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.set_project_config(Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![SourceRootId(0)], + top_modules: Vec::new(), + preprocess: PreprocessConfig { + include_dirs: vec![root], + ..PreprocessConfig::default() + }, + }], + ))); + change.add_changed_file(ChangedFile::create(class_file, class_text)); + change.add_changed_file(ChangedFile::create(module_file, "module b;\nendmodule\n")); + let mut host = AnalysisHost::default(); + host.apply_change(change); + + let tag = TextSize::from(class_text.find("tag").unwrap() as u32); + let before = expect_ready(lookup_at(&host, class_file, tag)).expect("tag before the edit"); + assert_eq!(before.owner_class, "holder"); + + let mut edit = Change::new(); + edit.add_changed_file(ChangedFile::modify( + module_file, + "module b;\n wire w;\nendmodule\n", + )); + host.apply_change(edit); + + let after = expect_ready(lookup_at(&host, class_file, tag)).expect("tag after the edit"); + assert_eq!(after, before, "an unrelated edit must not change this answer"); + } + + fn modify_object(text: &str) -> Change { + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify(FileId::from_raw(0), text)); + change + } +} diff --git a/crates/ide/src/formatting.rs b/crates/ide/src/formatting.rs index 15d25c728..e773fa522 100644 --- a/crates/ide/src/formatting.rs +++ b/crates/ide/src/formatting.rs @@ -8,7 +8,6 @@ use std::{ use anyhow::Context as _; use base_db::source_db::SourceDb; use dissimilar::Chunk; -use hir_semantics::semantics::Semantics; use itertools::Itertools; use syntax::{ SyntaxCursor, SyntaxCursorExt, SyntaxKind, SyntaxTrivia, Trivia, has_text_range::HasTextRange, @@ -24,7 +23,7 @@ use utils::{ }; use vfs::FileId; -use crate::{FilePosition, db::root_db::RootDb}; +use crate::{FilePosition, analysis::AnalysisContext, db::root_db::RootDb}; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] #[serde(rename_all = "lowercase")] @@ -55,7 +54,7 @@ impl FmtConfig { } pub(crate) fn format( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, line_range: Option>, LineInfo { ending, .. }: &LineInfo, @@ -63,7 +62,7 @@ pub(crate) fn format( cancellation: &CancellationToken, ) -> anyhow::Result> { if db.file_kind(file_id).is_project_manifest() { - return crate::manifest::format(db, file_id, line_range.is_some(), cancellation); + return crate::manifest::format(db.db, file_id, line_range.is_some(), cancellation); } let text = db.file_text(file_id); format_inner(text.as_ref(), line_range, ending, config, cancellation) @@ -167,8 +166,8 @@ macro_rules! check { }; } -pub fn format_on_type( - db: &RootDb, +pub(crate) fn format_on_type( + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ch: String, line_info: &LineInfo, @@ -183,7 +182,7 @@ pub fn format_on_type( return Ok(None); } - let sema = Semantics::new(db); + let sema = db.semantics(); let parsed_file = sema.parse_file(file_id); let Some(root) = parsed_file.root() else { return Ok(None); @@ -220,7 +219,7 @@ pub fn format_on_type( && config.provider.supports_range_formatting() && let Some(trivias) = trivias.get(..idx.unwrap_or(trivias.len())) && let Some(edits) = - format_previous(db, file_id, trivias, &mut cursor, line_info, config, cancellation) + format_previous(db.db, file_id, trivias, &mut cursor, line_info, config, cancellation) { res.union(edits) .map_err(|_| anyhow::format_err!("on-type formatting produced overlapping edits"))?; @@ -392,10 +391,11 @@ mod tests { use super::{FmtConfig, FormatterProvider, format_on_type}; use crate::{ FilePosition, + analysis_host::AnalysisHost, db::{line_index_db::LineIndexDb, root_db::RootDb}, }; - fn db_with_file(text: &str) -> (RootDb, FileId) { + fn db_with_file(text: &str) -> (AnalysisHost, FileId) { let file_id = FileId::from_raw(0); let path = VfsPath::new_virtual_path("/test.sv".to_owned()); @@ -407,9 +407,9 @@ mod tests { change.set_roots(vec![root]); change.add_changed_file(ChangedFile::create(file_id, text)); - let mut db = RootDb::new(None); - change.apply(&mut db); - (db, file_id) + let mut host = AnalysisHost::default(); + host.apply_change(change); + (host, file_id) } fn line_info(db: &RootDb, file_id: FileId) -> LineInfo { @@ -439,7 +439,8 @@ mod tests { ("unsupported trigger", "module A;\nendmodule", 0, "."), ("first line inside block comment", "/*\n*/", 3, "\n"), ] { - let (db, file_id) = db_with_file(text); + let (host, file_id) = db_with_file(text); + let db = host.ctx(); let edit = format_on_type( &db, FilePosition { file_id, offset: TextSize::from(offset) }, diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 8fdd45aa8..53bcf0412 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -1,42 +1,39 @@ -use hir_semantics::semantics::Semantics; use itertools::Itertools; use preproc_expand::file::HirFileId; use utils::line_index::covering_range; use crate::{ FilePosition, RangeInfo, - db::root_db::RootDb, + analysis::AnalysisContext, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, semantic_target::{SemanticTarget, SourceTarget, TargetIntent, resolve_semantic_target}, }; pub(crate) fn goto_declaration( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { - let sema = Semantics::new(db); + if let Some(target) = crate::design_unit::goto_definition(db, FilePosition { file_id, offset }) + { + return Some(target); + } + let sema = db.semantics(); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let target = resolve_semantic_target( - db, + db.db, file_id, offset, parsed_file.root(), crate::token::navigation_precedence, ); - render_declaration_target( - db, - hir_file_id, - &sema, - target.targets_for_intent(TargetIntent::Navigate), - ) + render_declaration_target(db, hir_file_id, target.targets_for_intent(TargetIntent::Navigate)) } fn render_declaration_target( - db: &RootDb, + db: &AnalysisContext<'_>, hir_file_id: HirFileId, - sema: &Semantics, targets: Vec>, ) -> Option>> { let mut ranges = Vec::new(); @@ -45,7 +42,7 @@ fn render_declaration_target( let target = match target { SemanticTarget::Manifest(target) => crate::manifest::definition_target(db, target), SemanticTarget::Source(target) => { - render_source_declaration_target(db, hir_file_id, sema, target) + render_source_declaration_target(db, hir_file_id, target) } SemanticTarget::PreprocMacro(_) | SemanticTarget::Include(_) => None, }; @@ -59,9 +56,8 @@ fn render_declaration_target( } fn render_source_declaration_target( - db: &RootDb, + db: &AnalysisContext<'_>, hir_file_id: HirFileId, - sema: &Semantics, target: SourceTarget<'_>, ) -> Option>> { let (range, tokens) = target.into_parts(); @@ -69,10 +65,12 @@ fn render_source_declaration_target( let origins = tokens .into_iter() .flat_map(|token| { - DefinitionClass::resolve(sema.db, hir_file_id, token).into_candidates().into_iter().map( + DefinitionClass::resolve(db, hir_file_id, token).into_candidates().into_iter().map( |class| match class { - DefinitionClass::Definition(definition) => definition.declaration_origin(db), - DefinitionClass::PortConnShorthand { port, .. } => port.declaration_origin(db), + DefinitionClass::Definition(definition) => definition.declaration_origin(db.db), + DefinitionClass::PortConnShorthand { port, .. } => { + port.declaration_origin(db.db) + } }, ) }) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index aa9406584..cba9a0b97 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -1,5 +1,4 @@ use hir_def::container::InFile; -use hir_semantics::semantics::Semantics; use itertools::Itertools; use preproc_expand::{ file::HirFileId, @@ -11,6 +10,7 @@ use vfs::FileId; use crate::{ FilePosition, RangeInfo, + analysis::AnalysisContext, db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, @@ -21,25 +21,27 @@ use crate::{ }; pub(crate) fn goto_definition( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { - let sema = Semantics::new(db); - let parsed_file = sema.parse_file(file_id); + if let Some(target) = crate::design_unit::goto_definition(db, FilePosition { file_id, offset }) + { + return Some(target); + } + let tree = db.parse_file(file_id); let target = resolve_semantic_target( - db, + db.db, file_id, offset, - parsed_file.root(), + Some(tree.root()), crate::token::navigation_precedence, ); - render_definition_target(db, file_id, &sema, target) + render_definition_target(db, file_id, target) } fn render_definition_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, - sema: &Semantics, target: TargetResolution<'_>, ) -> Option>> { let mut ranges = Vec::new(); @@ -49,9 +51,7 @@ fn render_definition_target( SemanticTarget::PreprocMacro(target) => render_preproc_definition_target(target), SemanticTarget::Include(includes) => render_include_definition_target(db, includes), SemanticTarget::Manifest(target) => crate::manifest::definition_target(db, target), - SemanticTarget::Source(target) => { - render_source_definition_target(db, file_id, sema, target) - } + SemanticTarget::Source(target) => render_source_definition_target(db, file_id, target), }?; ranges.push(target.range); navs.extend(target.info); @@ -66,16 +66,15 @@ fn render_definition_target( } fn render_source_definition_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, - sema: &Semantics, target: SourceTarget<'_>, ) -> Option>> { let hir_file_id = file_id.into(); let (range, tokens) = target.into_parts(); let navs = tokens .into_iter() - .filter_map(|token| nav_targets_for_token(db, sema, hir_file_id, token)) + .filter_map(|token| nav_targets_for_token(db, hir_file_id, token)) .flatten() .unique() .collect_vec(); @@ -87,23 +86,70 @@ fn render_source_definition_target( } fn nav_targets_for_token( - db: &RootDb, - sema: &Semantics, + db: &AnalysisContext<'_>, hir_file_id: HirFileId, token: SyntaxTokenWithParent, ) -> Option> { - handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { - let navs = DefinitionClass::resolve(sema.db, hir_file_id, token) + handle_ctrl_flow_kw(db.db, hir_file_id, token).or_else(|| { + let navs = DefinitionClass::resolve(db, hir_file_id, token) .into_candidates() .into_iter() - .flat_map(|class| class.origins(db)) + .flat_map(|class| class.origins(db.db)) .unique() - .filter_map(|def| def.to_nav(db)) + .filter_map(|def| def.to_nav(db.db)) + .map(compact_design_unit_target) .collect_vec(); - (!navs.is_empty()).then_some(navs) + if !navs.is_empty() { + return Some(navs); + } + slang_scoped_nav(db, hir_file_id, token) }) } +fn slang_scoped_nav( + db: &AnalysisContext<'_>, + hir_file_id: HirFileId, + token: SyntaxTokenWithParent<'_>, +) -> Option> { + let file = hir_file_id.as_file()?; + let (left, right) = crate::definitions::colon_colon_query(token)?; + let info = crate::slang_class::lookup_scoped_at(db, file, &left, &right) + .answered("goto definition")?; + if info.def_file.is_empty() { + return None; + } + let file_id = crate::anchor::file_id_for_slang_path(db.db, &info.def_file); + let start = utils::line_index::TextSize::from(info.def_offset as u32); + let len = utils::line_index::TextSize::from(info.name.len() as u32); + let focus = utils::line_index::TextRange::new(start, start + len); + Some(vec![NavTarget { + file_id, + full_range: focus, + focus_range: Some(focus), + name: Some(smol_str::SmolStr::from(info.name.as_str())), + kind: None, + container_name: None, + description: None, + }]) +} + +fn compact_design_unit_target(mut target: NavTarget) -> NavTarget { + if matches!( + target.kind, + Some( + crate::DefKind::Module + | crate::DefKind::Interface + | crate::DefKind::Program + | crate::DefKind::Checker + | crate::DefKind::Covergroup + ) + ) && let Some(focus_range) = target.focus_range + { + target.full_range = focus_range; + } + target +} + fn render_preproc_definition_target( target: PreprocMacroTarget, ) -> Option>> { @@ -185,11 +231,11 @@ fn render_include_definition_target( } fn handle_ctrl_flow_kw( - sema: &Semantics, + db: &RootDb, file_id: HirFileId, tp @ SyntaxTokenWithParent { .. }: SyntaxTokenWithParent, ) -> Option> { let (beg, _) = crate::token::ctrl_flow_pair(tp)?; let tok = InFile::new(file_id, beg); - Some(vec![tok.to_nav(sema.db)?]) + Some(vec![tok.to_nav(db)?]) } diff --git a/crates/ide/src/hier.rs b/crates/ide/src/hier.rs new file mode 100644 index 000000000..82807c85c --- /dev/null +++ b/crates/ide/src/hier.rs @@ -0,0 +1,32 @@ +//! Elaborated instance identity. +//! +//! A hierarchical path is slang's name for one instance after elaboration. +//! Vide stores the path; the live compilation answers where it is in source. + +use std::fmt; + +/// Stable key for one elaborated instance (`top.u0`, `top.u0[1].inner`). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HierPath(String); + +impl HierPath { + pub fn new(path: impl Into) -> Self { + Self(path.into()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for HierPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl From for HierPath { + fn from(path: String) -> Self { + Self(path) + } +} diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 3c71d6e8e..1e962d732 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -16,6 +16,7 @@ use vfs::FileId; use crate::{ FilePosition, RangeInfo, + analysis::AnalysisContext, db::root_db::RootDb, definitions::DefinitionClass, hover::{ @@ -48,37 +49,41 @@ pub struct HoverConfig { } pub(crate) fn hover( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ) -> Option> { let _span = tracing::debug_span!("ide.hover", ?file_id, ?offset).entered(); - let sema = Semantics::new(db); - let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); - render_hover_target(db, file_id, offset, &sema, target) + if let Some(hover) = crate::design_unit::hover(db, FilePosition { file_id, offset }) { + return Some(hover); + } + let tree = db.parse_file(file_id); + let target = + resolve_semantic_target(db.db, file_id, offset, Some(tree.root()), token_precedence); + render_hover_target(db, file_id, offset, target) } fn render_hover_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, offset: TextSize, - sema: &Semantics, target: TargetResolution<'_>, ) -> Option> { let mut ranges = Vec::new(); let mut markups = Vec::new(); let mut has_source_target = false; + let mut sema = None; for target in target.targets_for_intent(TargetIntent::Describe) { let hover = match target { SemanticTarget::PreprocMacro(target) => { - render_macro_hover_target(db, file_id, offset, target) + render_macro_hover_target(db.db, file_id, offset, target) } - SemanticTarget::Include(includes) => render_include_hover(db, includes), - SemanticTarget::Manifest(target) => crate::manifest::hover_target(db, target), + SemanticTarget::Include(includes) => render_include_hover(db.db, includes), + SemanticTarget::Manifest(target) => crate::manifest::hover_target(db.db, target), SemanticTarget::Source(target) => { has_source_target = true; - hover_for_source_target(sema, file_id.into(), target) + let sema = sema.get_or_insert_with(|| db.semantics()); + hover_for_source_target(db, sema, file_id.into(), target) } }?; ranges.push(hover.range); @@ -88,22 +93,24 @@ fn render_hover_target( let range = covering_range(&ranges)?; let hover = RangeInfo::new(range, merge_hover_results(markups)?); Some(if has_source_target { - with_expanded_macro_hover(db, file_id, offset, hover) + with_expanded_macro_hover(db.db, file_id, offset, hover) } else { hover }) } fn hover_for_source_target( + db: &AnalysisContext<'_>, sema: &Semantics, hir_file_id: HirFileId, target: SourceTarget<'_>, ) -> Option> { let (range, tokens) = target.into_parts(); - hover_for_token_selection(sema, hir_file_id, range, tokens) + hover_for_token_selection(db, sema, hir_file_id, range, tokens) } fn hover_for_token_selection( + db: &AnalysisContext<'_>, sema: &Semantics, hir_file_id: HirFileId, range: TextRange, @@ -111,7 +118,7 @@ fn hover_for_token_selection( ) -> Option> { let markups = tokens .into_iter() - .filter_map(|token| hover_for_token(sema, hir_file_id, token)) + .filter_map(|token| hover_for_token(db, sema, hir_file_id, token)) .collect::>(); let res = merge_hover_results(markups)?; Some(RangeInfo::new(range, res)) @@ -161,13 +168,14 @@ fn handle_system_subroutine(tp: &SyntaxTokenWithParent<'_>) -> Option { } fn hover_for_token( + db: &AnalysisContext<'_>, sema: &Semantics, file_id: HirFileId, token: SyntaxTokenWithParent, ) -> Option { handle_literal(sema, file_id, token) .or_else(|| handle_system_subroutine(&token)) - .or_else(|| handle_definition(sema, file_id, token)) + .or_else(|| handle_definition(db, sema, file_id, token)) } fn merge_hover_results(markups: Vec) -> Option { @@ -186,12 +194,13 @@ fn merge_hover_results(markups: Vec) -> Option { } fn handle_definition( + db: &AnalysisContext<'_>, sema: &Semantics, file_id: HirFileId, tp: SyntaxTokenWithParent, ) -> Option { let token_text = token_text(sema.db, file_id, &tp); - let def = DefinitionClass::resolve(sema.db, file_id, tp); + let def = DefinitionClass::resolve(db, file_id, tp); let anchor_file_id = file_id.expect_file(); let mut res = Markup::new(); @@ -238,10 +247,38 @@ fn handle_definition( } } } - hir_def::symbol::Resolution::Unresolved => return None, + hir_def::symbol::Resolution::Unresolved => {} } - Some(res) + if res.is_empty() + && let Some(ty) = slang_type_line(db, file_id, tp) + { + res.push_with_code_fence(&ty); + } + (!res.is_empty()).then_some(res) +} + +fn slang_type_line( + db: &AnalysisContext<'_>, + file_id: HirFileId, + tp: SyntaxTokenWithParent<'_>, +) -> Option { + let file = file_id.as_file()?; + let range = tp.text_range()?; + let info = crate::slang_class::lookup_symbol_at(db, file, usize::from(range.start())) + .answered("hover")?; + if info.type_name.is_empty() { + return None; + } + if info.owner_class.is_empty() { + Some(info.type_name) + } else { + Some(crate::slang_class::format_class_member( + &info.owner_class, + &info.type_name, + &info.inheritance, + )) + } } fn token_text( diff --git a/crates/ide/src/incrementality.rs b/crates/ide/src/incrementality.rs new file mode 100644 index 000000000..4cb666868 --- /dev/null +++ b/crates/ide/src/incrementality.rs @@ -0,0 +1,24 @@ +//! Parse-dependency book-keeping for workspace products. +//! +//! Salsa tracks per-file queries and the L0 source catalog +//! (`source_unit_catalog`). This module stores values that are not salsa +//! inputs: the include edges of a paid parse. Those files are the locator +//! for macro-generated owners (`HirFileId::Macro`). Resolution does not +//! merge generated names into the catalog. +//! +//! Once a per-file query reads `unit_scope` through Salsa, every file hangs +//! off the whole project; resolution is therefore derived from the current +//! locator on each request, not stored as a salsa query. +//! +//! `file_decls` is unbounded; `file_facts` keeps the parse LRU. Sharing +//! that LRU made a 1280-file 2000-wire `file_decls` refetch after one +//! edit cost 379ms. With decls unbounded it is 0.17ms +//! (`design_graph_refold_after_body_edit`). +//! +//! New caches belong in Salsa (per-file, dependency-tracked) or in +//! [`ProductStore`] (parse-deps). A third cache in a feature function or +//! on `RootDb` is a bug. + +mod store; + +pub(crate) use store::ProductStore; diff --git a/crates/ide/src/incrementality/store.rs b/crates/ide/src/incrementality/store.rs new file mode 100644 index 000000000..1c34a12e5 --- /dev/null +++ b/crates/ide/src/incrementality/store.rs @@ -0,0 +1,98 @@ +use base_db::source_db::SourceDb; +use parking_lot::Mutex; +use rustc_hash::{FxHashMap, FxHashSet}; +use triomphe::Arc; +use vfs::FileId; + +use crate::db::root_db::RootDb; + +#[derive(Clone, Default)] +struct Inner { + /// Authoritative standalone parses retained by this store lineage: + /// compilation root -> files named by emitted preprocessor include edges. + parse_dependencies: FxHashMap>, +} + +/// Parse-dependency book-keeping, forked on every change so previously +/// created [`crate::analysis::AnalysisSnapshot`]s keep the previous paid-file +/// set. Source catalogs live in salsa. This store does not memoize them. +/// +/// Owned by [`crate::analysis_host::AnalysisHost`]. +#[derive(Default)] +pub(crate) struct ProductStore { + inner: Mutex, +} + +impl std::panic::RefUnwindSafe for ProductStore {} +impl std::panic::UnwindSafe for ProductStore {} + +impl std::fmt::Debug for ProductStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProductStore").finish() + } +} + +impl ProductStore { + /// One revision transition. Fork parse-deps, apply the salsa change. + pub(crate) fn transition( + current: &triomphe::Arc, + db: &mut RootDb, + change: base_db::change::Change, + ) -> (triomphe::Arc, Vec) { + let dirty_files: Vec<_> = change.changed_files.iter().map(|file| file.file_id).collect(); + if change.project_config.is_some() { + db.apply_change(change); + let files = db.files().iter().copied().collect(); + return (triomphe::Arc::new(Self::default()), files); + } + let dependent_files = current.parsed_dependents(&dirty_files); + let mut affected_files = dirty_files; + affected_files.extend(dependent_files); + affected_files.sort_unstable_by_key(|file| file.index()); + affected_files.dedup(); + if affected_files.is_empty() { + db.apply_change(change); + return (current.clone(), Vec::new()); + } + let store = current.fork(); + db.apply_change(change); + (triomphe::Arc::new(store), affected_files) + } + + pub(crate) fn fork(&self) -> Self { + Self { inner: Mutex::new(self.inner.lock().clone()) } + } + + pub(crate) fn record_parse_dependencies(&self, file_id: FileId, dependencies: Arc<[FileId]>) { + self.inner.lock().parse_dependencies.insert(file_id, dependencies); + } + + pub(crate) fn record_paid_file(&self, file_id: FileId) { + self.inner + .lock() + .parse_dependencies + .entry(file_id) + .or_insert_with(|| Arc::from(Vec::::new())); + } + + /// Files whose paid parse may be consulted for macro-generated owners. + pub(crate) fn paid_files(&self) -> Vec { + let mut files: Vec<_> = self.inner.lock().parse_dependencies.keys().copied().collect(); + files.sort_by_key(|file| file.index()); + files + } + + pub(crate) fn parsed_dependents(&self, changed: &[FileId]) -> Vec { + let changed = changed.iter().copied().collect::>(); + self.inner + .lock() + .parse_dependencies + .iter() + .filter_map(|(&file_id, dependencies)| { + (!changed.contains(&file_id) + && dependencies.iter().any(|dependency| changed.contains(dependency))) + .then_some(file_id) + }) + .collect() + } +} diff --git a/crates/ide/src/incrementality_benches.rs b/crates/ide/src/incrementality_benches.rs new file mode 100644 index 000000000..aab8f2649 --- /dev/null +++ b/crates/ide/src/incrementality_benches.rs @@ -0,0 +1,201 @@ +//! Synthetic incrementality benches. Run with: +//! `cargo test -p ide --release --lib incrementality_benches -- --ignored +//! --nocapture --test-threads=1` + +use std::{fmt::Write as _, time::Instant}; + +use base_db::{change::Change, source_root::SourceRoot}; +use design_graph::DesignGraphDb; +use vfs::{ChangedFile, FileId, FileSet, VfsPath}; + +use crate::{FilePosition, analysis_host::AnalysisHost}; + +/// Heavier than `module mN; endmodule` so a file_facts LRU miss is not free. +/// Eight assignments is still synthetic, but it is enough to tell a memo hit +/// from a re-extract. The previous one-line corpus hid the parse LRU behind +/// a fold that never re-queried. +fn module_text(index: usize) -> String { + let mut text = format!("module m{index};\n"); + for wire in 0..8 { + text.push_str(&format!(" wire w{wire};\n assign w{wire} = 1'b0;\n")); + } + text.push_str("endmodule\n"); + text +} + +/// Drop joins an in-flight prewarm. An empty change cancels that worker +/// without starting another, so the bench process cannot hang on join. +fn finish(host: &mut AnalysisHost) { + host.apply_change(Change::new()); +} + +fn assignment_body(wires: usize) -> String { + let mut body = String::with_capacity(wires * 40); + for wire in 0..wires { + let _ = write!(body, " wire w{wire};\n assign w{wire} = 1'b0;\n"); + } + body +} + +fn module_with_body(index: usize, body: &str) -> String { + format!("module m{index};\n{body}endmodule\n") +} + +fn workspace_with_body(n: usize, body: &str, lru: Option) -> AnalysisHost { + let mut file_set = FileSet::default(); + let mut change = Change::new(); + for index in 0..n { + let file_id = FileId::from_raw(index as u32); + file_set.insert(file_id, VfsPath::new_virtual_path(format!("/m{index}.sv"))); + change.add_changed_file(ChangedFile::create(file_id, module_with_body(index, body))); + } + change.set_roots(vec![SourceRoot::new_local(file_set)]); + let mut host = AnalysisHost::new(lru); + host.apply_change_without_prewarm(change); + host +} + +fn workspace_with_modules(n: usize) -> AnalysisHost { + workspace_with_body(n, &assignment_body(8), None) +} + +fn print_ms(label: &str, files: usize, elapsed: std::time::Duration) { + println!("{label}\tfiles={files}\t{:.3}ms", elapsed.as_secs_f64() * 1000.0); +} + +#[test] +#[ignore = "run with --release -- --ignored --nocapture"] +fn design_graph_fold_by_workspace_size() { + for files in [64, 256, 1024, 1280] { + let mut host = workspace_with_modules(files); + let started = Instant::now(); + let graph = host.ctx().unit_catalog(); + print_ms("design_graph.fold", files, started.elapsed()); + assert_eq!(graph.node_count(), files); + finish(&mut host); + } +} + +/// Cold fold never crosses a revision, so it cannot show LRU eviction. +/// Default parse LRU is 1024. After a 1280-file 2000-wire fold, a body +/// edit starts a revision and salsa evicts ~256 `file_facts` memos. +/// Refetching every `file_decls` is the work a salsa catalog revalidation +/// does after an edit. Coupled to the parse LRU that refetch was 379ms; +/// unbounded `file_decls` brings it to <1ms. A live salsa +/// `source_unit_catalog` memo would pin those deps and hide the cliff, +/// so this times the per-file refetch. +#[test] +#[ignore = "run with --release -- --ignored --nocapture"] +fn design_graph_refold_after_body_edit() { + const FILES: usize = 1280; + const WIRES: usize = 2000; + let body = assignment_body(WIRES); + let mut host = workspace_with_body(FILES, &body, None); + let started = Instant::now(); + let first = host.ctx().unit_catalog(); + print_ms("design_graph.fold", FILES, started.elapsed()); + assert_eq!(first.node_count(), FILES); + for index in 0..FILES { + let _ = ::file_decls(host.ctx().db, FileId::from_raw(index as u32)); + } + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify( + FileId::from_raw((FILES - 1) as u32), + format!("module m{};\n{body} wire x;\nendmodule\n", FILES - 1), + )); + host.apply_change_without_prewarm(change); + + let started = Instant::now(); + for index in 0..FILES { + let _ = ::file_decls(host.ctx().db, FileId::from_raw(index as u32)); + } + print_ms("file_decls.refetch_after_edit", FILES, started.elapsed()); + + let started = Instant::now(); + let production = host.ctx().unit_catalog(); + print_ms("product_store.refold", FILES, started.elapsed()); + assert_eq!(production.node_count(), FILES); + finish(&mut host); +} + +/// Salsa LRU evicts at the start of a new revision, not during a fold. +/// Capacity 2, three files, touch 0 then 1 then 2, edit file 2: file 0 is +/// the victim, file 1 stays and is still valid. +#[test] +#[ignore = "run with --release -- --ignored --nocapture"] +fn file_facts_lru_miss_is_not_free() { + fn large_module(index: usize) -> String { + let mut text = format!("module m{index};\n"); + for wire in 0..2000 { + text.push_str(&format!(" wire w{wire};\n assign w{wire} = 1'b0;\n")); + } + text.push_str("endmodule\n"); + text + } + let mut file_set = FileSet::default(); + let mut change = Change::new(); + for index in 0..3 { + let file_id = FileId::from_raw(index as u32); + file_set.insert(file_id, VfsPath::new_virtual_path(format!("/m{index}.sv"))); + change.add_changed_file(ChangedFile::create(file_id, large_module(index))); + } + change.set_roots(vec![SourceRoot::new_local(file_set)]); + let mut host = AnalysisHost::new(Some(2)); + host.apply_change(change); + let files = [FileId::from_raw(0), FileId::from_raw(1), FileId::from_raw(2)]; + for file in files { + let _ = host.ctx().file_facts(file); + } + let mut change = Change::new(); + let mut edited = large_module(2); + edited.insert_str(edited.find("endmodule").expect("large_module"), " wire x;\n"); + change.add_changed_file(ChangedFile::modify(files[2], edited)); + host.apply_change_without_prewarm(change); + let started = Instant::now(); + let _ = host.ctx().file_facts(files[0]); + print_ms("file_facts.lru_miss", 3, started.elapsed()); + let started = Instant::now(); + let _ = host.ctx().file_facts(files[1]); + print_ms("file_facts.lru_hit", 3, started.elapsed()); + finish(&mut host); +} + +#[test] +#[ignore = "run with --release -- --ignored --nocapture"] +fn first_request_after_body_edit() { + let files = 256; + let mut host = workspace_with_modules(files); + let _ = host.ctx().unit_catalog(); + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::modify(FileId::from_raw(0), { + let mut text = module_text(0); + text.insert_str(text.find("endmodule").expect("module_text"), " wire x;\n"); + text + })); + host.apply_change_without_prewarm(change); + + let started = Instant::now(); + let hover = host + .make_analysis() + .hover(FilePosition { + file_id: FileId::from_raw(0), + offset: "module ".len().try_into().unwrap(), + }) + .unwrap(); + print_ms("post_edit.hover", files, started.elapsed()); + assert!(hover.is_some(), "body-only edit must still hover the module name"); + + let started = Instant::now(); + let nav = host + .make_analysis() + .goto_definition(FilePosition { + file_id: FileId::from_raw(0), + offset: "module ".len().try_into().unwrap(), + }) + .unwrap(); + print_ms("post_edit.goto", files, started.elapsed()); + assert!(nav.is_some(), "body-only edit must still go to the module name"); + finish(&mut host); +} diff --git a/crates/ide/src/index_benchmarks.rs b/crates/ide/src/index_benchmarks.rs deleted file mode 100644 index ad7b31db2..000000000 --- a/crates/ide/src/index_benchmarks.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Ignored benchmarks for the per-source-root semantic index. -//! -//! These measure the *current* architecture's costs: -//! -//! - B2 `index_build_scales_with_file_size`: cold-build cost of -//! `SemanticIndex::for_source_root` (plus the `ModuleIndex` it pulls in) as a -//! function of file size. A linear-resolver design should cost O(bytes); -//! super-linear growth points at per-token scans. -//! - B3 `index_rebuild_after_single_file_change`: after touching one small file -//! in a root, the cost of re-serving the root index. If this is close to the -//! cold-build cost, the whole root is re-resolved on every change. -//! -//! Run with: -//! -//! ```text -//! cargo test -p ide --release -- --ignored --nocapture index_benchmarks -//! ``` - -use std::{ - fs, - time::{Duration, Instant}, -}; - -use base_db::{change::Change, source_db::SourceRootDb, source_root::SourceRoot}; -use utils::line_index::{TextRange, TextSize}; -use vfs::{ChangedFile, FileId, FileSet, VfsPath}; - -use crate::{ - FilePosition, ScopeVisibility, - analysis_host::AnalysisHost, - db::workspace_symbol_index_db::{ - source_root_module_index_for_root, source_root_semantic_index_for_root, - }, - document_highlight::DocumentHighlightConfig, - goto_definition, - references::ReferencesConfig, - semantic_index::{incoming_module_edges, outgoing_module_edges}, - test_utils::normalize_fixture_text, -}; - -/// One repeated module body; roughly 130 bytes with ~15 name-like tokens. -fn module_text(name: u32) -> String { - format!( - "module m{name}(input logic clk);\n logic a{name}, b{name};\n assign a{name} = b{name} ^ clk;\n always_ff @(posedge clk) b{name} <= a{name};\nendmodule\n\n" - ) -} - -/// A file dominated by macro expansions: one object-like macro emitting a -/// full module body, invoked once per generated module. Every expanded token -/// resolves inside a macro region, so this exercises the shared emitted-token -/// index path of `collect_file`. -fn macro_dense_text(modules: u32) -> String { - let mut text = String::from( - "`define GEN(n) module m{n}(input logic clk);\n logic a{n}, b{n};\n assign a{n} = b{n} ^ clk;\n always_ff @(posedge clk) b{n} <= a{n};\nendmodule\n", - ); - for n in 0..modules { - text.push_str(&format!("`GEN({n})\n")); - } - text -} - -fn file_text(modules: u32) -> String { - (0..modules).map(module_text).collect() -} - -fn bytes_of(modules: u32) -> usize { - file_text(modules).len() -} - -fn host_with_single_file(text: &str) -> (AnalysisHost, FileId) { - let text = normalize_fixture_text(text); - let file_id = FileId::from_raw(0); - let mut file_set = FileSet::default(); - file_set.insert(file_id, VfsPath::new_virtual_path("/bench.sv".to_owned())); - let mut change = Change::new(); - change.set_roots(vec![SourceRoot::new_local(file_set)]); - change.add_changed_file(ChangedFile::create(file_id, text.as_str())); - let mut host = AnalysisHost::default(); - host.apply_change(change); - (host, file_id) -} - -fn timed T, T>(f: F) -> (T, Duration) { - let start = Instant::now(); - let value = f(); - (value, start.elapsed()) -} - -#[test] -#[ignore] -fn index_benchmarks_macro_dense_build() { - let counts = [128u32, 256, 512, 1024]; - println!("\n== B4: cold SemanticIndex build, macro-dense file (release) =="); - println!("{:<10} {:<10} {:<14}", "calls", "bytes", "semantic_idx"); - for count in counts { - let text = macro_dense_text(count); - let (host, file_id) = host_with_single_file(&text); - let db = host.raw_db(); - let root_id = db.source_root_id(file_id); - let (_, semantic_cost) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - println!("{:<10} {:<10} {:<14?}", count, text.len(), semantic_cost); - } -} - -#[test] -#[ignore] -fn index_benchmarks_build_scales_with_file_size() { - let modules = [32usize, 64, 128, 256, 512, 1024]; - println!("\n== B2: cold SemanticIndex + ModuleIndex build vs file size (release) =="); - println!("{:<10} {:<10} {:<14} {:<14}", "modules", "bytes", "module_idx", "semantic_idx"); - for count in modules { - let text = file_text(count as u32); - let (host, file_id) = host_with_single_file(&text); - let db = host.raw_db(); - let root_id = db.source_root_id(file_id); - - let (_, module_cost) = - timed(|| std::hint::black_box(source_root_module_index_for_root(db, root_id))); - let (_, semantic_cost) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - - println!( - "{:<10} {:<10} {:<14?} {:<14?}", - count, - bytes_of(count as u32), - module_cost, - semantic_cost - ); - } -} - -/// Real-file soak test: loads `$VIDE_BENCH_FILE` as a single-file root and -/// times the cold parse, module index, semantic index, one representative -/// request of each navigation feature, and the incremental rebuild after a -/// one-byte touch at the end of the file. -/// -/// Set `$VIDE_BENCH_PROBE` to a module identifier when the file does not use -/// the fixture's default `array_0_ext` probe. -/// -/// Run with: -/// -/// ```text -/// VIDE_BENCH_FILE=~/Downloads/XS.v VIDE_BENCH_PROBE=top \ -/// cargo test -p ide --release -- --ignored --nocapture index_benchmarks_real_file -/// ``` -#[test] -#[ignore] -fn index_benchmarks_real_file() { - let Some(path) = std::env::var_os("VIDE_BENCH_FILE") else { - println!("VIDE_BENCH_FILE not set; skipping real-file benchmark"); - return; - }; - let path = std::path::PathBuf::from(path); - let text = fs::read_to_string(&path).expect("read benchmark file"); - let line_count = text.lines().count(); - eprintln!( - "\n== B5: real-file soak test ({path:?}, {line_count} lines, {} bytes) ==", - text.len() - ); - - let file_id = FileId::from_raw(0); - let mut file_set = FileSet::default(); - file_set.insert(file_id, VfsPath::new_virtual_path("/XS.v".to_owned())); - let mut change = Change::new(); - change.set_roots(vec![SourceRoot::new_local(file_set)]); - change.add_changed_file(ChangedFile::create(file_id, text.as_str())); - let mut host = AnalysisHost::default(); - host.apply_change(change); - - let db = host.raw_db(); - let root_id = db.source_root_id(file_id); - - let (_, parse_cost) = timed(|| std::hint::black_box(db.parse(file_id.into()))); - eprintln!("cold parse: {parse_cost:?}"); - - let (_, module_cost) = - timed(|| std::hint::black_box(source_root_module_index_for_root(db, root_id))); - eprintln!("module index: {module_cost:?}"); - - let (_, semantic_cost) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - eprintln!("semantic index (cold, first build): {semantic_cost:?}"); - - let probe = std::env::var("VIDE_BENCH_PROBE").unwrap_or_else(|_| "array_0_ext".to_owned()); - let probe_offset = TextSize::from( - u32::try_from( - text.find(&probe).unwrap_or_else(|| panic!("probe module {probe:?} should exist")), - ) - .unwrap(), - ); - let position = FilePosition { file_id, offset: probe_offset }; - - let (nav, goto_cost) = timed(|| goto_definition::goto_definition(db, position)); - eprintln!( - "goto definition on first module ({probe}): {goto_cost:?} ({} targets)", - nav.map_or(0, |info| info.info.len()) - ); - - let (highlights, highlight_cost) = timed(|| { - crate::document_highlight::document_highlight( - db, - position, - DocumentHighlightConfig { scope_visibility: ScopeVisibility::Public }, - ) - }); - eprintln!( - "document highlight: {highlight_cost:?} ({} highlights)", - highlights.map_or(0, |h| h.len()) - ); - - let (refs, refs_cost) = timed(|| { - crate::references::references( - db, - position, - ReferencesConfig::new(ScopeVisibility::Public, None), - ) - }); - let ref_count = - refs.map_or(0, |rs| rs.iter().map(|r| r.refs.values().map(Vec::len).sum::()).sum()); - eprintln!("find references (workspace): {refs_cost:?} ({ref_count} refs)"); - - let probe_range = TextRange::new(probe_offset, probe_offset + TextSize::of(&probe)); - let (incoming, incoming_cost) = timed(|| incoming_module_edges(db, file_id, probe_range)); - eprintln!( - "call hierarchy incoming: {incoming_cost:?} ({} edges)", - incoming.len() - ); - let (outgoing, outgoing_cost) = timed(|| outgoing_module_edges(db, file_id, probe_range)); - eprintln!( - "call hierarchy outgoing: {outgoing_cost:?} ({} edges)", - outgoing.len() - ); - - // One-byte touch at the end of the file, then rebuild. - let mut touch = Change::new(); - let touched = format!("{text} "); - touch.add_changed_file(ChangedFile::create(file_id, touched.as_str())); - host.apply_change(touch); - let db = host.raw_db(); - let (_, rebuild_cost) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - eprintln!("semantic index (rebuild after one-byte touch): {rebuild_cost:?}"); -} - -/// Micro-benchmark separating the per-token nameres costs: the salsa -/// `scope_for` hit, the `NameScope::lookup` hash, and the `ScopeParent` walk. -/// Debug instrumentation for the index-build fast path. -#[test] -#[ignore] -fn index_benchmarks_nameres_primitives() { - println!("retired: superseded by the scope-chain fast path"); -} - -#[test] -#[ignore] -fn index_benchmarks_rebuild_after_single_file_change() { - println!("\n== B3: root index rebuild after touching one small file (release) =="); - - let big_text = file_text(512); // ~64 KB - let small_text = "module small;\n logic s;\nendmodule\n"; - - let big_file = FileId::from_raw(0); - let small_file = FileId::from_raw(1); - let mut file_set = FileSet::default(); - file_set.insert(big_file, VfsPath::new_virtual_path("/big.sv".to_owned())); - file_set.insert(small_file, VfsPath::new_virtual_path("/small.sv".to_owned())); - - let mut change = Change::new(); - change.set_roots(vec![SourceRoot::new_local(file_set)]); - change.add_changed_file(ChangedFile::create(big_file, big_text.as_str())); - change.add_changed_file(ChangedFile::create(small_file, small_text)); - let mut host = AnalysisHost::default(); - host.apply_change(change); - - let db = host.raw_db(); - let root_id = db.source_root_id(big_file); - - let (_, cold) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - println!("cold build of root (64KB big file + small file): {cold:?}"); - - // Touch only the small file: append a comment. - let mut touch = Change::new(); - touch.add_changed_file(ChangedFile::create( - small_file, - "module small;\n logic s; // touched\nendmodule\n", - )); - host.apply_change(touch); - - let db = host.raw_db(); - let (_, rebuild) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(db, root_id))); - println!("rebuild after touching only the small file: {rebuild:?}"); - - // Lower bound: building an index for a root containing only the small - // file. If `rebuild` is close to `cold` instead of close to this, the - // whole root is re-resolved on every change. - let mut single_set = FileSet::default(); - single_set.insert(small_file, VfsPath::new_virtual_path("/small.sv".to_owned())); - let mut single_change = Change::new(); - single_change.set_roots(vec![SourceRoot::new_local(single_set)]); - single_change.add_changed_file(ChangedFile::create( - small_file, - "module small;\n logic s; // touched\nendmodule\n", - )); - let mut single_host = AnalysisHost::default(); - single_host.apply_change(single_change); - let single_db = single_host.raw_db(); - let single_root = single_db.source_root_id(small_file); - let (_, lower_bound) = - timed(|| std::hint::black_box(source_root_semantic_index_for_root(single_db, single_root))); - println!("lower bound (indexing only the small file alone): {lower_bound:?}"); -} diff --git a/crates/ide/src/inlay_hint.rs b/crates/ide/src/inlay_hint.rs index 25fa71fe8..8971e74cb 100644 --- a/crates/ide/src/inlay_hint.rs +++ b/crates/ide/src/inlay_hint.rs @@ -202,6 +202,7 @@ impl InlayHintCollector { pub(crate) fn inlay_hint( db: &RootDb, + context: &hir_def::pathres::ResolutionContext, file_id: FileId, range: TextRange, config: InlayHintConfig, @@ -231,7 +232,7 @@ pub(crate) fn inlay_hint( }; if collector.intersect(range) { - collect_module_items(db, module_id, module_src, &mut collector); + collect_module_items(db, context, module_id, module_src, &mut collector); } } _ => {} @@ -297,6 +298,7 @@ fn collect_macro_argument_hints_for_call( fn collect_module_items( db: &RootDb, + context: &hir_def::pathres::ResolutionContext, module_id: OwnerId, module_src: SourceAstId, collector: &mut InlayHintCollector, @@ -304,7 +306,7 @@ fn collect_module_items( let module = db.body_with_source_map(module_id); if collector.config.instantiation() { - collect_instantiations_in_body(db, module_id, &module, collector); + collect_instantiations_in_body(db, context, module_id, &module, collector); } if collector.config.end_structure @@ -321,6 +323,7 @@ fn collect_module_items( fn collect_instantiations_in_body( db: &RootDb, + context: &hir_def::pathres::ResolutionContext, module_id: OwnerId, body: &Lowered, collector: &mut InlayHintCollector, @@ -332,18 +335,18 @@ fn collect_instantiations_in_body( if let Some(range) = body.source_range(db, *instantiation_id) && collector.intersect(range) { - process_instantiation(db, module_id, body, instantiation, collector); + process_instantiation(db, context, module_id, body, instantiation, collector); } } BodyItem::GenerateRegionId(region_id) => { let region = body.get(*region_id); for item in ®ion.items { - collect_instantiation_item(db, module_id, body, item, collector); + collect_instantiation_item(db, context, module_id, body, item, collector); } } BodyItem::GenerateBlockOwner(owner) => { let generate_body = db.body_with_source_map(*owner); - collect_instantiations_in_body(db, module_id, &generate_body, collector); + collect_instantiations_in_body(db, context, module_id, &generate_body, collector); } _ => {} } @@ -352,6 +355,7 @@ fn collect_instantiations_in_body( fn collect_instantiation_item( db: &RootDb, + context: &hir_def::pathres::ResolutionContext, module_id: OwnerId, body: &Lowered, item: &BodyItem, @@ -363,12 +367,12 @@ fn collect_instantiation_item( if let Some(range) = body.source_range(db, *instantiation_id) && collector.intersect(range) { - process_instantiation(db, module_id, body, instantiation, collector); + process_instantiation(db, context, module_id, body, instantiation, collector); } } BodyItem::GenerateBlockOwner(owner) => { let generate_body = db.body_with_source_map(*owner); - collect_instantiations_in_body(db, module_id, &generate_body, collector); + collect_instantiations_in_body(db, context, module_id, &generate_body, collector); } _ => {} } @@ -427,14 +431,14 @@ fn module_end_range(db: &RootDb, file_id: HirFileId, source: SourceAstId) -> Opt fn process_instantiation( db: &RootDb, - module_id: OwnerId, + context: &hir_def::pathres::ResolutionContext, + _module_id: OwnerId, module: &Lowered, instantiation: &Instantiation, collector: &mut InlayHintCollector, ) -> Option<()> { - let from_file = module_id.file(db).source_file_id(db)?; let target_module_id = - resolve_module_name(db, from_file, instantiation.module_name.as_ref()?).unique()?; + resolve_module_name(db, context, instantiation.module_name.as_ref()?).unique()?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); @@ -785,7 +789,13 @@ mod tests { let source = "module child(output instr_addr_o); endmodule\n\ module top; logic instr_addr_o; child u(instr_addr_o); endmodule\n"; let (db, file_id) = db_with_file(source); - let hints = inlay_hint(&db, file_id, TextRange::up_to(TextSize::of(source)), port_config()); + let hints = inlay_hint( + &db, + &hir_def::unit::test_resolution(&db), + file_id, + TextRange::up_to(TextSize::of(source)), + port_config(), + ); let hint = hints.iter().find(|hint| hint.label == "→").expect("same-name port hint"); assert!(hint.target_location.is_some()); @@ -808,7 +818,13 @@ mod tests { endgenerate\n\ endmodule\n"; let (db, file_id) = db_with_file(source); - let hints = inlay_hint(&db, file_id, TextRange::up_to(TextSize::of(source)), port_config()); + let hints = inlay_hint( + &db, + &hir_def::unit::test_resolution(&db), + file_id, + TextRange::up_to(TextSize::of(source)), + port_config(), + ); assert!( hints.iter().any(|hint| hint.label == "→"), @@ -824,6 +840,7 @@ mod tests { let (db, file_id) = db_with_file(&fixture.source); let hints = inlay_hint( &db, + &hir_def::unit::test_resolution(&db), file_id, fixture.range.expect("fixture range should be initialized"), fixture.config, diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 5ff12756e..6f7d3df45 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -11,7 +11,9 @@ pub type Cancellable = Result; pub mod analysis; pub mod analysis_host; +pub mod anchor; pub mod definitions; +pub mod hier; pub(crate) mod manifest; pub mod markup; pub(crate) mod module_resolution; @@ -23,27 +25,31 @@ pub mod code_action; pub mod code_lens; pub mod completion; pub mod db; +pub(crate) mod design_unit; pub mod diagnostics; pub mod document_highlight; pub mod document_symbols; +pub(crate) mod elaboration; pub mod folding_ranges; pub mod formatting; pub mod goto_declaration; pub mod goto_definition; pub mod hover; +pub(crate) mod incrementality; #[cfg(test)] -mod index_benchmarks; +mod incrementality_benches; pub mod inlay_hint; #[cfg(test)] mod macro_hover_tests; pub mod range; +pub mod reference_support; pub mod references; pub mod rename; pub mod selection_ranges; -pub mod semantic_index; pub(crate) mod semantic_target; pub mod semantic_tokens; pub mod signature_help; +pub(crate) mod slang_class; #[cfg(test)] mod test_utils; pub(crate) mod token; diff --git a/crates/ide/src/manifest.rs b/crates/ide/src/manifest.rs index 19b27f5df..45302899f 100644 --- a/crates/ide/src/manifest.rs +++ b/crates/ide/src/manifest.rs @@ -559,16 +559,16 @@ pub(crate) fn highlights_target( } pub(crate) fn references_target( - db: &RootDb, + db: &crate::analysis::AnalysisContext<'_>, target: ManifestTarget, config: ReferencesConfig, ) -> Option> { - let info = target_info(db, target)?; + let info = target_info(db.db, target)?; let selected = info.selected_value?; if info.key != "top_modules" { return None; } - let modules = module_targets(db, &selected.text); + let modules = module_targets(db.db, &selected.text); let [module] = modules.as_slice() else { tracing::debug!( ?info.file_id, @@ -598,19 +598,19 @@ pub(crate) fn target_range(db: &RootDb, target: ManifestTarget) -> Option, target: ManifestTarget, config: &crate::rename::RenameConfig, new_name: &str, ) -> Result { - let info = target_info(db, target).ok_or(crate::rename::RenameError::NoRefFound)?; + let info = target_info(db.db, target).ok_or(crate::rename::RenameError::NoRefFound)?; let value = info.selected_value.ok_or(crate::rename::RenameError::NoRefFound)?; if info.key != "top_modules" { return Err(crate::rename::RenameError::NoRefFound); } let edit_range = value.edit_range.ok_or(crate::rename::RenameError::NoRefFound)?; - let modules = module_targets(db, &value.text); + let modules = module_targets(db.db, &value.text); let [module] = modules.as_slice() else { tracing::debug!( ?info.file_id, diff --git a/crates/ide/src/module_resolution.rs b/crates/ide/src/module_resolution.rs index 0b1410bf0..c540e787a 100644 --- a/crates/ide/src/module_resolution.rs +++ b/crates/ide/src/module_resolution.rs @@ -1,6 +1,3 @@ -use std::cmp::Ordering; - -use base_db::source_root::SourceRootRole; use hir_def::{ Ident, body::Body, @@ -24,80 +21,49 @@ use syntax::{ SyntaxAncestors, ast::{self, AstNode}, }; -use vfs::{FileId, VfsPath}; - -use crate::db::workspace_symbol_index_db::{ - WorkspaceSymbolIndexDb, source_root_module_index_for_root, -}; -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ModuleResolution { - Unique(OwnerId), - BestEffortProximity { selected: OwnerId, candidates: Vec }, - Ambiguous { candidates: Vec, kind: ModuleResolutionAmbiguity }, - Unresolved, -} +use crate::db::workspace_symbol_index_db::WorkspaceSymbolIndexDb; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ModuleResolutionAmbiguity { - Strict, - BestEffortTie, -} +pub(crate) type ModuleResolution = Resolution; -impl ModuleResolution { - pub(crate) fn unique(&self) -> Option { - match self { - ModuleResolution::Unique(module_id) => Some(*module_id), - ModuleResolution::BestEffortProximity { selected, .. } => Some(*selected), - ModuleResolution::Ambiguous { .. } | ModuleResolution::Unresolved => None, - } - } - - fn into_resolution(self) -> Resolution { - match self { - ModuleResolution::Unique(module_id) - | ModuleResolution::BestEffortProximity { selected: module_id, .. } => { - Resolution::Unique(module_id) - } - ModuleResolution::Ambiguous { candidates, .. } => { - Resolution::from_candidates(candidates) - } - ModuleResolution::Unresolved => Resolution::Unresolved, - } - } +fn module_resolution_from_context( + db: &dyn HirDefDb, + context: &hir_def::pathres::ResolutionContext, + name: &Ident, +) -> ModuleResolution { + Resolution::from_candidates(context.locate_hierarchy_targets(db, name)) } pub(crate) fn resolve_instantiation_target( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, instantiation: ast::HierarchyInstantiation, ) -> ModuleResolution { let Some(name) = lower_ident_opt(instantiation.type_()) else { return ModuleResolution::Unresolved; }; - resolve_module_name(db, from_file, &name) + resolve_module_name(db, context, &name) } pub(crate) fn resolve_hir_instantiation_target( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, instantiation: &Instantiation, ) -> Option { - resolve_module_name(db, from_file, instantiation.module_name.as_ref()?).unique() + resolve_module_name(db, context, instantiation.module_name.as_ref()?).unique() } pub(crate) fn resolve_module_name( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, name: &Ident, ) -> ModuleResolution { - let policy = ModuleResolutionPolicy::for_file(db, from_file); - resolve_module_name_with_policy(db, name, policy) + module_resolution_from_context(db, context, name) } pub(crate) fn resolve_named_port_connection( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, conn: ast::NamedPortConnection, ) -> Resolution { let Some(name) = lower_ident_opt(conn.name()) else { @@ -108,12 +74,12 @@ pub(crate) fn resolve_named_port_connection( else { return Resolution::Unresolved; }; - resolve_named_port_in_instantiation(db, from_file, instantiation, &name) + resolve_named_port_in_instantiation(db, context, instantiation, &name) } pub(crate) fn resolve_named_param_assignment( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, assign: ast::NamedParamAssignment, ) -> Resolution { let Some(name) = lower_ident_opt(assign.name()) else { @@ -124,28 +90,26 @@ pub(crate) fn resolve_named_param_assignment( else { return Resolution::Unresolved; }; - resolve_named_param_in_instantiation(db, from_file, instantiation, &name) + resolve_named_param_in_instantiation(db, context, instantiation, &name) } fn resolve_named_port_in_instantiation( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, instantiation: ast::HierarchyInstantiation, port_name: &Ident, ) -> Resolution { - resolve_instantiation_target(db, from_file, instantiation) - .into_resolution() + resolve_instantiation_target(db, context, instantiation) .and_then(|module_id| resolve_named_port_in_module(db, module_id, port_name)) } fn resolve_named_param_in_instantiation( db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, + context: &hir_def::pathres::ResolutionContext, instantiation: ast::HierarchyInstantiation, param_name: &Ident, ) -> Resolution { - resolve_instantiation_target(db, from_file, instantiation) - .into_resolution() + resolve_instantiation_target(db, context, instantiation) .and_then(|module_id| resolve_named_param_in_module(db, module_id, param_name)) } @@ -296,173 +260,6 @@ pub(crate) fn resolve_named_param_in_module( })) } -fn resolve_module_name_with_policy( - db: &dyn WorkspaceSymbolIndexDb, - name: &Ident, - policy: ModuleResolutionPolicy, -) -> ModuleResolution { - let candidates = module_candidates(db, name); - match candidates.as_slice() { - [module_id] => ModuleResolution::Unique(*module_id), - [] => ModuleResolution::Unresolved, - _ => policy.resolve_ambiguous(db, candidates), - } -} - -fn module_candidates(db: &dyn WorkspaceSymbolIndexDb, name: &Ident) -> Vec { - let mut candidates = Vec::new(); - for source_root_id in db.workspace_source_root_ids().iter().copied() { - let module_index = source_root_module_index_for_root(db, source_root_id); - candidates.extend( - module_index - .module_definitions(name) - .iter() - .map(|module| (module.file_id, module.name_range.start(), module.module_id)), - ); - } - - candidates.sort_by_key(|(file_id, name_start, _)| (file_id.index(), *name_start)); - candidates.dedup_by_key(|(_, _, module_id)| *module_id); - candidates.into_iter().map(|(_, _, module_id)| module_id).collect() -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ModuleResolutionPolicy { - Strict, - // Best-effort indexing has no manifest-backed compilation profile. Use - // source proximity as an IDE-only tie breaker, but only when it produces a - // unique candidate; configured roots keep duplicate module names ambiguous. - BestEffortProximity { from_file: FileId }, -} - -impl ModuleResolutionPolicy { - fn for_file(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Self { - match source_root_role(db, file_id) { - SourceRootRole::BestEffortIndex => Self::BestEffortProximity { from_file: file_id }, - SourceRootRole::Local | SourceRootRole::Library | SourceRootRole::Ignored => { - Self::Strict - } - } - } - - fn resolve_ambiguous( - self, - db: &dyn WorkspaceSymbolIndexDb, - candidates: Vec, - ) -> ModuleResolution { - match self { - Self::Strict => { - ModuleResolution::Ambiguous { candidates, kind: ModuleResolutionAmbiguity::Strict } - } - Self::BestEffortProximity { from_file } => { - resolve_by_proximity(db, from_file, candidates) - } - } - } -} - -fn resolve_by_proximity( - db: &dyn WorkspaceSymbolIndexDb, - from_file: FileId, - mut candidates: Vec, -) -> ModuleResolution { - let mut best_score = None; - let mut best_modules = Vec::new(); - - for module_id in candidates.iter().copied() { - let Some(score_file) = module_id.file(db).source_file_id(db) else { - continue; - }; - let score = ProximityScore::new(db, from_file, score_file); - match best_score { - None => { - best_score = Some(score); - best_modules.push(module_id); - } - Some(best) => match score.preference_cmp(&best) { - Ordering::Greater => { - best_score = Some(score); - best_modules.clear(); - best_modules.push(module_id); - } - Ordering::Equal => best_modules.push(module_id), - Ordering::Less => {} - }, - } - } - - candidates.sort_by_key(|module_id| { - module_id.file(db).source_file_id(db).map_or(u32::MAX, FileId::index) - }); - - match best_modules.as_slice() { - [] => ModuleResolution::Unresolved, - [selected] => ModuleResolution::BestEffortProximity { selected: *selected, candidates }, - _ => ModuleResolution::Ambiguous { - candidates, - kind: ModuleResolutionAmbiguity::BestEffortTie, - }, - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ProximityScore { - same_file: bool, - common_dir_depth: usize, - same_source_root: bool, -} - -impl ProximityScore { - fn new(db: &dyn WorkspaceSymbolIndexDb, from_file: FileId, candidate_file: FileId) -> Self { - Self { - same_file: from_file == candidate_file, - common_dir_depth: common_dir_depth( - file_path(db, from_file), - file_path(db, candidate_file), - ), - same_source_root: db.source_root_id(from_file) == db.source_root_id(candidate_file), - } - } - - fn preference_cmp(&self, other: &Self) -> Ordering { - // Prefer exact file matches, then nearest directory, then source-root locality. - self.same_file - .cmp(&other.same_file) - .then_with(|| self.common_dir_depth.cmp(&other.common_dir_depth)) - .then_with(|| self.same_source_root.cmp(&other.same_source_root)) - } -} - -fn source_root_role(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> SourceRootRole { - let source_root_id = db.source_root_id(file_id); - db.source_root(source_root_id).role() -} - -fn file_path(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Option { - let source_root_id = db.source_root_id(file_id); - db.source_root(source_root_id).path_for_file(&file_id).cloned() -} - -fn common_dir_depth(left: Option, right: Option) -> usize { - let (Some(left), Some(right)) = (left, right) else { - return 0; - }; - let left = dir_ancestors(left); - let right = dir_ancestors(right); - left.iter().zip(right.iter()).take_while(|(left, right)| left == right).count() -} - -fn dir_ancestors(path: VfsPath) -> Vec { - let mut ancestors = Vec::new(); - let mut current = path.parent(); - while let Some(path) = current { - current = path.parent(); - ancestors.push(path); - } - ancestors.reverse(); - ancestors -} - #[cfg(test)] mod tests { use std::path::Path; @@ -472,7 +269,7 @@ mod tests { use smol_str::SmolStr; use syntax::{SyntaxNodeExt, ast}; use utils::text_edit::TextSize; - use vfs::{ChangedFile, FileId, FileSet}; + use vfs::{ChangedFile, FileId, FileSet, VfsPath}; use super::*; use crate::db::root_db::RootDb; @@ -618,7 +415,8 @@ mod tests { match fixture.query { Query::Module(module) => { - let result = resolve_module_name(&db, fixture.focus, &module); + let result = + resolve_module_name(&db, &hir_def::unit::test_resolution(&db), &module); format_module_resolution(&db, &fixture.files, result) } Query::NamedPort => { @@ -628,14 +426,12 @@ mod tests { let port_conn = root .find_node_at_offset::(offset) .expect("named port connection should parse at /*caret*/"); - let res = resolve_named_port_connection(&db, fixture.focus, port_conn); - match resolution_module_id(&db, &res, DefKind::Port) { - Some(module_id) => format!( - "AnsiPort module={}", - file_path(&fixture.files, module_id.file(&db).as_file().unwrap()) - ), - None => format!("{res:?}"), - } + let res = resolve_named_port_connection( + &db, + &hir_def::unit::test_resolution(&db), + port_conn, + ); + format_def_resolution(&db, &fixture.files, &res, DefKind::Port, "AnsiPort") } Query::NamedParam => { let offset = fixture.offset.expect("named_param query requires /*caret*/"); @@ -644,14 +440,12 @@ mod tests { let param_assign = root .find_node_at_offset::(offset) .expect("named parameter assignment should parse at /*caret*/"); - let res = resolve_named_param_assignment(&db, fixture.focus, param_assign); - match resolution_module_id(&db, &res, DefKind::Param) { - Some(module_id) => format!( - "ParamDecl module={}", - file_path(&fixture.files, module_id.file(&db).as_file().unwrap()) - ), - None => format!("{res:?}"), - } + let res = resolve_named_param_assignment( + &db, + &hir_def::unit::test_resolution(&db), + param_assign, + ); + format_def_resolution(&db, &fixture.files, &res, DefKind::Param, "ParamDecl") } } } @@ -668,6 +462,32 @@ mod tests { Some(def_id.container_id(db)) } + fn format_def_resolution( + db: &RootDb, + files: &[(String, String)], + res: &Resolution, + kind: DefKind, + unique_label: &str, + ) -> String { + match resolution_module_id(db, res, kind) { + Some(module_id) => format!( + "{unique_label} module={}", + file_path(files, module_id.file(db).as_file().unwrap()) + ), + None => match res { + Resolution::Ambiguous(candidates) => { + let owners = candidates + .iter() + .filter(|def_id| def_id.kind(db) == kind) + .map(|def_id| def_id.container_id(db)) + .collect(); + format!("Ambiguous candidates={:?}", candidate_paths(db, files, owners)) + } + other => format!("{other:?}"), + }, + } + } + fn format_module_resolution( db: &RootDb, files: &[(String, String)], @@ -680,15 +500,10 @@ mod tests { file_path(files, module_id.file(db).as_file().unwrap()) ) } - ModuleResolution::BestEffortProximity { selected, candidates } => format!( - "BestEffortProximity selected={} candidates={:?}", - file_path(files, selected.file(db).as_file().unwrap()), - candidate_paths(db, files, candidates) - ), - ModuleResolution::Ambiguous { candidates, kind } => { + ModuleResolution::Ambiguous(candidates) => { format!( - "Ambiguous kind={kind:?} candidates={:?}", - candidate_paths(db, files, candidates) + "Ambiguous candidates={:?}", + candidate_paths(db, files, candidates.into_iter().collect()) ) } ModuleResolution::Unresolved => "Unresolved".to_string(), diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs index ec4c8dfbb..093cc4859 100644 --- a/crates/ide/src/navigation_target.rs +++ b/crates/ide/src/navigation_target.rs @@ -1,5 +1,6 @@ use hir_def::{ container::{InFile, OwnerRef}, + db::HirDefDb, def_id::DefId, expr::declarator::DeclId, file::{config::ConfigDeclId, library::LibraryDeclId, udp::UdpDeclId}, @@ -11,7 +12,6 @@ use hir_def::{ symbol::DefOrigin, typedef::TypedefId, }; -use hir_ty::db::TyDb; use preproc_expand::file::HirFileId; use smol_str::SmolStr; use syntax::{SyntaxTokenWithParent, has_text_range::HasTextRange}; @@ -132,7 +132,7 @@ fn build( /// macro call. Returns `None` when a macro expansion's call site cannot be /// resolved. pub(crate) fn nav_location( - db: &dyn TyDb, + db: &dyn HirDefDb, file_id: HirFileId, name_range: Option, full_range: TextRange, diff --git a/crates/ide/src/reference_support.rs b/crates/ide/src/reference_support.rs new file mode 100644 index 000000000..5325d474b --- /dev/null +++ b/crates/ide/src/reference_support.rs @@ -0,0 +1,757 @@ +use design_graph::UnitId; +use hir_def::def_id::DefId; +use utils::line_index::TextRange; +use vfs::FileId; + +pub(crate) mod build; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct SemanticDefinitionRange { + pub file_id: FileId, + pub range: TextRange, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ConnSide { + /// The reference is the port side of a shorthand connection (`.name`). + Port, + /// The reference is the local side of a shorthand connection (`.name`). + Local, +} + +/// Context of a reference token inside a named port connection, resolved +/// on demand when references or rename walk the current file. +/// +/// `paired` is `Some` exactly when the connection is a same-name connection +/// (the `.name` and the data identifier have the same text): for the name +/// side it is the local definition, for the data side it is the port +/// definition. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ReferenceContext { + Plain, + /// The token is the `.name` of a named port connection. + ConnName { + /// Range of the data identifier, when the data is a simple identifier. + ident_range: Option, + /// Range from the name token start to the closing paren end. + collapse_range: Option, + /// No-parens shorthand connection (`.name`). + shorthand: bool, + /// The side of a shorthand connection this reference belongs to. + side: ConnSide, + /// Same-name connections: the local definition of the data identifier. + paired: Option, + }, + /// The token is a simple identifier in the data position of a named port + /// connection. + ConnData { + /// Range of the connection's `.name` token. + name_range: TextRange, + /// Range from the name token start to the closing paren end. + collapse_range: Option, + /// Same-name connections: the port definition of the name token. + paired: Option, + }, +} + +impl ReferenceContext { + /// The paired same-name connection definition, when the connection is + /// same-name: the local def for name tokens, the port def for data + /// tokens, and the counterpart def for shorthand references. + pub(crate) fn paired(&self) -> Option<&DefId> { + match self { + ReferenceContext::Plain => None, + ReferenceContext::ConnName { paired, .. } + | ReferenceContext::ConnData { paired, .. } => paired.as_ref(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModuleCallItem { + pub file_id: FileId, + pub name: String, + pub full_range: TextRange, + pub name_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModuleCallEdge { + pub caller: ModuleCallItem, + pub callee: ModuleCallItem, + pub call_range: TextRange, +} + +pub(crate) fn incoming_module_edges( + db: &crate::analysis::AnalysisContext<'_>, + file_id: FileId, + name_range: TextRange, +) -> Vec { + let Some(callee) = unit_at_name_range(db, file_id, name_range) else { + return Vec::new(); + }; + let graph = db.unit_catalog(); + let mut edges = Vec::new(); + for file in reference_files(db) { + let facts = db.file_facts(file); + for site in facts.instantiations.iter() { + let Some(caller) = site.container.clone() else { + continue; + }; + let targets = graph.candidates(&site.name, site.role); + if targets.len() == 1 && targets[0] == callee { + edges.push(ModuleCallEdge { + caller: call_item(db, &caller), + callee: call_item(db, &callee), + call_range: site.range, + }); + } + } + } + sort_and_dedup_edges(&mut edges); + edges +} + +pub(crate) fn outgoing_module_edges( + db: &crate::analysis::AnalysisContext<'_>, + file_id: FileId, + name_range: TextRange, +) -> Vec { + let Some(caller) = unit_at_name_range(db, file_id, name_range) else { + return Vec::new(); + }; + let graph = db.unit_catalog(); + let facts = db.file_facts(file_id); + let mut edges = Vec::new(); + for site in facts.instantiations.iter().filter(|site| site.container.as_ref() == Some(&caller)) + { + let targets = graph.candidates(&site.name, site.role); + if targets.len() != 1 { + continue; + } + edges.push(ModuleCallEdge { + caller: call_item(db, &caller), + callee: call_item(db, &targets[0]), + call_range: site.range, + }); + } + sort_and_dedup_edges(&mut edges); + edges +} + +fn unit_at_name_range( + db: &crate::analysis::AnalysisContext<'_>, + file_id: FileId, + name_range: TextRange, +) -> Option { + db.file_facts(file_id).unit_at_name_range(name_range).map(|unit| unit.id.clone()) +} + +fn call_item(db: &crate::analysis::AnalysisContext<'_>, unit: &UnitId) -> ModuleCallItem { + let facts = db.file_facts(unit.file); + let node = facts.unit(unit.clone()); + let name_range = node + .and_then(|node| node.name_range) + .unwrap_or_else(|| TextRange::empty(utils::line_index::TextSize::new(0))); + ModuleCallItem { + file_id: unit.file, + name: unit.name.to_string(), + full_range: node.and_then(|node| node.header_range).unwrap_or(name_range), + name_range, + } +} + +fn reference_files(db: &crate::analysis::AnalysisContext<'_>) -> Vec { + db.files() + .iter() + .copied() + .filter(|&file| db.file_kind(file).is_semantic_compilation_unit()) + .collect() +} + +fn sort_and_dedup_edges(edges: &mut Vec) { + edges.sort_by_key(|edge| { + ( + edge.caller.file_id.index(), + edge.caller.name_range.start(), + edge.callee.file_id.index(), + edge.callee.name_range.start(), + edge.call_range.start(), + ) + }); + edges.dedup(); +} + +#[cfg(test)] +mod tests { + use hir_def::symbol::NameContext; + use hir_semantics::semantics::SemanticsImpl; + use preproc_expand::file::HirFileId; + use syntax::{ + SyntaxElement, SyntaxNodeExt, WalkEvent, + ast::{self, AstNode}, + has_text_range::HasTextRange, + token::TokenKindExt, + }; + use triomphe::Arc; + use utils::line_index::{TextRange, TextSize}; + + use super::*; + use crate::{ + ScopeVisibility, + definitions::DefinitionClass, + reference_support::build::{ + ContainerCache, ScopeChainCache, definition_ranges_for, token_in_special_context, + }, + references::{ + ReferencesConfig, + search::{SearchScope, search_references}, + }, + semantic_target::{ + SemanticTarget, TargetIntent, preproc::emit_token_index, + resolve_semantic_target_with_emitted, + }, + test_utils::{setup_marked, setup_marked_files}, + }; + + fn def_named_at( + db: &crate::analysis::AnalysisContext<'_>, + file_id: FileId, + range: TextRange, + ) -> DefId { + let tree = db.parse(HirFileId::from(file_id)); + let token = tree + .root() + .token_at_offset(range.start()) + .find(|token| token.text_range() == Some(range)) + .expect("definition token"); + match DefinitionClass::resolve(db, file_id.into(), token).unique().expect("unique def") { + DefinitionClass::Definition(def) => def, + DefinitionClass::PortConnShorthand { port, .. } => port, + } + } + + fn workspace_refs( + db: &crate::analysis::AnalysisContext<'_>, + def: DefId, + ) -> Vec<(FileId, TextRange, ReferenceContext)> { + let scope = + SearchScope::new(db.db, &def, ReferencesConfig::new(ScopeVisibility::Public, None)); + search_references(db, &def, scope) + .into_iter() + .flat_map(|(file_id, tokens)| { + tokens + .into_iter() + .map(move |token| (file_id, token.range(), token.context().clone())) + }) + .collect() + } + + /// A non-structural (body-only) edit must drop a removed usage from the + /// next search without mutating a previously observed name table. + #[test] + fn incremental_rebuild_drops_removed_reference() { + use base_db::change::Change; + use vfs::ChangedFile; + + let (mut host, marked) = setup_marked_files(&[ + ( + "/child.sv", + "module child;\n logic /*marker:def*/a;\n logic b;\n always_comb b = /*marker:use*/a;\nendmodule\n", + ), + ("/top.sv", "module top;\n child u();\nendmodule\n"), + ]); + let child_id = marked[0].0; + let markers = &marked[0].2; + let def_range = TextRange::new(markers["def"], markers["def"] + TextSize::of("a")); + let db = host.ctx(); + let def = def_named_at(&db, child_id, def_range); + assert_eq!(workspace_refs(&db, def).len(), 1, "wire a has one usage"); + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::create( + child_id, + "module child;\n logic a;\n logic b;\n always_comb b = 1'b0;\nendmodule\n", + )); + host.apply_change(change); + let db = host.ctx(); + + assert!( + workspace_refs(&db, def).is_empty(), + "removing the only usage must drop the reference" + ); + } + + #[test] + fn request_resolution_context_reuses_body_edits_and_rebuilds_structural_edits() { + use base_db::change::Change; + use vfs::ChangedFile; + + let (mut host, file_id, clean, _) = setup_marked("module top; logic a; endmodule\n"); + let before = host.ctx().resolution(); + + let mut body_edit = Change::new(); + body_edit.add_changed_file(ChangedFile::create( + file_id, + format!("{clean} // body-only\n").as_str(), + )); + host.apply_change(body_edit); + let after_body = host.ctx().resolution(); + assert_eq!( + before.graph(), + after_body.graph(), + "position-free structure is unchanged, so the catalog must be equal" + ); + + let mut structural_edit = Change::new(); + structural_edit + .add_changed_file(ChangedFile::create(file_id, "module renamed; logic a; endmodule\n")); + host.apply_change(structural_edit); + let after_structure = host.ctx().resolution(); + assert_ne!( + after_body.graph(), + after_structure.graph(), + "a changed declaration must produce a different catalog" + ); + } + + #[test] + fn body_edit_of_a_file_with_includes_reuses_resolution() { + use base_db::change::Change; + use vfs::ChangedFile; + + let (mut host, marked) = setup_marked_files(&[ + ("/defs.svh", "`define WIDTH 8\n"), + ("/top.sv", "`include \"defs.svh\"\nmodule top; logic a; endmodule\n"), + ]); + let top = marked[1].0; + let before = host.ctx().resolution(); + + let mut body_edit = Change::new(); + body_edit.add_changed_file(ChangedFile::create( + top, + "`include \"defs.svh\"\nmodule top; logic a; endmodule\n// body-only\n", + )); + host.apply_change(body_edit); + let after_body = host.ctx().resolution(); + assert_eq!( + before.graph(), + after_body.graph(), + "a body-only comment must not change the name catalog" + ); + } + + #[test] + fn recorded_include_dependency_survives_an_include_edit() { + use base_db::change::Change; + use vfs::ChangedFile; + + let (mut host, marked) = setup_marked_files(&[ + ("/defs.svh", "`define UNIT_NAME top\n"), + ("/top.sv", "`include \"defs.svh\"\nmodule `UNIT_NAME; endmodule\n"), + ]); + let defs = marked[0].0; + let top = marked[1].0; + let db = host.ctx(); + db.store.record_parse_dependencies(top, Arc::from(vec![top, defs])); + assert_eq!(db.store.parsed_dependents(&[defs]), vec![top]); + let before = db.unit_catalog(); + + let mut change = Change::new(); + change.add_changed_file(ChangedFile::create(defs, "`define UNIT_NAME renamed\n")); + host.apply_change(change); + let after = host.ctx().unit_catalog(); + assert_eq!( + before.as_ref(), + after.as_ref(), + "an include edit does not change the including file's L0 decls" + ); + assert_eq!( + host.ctx().store.parsed_dependents(&[defs]), + vec![top], + "the paid parse still names the include as a dependency" + ); + } + + /// Two body edits without a request between them must both be visible. + /// A replacing dirty set would drop the first file's dirtiness and leave + /// its removed reference in the merged index. + #[test] + fn consecutive_body_edits_both_reach_the_merged_index() { + use base_db::change::Change; + use vfs::ChangedFile; + + let (mut host, marked) = setup_marked_files(&[ + ( + "/a.sv", + "module a;\n logic /*marker:x*/x;\n logic y;\n always_comb y = x;\nendmodule\n", + ), + ( + "/b.sv", + "module b;\n logic /*marker:p*/p;\n logic q;\n always_comb q = p;\nendmodule\n", + ), + ]); + let a = marked[0].0; + let b = marked[1].0; + let x_range = TextRange::new(marked[0].2["x"], marked[0].2["x"] + TextSize::of("x")); + let p_range = TextRange::new(marked[1].2["p"], marked[1].2["p"] + TextSize::of("p")); + let db = host.ctx(); + let def_x = def_named_at(&db, a, x_range); + let def_p = def_named_at(&db, b, p_range); + assert_eq!(workspace_refs(&db, def_x).len(), 1); + assert_eq!(workspace_refs(&db, def_p).len(), 1); + + let mut first = Change::new(); + first.add_changed_file(ChangedFile::create( + a, + "module a;\n logic x;\n logic y;\n always_comb y = 1'b0;\nendmodule\n", + )); + host.apply_change(first); + + let mut second = Change::new(); + second.add_changed_file(ChangedFile::create( + b, + "module b;\n logic p;\n logic q;\n always_comb q = 1'b0;\nendmodule\n", + )); + host.apply_change(second); + + let db = host.ctx(); + assert!( + workspace_refs(&db, def_x).is_empty(), + "the first edit must not be dropped when a second edit arrives before a request" + ); + assert!(workspace_refs(&db, def_p).is_empty(), "the second edit must still be applied"); + } + + /// The container stack must agree with `find_container` for every + /// name-like token of a file exercising modules, blocks, subroutines, + /// explicit generate blocks, single-member generate branches and + /// instantiations. This is the safety net for the dispatch that mirrors + /// `source_to_def::container_to_def`. + #[test] + fn container_stack_matches_find_container_for_every_token() { + let text = r#" +`define TWO_MODULES module first; endmodule module second; endmodule +`TWO_MODULES +module top(input logic clk); + logic sig; + always_ff @(posedge clk) begin + if (sig) begin + logic inner; + end + end + generate + if (1) begin : gen_if + wire g; + end + endgenerate + function automatic logic f(); + return sig; + endfunction + sub u_sub(); +endmodule +"#; + let (host, file_id, _clean, _markers) = setup_marked(text); + let db = host.ctx(); + let hir_file_id = HirFileId::from(file_id); + let tree = db.parse(hir_file_id); + let root = tree.root(); + let macro_modules = root + .elem_preorder() + .filter_map(|event| match event { + WalkEvent::Enter(SyntaxElement::Node(node)) => { + ast::ModuleDeclaration::cast(node).map(|module| module.syntax()) + } + _ => None, + }) + .collect::>(); + assert!( + macro_modules.windows(2).any(|modules| { + modules[0].kind() == modules[1].kind() + && modules[0].text_range() == modules[1].text_range() + && modules[0] != modules[1] + }), + "macro expansion should contain distinct module nodes with the same display identity" + ); + let sema = SemanticsImpl::new_with_context(db.db, hir_def::unit::test_resolution(db.db)); + let mut containers = ContainerCache::new(); + for event in root.elem_preorder() { + match event { + WalkEvent::Enter(SyntaxElement::Node(_)) => {} + WalkEvent::Leave(SyntaxElement::Node(_)) => {} + WalkEvent::Enter(SyntaxElement::Token(token)) => { + if !token.kind().name_like() { + continue; + } + let cached = containers.container_for(&sema, hir_file_id, token.parent); + let expected = + sema.container_for_node(hir_file_id, token.parent).unwrap_or_else(|| { + db.owner_table(hir_file_id).file_owner().expect("file owner") + }); + if cached != expected { + eprintln!("cached owner={cached:?}"); + eprintln!("expected owner={expected:?}"); + } + assert_eq!(cached, expected, "container mismatch at {:?}", token.raw_text()); + } + WalkEvent::Leave(SyntaxElement::Token(_)) => {} + } + } + } + + /// The fast path must agree with the full heuristic chain for every + /// token: `token_in_special_context` has to cover exactly the syntax + /// positions where `DefinitionClass::resolve_in` diverges from plain + /// value-name resolution. The fixture exercises member accesses, scoped + /// names, packages, checkers, module-like declarations, hierarchy / + /// primitive instantiations, named port connections, named types, package + /// imports and a macro emitting a member access. + #[test] + fn fast_path_agrees_with_full_resolution_chain_for_every_token() { + let text = r#" +`define M(a) a.x +package pkg; + logic field; +endpackage + +checker chk(input logic a); +endchecker + +module sub(input logic in, output logic out); + logic internal; + assign out = in & internal; +endmodule + +module top(input logic clk, input logic [3:0] data); + logic sig; + wire [3:0] w; + pkg::field f_field; + initial begin + sig = clk; + `M(sig) + end + sub u_sub(.in(sig), .out(w)); + and g1(w, sig, clk); + chk c1(.a(sig)); + import pkg::*; +endmodule +"#; + let (host, file_id, _clean, _markers) = setup_marked(text); + let db = host.ctx(); + let context = hir_def::unit::test_resolution(db.db); + let hir_file_id = HirFileId::from(file_id); + let tree = db.parse(hir_file_id); + let root = tree.root(); + let sema = SemanticsImpl::new_with_context(db.db, hir_def::unit::test_resolution(db.db)); + let mut containers = ContainerCache::new(); + let mut chains = ScopeChainCache::new(); + let mut checked = 0usize; + for event in root.elem_preorder() { + if let WalkEvent::Enter(SyntaxElement::Token(token)) = event { + if !token.kind().name_like() { + continue; + } + checked += 1; + let container = containers.container_for(&sema, hir_file_id, token.parent); + let chosen = if token_in_special_context(token) { + DefinitionClass::resolve_in( + db.db, + context.clone(), + hir_file_id, + token, + Some(container), + ) + .unique() + } else { + let chain = chains.chain_for(db.db, container); + sema.nameres_ident_in_scopes_at(hir_file_id, token, NameContext::Value, &chain) + .map(DefinitionClass::Definition) + .unique() + }; + let full = DefinitionClass::resolve_in( + db.db, + context.clone(), + hir_file_id, + token, + Some(container), + ) + .unique(); + assert_eq!( + chosen, + full, + "fast path diverges at {:?} (parent={:?}, special={})", + token.raw_text(), + token.parent.kind(), + token_in_special_context(token) + ); + } + } + assert!(checked > 20, "test should exercise a non-trivial token set"); + } + + /// Named port connections must record their shape (name/data roles, + /// collapse ranges, shorthand sides and same-name pairing) on the + /// references, so rename never re-resolves or re-parses. + #[test] + fn reference_contexts_capture_named_connection_shapes() { + let text = r#" +module child(input /*marker:child_a*/a, input /*marker:child_b*/b); +endmodule +module top; + logic /*marker:local_a*/a; + logic /*marker:local_b*/b; + logic /*marker:local_c*/c; + logic /*marker:plain_c*/d; + assign d = /*marker:plain*/c; + child u(/*marker:same_name*/.a(/*marker:same_name_data*/a), /*marker:other_name*/.b(/*marker:other_data*/c)); + child v(/*marker:shorthand*/.b); +endmodule +"#; + let (host, file_id, _clean, markers) = setup_marked(text); + let db = host.ctx(); + + let range_at = |marker: &str| { + let start = markers[marker]; + let end = markers[marker] + TextSize::of("a"); + TextRange::new(start, end) + }; + // Conn name markers sit on the leading dot; the name token follows it. + let conn_name_at = |marker: &str| { + let start = markers[marker] + TextSize::of("."); + TextRange::new(start, start + TextSize::of("a")) + }; + let def_range = |marker: &str| range_at(marker); + let refs_of = |def_marker: &str| { + let def = def_named_at(&db, file_id, def_range(def_marker)); + workspace_refs(&db, def) + }; + let reference = |def_marker: &str, range: TextRange| -> (TextRange, ReferenceContext) { + let refs = refs_of(def_marker); + let found = refs + .iter() + .find(|(_, found, _)| *found == range) + .unwrap_or_else(|| panic!("missing reference at {range:?} for {def_marker}")); + (range, found.2.clone()) + }; + let paired_is = |paired: DefId, marker: &str| { + definition_ranges_for(db.db, paired) + .iter() + .any(|range| range.file_id == file_id && range.range == def_range(marker)) + }; + + // Same-name connection `.a(a)`: the name token pairs the local def, + // the data token pairs the port def, both share the collapse range. + let same_name_range = conn_name_at("same_name"); + let same_name_data_range = range_at("same_name_data"); + let collapse = + TextRange::new(same_name_range.start(), same_name_data_range.end() + TextSize::of(")")); + let name_ref = reference("child_a", conn_name_at("same_name")); + let ReferenceContext::ConnName { ident_range, collapse_range, shorthand, side, paired } = + &name_ref.1 + else { + panic!("same-name name token should be ConnName: {:?}", name_ref.1); + }; + assert_eq!(ident_range, &Some(same_name_data_range)); + assert_eq!(collapse_range, &Some(collapse)); + assert!(!shorthand); + assert_eq!(side, &ConnSide::Port); + let paired = *paired.as_ref().expect("same-name conn should pair the local def"); + assert!(paired_is(paired, "local_a"), "paired local def should be top.a"); + let data_ref = reference("local_a", range_at("same_name_data")); + let ReferenceContext::ConnData { name_range, collapse_range, paired } = &data_ref.1 else { + panic!("same-name data token should be ConnData: {:?}", data_ref.1); + }; + assert_eq!(name_range, &same_name_range); + assert_eq!(collapse_range, &Some(collapse)); + let paired = *paired.as_ref().expect("same-name conn should pair the port def"); + assert!(paired_is(paired, "child_a"), "paired port def should be child.a"); + + // Non-same-name connection `.b(c)`: shape is recorded, no pairing. + let name_ref = reference("child_b", conn_name_at("other_name")); + let ReferenceContext::ConnName { ident_range, paired, .. } = &name_ref.1 else { + panic!("non-same-name name token should be ConnName: {:?}", name_ref.1); + }; + assert_eq!(ident_range, &Some(range_at("other_data"))); + assert_eq!(paired, &None); + let data_ref = reference("local_c", range_at("other_data")); + let ReferenceContext::ConnData { name_range, paired, .. } = &data_ref.1 else { + panic!("non-same-name data token should be ConnData: {:?}", data_ref.1); + }; + assert_eq!(name_range, &conn_name_at("other_name")); + assert_eq!(paired, &None); + + // Shorthand `.b`: one reference in each side's group. + let port_ref = reference("child_b", conn_name_at("shorthand")); + let ReferenceContext::ConnName { collapse_range, shorthand, side, paired, .. } = + &port_ref.1 + else { + panic!("shorthand port reference should be ConnName: {:?}", port_ref.1); + }; + assert!(shorthand); + assert_eq!(collapse_range, &None); + assert_eq!(side, &ConnSide::Port); + let paired = *paired.as_ref().expect("shorthand should pair the local def"); + assert!(paired_is(paired, "local_b"), "shorthand port side should pair top.b"); + let local_ref = reference("local_b", conn_name_at("shorthand")); + let ReferenceContext::ConnName { side, paired, .. } = &local_ref.1 else { + panic!("shorthand local reference should be ConnName: {:?}", local_ref.1); + }; + assert_eq!(side, &ConnSide::Local); + let paired = *paired.as_ref().expect("shorthand should pair the port def"); + assert!(paired_is(paired, "child_b"), "shorthand local side should pair child.b"); + + // Plain references stay Plain. + let plain = reference("local_c", range_at("plain")); + assert_eq!(plain.1, ReferenceContext::Plain); + } + + #[test] + fn semantic_index_skips_preprocessor_owned_identifiers() { + let text = r#" +`define BODY(/*marker:param*/x) /*marker:body*/x +module top; + wire /*marker:def*/x; + assign y = /*marker:ordinary*/x; + assign y = `BODY(/*marker:arg*/x); +endmodule +"#; + let (host, file_id, _clean, markers) = setup_marked(text); + let db = host.ctx(); + let tree = db.parse(HirFileId::from(file_id)); + let root = tree.root(); + let emitted = emit_token_index(root); + for marker in ["param", "body"] { + let target = resolve_semantic_target_with_emitted( + db.db, + file_id, + markers[marker], + Some(root), + crate::token::navigation_precedence, + Some(&emitted), + ) + .unique_for_intent(TargetIntent::FindReferences); + assert!( + matches!(target, Some(SemanticTarget::PreprocMacro(_))), + "{marker} must remain owned by the preprocessor: {target:?}" + ); + } + let definition_range = TextRange::new(markers["def"], markers["def"] + TextSize::of("x")); + let preproc_ranges = [ + TextRange::new(markers["param"], markers["param"] + TextSize::of("x")), + TextRange::new(markers["body"], markers["body"] + TextSize::of("x")), + ]; + let def = def_named_at(&db, file_id, definition_range); + let refs = workspace_refs(&db, def); + + assert!( + refs.iter().all(|(_, range, _)| !preproc_ranges.contains(range)), + "preprocessor-owned x tokens must not become HDL references: {refs:?}" + ); + assert!(refs.iter().any(|(_, range, _)| { + *range == TextRange::new(markers["ordinary"], markers["ordinary"] + TextSize::of("x")) + })); + assert!(refs.iter().any(|(_, range, _)| { + *range == TextRange::new(markers["arg"], markers["arg"] + TextSize::of("x")) + })); + } +} diff --git a/crates/ide/src/reference_support/build.rs b/crates/ide/src/reference_support/build.rs new file mode 100644 index 000000000..b300f6535 --- /dev/null +++ b/crates/ide/src/reference_support/build.rs @@ -0,0 +1,472 @@ +use hir_def::{ + container::{InFile, ScopeChain}, + def_id::DefId, + owner::{OwnerId, OwnerKind}, + pathres::ResolvedScopes, + symbol::NameContext, +}; +use hir_semantics::semantics::SemanticsImpl; +use itertools::Itertools; +use preproc_expand::file::HirFileId; +use rustc_hash::FxHashMap; +use syntax::{ + SyntaxAncestors, SyntaxNode, SyntaxToken, SyntaxTokenWithParent, + ast::{self, AstNode}, + has_text_range::HasTextRangeIn, +}; +use triomphe::Arc; +use utils::line_index::TextRange; + +use super::*; +use crate::{ + analysis::AnalysisContext, + db::workspace_symbol_index_db::WorkspaceSymbolIndexDb, + definitions::{DefinitionClass, rightmost_name_token}, + references::search::resolve_source_range, +}; + +/// Caches HIR container ids by syntax node while walking a tree. +/// +/// `source_to_def::find_container` finds a token's container by walking up +/// the ancestor chain and matching every node; doing that per token makes +/// a reference walk pay the ancestor walk for every name-like token. This +/// cache keeps the same walk shape (up to the nearest container node, then +/// a lookup), but computes each container id once instead of once per token. +/// +/// The node dispatch must stay in sync with +/// `hir_semantics::semantics::source_to_def::container_to_def`: the +/// module/block/subroutine arms use the public `Semantics` projections, and +/// generate blocks / single-member generate branches intern through +/// `intern_generate_block` with the nearest enclosing container as parent. +/// +/// The key is the Slang node itself, not `SyntaxNodePtr`: macro-emitted nodes +/// can share a display range and kind at their call site, while their pointer +/// identities remain distinct. +pub(crate) struct ContainerCache<'tree> { + by_node: FxHashMap, OwnerId>, +} + +impl<'tree> ContainerCache<'tree> { + pub(crate) fn new() -> Self { + Self { by_node: FxHashMap::default() } + } + + /// The container of a token: the nearest container node on its ancestor + /// chain whose id computes successfully, mirroring + /// `find_map(container_to_def)`; nodes that fail to lower are skipped. + pub(crate) fn container_for( + &mut self, + sema: &SemanticsImpl<'_>, + file_id: HirFileId, + token_parent: SyntaxNode<'tree>, + ) -> OwnerId { + for node in SyntaxAncestors::start_from(token_parent) { + if is_container_node(&node) + && let Some(id) = self.try_id_for(sema, file_id, node) + { + return id; + } + } + sema.db.owner_table(file_id).file_owner().expect("file owner") + } + + pub(super) fn try_id_for( + &mut self, + sema: &SemanticsImpl<'_>, + file_id: HirFileId, + node: SyntaxNode<'tree>, + ) -> Option { + if let Some(id) = self.by_node.get(&node) { + return Some(*id); + } + let id = container_id_for_node(sema, file_id, node, self)?; + self.by_node.insert(node, id); + Some(id) + } +} + +/// Resolved scope chains by container. The nameres fast path looks every +/// token up in its container's chain; resolving the chain once per container +/// avoids per-token salsa `scope_for` queries, whose memos revalidate against +/// every intervening query during a reference walk and recompute O(scope +/// size) on each miss. +pub(crate) struct ScopeChainCache { + by_container: FxHashMap>, +} + +impl ScopeChainCache { + pub(crate) fn new() -> Self { + Self { by_container: FxHashMap::default() } + } + + pub(super) fn chain_for( + &mut self, + db: &dyn WorkspaceSymbolIndexDb, + container: OwnerId, + ) -> Arc { + if let Some(chain) = self.by_container.get(&container) { + return chain.clone(); + } + let chain = Arc::new(ResolvedScopes::new(db, ScopeChain::from_inner(db, container))); + self.by_container.insert(container, chain.clone()); + chain + } +} + +/// Mirrors `source_to_def::container_to_def`'s node dispatch. Uses `cast` +/// (not `can_cast`) on every arm: slang's `can_cast` accepts sub-kind +/// relations (e.g. generate blocks pass `BlockStatement::can_cast`), which +/// would desynchronize enter/leave bookkeeping. +fn is_container_node(node: &SyntaxNode<'_>) -> bool { + ast::ModuleDeclaration::cast(*node).is_some() + || ast::AnonymousProgram::cast(*node).is_some() + || ast::CheckerDeclaration::cast(*node).is_some() + || ast::CovergroupDeclaration::cast(*node).is_some() + || ast::ClockingDeclaration::cast(*node).is_some() + || ast::BlockStatement::cast(*node).is_some() + || ast::ProceduralBlock::cast(*node).is_some() + || ast::FunctionDeclaration::cast(*node).is_some() + || ast::CompilationUnit::cast(*node).is_some() + || ast::GenerateBlock::cast(*node).is_some() + || (ast::Member::cast(*node).is_some() && is_generate_branch_member(*node)) +} + +fn container_id_for_node<'tree>( + sema: &SemanticsImpl<'_>, + file_id: HirFileId, + node: SyntaxNode<'tree>, + _cache: &mut ContainerCache<'tree>, +) -> Option { + if let Some(module) = ast::ModuleDeclaration::cast(node) { + return sema.module_to_def(file_id, module); + } + let kind = if ast::CheckerDeclaration::cast(node).is_some() { + Some(OwnerKind::Checker) + } else if ast::AnonymousProgram::cast(node).is_some() { + Some(OwnerKind::AnonymousProgram) + } else if ast::CovergroupDeclaration::cast(node).is_some() { + Some(OwnerKind::Covergroup) + } else if ast::ClockingDeclaration::cast(node).is_some() { + Some(OwnerKind::ClockingBlock) + } else if ast::ProceduralBlock::cast(node).is_some() { + Some(OwnerKind::ProceduralBlock) + } else if let Some(block) = ast::BlockStatement::cast(node) { + return sema.block_to_def(file_id, block); + } else if let Some(func) = ast::FunctionDeclaration::cast(node) { + return sema.subroutine_to_def(file_id, func); + } else if ast::CompilationUnit::cast(node).is_some() { + return sema.db.owner_table(file_id).file_owner(); + } else if ast::GenerateBlock::cast(node).is_some() + || (ast::Member::cast(node).is_some() && is_generate_branch_member(node)) + { + Some(OwnerKind::GenerateBlock) + } else { + None + }?; + + let owner_node = if kind == OwnerKind::GenerateBlock + && ast::GenerateBlock::cast(node).is_some() + && node.parent().is_some_and(|parent| ast::LoopGenerate::cast(parent).is_some()) + { + node.parent()? + } else { + node + }; + let tree = sema.db.parse(file_id); + let ast_id = sema.db.ast_id_map(file_id).id_of_node_in_tree(&tree, owner_node)?; + sema.db.owner_table(file_id).owner_by_ast(ast_id, kind) +} + +/// Mirrors `source_to_def::is_generate_branch_member`: a member is a +/// single-member generate branch when it sits inside an if/case generate and +/// no stronger container (module, block, generate region) separates it. +/// The predicate itself lives in `hir-semantics`; only the container +/// dispatch is mirrored here. +fn is_generate_branch_member(member: SyntaxNode<'_>) -> bool { + hir_semantics::semantics::is_generate_branch_member(member) +} + +/// The role of a token inside a named port connection, if any, computed from +/// the token's syntax position alone. +enum ConnTokenRole<'tree> { + /// The token is the `.name` of the connection. + Name(ast::NamedPortConnection<'tree>), + /// The token is a simple identifier in the data position. + Data(ast::NamedPortConnection<'tree>), +} + +fn conn_token_role<'tree>(token: SyntaxTokenWithParent<'tree>) -> Option> { + let SyntaxTokenWithParent { parent, tok } = token; + if let Some(conn) = ast::NamedPortConnection::cast(parent) { + return conn.name().is_some_and(|name| name == tok).then_some(ConnTokenRole::Name(conn)); + } + if ast::Name::can_cast(parent.kind()) { + // The data identifier of a simple named port connection sits at a + // fixed depth below the connection node (the wrapper expression + // nodes are virtual). + if let Some(node) = SyntaxAncestors::start_from(parent).nth(3) + && let Some(conn) = ast::NamedPortConnection::cast(node) + && conn_data_ident(conn).is_some_and(|ident| ident == tok) + { + return Some(ConnTokenRole::Data(conn)); + } + } + None +} + +/// The identifier token of a connection's data side, when the data is a +/// simple identifier (bare name or empty select). Mirrors the extraction in +/// the rename edit rules. +fn conn_data_ident(conn: ast::NamedPortConnection<'_>) -> Option> { + use ast::{Expression, Name}; + let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); + match expr { + Expression::Name(Name::IdentifierName(ident)) => ident.identifier(), + Expression::Name(Name::IdentifierSelectName(ident)) + if ident.selectors().children().next().is_none() => + { + ident.identifier() + } + _ => None, + } +} + +struct ConnShape { + name_range: TextRange, + ident_range: Option, + collapse_range: Option, + shorthand: bool, +} + +fn conn_shape(conn: ast::NamedPortConnection<'_>) -> Option { + let name_range = conn.name()?.text_range_in(conn.syntax())?; + let collapse_range = conn + .close_paren() + .and_then(|token| token.text_range_in(conn.syntax())) + .map(|range| TextRange::new(name_range.start(), range.end())); + let ident_range = conn_data_ident(conn).and_then(|token| token.text_range_in(conn.syntax())); + let shorthand = conn.open_paren().is_none() && conn.close_paren().is_none(); + Some(ConnShape { name_range, ident_range, collapse_range, shorthand }) +} + +fn range_text(text: &str, range: TextRange) -> &str { + &text[usize::from(range.start())..usize::from(range.end())] +} + +fn is_same_name_conn(text: &str, conn: &ConnShape) -> bool { + conn.ident_range + .is_some_and(|ident| range_text(text, conn.name_range) == range_text(text, ident)) +} + +/// The [`ReferenceContext`] of a token resolved to `class`. `side` selects +/// the shorthand side; non-shorthand tokens produce the same context for +/// either side. +#[allow(clippy::too_many_arguments)] +pub(crate) fn reference_context( + db: &dyn WorkspaceSymbolIndexDb, + sema: &SemanticsImpl<'_>, + context: triomphe::Arc, + file_id: HirFileId, + token: SyntaxTokenWithParent<'_>, + class: &DefinitionClass, + container: OwnerId, + chains: &mut ScopeChainCache, + conn_port_by_name: &mut FxHashMap, + text: &str, + side: ConnSide, +) -> ReferenceContext { + let Some(role) = conn_token_role(token) else { + return ReferenceContext::Plain; + }; + match role { + ConnTokenRole::Data(conn) => { + let Some(shape) = conn_shape(conn) else { + return ReferenceContext::Plain; + }; + let paired = is_same_name_conn(text, &shape) + .then(|| { + if let Some(port) = conn_port_by_name.get(&shape.name_range) { + return Some(*port); + } + let name = conn.name()?; + let name_token = SyntaxTokenWithParent { parent: conn.syntax(), tok: name }; + match DefinitionClass::resolve_in( + db, + context.clone(), + file_id, + name_token, + Some(container), + ) + .unique()? + { + DefinitionClass::Definition(port) => Some(port), + DefinitionClass::PortConnShorthand { port, .. } => Some(port), + } + }) + .flatten(); + ReferenceContext::ConnData { + name_range: shape.name_range, + collapse_range: shape.collapse_range, + paired, + } + } + ConnTokenRole::Name(conn) => { + let Some(shape) = conn_shape(conn) else { + return ReferenceContext::Plain; + }; + if shape.shorthand { + let (side, paired) = match class { + DefinitionClass::PortConnShorthand { port, local } => { + let paired = match side { + ConnSide::Port => Some(*local), + ConnSide::Local => Some(*port), + }; + (side, paired) + } + DefinitionClass::Definition(def) => { + // One-sided shorthand resolution: the local side is the + // definition when plain value resolution matches it. + let chain = chains.chain_for(db, container); + let is_local = sema + .nameres_ident_in_scopes(token, NameContext::Value, &chain, None) + .unique() + .is_some_and(|local| local == *def); + (if is_local { ConnSide::Local } else { ConnSide::Port }, None) + } + }; + return ReferenceContext::ConnName { + ident_range: None, + collapse_range: None, + shorthand: true, + side, + paired, + }; + } + let same_name = is_same_name_conn(text, &shape); + let paired = same_name + .then(|| { + let chain = chains.chain_for(db, container); + conn_data_ident(conn).and_then(|ident| { + sema.nameres_ident_in_scopes( + SyntaxTokenWithParent { parent: conn.syntax(), tok: ident }, + NameContext::Value, + &chain, + None, + ) + .unique() + }) + }) + .flatten(); + if let DefinitionClass::Definition(port) = class { + conn_port_by_name.insert(shape.name_range, *port); + } + ReferenceContext::ConnName { + ident_range: shape.ident_range, + collapse_range: shape.collapse_range, + shorthand: false, + side: ConnSide::Port, + paired, + } + } + } +} + +/// True when the token sits at one of the syntax positions where +/// `DefinitionClass::resolve_in` diverges from plain value-identifier +/// resolution. Those positions are the direct token children of the listed +/// nodes (member access fields, module-like declaration names, instantiation +/// type names, package import names, named parameter/port connection names) +/// and identifiers wrapped in a `Name` node under a scoped name, a named +/// type (they select the Type name context) or a checker instantiation +/// (its type name resolves in the Type namespace). +/// +/// Every check is O(1) on the token's parent (and grandparent); the subtree +/// walk from the old fast-path gate was dropped because it also flagged every +/// token inside a module body, which made the fast path dead on module-heavy +/// files. +pub(crate) fn token_in_special_context( + SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent<'_>, +) -> bool { + if ast::MemberAccessExpression::cast(parent).is_some_and(|node| node.name() == Some(tok)) + || ast::ModuleHeader::cast(parent).is_some_and(|node| node.name() == Some(tok)) + || ast::PrimitiveInstantiation::cast(parent).is_some_and(|node| node.type_() == Some(tok)) + || ast::HierarchyInstantiation::cast(parent).is_some_and(|node| node.type_() == Some(tok)) + || ast::PackageImportItem::cast(parent) + .is_some_and(|node| node.package() == Some(tok) || node.item() == Some(tok)) + || ast::NamedParamAssignment::cast(parent).is_some_and(|node| node.name() == Some(tok)) + || ast::NamedPortConnection::cast(parent).is_some_and(|node| node.name() == Some(tok)) + { + return true; + } + + // Identifier tokens are wrapped in a `Name` node; the divergent context is + // the Name's parent. + if !ast::Name::can_cast(parent.kind()) { + return false; + } + let Some(grandparent) = parent.parent() else { + return false; + }; + if ast::ScopedName::can_cast(grandparent.kind()) || ast::NamedType::can_cast(grandparent.kind()) + { + return true; + } + ast::CheckerInstantiation::cast(grandparent) + .is_some_and(|node| rightmost_name_token(node.type_()) == Some(tok)) +} + +pub(crate) fn definition_class_for_token( + db: &AnalysisContext<'_>, + sema: &SemanticsImpl<'_>, + file_id: HirFileId, + token: SyntaxTokenWithParent<'_>, + container: OwnerId, + special: bool, + chains: &mut ScopeChainCache, +) -> Option { + if special { + if let Some(resolution) = crate::definitions::slang_colon_colon(db, file_id, token) { + return resolution.unique(); + } + DefinitionClass::resolve_in( + db.db, + sema.resolution_context(), + file_id, + token, + Some(container), + ) + .unique() + } else { + let chain = chains.chain_for(db.db, container); + sema.nameres_ident_in_scopes_at(file_id, token, NameContext::Value, &chain) + .map(DefinitionClass::Definition) + .unique() + } +} + +/// Definition name ranges of `definition` mapped to user-facing files, in +/// origin order. File-level callers own memoization for this pure projection. +#[salsa::tracked(returns(clone))] +fn definition_ranges( + db: &dyn WorkspaceSymbolIndexDb, + key: crate::db::DefinitionRangeKey, +) -> Vec { + let definition = key.def_id(db); + definition + .origins(db) + .iter() + .filter_map(|origin| { + let InFile { file_id, value } = origin.name_range(db)?; + let (file_id, range) = resolve_source_range(db, file_id, value)?; + Some(SemanticDefinitionRange { file_id, range }) + }) + .unique() + .collect_vec() +} + +pub(crate) fn definition_ranges_for( + db: &dyn WorkspaceSymbolIndexDb, + definition: DefId, +) -> Vec { + definition_ranges(db, crate::db::DefinitionRangeKey::new(db, definition)) +} diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 782c33914..d056be592 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -11,6 +11,7 @@ use vfs::FileId; use self::preproc::render_preproc_references_target; use crate::{ FilePosition, ScopeVisibility, + analysis::AnalysisContext, db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, @@ -85,18 +86,24 @@ impl ReferencesStatus { } pub(crate) fn references( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, config: ReferencesConfig, ) -> Option> { - let sema = Semantics::new(db); + if let Some(refs) = + crate::design_unit::references(db, FilePosition { file_id, offset }, &config) + { + return Some(refs); + } + let sema = db.semantics(); let parsed_file = sema.parse_file(file_id); - let target = resolve_semantic_target(db, file_id, offset, parsed_file.root(), token_precedence); + let target = + resolve_semantic_target(db.db, file_id, offset, parsed_file.root(), token_precedence); render_references_target(db, file_id, &sema, target, config) } fn render_references_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, sema: &Semantics, target: TargetResolution<'_>, @@ -104,17 +111,18 @@ fn render_references_target( ) -> Option> { match target.unique_for_intent(TargetIntent::FindReferences)? { SemanticTarget::PreprocMacro(target) => { - render_preproc_references_target(db, file_id, target, &config) + render_preproc_references_target(db.db, file_id, target, &config) } SemanticTarget::Include(_) => None, SemanticTarget::Manifest(target) => crate::manifest::references_target(db, target, config), SemanticTarget::Source(target) => { - render_source_references_target(sema, file_id, target, config) + render_source_references_target(db, sema, file_id, target, config) } } } fn render_source_references_target( + db: &AnalysisContext<'_>, sema: &Semantics, file_id: FileId, target: SourceTarget<'_>, @@ -124,24 +132,25 @@ fn render_source_references_target( let tokens = target.into_tokens(); let references = tokens .into_iter() - .filter_map(|token| references_for_token(sema, hir_file_id, token, config.clone())) + .filter_map(|token| references_for_token(db, sema, hir_file_id, token, config.clone())) .flatten() .collect_vec(); (!references.is_empty()).then_some(references) } fn references_for_token( + db: &AnalysisContext<'_>, sema: &Semantics, hir_file_id: HirFileId, token: SyntaxTokenWithParent, config: ReferencesConfig, ) -> Option> { handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { - let def = match DefinitionClass::resolve(sema.db, hir_file_id, token).unique()? { + let def = match DefinitionClass::resolve(db, hir_file_id, token).unique()? { DefinitionClass::Definition(def) => def, DefinitionClass::PortConnShorthand { local, .. } => local, }; - Some(vec![search_refs(sema, def, config)]) + Some(vec![search_refs(db, def, config)]) }) } @@ -168,12 +177,8 @@ pub(crate) fn handle_ctrl_flow_kw( }]) } -fn search_refs<'a>( - sema: &'a Semantics<'a, RootDb>, - def: DefId, - config: ReferencesConfig, -) -> References { - let refs = ReferencesCtx::new(sema, &def, config) +fn search_refs(db: &AnalysisContext<'_>, def: DefId, config: ReferencesConfig) -> References { + let refs = ReferencesCtx::new(db, &def, config) .search() .into_iter() .map(|(file_id, tokens)| { @@ -181,8 +186,7 @@ fn search_refs<'a>( (file_id, res) }) .collect(); - let def = - def.origins(sema.db).iter().filter_map(|def| def.to_nav(sema.db)).collect_vec().into(); + let def = def.origins(db.db).iter().filter_map(|def| def.to_nav(db.db)).collect_vec().into(); References { def, refs, status: ReferencesStatus::Complete } } diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index b6e5df71f..d5422caab 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -1,28 +1,34 @@ use base_db::source_root::SourceRootId; use hir_def::{ container::InFile, + db::HirDefDb, def_id::DefId, has_source::HasSource, module::ModuleKind, owner::{OwnerId, OwnerKind}, }; -use hir_semantics::semantics::Semantics; -use hir_ty::db::TyDb; +use hir_semantics::semantics::SemanticsImpl; use nohash_hasher::IntMap; use preproc_expand::{file::HirFileId, macro_file::macro_file_call_site}; use rustc_hash::FxHashMap; -use syntax::{SyntaxTokenWithParent, ptr::SyntaxTokenPtr}; +use syntax::{SyntaxTokenWithParent, has_text_range::HasTextRange, ptr::SyntaxTokenPtr}; use utils::line_index::TextRange; use vfs::FileId; use super::{ReferenceCategory, ReferencesConfig}; use crate::{ ScopeVisibility, - db::{ - root_db::RootDb, - workspace_symbol_index_db::{WorkspaceSymbolIndexDb, source_root_semantic_index_for_root}, + analysis::AnalysisContext, + db::workspace_symbol_index_db::WorkspaceSymbolIndexDb, + definitions::DefinitionClass, + reference_support::{ + ReferenceContext, + build::{ + ContainerCache, ScopeChainCache, definition_class_for_token, definition_ranges_for, + reference_context, token_in_special_context, + }, }, - semantic_index::{ReferenceContext, SemanticReference}, + semantic_target::preproc::{EmittedTokenIndex, emit_token_index}, }; /// A search scope is a set of files and ranges within those files that should @@ -130,8 +136,12 @@ impl SearchScope { self.0.keys().all(|candidate| *candidate == file_id) } + pub(crate) fn files(&self) -> impl Iterator + '_ { + self.0.keys().copied() + } + /// The single file of the scope, if it covers exactly one file. - fn single_file_id(&self) -> Option { + pub(crate) fn single_file_id(&self) -> Option { let mut keys = self.0.keys(); let first = keys.next()?; keys.next().is_none().then_some(*first) @@ -156,9 +166,9 @@ impl SearchScope { } } -pub(crate) struct ReferencesCtx<'a, 'b> { - sema: &'a Semantics<'a, RootDb>, - def: &'b DefId, +pub(crate) struct ReferencesCtx<'a> { + db: &'a AnalysisContext<'a>, + def: DefId, scope: SearchScope, } @@ -171,15 +181,6 @@ pub(crate) struct ReferenceToken { } impl ReferenceToken { - pub(crate) fn from_semantic_reference(reference: &SemanticReference) -> Self { - Self { - ptr: reference.ptr, - range: reference.range, - category: reference.category, - context: reference.context.clone(), - } - } - pub fn range(&self) -> TextRange { self.range } @@ -197,71 +198,179 @@ impl ReferenceToken { } } -impl<'a, 'b> ReferencesCtx<'a, 'b> { +impl<'a> ReferencesCtx<'a> { const FILE_REF_CAPACITY: usize = 8; - pub(crate) fn new( - sema: &'a Semantics<'a, RootDb>, - def: &'b DefId, - cfg: ReferencesConfig, - ) -> Self { - let scope = SearchScope::new(sema.db, def, cfg); - Self { sema, def, scope } + pub(crate) fn new(db: &'a AnalysisContext<'a>, def: &DefId, cfg: ReferencesConfig) -> Self { + let scope = SearchScope::new(db.db, def, cfg); + Self { db, def: *def, scope } } pub(crate) fn search(&self) -> IntMap> { - search_references(self.sema.db, self.def, self.scope.clone()) + search_references(self.db, &self.def, self.scope.clone()) } } -/// Collects the references of `def` inside `scope`. The work is shared by -/// find-references, document highlight, rename and the recursive rename -/// closure query; it only touches salsa queries, so it can run on a `dyn` -/// database. +/// Collects the references of `def` inside `scope`. +/// +/// Candidate files are those whose `FileFacts` mention the identifier text. +/// Resolution happens on demand; there is no workspace `DefId` map. pub(crate) fn search_references( - db: &dyn WorkspaceSymbolIndexDb, + db: &AnalysisContext<'_>, def: &DefId, scope: SearchScope, ) -> IntMap> { let mut res: IntMap<_, Vec<_>> = IntMap::default(); + let Some(name) = def.name(db.db) else { + return res; + }; - // Single-file scopes (document highlight, single-file rename) read - // the file's own index directly and skip the root merge pass. if let Some(file_id) = scope.single_file_id() { db.unwind_if_revision_cancelled(); - let index = db.file_semantic_index(file_id); - let Some(group) = index.references_for_definition(*def) else { - return res; - }; - for reference in group.references().iter() { - if !scope.contains(reference.file_id, reference.range) { + collect_file_references(db, file_id, def, &name, &scope, &mut res); + return res; + } + + for source_root_id in scope.source_root_ids(db.db) { + db.unwind_if_revision_cancelled(); + for file_id in files_for_root(db, source_root_id) { + if scope.range_for_file(file_id).is_none() { continue; } - res.entry(reference.file_id) - .or_insert_with(|| Vec::with_capacity(ReferencesCtx::FILE_REF_CAPACITY)) - .push(ReferenceToken::from_semantic_reference(reference)); + db.unwind_if_revision_cancelled(); + collect_file_references(db, file_id, def, &name, &scope, &mut res); } - return res; } - for source_root_id in scope.source_root_ids(db) { - db.unwind_if_revision_cancelled(); - let index = source_root_semantic_index_for_root(db, source_root_id); - let Some(group) = index.references_for_definition(*def) else { + res +} + +/// Compilation-plan files of `source_root_id`, not every VFS path. +fn files_for_root(ctx: &AnalysisContext<'_>, source_root_id: SourceRootId) -> Vec { + let plan = ctx.compilation_plan_for_root(source_root_id); + let mut files: Vec = plan + .all_file_ids() + .into_iter() + .filter(|&file_id| ctx.source_root_id(file_id) == source_root_id) + .collect(); + files.sort_by_key(|file_id| file_id.index()); + files.dedup(); + files +} + +fn collect_file_references( + db: &AnalysisContext<'_>, + file_id: FileId, + def: &DefId, + name: &str, + scope: &SearchScope, + res: &mut IntMap>, +) { + let facts = db.file_facts(file_id); + if !facts.mentions_name(name) { + return; + } + + let context = db.resolution(); + let hir_file_id = HirFileId::from(file_id); + let tree = db.parse_file(file_id); + let emitted = emit_token_index(tree.root()); + let text = db.file_text(file_id); + let sema = SemanticsImpl::new_with_context(db.db, context.clone()); + let mut containers = ContainerCache::new(); + let mut chains = ScopeChainCache::new(); + let mut conn_port_by_name = FxHashMap::default(); + let definition_ranges = definition_ranges_for(db.db, *def); + + for mention in facts.mentions_of(name) { + if !scope.contains(file_id, mention.range) { + continue; + } + if definition_ranges.iter().any(|definition_range| { + definition_range.file_id == file_id && definition_range.range == mention.range + }) { + continue; + } + let Some(token) = token_for_mention(&tree, &emitted, mention) else { + continue; + }; + let container = containers.container_for(&sema, hir_file_id, token.parent); + let Some(class) = definition_class_for_token( + db, + &sema, + hir_file_id, + token, + container, + token_in_special_context(token), + &mut chains, + ) else { continue; }; - for reference in group.references.iter() { - if !scope.contains(reference.file_id, reference.range) { + let sides = match &class { + DefinitionClass::Definition(found) if found == def => { + &[crate::reference_support::ConnSide::Port][..] + } + DefinitionClass::PortConnShorthand { port, local } if port == def || local == def => { + if port == def { + &[crate::reference_support::ConnSide::Port][..] + } else { + &[crate::reference_support::ConnSide::Local][..] + } + } + _ => continue, + }; + + for &side in sides { + let reference_context = reference_context( + db.db, + &sema, + context.clone(), + hir_file_id, + token, + &class, + container, + &mut chains, + &mut conn_port_by_name, + &text, + side, + ); + let tokens = res + .entry(file_id) + .or_insert_with(|| Vec::with_capacity(ReferencesCtx::FILE_REF_CAPACITY)); + if tokens.iter().any(|existing| existing.range == mention.range) { continue; } - res.entry(reference.file_id) - .or_insert_with(|| Vec::with_capacity(ReferencesCtx::FILE_REF_CAPACITY)) - .push(ReferenceToken::from_semantic_reference(reference)); + tokens.push(ReferenceToken { + ptr: SyntaxTokenPtr::from_token(token), + range: mention.range, + category: ReferenceCategory::from_tok(token), + context: reference_context, + }); } } +} - res +pub(crate) fn token_for_mention<'tree>( + tree: &'tree syntax::SyntaxTree, + emitted: &EmittedTokenIndex<'tree>, + mention: &design_graph::Mention, +) -> Option> { + if let Some(emitted_id) = mention + .emitted + .and_then(|index| usize::try_from(index).ok()) + .map(preproc_expand::macro_file::SourceEmittedTokenId::new) + && let Some(token) = emitted.get(&emitted_id).and_then(|tokens| { + tokens.iter().copied().find(|token| { + token.kind() == mention.kind && token.text_range() == Some(mention.range) + }) + }) + { + return Some(token); + } + // L0 extract and the request parse can disagree on emitted indices when + // includes expand. Fall back to (kind, range) rather than dropping the hit. + SyntaxTokenPtr::from_kind_range(mention.kind, mention.range).to_token(tree) } /// Resolves a HIR file location to a user-facing source file and range. @@ -271,7 +380,7 @@ pub(crate) fn search_references( /// not a file the user can open. Returns `None` when a macro expansion's call /// site cannot be resolved. pub(crate) fn resolve_source_range( - db: &dyn TyDb, + db: &dyn HirDefDb, file_id: HirFileId, range: TextRange, ) -> Option<(FileId, TextRange)> { @@ -283,3 +392,42 @@ pub(crate) fn resolve_source_range( } } } + +#[cfg(test)] +mod tests { + use syntax::{has_text_range::HasTextRange, token::TokenKindExt}; + use utils::line_index::TextSize; + + use crate::{semantic_target::preproc::emit_token_index, test_utils::setup_marked}; + + #[test] + fn macro_argument_mention_recovers_via_emitted_id() { + let text = r#" +`define NEXT(value) (value + 1) +module top(input logic /*marker:def*/payload_i); + logic active_data; + assign active_data = `NEXT(/*marker:arg*/payload_i); +endmodule +"#; + let (host, file_id, _clean, markers) = setup_marked(text); + let db = host.ctx(); + let arg = utils::line_index::TextRange::new( + markers["arg"], + markers["arg"] + TextSize::of("payload_i"), + ); + let facts = db.file_facts(file_id); + let mention = facts + .mentions_of("payload_i") + .find(|mention| mention.range == arg) + .expect("FileFacts records the macro argument identifier"); + assert!(mention.emitted.is_some(), "macro-argument tokens have a trace identity"); + + let tree = db.parse(preproc_expand::file::HirFileId::from(file_id)); + let emitted = emit_token_index(tree.root()); + let token = super::token_for_mention(&tree, &emitted, mention) + .expect("emitted-id lookup recovers the argument token"); + assert!(token.kind().name_like()); + assert_eq!(token.text_range(), Some(arg)); + assert_eq!(token.raw_text(), "payload_i"); + } +} diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 046dabd8a..7f4a82342 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -18,15 +18,17 @@ use vfs::FileId; use crate::{ FilePosition, ScopeVisibility, - db::{root_db::RootDb, workspace_symbol_index_db::WorkspaceSymbolIndexDb}, + analysis::AnalysisContext, + db::root_db::RootDb, definitions::DefinitionClass, + reference_support::{ConnSide, ReferenceContext}, references::{ ReferencesConfig, search::{ReferenceToken, ReferencesCtx, SearchScope, search_references}, }, - semantic_index::{ConnSide, ReferenceContext}, semantic_target::{ - PreprocMacroTarget, SemanticTarget, SourceTarget, TargetIntent, resolve_semantic_target, + PreprocMacroTarget, SemanticTarget, SourceTarget, TargetIntent, is_preproc_free_file, + resolve_semantic_target, }, source_change::SourceChange, }; @@ -110,30 +112,32 @@ pub struct RenameCollisionInfo { } pub(crate) fn prepare_rename( - db: &RootDb, + db: &AnalysisContext<'_>, position @ FilePosition { file_id, .. }: FilePosition, config: RenameConfig, ) -> RenameResult { - let sema = Semantics::new(db); - let target = resolve_rename_target(&sema, position)?; + crate::design_unit::rename_guard(db, position)?; + let sema = db.semantics(); + let target = resolve_rename_target(db, &sema, position)?; match &target { RenameTarget::Hdl(target) => { - let _ = config.references_config(db, &target.selected_def, file_id)?; + let _ = config.references_config(db.db, &target.selected_def, file_id)?; } RenameTarget::Macro(_) | RenameTarget::Manifest(_) => {} } - target.range(db).ok_or(RenameError::NoRefFound) + target.range(db.db).ok_or(RenameError::NoRefFound) } pub(crate) fn rename( - db: &RootDb, + db: &AnalysisContext<'_>, position @ FilePosition { file_id, .. }: FilePosition, config: RenameConfig, new_name: &str, ) -> RenameResult { - let sema = Semantics::new(db); - match resolve_rename_target(&sema, position)? { - RenameTarget::Macro(target) => rename_macro(db, file_id, &config, target, new_name), + crate::design_unit::rename_guard(db, position)?; + let sema = db.semantics(); + match resolve_rename_target(db, &sema, position)? { + RenameTarget::Macro(target) => rename_macro(db.db, file_id, &config, target, new_name), RenameTarget::Manifest(target) => { crate::manifest::rename_target(db, target, &config, new_name) } @@ -141,7 +145,7 @@ pub(crate) fn rename( let mut source_change = rename_definition(db, &sema, file_id, &config, &selected_def, new_name, None)?; crate::manifest::rename_module_references( - db, + db.db, file_id, &selected_def, &config, @@ -154,12 +158,12 @@ pub(crate) fn rename( } pub(crate) fn rename_expansion_info( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, config: RenameConfig, ) -> RenameResult { - let sema = Semantics::new(db); - let resolved = match resolve_rename_target(&sema, position)? { + let sema = db.semantics(); + let resolved = match resolve_rename_target(db, &sema, position)? { RenameTarget::Macro(_) => { // Recursive rename follows same-name port connections; macros have // no such semantics. @@ -170,39 +174,39 @@ pub(crate) fn rename_expansion_info( } RenameTarget::Hdl(target) => target, }; - let targets = recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)?; + let targets = recursive_rename_targets(db, position.file_id, &config, resolved.targets)?; let additional_symbols = targets.len().saturating_sub(1); Ok(RecursiveRenameInfo { additional_symbols }) } pub(crate) fn expanded_rename( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, config: RenameConfig, new_name: &str, ) -> RenameResult { - let sema = Semantics::new(db); - match resolve_rename_target(&sema, position)? { + let sema = db.semantics(); + match resolve_rename_target(db, &sema, position)? { // Macros have no recursive semantics; the expanded rename is the // plain rename. RenameTarget::Macro(target) => { - rename_macro(db, position.file_id, &config, target, new_name) + rename_macro(db.db, position.file_id, &config, target, new_name) } RenameTarget::Manifest(target) => { crate::manifest::rename_target(db, target, &config, new_name) } RenameTarget::Hdl(resolved) => { let targets = - recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)?; + recursive_rename_targets(db, position.file_id, &config, resolved.targets)?; let mut rename_targets = UniqVec::<(), DefOrigin>::default(); for target in &targets { - rename_targets.push(target.def.origins(db), ()); + rename_targets.push(target.def.origins(db.db), ()); } let mut source_changes = SourceChange::default(); for target in &targets { let changes = rename_definition_with_refs( - db, + db.db, &sema, &target.def, new_name, @@ -222,14 +226,14 @@ pub(crate) fn expanded_rename( } pub(crate) fn rename_conflict_info( - db: &RootDb, + db: &AnalysisContext<'_>, position: FilePosition, config: RenameConfig, new_name: &str, recursive: bool, ) -> RenameResult { - let sema = Semantics::new(db); - let resolved = match resolve_rename_target(&sema, position)? { + let sema = db.semantics(); + let resolved = match resolve_rename_target(db, &sema, position)? { // The preproc model has no name-scope query for macros yet; report no // collisions for macro renames. RenameTarget::Macro(_) => return Ok(RenameCollisionInfo { conflicts: 0 }), @@ -237,7 +241,7 @@ pub(crate) fn rename_conflict_info( RenameTarget::Hdl(target) => target, }; let targets: Vec = if recursive { - recursive_rename_targets(db, &sema, position.file_id, &config, resolved.targets)? + recursive_rename_targets(db, position.file_id, &config, resolved.targets)? .into_iter() .map(|target| target.def) .collect() @@ -248,17 +252,17 @@ pub(crate) fn rename_conflict_info( let new_name = SmolStr::new(new_name); let mut target_index = UniqVec::<(), DefOrigin>::default(); for target in &targets { - target_index.push(target.origins(db), ()); + target_index.push(target.origins(db.db), ()); } let mut conflicts = UniqVec::::default(); - for collision in targets.iter().flat_map(|target| target.origins(db)).flat_map(|origin| { - sema.resolve_name(origin.container_id(db), &new_name, origin.kind(db).name_context()) + for collision in targets.iter().flat_map(|target| target.origins(db.db)).flat_map(|origin| { + sema.resolve_name(origin.container_id(db.db), &new_name, origin.kind(db.db).name_context()) .into_candidates() }) { - if collision.origins(db).iter().any(|origin| target_index.contains(origin)) { + if collision.origins(db.db).iter().any(|origin| target_index.contains(origin)) { continue; } - conflicts.push(collision.origins(db), collision); + conflicts.push(collision.origins(db.db), collision); } Ok(RenameCollisionInfo { conflicts: conflicts.len() }) @@ -321,6 +325,7 @@ enum ReferenceEdit { } fn resolve_rename_target( + db: &AnalysisContext<'_>, sema: &Semantics<'_, RootDb>, FilePosition { file_id, offset }: FilePosition, ) -> RenameResult { @@ -338,7 +343,7 @@ fn resolve_rename_target( SemanticTarget::Include(_) => Err(RenameError::NoRefFound), SemanticTarget::Manifest(target) => Ok(RenameTarget::Manifest(target)), SemanticTarget::Source(target) => { - resolve_hdl_rename_target(sema, hir_file_id, target).map(RenameTarget::Hdl) + resolve_hdl_rename_target(db, sema, hir_file_id, target).map(RenameTarget::Hdl) } } } @@ -391,6 +396,7 @@ fn unique_macro_param_definition( } fn resolve_hdl_rename_target( + db: &AnalysisContext<'_>, sema: &Semantics<'_, RootDb>, hir_file_id: HirFileId, target: SourceTarget<'_>, @@ -400,7 +406,7 @@ fn resolve_hdl_rename_target( let mut targets = UniqVec::::default(); for token in tokens { - let token_selected = match DefinitionClass::resolve(sema.db, hir_file_id, token) + let token_selected = match DefinitionClass::resolve(db, hir_file_id, token) .unique() .ok_or(RenameError::NoDefFound)? { @@ -429,7 +435,7 @@ fn resolve_hdl_rename_target( if targets .iter() .flat_map(|def| def.origins(sema.db)) - .any(|origin| origin_is_macro_generated(sema.db, origin)) + .any(|origin| origin_is_macro_generated(db, origin)) { return Err(RenameError::MacroDefinitionNotEditable); } @@ -511,7 +517,7 @@ fn macro_reference_name_range(db: &RootDb, reference: &MacroReference) -> TextRa } fn rename_definition( - db: &RootDb, + db: &AnalysisContext<'_>, sema: &Semantics<'_, RootDb>, request_file_id: FileId, config: &RenameConfig, @@ -519,19 +525,18 @@ fn rename_definition( new_name: &str, rename_targets: Option<&UniqVec<(), DefOrigin>>, ) -> RenameResult { - let refs = references_for_definition(db, sema, request_file_id, config, def)?; - rename_definition_with_refs(db, sema, def, new_name, rename_targets, &refs) + let refs = references_for_definition(db, request_file_id, config, def)?; + rename_definition_with_refs(db.db, sema, def, new_name, rename_targets, &refs) } fn references_for_definition( - db: &RootDb, - sema: &Semantics<'_, RootDb>, + db: &AnalysisContext<'_>, request_file_id: FileId, config: &RenameConfig, def: &DefId, ) -> RenameResult { - let refs_config = config.references_config(db, def, request_file_id)?; - Ok(ReferencesCtx::new(sema, def, refs_config).search()) + let refs_config = config.references_config(db.db, def, request_file_id)?; + Ok(ReferencesCtx::new(db, def, refs_config).search()) } fn rename_definition_with_refs( @@ -674,26 +679,26 @@ fn range_text(text: &str, range: TextRange) -> &str { /// salsa query so the recursive rename info, conflict and edit commands share /// one computation across requests. pub(crate) fn recursive_rename_closure_impl( - db: &dyn WorkspaceSymbolIndexDb, + db: &AnalysisContext<'_>, def: DefId, visibility: ScopeVisibility, single_file: Option, ) -> Vec { let config = ReferencesConfig::new(visibility, single_file.map(SearchScope::single_file)); let mut targets = UniqVec::::default(); - targets.push(def.origins(db), def); + targets.push(def.origins(db.db), def); let mut idx = 0; while idx < targets.len() { let current = *targets.get(idx); idx += 1; - let scope = SearchScope::new(db, ¤t, config.clone()); + let scope = SearchScope::new(db.db, ¤t, config.clone()); let refs = search_references(db, ¤t, scope); // Same-name connections connect their paired definition: follow them // to close the recursive rename set. for toks in refs.values() { for token_ref in toks { if let Some(paired) = token_ref.context().paired() { - targets.push(paired.origins(db), *paired); + targets.push(paired.origins(db.db), *paired); } } } @@ -702,8 +707,7 @@ pub(crate) fn recursive_rename_closure_impl( } fn recursive_rename_targets( - db: &RootDb, - sema: &Semantics<'_, RootDb>, + db: &AnalysisContext<'_>, file_id: FileId, config: &RenameConfig, initial_targets: Vec, @@ -716,29 +720,35 @@ fn recursive_rename_targets( for target in initial_targets { let closure = db.recursive_rename_closure(target, config.scope_visibility, single_file); for def in closure.iter() { - targets.push(def.origins(db), *def); + targets.push(def.origins(db.db), *def); } } let mut resolved_targets = Vec::new(); for def in targets.into_vec() { - let refs = references_for_definition(db, sema, file_id, config, &def)?; + let refs = references_for_definition(db, file_id, config, &def)?; resolved_targets.push(RecursiveRenameTarget { def, refs }); } Ok(resolved_targets) } -fn origin_is_macro_generated(db: &RootDb, origin: DefOrigin) -> bool { - if matches!(origin.container_id(db).file(db), HirFileId::Macro(_)) { +fn origin_is_macro_generated(db: &AnalysisContext<'_>, origin: DefOrigin) -> bool { + if matches!(origin.container_id(db.db).file(db.db), HirFileId::Macro(_)) { return true; } - let Some(InFile { file_id: HirFileId::File(file_id), value: range }) = origin.name_range(db) + let Some(InFile { file_id: HirFileId::File(file_id), value: range }) = origin.name_range(db.db) else { return false; }; + if is_preproc_free_file(db.db, file_id) { + return false; + } - macro_files_at_offset(db, file_id, range.start()).into_iter().any(|macro_file| { - macro_file_call_site(db, macro_file).is_some_and(|call_site| { + if let Some(generated) = db.source_semantic_map(file_id).macro_origin_for_range(range) { + return generated; + } + macro_files_at_offset(db.db, file_id, range.start()).into_iter().any(|macro_file| { + macro_file_call_site(db.db, macro_file).is_some_and(|call_site| { call_site.call_file_id == file_id && call_site.call_range == range }) }) @@ -764,8 +774,9 @@ mod tests { use vfs::{ChangedFile, FileId, FileSet, VfsPath}; use super::*; + use crate::analysis_host::AnalysisHost; - fn db_with_text(text: &str) -> (RootDb, FileId) { + fn db_with_text(text: &str) -> (AnalysisHost, FileId) { let file_id = FileId::from_raw(0); let mut file_set = FileSet::default(); file_set.insert(file_id, VfsPath::new_virtual_path("/test.sv".to_owned())); @@ -774,21 +785,22 @@ mod tests { change.set_roots(vec![SourceRoot::new_local(file_set)]); change.add_changed_file(ChangedFile::create(file_id, text)); - let mut db = RootDb::new(None); - db.apply_change(change); - (db, file_id) + let mut host = AnalysisHost::default(); + host.apply_change(change); + (host, file_id) } - fn db_with_caret(text: &str) -> (RootDb, FileId, TextSize) { + fn db_with_caret(text: &str) -> (AnalysisHost, FileId, TextSize) { let marker = "/*caret*/"; let offset = text.find(marker).expect("missing caret marker"); let text = text.replace(marker, ""); - let (db, file_id) = db_with_text(&text); - (db, file_id, TextSize::from(offset as u32)) + let (host, file_id) = db_with_text(&text); + (host, file_id, TextSize::from(offset as u32)) } fn apply_rename(text: &str, new_name: &str, recursive: bool) -> String { - let (db, file_id, offset) = db_with_caret(text); + let (host, file_id, offset) = db_with_caret(text); + let db = host.ctx(); let config = RenameConfig::workspace(ScopeVisibility::Public); let position = FilePosition { file_id, offset }; let change = if recursive { @@ -953,9 +965,9 @@ mod tests { ); let config = RenameConfig::workspace(ScopeVisibility::Public); let position = FilePosition { file_id, offset }; - let info = rename_expansion_info(&db, position, config.clone()).unwrap(); + let info = rename_expansion_info(&db.ctx(), position, config.clone()).unwrap(); assert_eq!(info.additional_symbols, 0); - let conflicts = rename_conflict_info(&db, position, config, "BAR", false).unwrap(); + let conflicts = rename_conflict_info(&db.ctx(), position, config, "BAR", false).unwrap(); assert_eq!(conflicts.conflicts, 0); } } diff --git a/crates/ide/src/render.rs b/crates/ide/src/render.rs index 935c6e510..0859d2386 100644 --- a/crates/ide/src/render.rs +++ b/crates/ide/src/render.rs @@ -1,3 +1,15 @@ +/// Writing a lowered `hir-def` item back out as SystemVerilog source text. +/// +/// Hover, signature help, and navigation labels need a declaration spelled +/// out: `logic [7:0]`, a port list, an enum body. That is a syntactic job — +/// it reads what `hir-def` lowered and prints it. +/// +/// It is not type inference. What a name's type actually *is* comes from the +/// resident slang elaboration service; nothing here computes a type, and a +/// string produced here is never an answer about semantics. This lived in a +/// crate called `hir-ty` for exactly as long as that was untrue. +pub(crate) mod hir_display; + use base_db::source_db::SourceRootDb; use hir_def::{ container::{InFile, OwnerRef, ScopeParent}, @@ -20,7 +32,6 @@ use hir_def::{ symbol::{DefKind, DefOrigin}, }; use hir_semantics::semantics::Semantics; -use hir_ty::display::HirDisplay; use itertools::Itertools; use syntax::{ SyntaxCursorExt, SyntaxNodeExt, @@ -35,6 +46,7 @@ use crate::{ markup::{Markup, display_project_path, file_link_target, inline_code, markdown_link}, module_resolution::resolve_module_name, references::search::resolve_source_range, + render::hir_display::HirDisplay, }; pub(crate) fn render_literal(literal: &Literal) -> Option { @@ -333,9 +345,9 @@ fn render_signature(sema: &Semantics, origin: &DefOrigin) -> Option origin.as_decl(db).and_then(|id| render_decl_signature(db, id)), DefKind::Typedef => origin.as_typedef(db).and_then(|id| id.display_signature(db).ok()), - DefKind::Instance => { - origin.as_instance(db).and_then(|id| render_instance_signature(db, id)) - } + DefKind::Instance => origin + .as_instance(db) + .and_then(|id| render_instance_signature(db, sema.resolution_context().as_ref(), id)), DefKind::ClockingBlock => { origin.as_clocking_block(db).and_then(|id| render_clocking_block_signature(db, id)) } @@ -508,7 +520,11 @@ fn render_non_ansi_port_signature(db: &RootDb, port_id: OwnerRef) Some(format!("port {label}")) } -fn render_instance_signature(db: &RootDb, instance_id: OwnerRef) -> Option { +fn render_instance_signature( + db: &RootDb, + context: &hir_def::pathres::ResolutionContext, + instance_id: OwnerRef, +) -> Option { let parent_module = db.body_with_source_map(instance_id.cont_id); let instance = parent_module.get(instance_id.value); let instance_name = instance.name.as_ref()?; @@ -516,8 +532,8 @@ fn render_instance_signature(db: &RootDb, instance_id: OwnerRef) -> let module_name = instantiation.module_name.as_ref()?; let mut signature = format!("instance {instance_name} of {module_name}"); - if let Some(from_file) = instance_id.cont_id.file(db).source_file_id(db) - && let Some(target_module_id) = resolve_module_name(db, from_file, module_name).unique() + if instance_id.cont_id.file(db).source_file_id(db).is_some() + && let Some(target_module_id) = resolve_module_name(db, context, module_name).unique() && let Some(module_signature) = render_module_signature(db, target_module_id) { signature.push_str("\n\n"); diff --git a/crates/hir-ty/src/display.rs b/crates/ide/src/render/hir_display.rs similarity index 88% rename from crates/hir-ty/src/display.rs rename to crates/ide/src/render/hir_display.rs index 5d1d95486..cb96c8701 100644 --- a/crates/hir-ty/src/display.rs +++ b/crates/ide/src/render/hir_display.rs @@ -4,7 +4,7 @@ use hir_def::{ aggregate::StructKind, constraint::DistItem, container::OwnerRef, - def_id::DefId, + db::HirDefDb, expr::{ Arg, AssignOp, AssignmentPattern, AssignmentPatternItem, BinaryOp, Expr, ExprId, IncDecOp, InsideRange, PropertyCaseItem, PropertyExpr, Selector, SequenceExpr, SequenceRepetition, @@ -17,20 +17,14 @@ use hir_def::{ literal::Literal, module::port::{PortDirection, PortHeader}, subroutine::SubroutinePortDir, - symbol::DefKind, ty::{NetKind, NetType}, typedef::TypedefId, }; use syntax::value::TimeUnit; use triomphe::Arc; -use crate::{ - db::TyDb, - ty::{BuiltinTy, Ty}, -}; - pub struct HirFormatter<'a> { - pub db: &'a dyn TyDb, + pub db: &'a dyn HirDefDb, f: &'a mut dyn HirWrite, simplified_ty: bool, } @@ -81,13 +75,13 @@ impl From for HirDisplayError { pub trait HirDisplay { fn hir_fmt(&self, f: &mut HirFormatter<'_>) -> Result<(), HirDisplayError>; - fn display_source(&self, db: &dyn TyDb) -> Result { + fn display_source(&self, db: &dyn HirDefDb) -> Result { let mut res = String::new(); self.hir_fmt(&mut HirFormatter { db, f: &mut res, simplified_ty: false })?; Ok(res) } - fn display_signature(&self, db: &dyn TyDb) -> Result { + fn display_signature(&self, db: &dyn HirDefDb) -> Result { let mut res = String::new(); self.hir_fmt(&mut HirFormatter { db, f: &mut res, simplified_ty: true })?; Ok(res) @@ -100,137 +94,6 @@ impl HirDisplay for Arc { } } -impl HirDisplay for Ty { - fn hir_fmt(&self, f: &mut HirFormatter<'_>) -> Result<(), HirDisplayError> { - match self { - Ty::Unknown => f.write_str("unknown"), - Ty::Error => f.write_str("error"), - Ty::Void => f.write_str("void"), - Ty::Builtin(BuiltinTy::Data { id, container }) => { - OwnerRef::new(*container, DataTy::Builtin(id.clone())).hir_fmt(f) - } - Ty::Struct(struct_ref) => { - OwnerRef::new(struct_ref.cont_id, DataTy::Struct(*struct_ref)).hir_fmt(f) - } - Ty::Enum(def) => hir_fmt_def_backed_type(f, "enum", *def), - Ty::Union(def) => hir_fmt_def_backed_type(f, "union", *def), - Ty::Queue { elem, size } => { - elem.hir_fmt(f)?; - f.write_str(" [$")?; - if let (Some(size), Some(container)) = (size, ty_expr_container(f.db, elem)) { - f.write_str(":")?; - OwnerRef::new(container, *size).hir_fmt(f)?; - } - f.write_str("]") - } - Ty::Assoc { key, elem } => { - elem.hir_fmt(f)?; - f.write_str(" [")?; - if matches!(key.as_ref(), Ty::Unknown) { - f.write_str("*")?; - } else { - key.hir_fmt(f)?; - } - f.write_str("]") - } - Ty::Dynamic(elem) => { - elem.hir_fmt(f)?; - f.write_str(" []") - } - Ty::Event => f.write_str("event"), - Ty::Chandle => f.write_str("chandle"), - Ty::Alias { typedef, target } => { - let container = typedef.cont_id.data(f.db); - if let Some(name) = &container.typedef(typedef.value).name { - f.write_str(name) - } else { - target.hir_fmt(f) - } - } - Ty::Module(module_id) => { - let module = f.db.body(*module_id); - if let Some(name) = &module.name { - f.write_str(name) - } else { - f.write_str("module") - } - } - Ty::Checker(def) => hir_fmt_named_def_type(f, "checker", *def), - Ty::Covergroup(def) => hir_fmt_named_def_type(f, "covergroup", *def), - 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.as_ref().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.body(*generate_block_id); - if let Some(name) = &block.name { - f.write_str(name) - } else { - f.write_str("generate block") - } - } - Ty::Block(owner) => { - if let Some(name) = owner.name(f.db) { - f.write_str(&name) - } else { - f.write_str("block") - } - } - } - } -} - -fn hir_fmt_def_backed_type( - f: &mut HirFormatter<'_>, - keyword: &str, - def: DefId, -) -> Result<(), HirDisplayError> { - f.write_str(keyword)?; - if def.kind(f.db) == DefKind::Typedef - && let Some(name) = def.name(f.db) - { - f.write_str(" ")?; - f.write_str(&name)?; - } - Ok(()) -} - -fn hir_fmt_named_def_type( - f: &mut HirFormatter<'_>, - keyword: &str, - def: DefId, -) -> Result<(), HirDisplayError> { - f.write_str(keyword)?; - if let Some(name) = def.name(f.db) { - f.write_str(" ")?; - f.write_str(&name)?; - } - Ok(()) -} - -fn ty_expr_container(db: &dyn crate::db::TyDb, ty: &Ty) -> Option { - match ty { - Ty::Builtin(BuiltinTy::Data { container, .. }) => Some(*container), - Ty::Struct(struct_ref) => Some(struct_ref.cont_id), - Ty::Alias { typedef, .. } => Some(typedef.cont_id), - Ty::Enum(def) | Ty::Union(def) => def.type_container(db), - Ty::Queue { elem, .. } | Ty::Assoc { elem, .. } | Ty::Dynamic(elem) => { - ty_expr_container(db, elem) - } - _ => None, - } -} - impl HirDisplay for PortDirection { fn hir_fmt(&self, f: &mut HirFormatter<'_>) -> Result<(), HirDisplayError> { match self { @@ -1159,3 +1022,6 @@ impl HirDisplay for OwnerRef { f.write_str("]") } } + +#[cfg(test)] +mod tests; diff --git a/crates/ide/src/render/hir_display/tests.rs b/crates/ide/src/render/hir_display/tests.rs new file mode 100644 index 000000000..91c035867 --- /dev/null +++ b/crates/ide/src/render/hir_display/tests.rs @@ -0,0 +1,218 @@ +//! Rendering lowered declarations back to SystemVerilog source text. + +use base_db::{ + diagnostics_config::DiagnosticsConfig, + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + salsa::Durability, + source_db::{SourceDb, SourceFileKind, SourceRootDb}, + source_root::{SourceRoot, SourceRootId}, +}; +use hir_def::{ + Ident, + constraint::Constraint, + container::OwnerRef, + covergroup::CoverageBinInitializer, + expr::{ + Expr, + data_ty::{DataTy, TypePathKind}, + }, + owner::OwnerId, +}; +use rustc_hash::FxHashSet; +use smol_str::SmolStr; +use triomphe::Arc; +use utils::paths::{AbsPathBuf, Utf8PathBuf}; +use vfs::{FileId, FileSet, VfsPath}; + +use super::HirDisplay; +use crate::db::root_db::RootDb; + +const TOP: FileId = FileId::from_raw(0); +const ROOT: SourceRootId = SourceRootId(0); +const PROFILE: CompilationProfileId = CompilationProfileId(0); + +fn db_with_root_text(root_text: &str) -> RootDb { + 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 = RootDb::new(None); + db.set_files_with_durability(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_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); + db.set_file_text_with_durability(TOP, Arc::from(root_text), 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 module_id(db: &RootDb, name: &str) -> OwnerId { + hir_def::unit::test_module_owner(db, name) +} + +#[test] +fn enum_definition_preserves_base_members_and_initializers() { + let db = db_with_root_text( + r#" +module m; + typedef enum logic [1:0] { + Idle = 2'd0, + Busy, + Error = 2'd3 + } state_t; +endmodule +"#, + ); + let module = module_id(&db, "m"); + let body = db.body(module); + let enum_def = body.enums.values().next().expect("enum definition should be lowered"); + assert!(enum_def.base_ty.is_some(), "enum base type must be retained"); + assert_eq!( + enum_def.members.iter().map(|member| member.name.as_deref()).collect::>(), + [Some("Idle"), Some("Busy"), Some("Error")] + ); + assert!(enum_def.members[0].initializer.is_some()); + assert!(enum_def.members[1].initializer.is_none()); + assert!(enum_def.members[2].initializer.is_some()); +} + +#[test] +fn constraint_declaration_preserves_dist_and_nested_items() { + let db = db_with_root_text( + r#" +module m; + logic x; + constraint c { + x dist { 1 := 2, default }; + unique { x }; + } +endmodule +"#, + ); + let module = module_id(&db, "m"); + let body = db.body(module); + let definition = + body.constraint_defs.values().next().expect("constraint declaration should be lowered"); + assert_eq!(definition.name.as_deref(), Some("c")); + let Constraint::Block(items) = &body.constraints[definition.constraint] else { + panic!("constraint declaration should lower to a block"); + }; + assert_eq!(items.len(), 2); + let Constraint::Expression { expr, .. } = body.constraints[items[0]] else { + panic!("distribution item should lower as an expression constraint"); + }; + let Expr::Dist { distribution, .. } = &body.exprs[expr] else { + panic!("expression-or-dist should preserve its distribution"); + }; + assert_eq!(distribution.items.len(), 2); + assert!(matches!(body.constraints[items[items.len() - 1]], Constraint::Uniqueness { .. })); +} + +#[test] +fn coverpoint_bins_preserve_sample_expression_and_ranges() { + let db = db_with_root_text( + r#" +covergroup cg; + cp: coverpoint 1 { + bins low[2] = {[0:3]}; + } +endgroup +module m; +endmodule +"#, + ); + let table = db.owner_table(preproc_expand::file::HirFileId::File(TOP)); + let covergroup = *table + .owners_named("cg", hir_def::owner::OwnerKind::Covergroup) + .first() + .expect("covergroup should project"); + let body = db.body(covergroup); + let definition = body.covergroups.values().next().expect("covergroup should lower"); + let coverpoint = &body.coverpoints[definition.coverpoints[0]]; + assert_eq!(coverpoint.bins.len(), 1); + assert!(matches!(coverpoint.bins[0].initializer, CoverageBinInitializer::Ranges { .. })); + assert!(coverpoint.bins[0].size.is_some()); +} + +#[test] +fn qualified_type_paths_preserve_separator_and_source_projection() { + let db = db_with_root_text( + r#" +package p; + typedef logic t; +endpackage + +module m; + p::t value; +endmodule +"#, + ); + let module = module_id(&db, "m"); + let lowered = db.body_with_source_map(module); + let type_ref = lowered + .data_ref() + .declarations + .iter() + .find_map(|(_, declaration)| match declaration.ty() { + DataTy::Named(type_ref) => Some(type_ref), + _ => None, + }) + .expect("the value declaration should retain its named type"); + + assert_eq!(type_ref.path_kind(), TypePathKind::Package); + assert_eq!(type_ref.segments(), &[ident("p"), ident("t")]); + assert_eq!(type_ref.segment_sources().len(), type_ref.segments().len()); + let source = db + .source_projection(module.file(&db)) + .origin(type_ref.source()) + .expect("type path source identity must project to source data"); + assert_eq!(source.file_id(), module.file(&db)); + assert!(source.full_range().is_some()); +} + +#[test] +fn streaming_with_range_display_preserves_with_keyword() { + let db = db_with_root_text( + r#" +module m(input logic [3:0] a); + logic [3:0] x = {<<{a with [3:0]}}; +endmodule +"#, + ); + let module = module_id(&db, "m"); + let owner = module; + let body = db.body_with_source_map(owner); + let (stream_id, _) = body + .exprs + .iter() + .find(|(_, expr)| matches!(expr, hir_def::expr::Expr::Stream { .. })) + .expect("streaming concatenation should lower"); + + assert_eq!(OwnerRef::new(owner, stream_id).display_source(&db).unwrap(), "{<<{a with [3:0]}}"); +} diff --git a/crates/ide/src/selection_ranges.rs b/crates/ide/src/selection_ranges.rs index a702e398c..00e2aebdf 100644 --- a/crates/ide/src/selection_ranges.rs +++ b/crates/ide/src/selection_ranges.rs @@ -1,4 +1,3 @@ -use hir_semantics::semantics::Semantics; use itertools::Itertools; use preproc_expand::file::HirFileId; use syntax::{ @@ -8,16 +7,16 @@ use syntax::{ use utils::line_index::{TextRange, TextSize}; use vfs::FileId; -use crate::{FilePosition, db::root_db::RootDb}; +use crate::{FilePosition, analysis::AnalysisContext, db::root_db::RootDb}; pub(crate) fn selection_ranges( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ) -> Vec { if db.file_kind(file_id).is_project_manifest() { - return crate::manifest::selection_ranges(db, FilePosition { file_id, offset }); + return crate::manifest::selection_ranges(db.db, FilePosition { file_id, offset }); } - let sema = Semantics::new(db); + let sema = db.semantics(); let parsed_file = sema.parse_file(file_id); let Some(root) = parsed_file.root() else { return vec![TextRange::empty(offset)]; @@ -192,9 +191,9 @@ mod tests { use vfs::{ChangedFile, FileId, FileSet, VfsPath}; use super::selection_ranges; - use crate::{FilePosition, db::root_db::RootDb}; + use crate::{FilePosition, analysis_host::AnalysisHost}; - fn db_with_file(text: &str) -> (RootDb, FileId) { + fn db_with_file(text: &str) -> (AnalysisHost, FileId) { let file_id = FileId::from_raw(0); let path = VfsPath::new_virtual_path("/test.sv".to_owned()); @@ -206,9 +205,9 @@ mod tests { change.set_roots(vec![root]); change.add_changed_file(ChangedFile::create(file_id, text)); - let mut db = RootDb::new(None); - change.apply(&mut db); - (db, file_id) + let mut host = AnalysisHost::default(); + host.apply_change(change); + (host, file_id) } #[test] @@ -237,7 +236,8 @@ mod tests { ), ("at token boundary", "module top;\n assign y = a + b;\nendmodule\n", 31), ] { - let (db, file_id) = db_with_file(text); + let (host, file_id) = db_with_file(text); + let db = host.ctx(); let ranges = selection_ranges(&db, FilePosition { file_id, offset: offset.into() }); writeln!(&mut report, "{name}: {ranges:?}").unwrap(); } diff --git a/crates/ide/src/semantic_index.rs b/crates/ide/src/semantic_index.rs deleted file mode 100644 index ab43b849b..000000000 --- a/crates/ide/src/semantic_index.rs +++ /dev/null @@ -1,854 +0,0 @@ -use base_db::{source_db::SourceRootDb, source_root::SourceRootId}; -use hir_def::{Ident, container::InFile, def_id::DefId, item_tree::ModuleHeader, owner::OwnerId}; -use hir_ty::db::TyDb; -use preproc_expand::{db::PreprocDb, file::HirFileId, macro_file::macro_files_for_file}; -use rustc_hash::FxHashMap; -use syntax::{ - SyntaxNodeExt, TokenKind, has_text_range::HasTextRange, ptr::SyntaxTokenPtr, - token::TokenKindExt, -}; -use utils::line_index::TextRange; -use vfs::FileId; - -use crate::{ - db::{ - root_db::RootDb, - workspace_symbol_index_db::{ - WorkspaceSymbolIndexDb, source_root_module_index_for_root, - source_root_semantic_index_for_root, - }, - }, - navigation_target::nav_location, - references::ReferenceCategory, -}; - -mod build; -use build::definition_ranges_for; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) struct SemanticDefinitionRange { - pub file_id: FileId, - pub range: TextRange, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ConnSide { - /// The reference is the port side of a shorthand connection (`.name`). - Port, - /// The reference is the local side of a shorthand connection (`.name`). - Local, -} - -/// Context of a reference token inside a named port connection, computed at -/// index build time so rename and other reference consumers never re-resolve. -/// -/// `paired` is `Some` exactly when the connection is a same-name connection -/// (the `.name` and the data identifier have the same text): for the name -/// side it is the local definition, for the data side it is the port -/// definition. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ReferenceContext { - Plain, - /// The token is the `.name` of a named port connection. - ConnName { - /// Range of the data identifier, when the data is a simple identifier. - ident_range: Option, - /// Range from the name token start to the closing paren end. - collapse_range: Option, - /// No-parens shorthand connection (`.name`). - shorthand: bool, - /// The side of a shorthand connection this reference belongs to. - side: ConnSide, - /// Same-name connections: the local definition of the data identifier. - paired: Option, - }, - /// The token is a simple identifier in the data position of a named port - /// connection. - ConnData { - /// Range of the connection's `.name` token. - name_range: TextRange, - /// Range from the name token start to the closing paren end. - collapse_range: Option, - /// Same-name connections: the port definition of the name token. - paired: Option, - }, -} - -impl ReferenceContext { - /// The paired same-name connection definition, when the connection is - /// same-name: the local def for name tokens, the port def for data - /// tokens, and the counterpart def for shorthand references. - pub(crate) fn paired(&self) -> Option<&DefId> { - match self { - ReferenceContext::Plain => None, - ReferenceContext::ConnName { paired, .. } - | ReferenceContext::ConnData { paired, .. } => paired.as_ref(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct SemanticReference { - pub file_id: FileId, - pub range: TextRange, - pub category: ReferenceCategory, - pub ptr: SyntaxTokenPtr, - pub context: ReferenceContext, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct SemanticReferenceGroup { - pub name: String, - pub definition_ranges: Box<[SemanticDefinitionRange]>, - pub references: Box<[SemanticReference]>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct SemanticModuleDefinition { - pub module_id: OwnerId, - pub file_id: FileId, - pub name: Ident, - pub name_range: TextRange, - pub full_range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ModuleCallItem { - pub file_id: FileId, - pub name: String, - pub full_range: TextRange, - pub name_range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ModuleCallEdge { - pub caller: ModuleCallItem, - pub callee: ModuleCallItem, - pub call_range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct ModuleIndex { - modules_by_name: FxHashMap>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SemanticIndex { - references_by_definition: FxHashMap, - incoming_module_edges: FxHashMap>, - outgoing_module_edges: FxHashMap>, -} - -/// Per-file slice of the semantic index: reference groups without the -/// cross-file definition ranges, which are computed once at merge time. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct FileSemanticIndex { - groups: FxHashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileReferenceGroup { - name: String, - references: Vec, -} - -impl FileReferenceGroup { - pub(crate) fn references(&self) -> &[SemanticReference] { - &self.references - } -} - -/// Module definitions contributed by one file. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct FileModuleIndex { - modules: Vec, -} - -/// Module edges contributed by one file: the outgoing edges of the file's -/// modules, with caller and callee ids so the merge can build both maps. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct FileModuleEdges { - edges: Vec<(OwnerId, OwnerId, ModuleCallEdge)>, -} - -#[derive(Debug)] -struct SemanticReferenceGroupBuilder { - name: String, - definition_ranges: Vec, - references: Vec, -} - -impl ModuleIndex { - /// Merges the per-file module indexes of a source root. - pub(crate) fn for_source_root( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, - ) -> Self { - let source_root = db.source_root(source_root_id); - let mut modules_by_name: FxHashMap> = - FxHashMap::default(); - - let mut hir_files = Vec::new(); - for file_id in source_root.iter() { - hir_files.push(HirFileId::File(file_id)); - hir_files.extend(macro_files_for_file(db, file_id).into_iter().map(HirFileId::Macro)); - } - hir_files.sort_unstable(); - hir_files.dedup(); - - for hir_file_id in hir_files { - let item_tree = db.item_tree(hir_file_id); - for header in - item_tree.module_headers().filter(|header| header.kind().is_instantiable()) - { - let Some(module) = SemanticModuleDefinition::from_header(db, hir_file_id, header) - else { - continue; - }; - modules_by_name.entry(module.name.clone()).or_default().push(module); - } - } - - Self { - modules_by_name: modules_by_name - .into_iter() - .map(|(name, mut modules)| { - modules - .sort_by_key(|module| (module.file_id.index(), module.name_range.start())); - modules.dedup_by(|lhs, rhs| { - lhs.module_id == rhs.module_id - || (lhs.file_id == rhs.file_id && lhs.name_range == rhs.name_range) - }); - (name, modules.into_boxed_slice()) - }) - .collect(), - } - } - - pub(crate) fn module_definitions(&self, name: &Ident) -> &[SemanticModuleDefinition] { - self.modules_by_name.get(name).map_or(&[], |modules| modules.as_ref()) - } - - fn module_definition_at( - &self, - file_id: FileId, - name_range: TextRange, - ) -> Option<&SemanticModuleDefinition> { - self.all_module_definitions() - .find(|module| module.file_id == file_id && module.name_range == name_range) - } - - fn all_module_definitions(&self) -> impl Iterator { - self.modules_by_name.values().flat_map(|modules| modules.iter()) - } -} - -impl SemanticModuleDefinition { - fn new(db: &dyn TyDb, module_id: OwnerId) -> Option { - let source_file = module_id.file(db); - let header = db - .item_tree(source_file) - .module_headers() - .find(|header| header.owner() == module_id)?; - Self::from_header(db, source_file, header) - } - - fn from_header(db: &dyn TyDb, source_file: HirFileId, header: ModuleHeader) -> Option { - let origin = db.source_projection(source_file).origin(header.source())?; - let full_range = origin.full_range()?; - let (file_id, name_range, full_range) = - nav_location(db, source_file, origin.focus_range(), full_range)?; - - Some(Self { - module_id: header.owner(), - file_id, - name: header.name().clone(), - name_range: name_range.unwrap_or(full_range), - full_range, - }) - } - - fn call_item(&self) -> ModuleCallItem { - ModuleCallItem { - file_id: self.file_id, - name: self.name.to_string(), - full_range: self.full_range, - name_range: self.name_range, - } - } -} - -impl SemanticIndex { - /// Merges the per-file semantic indexes and module edges of a source root. - /// - /// The merge is pure memory assembly: no name resolution happens here, so - /// a change in one file only re-runs that file's index and this pass. - pub(crate) fn for_source_root( - db: &dyn WorkspaceSymbolIndexDb, - source_root_id: SourceRootId, - ) -> Self { - let source_root = db.source_root(source_root_id); - let mut references_by_definition: FxHashMap = - FxHashMap::default(); - let mut incoming_module_edges: FxHashMap> = - FxHashMap::default(); - let mut outgoing_module_edges: FxHashMap> = - FxHashMap::default(); - - for file_id in source_root.iter() { - db.unwind_if_revision_cancelled(); - let file_index = db.file_semantic_index(file_id); - for (definition, group) in &file_index.groups { - let builder = references_by_definition.entry(*definition).or_insert_with(|| { - SemanticReferenceGroupBuilder { - name: group.name.clone(), - definition_ranges: definition_ranges_for(db, *definition), - references: Vec::new(), - } - }); - builder.references.extend(group.references.iter().cloned()); - } - for (caller, callee, edge) in &db.file_module_edges(file_id).edges { - push_unique_edge(outgoing_module_edges.entry(*caller).or_default(), edge.clone()); - push_unique_edge(incoming_module_edges.entry(*callee).or_default(), edge.clone()); - } - } - - SemanticIndex { - references_by_definition: references_by_definition - .into_iter() - .map(|(key, group)| (key, group.finish())) - .collect(), - incoming_module_edges: finish_edge_map(incoming_module_edges), - outgoing_module_edges: finish_edge_map(outgoing_module_edges), - } - } - - pub(crate) fn references_for_definition( - &self, - definition: DefId, - ) -> Option<&SemanticReferenceGroup> { - self.references_by_definition.get(&definition) - } - - pub(crate) fn incoming_module_edges(&self, module_id: OwnerId) -> &[ModuleCallEdge] { - self.incoming_module_edges.get(&module_id).map_or(&[], |edges| edges.as_ref()) - } - - pub(crate) fn outgoing_module_edges(&self, module_id: OwnerId) -> &[ModuleCallEdge] { - self.outgoing_module_edges.get(&module_id).map_or(&[], |edges| edges.as_ref()) - } - - #[cfg(test)] - pub(crate) fn reference_groups_named(&self, name: &str) -> Vec<&SemanticReferenceGroup> { - self.references_by_definition.values().filter(|group| group.name == name).collect() - } -} - -impl SemanticReferenceGroupBuilder { - fn finish(self) -> SemanticReferenceGroup { - SemanticReferenceGroup { - name: self.name, - definition_ranges: self.definition_ranges.into_boxed_slice(), - references: self.references.into_boxed_slice(), - } - } -} - -pub(crate) fn incoming_module_edges( - db: &RootDb, - file_id: FileId, - name_range: TextRange, -) -> Vec { - module_edges(db, file_id, name_range, |index, module_id| index.incoming_module_edges(module_id)) -} - -pub(crate) fn outgoing_module_edges( - db: &RootDb, - file_id: FileId, - name_range: TextRange, -) -> Vec { - module_edges(db, file_id, name_range, |index, module_id| index.outgoing_module_edges(module_id)) -} - -fn module_edges( - db: &RootDb, - file_id: FileId, - name_range: TextRange, - edges_for_index: impl Fn(&SemanticIndex, OwnerId) -> &[ModuleCallEdge], -) -> Vec { - let Some(module_id) = module_id_at_range(db, file_id, name_range) else { - return Vec::new(); - }; - - let mut edges = Vec::new(); - for source_root_id in db.workspace_source_root_ids().iter().copied() { - let index = source_root_semantic_index_for_root(db, source_root_id); - edges.extend(edges_for_index(&index, module_id).iter().cloned()); - } - sort_and_dedup_edges(&mut edges); - edges -} - -fn module_id_at_range(db: &RootDb, file_id: FileId, name_range: TextRange) -> Option { - let module_index = source_root_module_index_for_root(db, db.source_root_id(file_id)); - module_index.module_definition_at(file_id, name_range).map(|module| module.module_id) -} - -fn instantiation_name_range( - db: &dyn PreprocDb, - file_id: FileId, - instantiation_range: TextRange, -) -> Option { - let tree = db.parse_src_for_compilation(file_id); - let root = tree.root(); - let mut offset = instantiation_range.start(); - - while offset < instantiation_range.end() { - let token = root.token_after_or_at_offset(offset)?; - let range = token.text_range()?; - if range.start() >= instantiation_range.end() { - return None; - } - if token.kind().name_like() { - return Some(range); - } - offset = range.end(); - } - - None -} - -fn push_unique_edge(edges: &mut Vec, edge: ModuleCallEdge) { - if !edges.iter().any(|existing| existing == &edge) { - edges.push(edge); - } -} - -fn finish_edge_map( - edges_by_module: FxHashMap>, -) -> FxHashMap> { - edges_by_module - .into_iter() - .map(|(key, mut edges)| { - sort_and_dedup_edges(&mut edges); - (key, edges.into_boxed_slice()) - }) - .collect() -} - -fn sort_and_dedup_edges(edges: &mut Vec) { - edges.sort_by_key(|edge| { - ( - edge.caller.file_id.index(), - edge.caller.name_range.start(), - edge.callee.file_id.index(), - edge.callee.name_range.start(), - edge.call_range.start(), - ) - }); - edges.dedup(); -} - -fn token_precedence(kind: TokenKind) -> usize { - crate::token::name_precedence(kind) -} - -#[cfg(test)] -mod tests { - use hir_def::symbol::NameContext; - use hir_semantics::semantics::SemanticsImpl; - use preproc_expand::file::HirFileId; - use syntax::{ - SyntaxElement, WalkEvent, - ast::{self, AstNode}, - has_text_range::HasTextRange, - token::TokenKindExt, - }; - use utils::line_index::{TextRange, TextSize}; - - use super::*; - use crate::{ - definitions::DefinitionClass, - semantic_index::build::{ContainerCache, ScopeChainCache, token_in_special_context}, - semantic_target::{ - SemanticTarget, TargetIntent, preproc::emit_token_index, - resolve_semantic_target_with_emitted, - }, - test_utils::setup_marked, - }; - - /// The container stack must agree with `find_container` for every - /// name-like token of a file exercising modules, blocks, subroutines, - /// explicit generate blocks, single-member generate branches and - /// instantiations. This is the safety net for the dispatch that mirrors - /// `source_to_def::container_to_def`. - #[test] - fn container_stack_matches_find_container_for_every_token() { - let text = r#" -`define TWO_MODULES module first; endmodule module second; endmodule -`TWO_MODULES -module top(input logic clk); - logic sig; - always_ff @(posedge clk) begin - if (sig) begin - logic inner; - end - end - generate - if (1) begin : gen_if - wire g; - end - endgenerate - function automatic logic f(); - return sig; - endfunction - sub u_sub(); -endmodule -"#; - let (host, file_id, _clean, _markers) = setup_marked(text); - let db = host.raw_db(); - let hir_file_id = HirFileId::from(file_id); - let tree = db.parse(hir_file_id); - let root = tree.root(); - let macro_modules = root - .elem_preorder() - .filter_map(|event| match event { - WalkEvent::Enter(SyntaxElement::Node(node)) => { - ast::ModuleDeclaration::cast(node).map(|module| module.syntax()) - } - _ => None, - }) - .collect::>(); - assert!( - macro_modules.windows(2).any(|modules| { - modules[0].kind() == modules[1].kind() - && modules[0].text_range() == modules[1].text_range() - && modules[0] != modules[1] - }), - "macro expansion should contain distinct module nodes with the same display identity" - ); - let sema = SemanticsImpl::new(db); - let mut containers = ContainerCache::new(); - for event in root.elem_preorder() { - match event { - WalkEvent::Enter(SyntaxElement::Node(_)) => {} - WalkEvent::Leave(SyntaxElement::Node(_)) => {} - WalkEvent::Enter(SyntaxElement::Token(token)) => { - if !token.kind().name_like() { - continue; - } - let cached = containers.container_for(&sema, hir_file_id, token.parent); - let expected = - sema.container_for_node(hir_file_id, token.parent).unwrap_or_else(|| { - db.owner_table(hir_file_id).file_owner().expect("file owner") - }); - if cached != expected { - eprintln!("cached owner={cached:?}"); - eprintln!("expected owner={expected:?}"); - } - assert_eq!(cached, expected, "container mismatch at {:?}", token.raw_text()); - } - WalkEvent::Leave(SyntaxElement::Token(_)) => {} - } - } - } - - /// The fast path must agree with the full heuristic chain for every - /// token: `token_in_special_context` has to cover exactly the syntax - /// positions where `DefinitionClass::resolve_in` diverges from plain - /// value-name resolution. The fixture exercises member accesses, scoped - /// names, packages, checkers, module-like declarations, hierarchy / - /// primitive instantiations, named port connections, named types, package - /// imports and a macro emitting a member access. - #[test] - fn fast_path_agrees_with_full_resolution_chain_for_every_token() { - let text = r#" -`define M(a) a.x -package pkg; - logic field; -endpackage - -checker chk(input logic a); -endchecker - -module sub(input logic in, output logic out); - logic internal; - assign out = in & internal; -endmodule - -module top(input logic clk, input logic [3:0] data); - logic sig; - wire [3:0] w; - pkg::field f_field; - initial begin - sig = clk; - `M(sig) - end - sub u_sub(.in(sig), .out(w)); - and g1(w, sig, clk); - chk c1(.a(sig)); - import pkg::*; -endmodule -"#; - let (host, file_id, _clean, _markers) = setup_marked(text); - let db = host.raw_db(); - let hir_file_id = HirFileId::from(file_id); - let tree = db.parse(hir_file_id); - let root = tree.root(); - let sema = SemanticsImpl::new(db); - let mut containers = ContainerCache::new(); - let mut chains = ScopeChainCache::new(); - let mut checked = 0usize; - for event in root.elem_preorder() { - if let WalkEvent::Enter(SyntaxElement::Token(token)) = event { - if !token.kind().name_like() { - continue; - } - checked += 1; - let container = containers.container_for(&sema, hir_file_id, token.parent); - let chosen = if token_in_special_context(token) { - DefinitionClass::resolve_in(db, hir_file_id, token, Some(container)).unique() - } else { - let chain = chains.chain_for(db, container); - sema.nameres_ident_in_scopes_at(hir_file_id, token, NameContext::Value, &chain) - .map(DefinitionClass::Definition) - .unique() - }; - let full = - DefinitionClass::resolve_in(db, hir_file_id, token, Some(container)).unique(); - assert_eq!( - chosen, - full, - "fast path diverges at {:?} (parent={:?}, special={})", - token.raw_text(), - token.parent.kind(), - token_in_special_context(token) - ); - } - } - assert!(checked > 20, "test should exercise a non-trivial token set"); - } - - /// Named port connections must record their shape (name/data roles, - /// collapse ranges, shorthand sides and same-name pairing) on the - /// references, so rename never re-resolves or re-parses. - #[test] - fn reference_contexts_capture_named_connection_shapes() { - let text = r#" -module child(input /*marker:child_a*/a, input /*marker:child_b*/b); -endmodule -module top; - logic /*marker:local_a*/a; - logic /*marker:local_b*/b; - logic /*marker:local_c*/c; - logic /*marker:plain_c*/d; - assign d = /*marker:plain*/c; - child u(/*marker:same_name*/.a(/*marker:same_name_data*/a), /*marker:other_name*/.b(/*marker:other_data*/c)); - child v(/*marker:shorthand*/.b); -endmodule -"#; - let (host, file_id, _clean, markers) = setup_marked(text); - let index = source_root_semantic_index_for_root(host.raw_db(), SourceRootId(0)); - - let range_at = |marker: &str| { - let start = markers[marker]; - let end = markers[marker] + TextSize::of("a"); - TextRange::new(start, end) - }; - // Conn name markers sit on the leading dot; the name token follows it. - let conn_name_at = |marker: &str| { - let start = markers[marker] + TextSize::of("."); - TextRange::new(start, start + TextSize::of("a")) - }; - let def_range = |marker: &str| range_at(marker); - let group = |name: &str, def_marker: &str| { - let def_range = def_range(def_marker); - index - .reference_groups_named(name) - .into_iter() - .find(|group| { - group - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == def_range) - }) - .unwrap_or_else(|| panic!("missing group {name} at {def_marker}")) - }; - let reference = - |group: &SemanticReferenceGroup, range: TextRange| -> (TextRange, ReferenceContext) { - let reference = group - .references - .iter() - .find(|reference| reference.range == range) - .unwrap_or_else(|| panic!("missing reference at {range:?}")); - (range, reference.context.clone()) - }; - - // Same-name connection `.a(a)`: the name token pairs the local def, - // the data token pairs the port def, both share the collapse range. - let same_name_range = conn_name_at("same_name"); - let same_name_data_range = range_at("same_name_data"); - let collapse = - TextRange::new(same_name_range.start(), same_name_data_range.end() + TextSize::of(")")); - let child_a = group("a", "child_a"); - let top_a = group("a", "local_a"); - let name_ref = reference(child_a, conn_name_at("same_name")); - let ReferenceContext::ConnName { ident_range, collapse_range, shorthand, side, paired } = - &name_ref.1 - else { - panic!("same-name name token should be ConnName: {:?}", name_ref.1); - }; - assert_eq!(ident_range, &Some(same_name_data_range)); - assert_eq!(collapse_range, &Some(collapse)); - assert!(!shorthand); - assert_eq!(side, &ConnSide::Port); - let paired = paired.as_ref().expect("same-name conn should pair the local def"); - assert!( - index - .references_for_definition(*paired) - .expect("paired def should have a group") - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == def_range("local_a")), - "paired local def should be top.a" - ); - let data_ref = reference(top_a, range_at("same_name_data")); - let ReferenceContext::ConnData { name_range, collapse_range, paired } = &data_ref.1 else { - panic!("same-name data token should be ConnData: {:?}", data_ref.1); - }; - assert_eq!(name_range, &same_name_range); - assert_eq!(collapse_range, &Some(collapse)); - let paired = paired.as_ref().expect("same-name conn should pair the port def"); - assert!( - index - .references_for_definition(*paired) - .expect("paired def should have a group") - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == def_range("child_a")), - "paired port def should be child.a" - ); - - // Non-same-name connection `.b(c)`: shape is recorded, no pairing. - let child_b = group("b", "child_b"); - let name_ref = reference(child_b, conn_name_at("other_name")); - let ReferenceContext::ConnName { ident_range, paired, .. } = &name_ref.1 else { - panic!("non-same-name name token should be ConnName: {:?}", name_ref.1); - }; - assert_eq!(ident_range, &Some(range_at("other_data"))); - assert_eq!(paired, &None); - let top_c = group("c", "local_c"); - let data_ref = reference(top_c, range_at("other_data")); - let ReferenceContext::ConnData { name_range, paired, .. } = &data_ref.1 else { - panic!("non-same-name data token should be ConnData: {:?}", data_ref.1); - }; - assert_eq!(name_range, &conn_name_at("other_name")); - assert_eq!(paired, &None); - - // Shorthand `.b`: one reference in each side's group. - let top_b = group("b", "local_b"); - let port_ref = reference(child_b, conn_name_at("shorthand")); - let ReferenceContext::ConnName { collapse_range, shorthand, side, paired, .. } = - &port_ref.1 - else { - panic!("shorthand port reference should be ConnName: {:?}", port_ref.1); - }; - assert!(shorthand); - assert_eq!(collapse_range, &None); - assert_eq!(side, &ConnSide::Port); - let paired = paired.as_ref().expect("shorthand should pair the local def"); - assert!( - index - .references_for_definition(*paired) - .expect("paired def should have a group") - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == def_range("local_b")), - "shorthand port side should pair top.b" - ); - let local_ref = reference(top_b, conn_name_at("shorthand")); - let ReferenceContext::ConnName { side, paired, .. } = &local_ref.1 else { - panic!("shorthand local reference should be ConnName: {:?}", local_ref.1); - }; - assert_eq!(side, &ConnSide::Local); - let paired = paired.as_ref().expect("shorthand should pair the port def"); - assert!( - index - .references_for_definition(*paired) - .expect("paired def should have a group") - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == def_range("child_b")), - "shorthand local side should pair child.b" - ); - - // Plain references stay Plain. - let plain = reference(top_c, range_at("plain")); - assert_eq!(plain.1, ReferenceContext::Plain); - } - - #[test] - fn semantic_index_skips_preprocessor_owned_identifiers() { - let text = r#" -`define BODY(/*marker:param*/x) /*marker:body*/x -module top; - wire /*marker:def*/x; - assign y = /*marker:ordinary*/x; - assign y = `BODY(/*marker:arg*/x); -endmodule -"#; - let (host, file_id, _clean, markers) = setup_marked(text); - let db = host.raw_db(); - let tree = db.parse(HirFileId::from(file_id)); - let root = tree.root(); - let emitted = emit_token_index(root); - for marker in ["param", "body"] { - let target = resolve_semantic_target_with_emitted( - db, - file_id, - markers[marker], - Some(root), - token_precedence, - Some(&emitted), - ) - .unique_for_intent(TargetIntent::FindReferences); - assert!( - matches!(target, Some(SemanticTarget::PreprocMacro(_))), - "{marker} must remain owned by the preprocessor: {target:?}" - ); - } - let index = source_root_semantic_index_for_root(host.raw_db(), SourceRootId(0)); - let definition_range = TextRange::new(markers["def"], markers["def"] + TextSize::of("x")); - let preproc_ranges = [ - TextRange::new(markers["param"], markers["param"] + TextSize::of("x")), - TextRange::new(markers["body"], markers["body"] + TextSize::of("x")), - ]; - let group = index - .reference_groups_named("x") - .into_iter() - .find(|group| { - group - .definition_ranges - .iter() - .any(|range| range.file_id == file_id && range.range == definition_range) - }) - .expect("the HDL declaration should have a semantic reference group"); - - assert!( - group - .references - .iter() - .all(|reference| { !preproc_ranges.iter().any(|range| range == &reference.range) }), - "preprocessor-owned x tokens must not become HDL references: {:?}", - group.references - ); - assert!(group.references.iter().any(|reference| { - reference.range - == TextRange::new(markers["ordinary"], markers["ordinary"] + TextSize::of("x")) - })); - assert!(group.references.iter().any(|reference| { - reference.range == TextRange::new(markers["arg"], markers["arg"] + TextSize::of("x")) - })); - } -} diff --git a/crates/ide/src/semantic_index/build.rs b/crates/ide/src/semantic_index/build.rs deleted file mode 100644 index a0fc6bb16..000000000 --- a/crates/ide/src/semantic_index/build.rs +++ /dev/null @@ -1,849 +0,0 @@ -use hir_def::{ - container::ScopeChain, - def_id::DefId, - owner::{OwnerId, OwnerKind}, - pathres::ResolvedScopes, - symbol::NameContext, -}; -use hir_semantics::semantics::SemanticsImpl; -use itertools::Itertools; -use preproc_expand::file::HirFileId; -use rustc_hash::FxHashMap; -use syntax::{ - SyntaxAncestors, SyntaxElement, SyntaxNode, SyntaxToken, SyntaxTokenWithParent, WalkEvent, - ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, - ptr::SyntaxTokenPtr, - token::TokenKindExt, -}; -use triomphe::Arc; -use utils::line_index::TextRange; -use vfs::FileId; - -use super::*; -use crate::{ - db::workspace_symbol_index_db::WorkspaceSymbolIndexDb, - definitions::{DefinitionClass, rightmost_name_token}, - module_resolution::resolve_hir_instantiation_target, - references::{ReferenceCategory, search::resolve_source_range}, - semantic_target::{ - SemanticTarget, TargetIntent, preproc::emit_token_index, resolve_plain_syntax_target, - resolve_semantic_target_with_emitted, - }, -}; - -impl FileSemanticIndex { - pub(crate) fn references_for_definition( - &self, - definition: DefId, - ) -> Option<&FileReferenceGroup> { - self.groups.get(&definition) - } - - pub(crate) fn for_file(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Self { - let tree = db.parse(file_id.into()); - let root = tree.root(); - let hir_file_id = HirFileId::from(file_id); - - // Macro-emitted tokens share the call-site display range. Ordinary - // source tokens carry a trace entry too, so presence is determined by - // directives, include edges, or non-source origins—not by trace size. - let has_preproc_tokens = { - let trace = tree.preprocessor_trace(); - !trace.include_edges.is_empty() - || trace.emitted_tokens.iter().any(|token| { - !matches!(token.origin, syntax::preproc::TokenOrigin::Source { .. }) - }) - }; - let emitted_index = has_preproc_tokens.then(|| emit_token_index(root)); - - let sema = SemanticsImpl::new(db); - let mut containers = ContainerCache::new(); - let mut chains = ScopeChainCache::new(); - let mut groups: FxHashMap = FxHashMap::default(); - let mut definition_ranges_by_def = - FxHashMap::>::default(); - let mut trace = IndexBuildTrace::start(); - // populated when the name token resolves (it precedes the data token - // in source order) and read back when the data token is collected. - let mut conn_port_by_name = FxHashMap::default(); - let text = db.file_text(file_id); - for event in root.elem_preorder() { - match event { - WalkEvent::Enter(SyntaxElement::Node(node)) => { - trace.count_special_kinds(&node); - } - WalkEvent::Leave(SyntaxElement::Node(_)) => {} - WalkEvent::Enter(SyntaxElement::Token(token)) => { - if !token.kind().name_like() { - continue; - } - trace.tokens += 1; - let (range_cost, range) = timed(|| token.text_range()); - trace.range += range_cost; - let Some(range) = range else { - continue; - }; - // Preserve the semantic target's preprocessor ownership - // checks while reusing the emitted-token index for macro - // expansion tokens. Preprocessor definitions, parameters, - // and includes are indexed by their own indexes rather - // than as HDL references. - let (target_cost, target) = timed(|| { - if has_preproc_tokens { - resolve_semantic_target_with_emitted( - db, - file_id, - range.start(), - Some(root), - token_precedence, - emitted_index.as_ref(), - ) - } else { - resolve_plain_syntax_target(root, range.start(), token_precedence) - } - .unique_for_intent(TargetIntent::FindReferences) - }); - trace.source_target += target_cost; - let Some(SemanticTarget::Source(target)) = target else { - continue; - }; - - let (container_cost, container) = - timed(|| containers.container_for(&sema, hir_file_id, token.parent)); - trace.container += container_cost; - for token in - target.into_tokens().into_iter().filter(|token| token.kind().name_like()) - { - // The heuristic chain in `DefinitionClass::resolve_in` - // can only diverge from plain value-name resolution at - // the token positions tested by `token_in_special_context`; - // every other token resolves as a plain value identifier. - let in_special_context = token_in_special_context(token); - if in_special_context { - trace.special_tokens += 1; - } - let (collect_cost, ()) = timed(|| { - collect_token( - db, - hir_file_id, - token, - container, - in_special_context, - &mut chains, - &mut conn_port_by_name, - &text, - &mut groups, - &mut definition_ranges_by_def, - &mut trace, - ) - }); - trace.collect += collect_cost; - } - } - WalkEvent::Leave(SyntaxElement::Token(_)) => {} - } - } - trace.report(file_id); - Self { groups } - } -} - -/// Set when `VIDE_INDEX_BUILD_TRACE` is set. -struct IndexBuildTrace { - enabled: bool, - range: std::time::Duration, - source_target: std::time::Duration, - container: std::time::Duration, - collect: std::time::Duration, - resolve: std::time::Duration, - resolve_fast: std::time::Duration, - resolve_slow: std::time::Duration, - chain_ns: u64, - nameres_ns: u64, - definition: std::time::Duration, - total: std::time::Instant, - tokens: usize, - special_tokens: usize, - kind_hits: [usize; 10], -} - -impl IndexBuildTrace { - fn start() -> Self { - Self { - enabled: std::env::var_os("VIDE_INDEX_BUILD_TRACE").is_some(), - range: std::time::Duration::ZERO, - source_target: std::time::Duration::ZERO, - container: std::time::Duration::ZERO, - collect: std::time::Duration::ZERO, - resolve: std::time::Duration::ZERO, - resolve_fast: std::time::Duration::ZERO, - resolve_slow: std::time::Duration::ZERO, - chain_ns: 0, - nameres_ns: 0, - definition: std::time::Duration::ZERO, - total: std::time::Instant::now(), - tokens: 0, - special_tokens: 0, - kind_hits: [0; 10], - } - } - - fn record_chain(&mut self, chain: std::time::Duration, nameres: std::time::Duration) { - self.chain_ns += chain.as_nanos() as u64; - self.nameres_ns += nameres.as_nanos() as u64; - } - - fn count_special_kinds(&mut self, node: &SyntaxNode<'_>) { - if !self.enabled { - return; - } - let kind = node.kind(); - self.kind_hits[0] += usize::from(ast::MemberAccessExpression::can_cast(kind)); - self.kind_hits[1] += usize::from(ast::ScopedName::can_cast(kind)); - self.kind_hits[2] += usize::from(ast::ModuleDeclaration::can_cast(kind)); - self.kind_hits[3] += usize::from(ast::PrimitiveInstantiation::can_cast(kind)); - self.kind_hits[4] += usize::from(ast::CheckerInstantiation::can_cast(kind)); - self.kind_hits[5] += usize::from(ast::HierarchyInstantiation::can_cast(kind)); - self.kind_hits[6] += usize::from(ast::PackageImportItem::can_cast(kind)); - self.kind_hits[7] += usize::from(ast::NamedParamAssignment::can_cast(kind)); - self.kind_hits[8] += usize::from(ast::NamedPortConnection::can_cast(kind)); - self.kind_hits[9] += usize::from(ast::NamedType::can_cast(kind)); - } - - fn report(&self, file_id: FileId) { - if !self.enabled { - return; - } - eprintln!( - "[index trace] file={file_id:?} tokens={} special={} total={:?}\n range={:?} source_target={:?} container={:?}\n collect={:?} (resolve={:?} [fast={:?} slow={:?}] chain={:?} nameres={:?} definition={:?})\n kind_hits={:?}", - self.tokens, - self.special_tokens, - self.total.elapsed(), - self.range, - self.source_target, - self.container, - self.collect, - self.resolve, - self.resolve_fast, - self.resolve_slow, - std::time::Duration::from_nanos(self.chain_ns), - std::time::Duration::from_nanos(self.nameres_ns), - self.definition, - self.kind_hits, - ); - } -} - -fn timed(f: impl FnOnce() -> T) -> (std::time::Duration, T) { - let start = std::time::Instant::now(); - let value = f(); - (start.elapsed(), value) -} - -/// Caches HIR container ids by syntax node while walking a tree. -/// -/// `source_to_def::find_container` finds a token's container by walking up -/// the ancestor chain and matching every node; doing that per token makes -/// the index build pay the ancestor walk for every name-like token. This -/// cache keeps the same walk shape (up to the nearest container node, then -/// a lookup), but computes each container id once instead of once per token. -/// -/// The node dispatch must stay in sync with -/// `hir_semantics::semantics::source_to_def::container_to_def`: the -/// module/block/subroutine arms use the public `Semantics` projections, and -/// generate blocks / single-member generate branches intern through -/// `intern_generate_block` with the nearest enclosing container as parent. -/// -/// The key is the Slang node itself, not `SyntaxNodePtr`: macro-emitted nodes -/// can share a display range and kind at their call site, while their pointer -/// identities remain distinct. -pub(super) struct ContainerCache<'tree> { - by_node: FxHashMap, OwnerId>, -} - -impl<'tree> ContainerCache<'tree> { - pub(super) fn new() -> Self { - Self { by_node: FxHashMap::default() } - } - - /// The container of a token: the nearest container node on its ancestor - /// chain whose id computes successfully, mirroring - /// `find_map(container_to_def)`; nodes that fail to lower are skipped. - pub(super) fn container_for( - &mut self, - sema: &SemanticsImpl<'_>, - file_id: HirFileId, - token_parent: SyntaxNode<'tree>, - ) -> OwnerId { - for node in SyntaxAncestors::start_from(token_parent) { - if is_container_node(&node) - && let Some(id) = self.try_id_for(sema, file_id, node) - { - return id; - } - } - sema.db.owner_table(file_id).file_owner().expect("file owner") - } - - pub(super) fn try_id_for( - &mut self, - sema: &SemanticsImpl<'_>, - file_id: HirFileId, - node: SyntaxNode<'tree>, - ) -> Option { - if let Some(id) = self.by_node.get(&node) { - return Some(*id); - } - let id = container_id_for_node(sema, file_id, node, self)?; - self.by_node.insert(node, id); - Some(id) - } -} - -/// Resolved scope chains by container. The nameres fast path looks every -/// token up in its container's chain; resolving the chain once per container -/// avoids per-token salsa `scope_for` queries, whose memos revalidate against -/// every intervening query during the index build and recompute O(scope -/// size) on each miss. -pub(super) struct ScopeChainCache { - by_container: FxHashMap>, -} - -impl ScopeChainCache { - pub(super) fn new() -> Self { - Self { by_container: FxHashMap::default() } - } - - pub(super) fn chain_for( - &mut self, - db: &dyn WorkspaceSymbolIndexDb, - container: OwnerId, - ) -> Arc { - if let Some(chain) = self.by_container.get(&container) { - return chain.clone(); - } - let chain = Arc::new(ResolvedScopes::new(db, ScopeChain::from_inner(db, container))); - self.by_container.insert(container, chain.clone()); - chain - } -} - -/// Mirrors `source_to_def::container_to_def`'s node dispatch. Uses `cast` -/// (not `can_cast`) on every arm: slang's `can_cast` accepts sub-kind -/// relations (e.g. generate blocks pass `BlockStatement::can_cast`), which -/// would desynchronize enter/leave bookkeeping. -fn is_container_node(node: &SyntaxNode<'_>) -> bool { - ast::ModuleDeclaration::cast(*node).is_some() - || ast::AnonymousProgram::cast(*node).is_some() - || ast::CheckerDeclaration::cast(*node).is_some() - || ast::CovergroupDeclaration::cast(*node).is_some() - || ast::ClockingDeclaration::cast(*node).is_some() - || ast::BlockStatement::cast(*node).is_some() - || ast::ProceduralBlock::cast(*node).is_some() - || ast::FunctionDeclaration::cast(*node).is_some() - || ast::CompilationUnit::cast(*node).is_some() - || ast::GenerateBlock::cast(*node).is_some() - || (ast::Member::cast(*node).is_some() && is_generate_branch_member(*node)) -} - -fn container_id_for_node<'tree>( - sema: &SemanticsImpl<'_>, - file_id: HirFileId, - node: SyntaxNode<'tree>, - _cache: &mut ContainerCache<'tree>, -) -> Option { - if let Some(module) = ast::ModuleDeclaration::cast(node) { - return sema.module_to_def(file_id, module); - } - let kind = if ast::CheckerDeclaration::cast(node).is_some() { - Some(OwnerKind::Checker) - } else if ast::AnonymousProgram::cast(node).is_some() { - Some(OwnerKind::AnonymousProgram) - } else if ast::CovergroupDeclaration::cast(node).is_some() { - Some(OwnerKind::Covergroup) - } else if ast::ClockingDeclaration::cast(node).is_some() { - Some(OwnerKind::ClockingBlock) - } else if ast::ProceduralBlock::cast(node).is_some() { - Some(OwnerKind::ProceduralBlock) - } else if let Some(block) = ast::BlockStatement::cast(node) { - return sema.block_to_def(file_id, block); - } else if let Some(func) = ast::FunctionDeclaration::cast(node) { - return sema.subroutine_to_def(file_id, func); - } else if ast::CompilationUnit::cast(node).is_some() { - return sema.db.owner_table(file_id).file_owner(); - } else if ast::GenerateBlock::cast(node).is_some() - || (ast::Member::cast(node).is_some() && is_generate_branch_member(node)) - { - Some(OwnerKind::GenerateBlock) - } else { - None - }?; - - let owner_node = if kind == OwnerKind::GenerateBlock - && ast::GenerateBlock::cast(node).is_some() - && node.parent().is_some_and(|parent| ast::LoopGenerate::cast(parent).is_some()) - { - node.parent()? - } else { - node - }; - let tree = sema.db.parse(file_id); - let ast_id = sema.db.ast_id_map(file_id).id_of_node_in_tree(&tree, owner_node)?; - sema.db.owner_table(file_id).owner_by_ast(ast_id, kind) -} - -/// Mirrors `source_to_def::is_generate_branch_member`: a member is a -/// single-member generate branch when it sits inside an if/case generate and -/// no stronger container (module, block, generate region) separates it. -/// The predicate itself lives in `hir-semantics`; only the container -/// dispatch is mirrored here. -fn is_generate_branch_member(member: SyntaxNode<'_>) -> bool { - hir_semantics::semantics::is_generate_branch_member(member) -} - -#[allow(clippy::too_many_arguments)] -fn collect_token( - db: &dyn WorkspaceSymbolIndexDb, - file_id: HirFileId, - token: SyntaxTokenWithParent<'_>, - container: OwnerId, - in_special_context: bool, - chains: &mut ScopeChainCache, - conn_port_by_name: &mut FxHashMap, - text: &str, - groups: &mut FxHashMap, - definition_ranges_by_def: &mut FxHashMap>, - trace: &mut IndexBuildTrace, -) { - let Some(range) = token.text_range() else { - return; - }; - let (resolve_cost, class) = timed(|| { - if in_special_context { - let start = std::time::Instant::now(); - let class = DefinitionClass::resolve_in(db, file_id, token, Some(container)).unique(); - trace.resolve_slow += start.elapsed(); - class - } else { - let start = std::time::Instant::now(); - // Fast path: outside every syntax context the heuristic chain in - // `DefinitionClass::resolve` (member access, scoped names, - // instantiations, package imports, named connections) is provably - // empty, so resolve as a plain value identifier directly. The - // scope chain is resolved once per container; per-token salsa - // `scope_for` queries revalidate their memos against every - // intervening query and recompute O(scope size) each time. - let sema = SemanticsImpl::new(db); - let chain_start = std::time::Instant::now(); - let chain = chains.chain_for(db, container); - let chain_cost = chain_start.elapsed(); - let class = sema - .nameres_ident_in_scopes_at(file_id, token, NameContext::Value, &chain) - .map(DefinitionClass::Definition) - .unique(); - if trace.enabled { - trace.record_chain(chain_cost, start.elapsed() - chain_cost); - } - trace.resolve_fast += start.elapsed(); - class - } - }); - trace.resolve += resolve_cost; - let Some(class) = class else { - return; - }; - - let (definition_cost, ()) = timed(|| match &class { - DefinitionClass::Definition(definition) => { - let context = reference_context( - db, - token, - &class, - container, - chains, - conn_port_by_name, - text, - ConnSide::Port, - ); - collect_definition_token( - db, - *definition, - file_id.expect_file(), - range, - token, - &context, - groups, - definition_ranges_by_def, - ) - } - DefinitionClass::PortConnShorthand { port, local } => { - let port_context = reference_context( - db, - token, - &class, - container, - chains, - conn_port_by_name, - text, - ConnSide::Port, - ); - let local_context = reference_context( - db, - token, - &class, - container, - chains, - conn_port_by_name, - text, - ConnSide::Local, - ); - collect_definition_token( - db, - *port, - file_id.expect_file(), - range, - token, - &port_context, - groups, - definition_ranges_by_def, - ); - collect_definition_token( - db, - *local, - file_id.expect_file(), - range, - token, - &local_context, - groups, - definition_ranges_by_def, - ); - } - }); - trace.definition += definition_cost; -} - -/// The role of a token inside a named port connection, if any, computed from -/// the token's syntax position alone. -enum ConnTokenRole<'tree> { - /// The token is the `.name` of the connection. - Name(ast::NamedPortConnection<'tree>), - /// The token is a simple identifier in the data position. - Data(ast::NamedPortConnection<'tree>), -} - -fn conn_token_role<'tree>(token: SyntaxTokenWithParent<'tree>) -> Option> { - let SyntaxTokenWithParent { parent, tok } = token; - if let Some(conn) = ast::NamedPortConnection::cast(parent) { - return conn.name().is_some_and(|name| name == tok).then_some(ConnTokenRole::Name(conn)); - } - if ast::Name::can_cast(parent.kind()) { - // The data identifier of a simple named port connection sits at a - // fixed depth below the connection node (the wrapper expression - // nodes are virtual). - if let Some(node) = SyntaxAncestors::start_from(parent).nth(3) - && let Some(conn) = ast::NamedPortConnection::cast(node) - && conn_data_ident(conn).is_some_and(|ident| ident == tok) - { - return Some(ConnTokenRole::Data(conn)); - } - } - None -} - -/// The identifier token of a connection's data side, when the data is a -/// simple identifier (bare name or empty select). Mirrors the extraction in -/// the rename edit rules. -fn conn_data_ident(conn: ast::NamedPortConnection<'_>) -> Option> { - use ast::{Expression, Name}; - let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); - match expr { - Expression::Name(Name::IdentifierName(ident)) => ident.identifier(), - Expression::Name(Name::IdentifierSelectName(ident)) - if ident.selectors().children().next().is_none() => - { - ident.identifier() - } - _ => None, - } -} - -struct ConnShape { - name_range: TextRange, - ident_range: Option, - collapse_range: Option, - shorthand: bool, -} - -fn conn_shape(conn: ast::NamedPortConnection<'_>) -> Option { - let name_range = conn.name()?.text_range_in(conn.syntax())?; - let collapse_range = conn - .close_paren() - .and_then(|token| token.text_range_in(conn.syntax())) - .map(|range| TextRange::new(name_range.start(), range.end())); - let ident_range = conn_data_ident(conn).and_then(|token| token.text_range_in(conn.syntax())); - let shorthand = conn.open_paren().is_none() && conn.close_paren().is_none(); - Some(ConnShape { name_range, ident_range, collapse_range, shorthand }) -} - -fn range_text(text: &str, range: TextRange) -> &str { - &text[usize::from(range.start())..usize::from(range.end())] -} - -fn is_same_name_conn(text: &str, conn: &ConnShape) -> bool { - conn.ident_range - .is_some_and(|ident| range_text(text, conn.name_range) == range_text(text, ident)) -} - -/// The [`ReferenceContext`] of a token resolved to `class`. `side` selects -/// the shorthand side; non-shorthand tokens produce the same context for -/// either side. -#[allow(clippy::too_many_arguments)] -fn reference_context( - db: &dyn WorkspaceSymbolIndexDb, - token: SyntaxTokenWithParent<'_>, - class: &DefinitionClass, - container: OwnerId, - chains: &mut ScopeChainCache, - conn_port_by_name: &mut FxHashMap, - text: &str, - side: ConnSide, -) -> ReferenceContext { - let Some(role) = conn_token_role(token) else { - return ReferenceContext::Plain; - }; - let sema = SemanticsImpl::new(db); - match role { - ConnTokenRole::Data(conn) => { - let Some(shape) = conn_shape(conn) else { - return ReferenceContext::Plain; - }; - ReferenceContext::ConnData { - name_range: shape.name_range, - collapse_range: shape.collapse_range, - paired: is_same_name_conn(text, &shape) - .then(|| conn_port_by_name.get(&shape.name_range).cloned()) - .flatten(), - } - } - ConnTokenRole::Name(conn) => { - let Some(shape) = conn_shape(conn) else { - return ReferenceContext::Plain; - }; - if shape.shorthand { - let (side, paired) = match class { - DefinitionClass::PortConnShorthand { port, local } => { - let paired = match side { - ConnSide::Port => Some(*local), - ConnSide::Local => Some(*port), - }; - (side, paired) - } - DefinitionClass::Definition(def) => { - // One-sided shorthand resolution: the local side is the - // definition when plain value resolution matches it. - let chain = chains.chain_for(db, container); - let is_local = sema - .nameres_ident_in_scopes(token, NameContext::Value, &chain, None) - .unique() - .is_some_and(|local| local == *def); - (if is_local { ConnSide::Local } else { ConnSide::Port }, None) - } - }; - return ReferenceContext::ConnName { - ident_range: None, - collapse_range: None, - shorthand: true, - side, - paired, - }; - } - let same_name = is_same_name_conn(text, &shape); - let paired = same_name - .then(|| { - let chain = chains.chain_for(db, container); - conn_data_ident(conn).and_then(|ident| { - sema.nameres_ident_in_scopes( - SyntaxTokenWithParent { parent: conn.syntax(), tok: ident }, - NameContext::Value, - &chain, - None, - ) - .unique() - }) - }) - .flatten(); - if let DefinitionClass::Definition(port) = class { - conn_port_by_name.insert(shape.name_range, *port); - } - ReferenceContext::ConnName { - ident_range: shape.ident_range, - collapse_range: shape.collapse_range, - shorthand: false, - side: ConnSide::Port, - paired, - } - } - } -} - -/// True when the token sits at one of the syntax positions where -/// `DefinitionClass::resolve_in` diverges from plain value-identifier -/// resolution. Those positions are the direct token children of the listed -/// nodes (member access fields, module-like declaration names, instantiation -/// type names, package import names, named parameter/port connection names) -/// and identifiers wrapped in a `Name` node under a scoped name, a named -/// type (they select the Type name context) or a checker instantiation -/// (its type name resolves in the Type namespace). -/// -/// Every check is O(1) on the token's parent (and grandparent); the subtree -/// walk from the old fast-path gate was dropped because it also flagged every -/// token inside a module body, which made the fast path dead on module-heavy -/// files. -pub(super) fn token_in_special_context( - SyntaxTokenWithParent { parent, tok }: SyntaxTokenWithParent<'_>, -) -> bool { - if ast::MemberAccessExpression::cast(parent).is_some_and(|node| node.name() == Some(tok)) - || ast::ModuleHeader::cast(parent).is_some_and(|node| node.name() == Some(tok)) - || ast::PrimitiveInstantiation::cast(parent).is_some_and(|node| node.type_() == Some(tok)) - || ast::HierarchyInstantiation::cast(parent).is_some_and(|node| node.type_() == Some(tok)) - || ast::PackageImportItem::cast(parent) - .is_some_and(|node| node.package() == Some(tok) || node.item() == Some(tok)) - || ast::NamedParamAssignment::cast(parent).is_some_and(|node| node.name() == Some(tok)) - || ast::NamedPortConnection::cast(parent).is_some_and(|node| node.name() == Some(tok)) - { - return true; - } - - // Identifier tokens are wrapped in a `Name` node; the divergent context is - // the Name's parent. - if !ast::Name::can_cast(parent.kind()) { - return false; - } - let Some(grandparent) = parent.parent() else { - return false; - }; - if ast::ScopedName::can_cast(grandparent.kind()) || ast::NamedType::can_cast(grandparent.kind()) - { - return true; - } - ast::CheckerInstantiation::cast(grandparent) - .is_some_and(|node| rightmost_name_token(node.type_()) == Some(tok)) -} - -#[allow(clippy::too_many_arguments)] -fn collect_definition_token( - db: &dyn WorkspaceSymbolIndexDb, - definition: DefId, - file_id: FileId, - range: TextRange, - token: SyntaxTokenWithParent<'_>, - context: &ReferenceContext, - groups: &mut FxHashMap, - definition_ranges_by_def: &mut FxHashMap>, -) { - let origins = definition.origins(db); - let Some(name) = origins.iter().find_map(|origin| origin.name(db)) else { - return; - }; - let definition_ranges = definition_ranges_by_def - .entry(definition) - .or_insert_with(|| definition_ranges_for(db, definition)); - let is_definition_site = definition_ranges.iter().any(|definition_range| { - definition_range.file_id == file_id && definition_range.range == range - }); - if is_definition_site { - return; - } - - let group = groups - .entry(definition) - .or_insert_with(|| FileReferenceGroup { name: name.to_string(), references: Vec::new() }); - let reference = SemanticReference { - file_id, - range, - category: ReferenceCategory::from_tok(token), - ptr: SyntaxTokenPtr::from_token(token), - context: context.clone(), - }; - if !group - .references - .iter() - .any(|existing| existing.file_id == reference.file_id && existing.range == reference.range) - { - group.references.push(reference); - } -} - -/// Definition name ranges of `definition` mapped to user-facing files, in -/// origin order. File-level callers own memoization for this pure projection. -pub(super) fn definition_ranges_for( - db: &dyn WorkspaceSymbolIndexDb, - definition: DefId, -) -> Vec { - definition - .origins(db) - .iter() - .filter_map(|origin| { - let InFile { file_id, value } = origin.name_range(db)?; - let (file_id, range) = resolve_source_range(db, file_id, value)?; - Some(SemanticDefinitionRange { file_id, range }) - }) - .unique() - .collect_vec() -} - -impl FileModuleIndex { - pub(crate) fn for_file(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Self { - let hir_file_id = HirFileId::from(file_id); - let item_tree = db.item_tree(hir_file_id); - let modules = item_tree - .module_headers() - .filter(|header| header.kind().is_instantiable()) - .filter_map(|header| SemanticModuleDefinition::from_header(db, hir_file_id, header)) - .collect(); - Self { modules } - } -} - -impl FileModuleEdges { - pub(crate) fn for_file(db: &dyn WorkspaceSymbolIndexDb, file_id: FileId) -> Self { - let hir_file_id = HirFileId::from(file_id); - let item_tree = db.item_tree(hir_file_id); - let mut edges = Vec::new(); - for header in item_tree.module_headers().filter(|header| header.kind().is_instantiable()) { - let caller = header.owner(); - let Some(caller_def) = SemanticModuleDefinition::from_header(db, hir_file_id, header) - else { - continue; - }; - let module = db.body_with_source_map(caller); - for (instantiation_id, instantiation) in module.instantiations.iter() { - let Some(callee_module_id) = - resolve_hir_instantiation_target(db, file_id, instantiation) - else { - continue; - }; - let Some(callee) = SemanticModuleDefinition::new(db, callee_module_id) else { - continue; - }; - let Some(call_range) = module - .source_range(db, instantiation_id) - .and_then(|range| instantiation_name_range(db, file_id, range)) - else { - continue; - }; - edges.push(( - caller, - callee.module_id, - ModuleCallEdge { - caller: caller_def.call_item(), - callee: callee.call_item(), - call_range, - }, - )); - } - } - Self { edges } - } -} diff --git a/crates/ide/src/semantic_target.rs b/crates/ide/src/semantic_target.rs index c4e47cef6..790d31c03 100644 --- a/crates/ide/src/semantic_target.rs +++ b/crates/ide/src/semantic_target.rs @@ -213,32 +213,26 @@ pub(crate) fn resolve_semantic_target<'tree, F>( where F: Fn(TokenKind) -> usize, { + if !db.file_kind(file_id).is_project_manifest() + && is_preproc_free_file(db, file_id) + && let Some(root) = root + { + return normal_syntax_source_target_at_offset(root, offset, &precedence).map_or( + TargetResolution::Unresolved, + |target| { + TargetResolution::Resolved(TargetCandidate::new( + SemanticTarget::Source(target), + source_capabilities(), + )) + }, + ); + } + resolve_semantic_target_with_emitted(db, file_id, offset, root, precedence, None) } -/// Resolves a source offset without consulting preprocessor state. -/// -/// Callers that have already proved that a file has no preprocessor-owned -/// tokens use this path to avoid four offset-index queries and include lookup -/// for every syntax token. -pub(crate) fn resolve_plain_syntax_target<'tree>( - root: SyntaxNode<'tree>, - offset: TextSize, - precedence: impl Fn(TokenKind) -> usize, -) -> TargetResolution<'tree> { - normal_syntax_source_target_at_offset(root, offset, &precedence).map_or( - TargetResolution::Unresolved, - |target| { - TargetResolution::Resolved(TargetCandidate::new( - SemanticTarget::Source(target), - source_capabilities(), - )) - }, - ) -} - /// Like [`resolve_semantic_target`], but reuses a prebuilt emitted-token /// index of `root`'s tree. Callers that resolve many offsets of one tree -/// (the semantic index build) should build the index once with +/// (a reference or call-hierarchy walk) should build the index once with /// [`emit_token_index`] and pass it here. pub(crate) fn resolve_semantic_target_with_emitted<'tree, F>( db: &dyn PreprocDb, @@ -275,6 +269,16 @@ where .unwrap_or(TargetResolution::Unresolved) } +pub(crate) fn is_preproc_free_file(db: &dyn PreprocDb, file_id: FileId) -> bool { + let trace = db.parse(file_id.into()).preprocessor_trace(); + trace.events.is_empty() + && trace.include_edges.is_empty() + && trace + .emitted_tokens + .iter() + .all(|token| matches!(token.origin, syntax::preproc::TokenOrigin::Source { .. })) +} + /// Resolves the caret offset to a semantic target, or `None` when the offset /// is not a resolvable token. Preprocessor-owned offsets (macro definitions, /// parameters, references, includes, macro-emitted tokens) resolve through diff --git a/crates/ide/src/semantic_target/preproc.rs b/crates/ide/src/semantic_target/preproc.rs index 23517051c..bb8e85664 100644 --- a/crates/ide/src/semantic_target/preproc.rs +++ b/crates/ide/src/semantic_target/preproc.rs @@ -27,7 +27,7 @@ use super::{ /// only stable token identity. One id can map to several tokens (a macro can /// emit the same argument more than once), so every copy is kept. /// -/// Callers that resolve many offsets of one tree (the semantic index build) +/// Callers that resolve many offsets of one tree (a reference walk) /// construct this once and share it across every resolution instead of /// re-walking the tree per token. pub(crate) type EmittedTokenIndex<'tree> = @@ -260,7 +260,7 @@ fn syntax_tokens_for_macro_emitted_tokens<'tree>( (!tokens.is_empty()).then_some(tokens) } -fn syntax_token_emitted_token_id( +pub(crate) fn syntax_token_emitted_token_id( token: &SyntaxTokenWithParent<'_>, ) -> Option { token diff --git a/crates/ide/src/semantic_target/tests.rs b/crates/ide/src/semantic_target/tests.rs index 337cb3d47..7e5e359be 100644 --- a/crates/ide/src/semantic_target/tests.rs +++ b/crates/ide/src/semantic_target/tests.rs @@ -19,18 +19,16 @@ use crate::{ analysis_host::AnalysisHost, db::root_db::RootDb, token::name_precedence as token_precedence, }; -mod bench_context; - #[test] fn source_token_target_is_complete_and_source_origin() { let (host, file_id, offset, range) = setup("module m; wire payload_i; endmodule\n", "payload_i"); - let sema = Semantics::new(host.raw_db()); + let sema = Semantics::new_with_context(host.ctx().db, host.ctx().resolution()); let parsed = sema.parse_file(file_id); let root = parsed.root().expect("test source should parse"); let resolution = - resolve_semantic_target(host.raw_db(), file_id, offset, Some(root), token_precedence); + resolve_semantic_target(host.ctx().db, file_id, offset, Some(root), token_precedence); assert!(matches!( resolution.clone().unique_for_intent(TargetIntent::Describe), Some(SemanticTarget::Source(_)) diff --git a/crates/ide/src/semantic_target/tests/bench_context.rs b/crates/ide/src/semantic_target/tests/bench_context.rs deleted file mode 100644 index a36d58df8..000000000 --- a/crates/ide/src/semantic_target/tests/bench_context.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Ignored micro-benchmark for the per-token macro context query -//! (`macro_context_at`), the indexed replacement for the removed -//! per-token text gate. -//! -//! `collect_file` consults the macro context for every name-like token before -//! falling back to plain syntax resolution. The old gate scanned the file -//! text backwards from each token offset (quadratic in file size); the -//! coverage index should make the per-token cost constant. -//! -//! Run with: -//! -//! ```text -//! cargo test -p ide --release -- --ignored --nocapture index_benchmarks -//! ``` - -use std::time::Instant; - -use preproc_expand::context::macro_context_at; - -use super::*; - -fn context_scan_all_name_tokens(db: &dyn PreprocDb, text: &str) -> (std::time::Duration, usize) { - let bytes = text.as_bytes(); - let mut total = std::time::Duration::ZERO; - let mut token_count = 0; - let mut i = 0; - while i < bytes.len() { - if bytes[i].is_ascii_alphabetic() || bytes[i] == b'_' { - let start = i; - while i < bytes.len() - && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' || bytes[i] == b'$') - { - i += 1; - } - let offset = TextSize::from(start as u32); - let start_time = Instant::now(); - std::hint::black_box(macro_context_at(db, FileId::from_raw(0), offset)); - total += start_time.elapsed(); - token_count += 1; - } else { - i += 1; - } - } - (total, token_count) -} - -fn bench_context_text(modules: u32) -> String { - (0..modules) - .map(|name| { - format!( - "module m{name}(input logic clk);\n logic a{name}, b{name};\n assign a{name} = b{name} ^ clk;\n always_ff @(posedge clk) b{name} <= a{name};\nendmodule\n\n" - ) - }) - .collect() -} - -#[test] -#[ignore] -fn index_benchmarks_macro_context_scales_with_offset() { - let modules = [64u32, 128, 256, 512, 1024, 2048]; - println!("\n== B1: per-token macro context cost vs file size (release) =="); - println!("{:<10} {:<10} {:<12} {:<16}", "modules", "bytes", "tokens", "total"); - for count in modules { - let text = bench_context_text(count); - let (host, file_id) = crate::test_utils::setup_with_path(&text, "/bench.sv"); - let db = host.raw_db(); - // Warm the coverage query once; the scan measures lookup cost only. - std::hint::black_box(macro_context_at(db, file_id, TextSize::from(0))); - let (total, tokens) = context_scan_all_name_tokens(db, &text); - let per_token = - std::time::Duration::from_nanos(total.as_nanos() as u64 / tokens.max(1) as u64); - println!( - "{:<10} {:<10} {:<12} {:<12?} {per_token:?}/tok", - count, - text.len(), - tokens, - total - ); - } -} diff --git a/crates/ide/src/semantic_tokens.rs b/crates/ide/src/semantic_tokens.rs index d47a9a4be..1d680ccdd 100644 --- a/crates/ide/src/semantic_tokens.rs +++ b/crates/ide/src/semantic_tokens.rs @@ -30,6 +30,7 @@ use utils::text_edit::TextRange; use vfs::FileId; use crate::{ + analysis::AnalysisContext, db::root_db::RootDb, module_resolution::{ resolve_named_param_assignment, resolve_named_port_connection, resolve_port_metadata, @@ -136,16 +137,16 @@ impl SemaToken { } pub(crate) fn semantic_tokens( - db: &RootDb, + db: &AnalysisContext<'_>, config: SemaTokenConfig, file_id: FileId, range: Option, ) -> Vec { let _span = tracing::debug_span!("ide.semantic_tokens", ?file_id, ?range).entered(); if db.file_kind(file_id).is_project_manifest() { - return crate::manifest::semantic_tokens(db, file_id, range); + return crate::manifest::semantic_tokens(db.db, file_id, range); } - let sema = Semantics::new(db); + let sema = db.semantics(); let parsed_file = sema.parse_file(file_id); let Some(root) = parsed_file.root() else { return Vec::new(); @@ -163,7 +164,7 @@ pub(crate) fn semantic_tokens( let mut collector = SemaTokenCollector::new(config, range); collect_file(&sema, file_id, &mut collector); - collect_preproc_macro_references(db, file_id.expect_file(), range, &mut collector); + collect_preproc_macro_references(db.db, file_id.expect_file(), range, &mut collector); collector.finish() } @@ -503,9 +504,15 @@ fn collect_named_param_assignments<'a>( }; check_range!(collector, range); - let res = from_file.map_or(Resolution::Unresolved, |f| { - resolve_named_param_assignment(sema.db, f, named_assign) - }); + let res = if from_file.is_some() { + resolve_named_param_assignment( + sema.db, + sema.resolution_context().as_ref(), + named_assign, + ) + } else { + Resolution::Unresolved + }; collect_resolved_path(sema, res, range, collector); } } @@ -529,9 +536,11 @@ fn collect_named_port_connections<'a>( }; check_range!(collector, range); - let res = from_file.map_or(Resolution::Unresolved, |f| { - resolve_named_port_connection(sema.db, f, named_conn) - }); + let res = if from_file.is_some() { + resolve_named_port_connection(sema.db, sema.resolution_context().as_ref(), named_conn) + } else { + Resolution::Unresolved + }; collect_resolved_path(sema, res, range, collector); } } @@ -553,7 +562,8 @@ fn collect_type_ref_like( range: TextRange, collector: &mut SemaTokenCollector, ) -> Option<()> { - let res = resolve_path(sema.db, cont_id, type_ref.segments(), NameContext::Type); + let context = sema.resolution_context(); + let res = resolve_path(sema.db, &context, cont_id, type_ref.segments(), NameContext::Type); collect_resolved_path(sema, res, range, collector) } diff --git a/crates/ide/src/semantic_tokens/port.rs b/crates/ide/src/semantic_tokens/port.rs index a7d059ef2..0615c682b 100644 --- a/crates/ide/src/semantic_tokens/port.rs +++ b/crates/ide/src/semantic_tokens/port.rs @@ -1,13 +1,13 @@ use std::sync::LazyLock; use hir_def::{ + db::HirDefDb, expr::data_ty::{BuiltinDataTy, DataTy}, module::port::{NonAnsiPort, PortDirection, Ports}, owner::OwnerId, symbol::NameContext, }; use hir_semantics::semantics::Semantics; -use hir_ty::db::TyDb; use regex::{Regex, RegexBuilder}; use smallvec::SmallVec; use utils::text_edit::TextRange; @@ -104,7 +104,7 @@ pub(super) fn collect_port( } pub(super) fn add_port_token( - _db: &dyn TyDb, + _db: &dyn HirDefDb, name: &str, dir: Option, ty: DataTy, diff --git a/crates/ide/src/signature_help.rs b/crates/ide/src/signature_help.rs index 6b5875134..42b12be98 100644 --- a/crates/ide/src/signature_help.rs +++ b/crates/ide/src/signature_help.rs @@ -12,7 +12,6 @@ use hir_def::{ symbol::Resolution, }; use hir_semantics::semantics::Semantics; -use hir_ty::display::HirDisplay; use itertools::Either; use preproc_expand::file::HirFileId; use syntax::{ @@ -27,8 +26,8 @@ use syntax::{ use utils::text_edit::{TextRange, TextSize}; use crate::{ - FilePosition, db::root_db::RootDb, markup::Markup, - module_resolution::resolve_instantiation_target, + FilePosition, analysis::AnalysisContext, db::root_db::RootDb, markup::Markup, + module_resolution::resolve_instantiation_target, render::hir_display::HirDisplay, }; #[derive(Debug)] @@ -62,14 +61,14 @@ impl SignatureHelp { } pub(crate) fn signature_help( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, config: SignatureHelpConfig, ) -> Option { if db.file_kind(file_id).is_project_manifest() { return None; } - let sema = Semantics::new(db); + let sema = db.semantics(); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; @@ -154,7 +153,8 @@ fn sig_help_for_instance( let instantiation = ast::HierarchyInstantiation::cast(instance.syntax().parent()?)?; let target_module_id = - resolve_instantiation_target(db, file_id.expect_file(), instantiation).unique()?; + resolve_instantiation_target(db, sema.resolution_context().as_ref(), instantiation) + .unique()?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); let target_module_name = @@ -276,7 +276,8 @@ fn sig_help_for_instantiation( }; let target_module_id = - resolve_instantiation_target(db, file_id.expect_file(), instantiation).unique()?; + resolve_instantiation_target(db, sema.resolution_context().as_ref(), instantiation) + .unique()?; let target_module = db.body_with_source_map(target_module_id); let target_body = db.body_with_source_map(target_module_id); let target_module_name = diff --git a/crates/ide/src/slang_class.rs b/crates/ide/src/slang_class.rs new file mode 100644 index 000000000..53867b93e --- /dev/null +++ b/crates/ide/src/slang_class.rs @@ -0,0 +1,239 @@ +//! Semantic lookups through the resident elaboration service. +//! +//! These functions only turn an IDE position into the arguments slang wants +//! and hand back the whole [`ElabResult`]. Deciding what a non-answer means +//! is the caller's job, and every caller does it the same way, through +//! [`ElabResult::answered`], so "slang is down" never reads as "no such +//! symbol". +//! +//! [`ElabResult::answered`]: crate::elaboration::ElabResult::answered + +use base_db::source_db::SourceRootDb; +use preproc_expand::compilation_plan; +use slang_sys::compilation::{MemberInfo, SymbolInfo}; +#[cfg(test)] +use syntax::SyntaxTreeOptions; +use vfs::FileId; + +use crate::{analysis::AnalysisContext, elaboration::ElabResult}; + +pub fn lookup_symbol_at( + ctx: &AnalysisContext<'_>, + file_id: FileId, + offset: usize, +) -> ElabResult { + let path = compilation_plan::source_buffer_path(ctx.db, file_id).to_string(); + let profile = ctx.db.file_compilation_profile(file_id); + ctx.elab.lookup_symbol(ctx.db, ctx.revision, profile, &path, offset) +} + +pub fn lookup_scoped_at( + ctx: &AnalysisContext<'_>, + file_id: FileId, + left: &str, + right: &str, +) -> ElabResult { + let profile = ctx.db.file_compilation_profile(file_id); + ctx.elab.lookup_scoped(ctx.db, ctx.revision, profile, left, right) +} + +/// Members of the scope a name denotes: a package, a class, or a +/// hierarchical instance path such as `top.u0` or `u0[0]`. +/// +/// Empty when `name` denotes no scope — including when it is an expression +/// rather than a name. Those belong to [`list_members_at`], which resolves +/// them at their own offset. +pub fn list_scope_members_at( + ctx: &AnalysisContext<'_>, + file_id: FileId, + name: &str, +) -> ElabResult> { + let profile = ctx.db.file_compilation_profile(file_id); + ctx.elab.list_scope_members(ctx.db, ctx.revision, profile, name) +} + +pub fn list_members_at( + ctx: &AnalysisContext<'_>, + file_id: FileId, + offset: usize, +) -> ElabResult> { + let path = compilation_plan::source_buffer_path(ctx.db, file_id).to_string(); + let profile = ctx.db.file_compilation_profile(file_id); + ctx.elab.list_members(ctx.db, ctx.revision, profile, &path, offset) +} + +pub fn lookup_type_at( + ctx: &AnalysisContext<'_>, + file_id: FileId, + start: usize, + end: usize, +) -> ElabResult { + let path = compilation_plan::source_buffer_path(ctx.db, file_id).to_string(); + let profile = ctx.db.file_compilation_profile(file_id); + ctx.elab.lookup_type(ctx.db, ctx.revision, profile, &path, start, end) +} + +/// `owner :: type extends base > base` for a class member. +pub fn format_class_member(owner_class: &str, type_name: &str, inheritance: &[String]) -> String { + let mut line = format!("{owner_class} :: {type_name}"); + if !inheritance.is_empty() { + line.push_str(" extends "); + line.push_str(&inheritance.join(" > ")); + } + line +} + +/// Independent `SourceAstId` computation on two parses of the same text. +/// This is the §3.7 check: same text + same options ⇒ same stable paths. +#[cfg(test)] +fn source_ast_ids_agree(text: &str, name: &str, path: &str) -> (usize, usize) { + let options = SyntaxTreeOptions::without_include_expansion(); + let tree_a = syntax::SyntaxTree::from_file_in_memory_with_options(text, name, path, &options); + let tree_b = syntax::SyntaxTree::from_file_in_memory_with_options(text, name, path, &options); + let map_a = hir_def::ast_id_map::AstIdMap::from_source(&tree_a); + let map_b = hir_def::ast_id_map::AstIdMap::from_source(&tree_b); + let ids = |tree: &syntax::SyntaxTree, map: &hir_def::ast_id_map::AstIdMap| { + let mut ids = Vec::new(); + for event in tree.root().node_preorder() { + let syntax::WalkEvent::Enter(node) = event else { + continue; + }; + ids.push(map.id_of_node(node)); + } + ids + }; + let a = ids(&tree_a, &map_a); + let b = ids(&tree_b, &map_b); + let compared = a.len().min(b.len()); + let matched = a.iter().zip(&b).filter(|(left, right)| left == right).count(); + (matched, compared) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{position, setup_marked}; + + const UVM_OBJECT: &str = r#" +virtual class uvm_void; +endclass +virtual class uvm_object extends uvm_void; + string /*marker:name*/m_leaf_name; + function string get_type_name(); + return ""; + endfunction +endclass +"#; + + /// Wait for the build, then use the shipped entry point. Nothing here may + /// fall back to a private compilation: a test that answers by a route + /// production does not take proves nothing about production. + fn shipped_symbol_at( + host: &crate::analysis_host::AnalysisHost, + file_id: FileId, + offset: utils::line_index::TextSize, + ) -> Option { + let ctx = host.ctx(); + let built = ctx.elab.prewarm(ctx.db, ctx.revision); + assert!(matches!(built, ElabResult::Ready(_)), "build must finish, got {built:?}"); + match lookup_symbol_at(&ctx, file_id, usize::from(offset)) { + ElabResult::Ready(info) => info, + other => panic!("shipped lookup must be Ready, got {other:?}"), + } + } + + #[test] + fn the_shipped_offset_entry_returns_the_class_member() { + let (host, file_id, _text, markers) = setup_marked(UVM_OBJECT); + let info = + shipped_symbol_at(&host, file_id, markers["name"]).expect("class property is a symbol"); + assert_eq!(info.owner_class, "uvm_object"); + assert!(info.inheritance.iter().any(|name| name == "uvm_void"), "{info:?}"); + assert!(info.type_name.contains("string"), "{info:?}"); + } + + #[test] + fn hover_shows_slang_type_for_a_net() { + let src = "module top;\n logic [7:0] /*marker:x*/x;\nendmodule\n"; + let (host, file_id, _text, markers) = setup_marked(src); + let hover = host.make_analysis().hover(position(file_id, &markers, "x")).unwrap(); + let markup = hover.expect("net hover"); + let text = markup.info.as_str(); + assert!(text.contains("logic"), "net hover must show the declaration type:\n{text}"); + } + + #[test] + fn hover_shows_slang_type() { + let (host, file_id, _text, markers) = setup_marked(UVM_OBJECT); + let hover = host.make_analysis().hover(position(file_id, &markers, "name")).unwrap(); + let markup = hover.expect("hover the UVM class type").info; + let text = markup.as_str(); + assert!(text.contains("string"), "class property hover must show the member type:\n{text}"); + assert!(!text.contains("hir-ty"), "TypeSystem is not the hover type answer:\n{text}"); + } + + #[test] + fn class_scope_goto_is_answered_by_slang() { + let src = r#" +class env; + static int /*marker:def*/count; +endclass +module top; + initial env::/*marker:use*/count = 1; +endmodule +"#; + let (host, file_id, _text, markers) = setup_marked(src); + let nav = host + .make_analysis() + .goto_definition(position(file_id, &markers, "use")) + .unwrap() + .expect("env::count"); + assert!( + nav.info.iter().any(|target| target.focus_range.map(|range| range.start()) + == Some(markers["def"])), + "class :: must jump to the member: {nav:?}" + ); + } + + #[test] + fn section_3_7_ids_agree_on_independent_parses() { + let (matched, compared) = + source_ast_ids_agree(UVM_OBJECT, "uvm_object.svh", "uvm_object.svh"); + assert!(compared > 0, "must compare at least one node"); + assert_eq!(matched, compared, "§3.7: {matched}/{compared} SourceAstId values matched"); + } + + #[test] + fn t4_gate_numbers() { + use std::time::Instant; + + let (host, file_id, _text, markers) = setup_marked(UVM_OBJECT); + let pos = position(file_id, &markers, "name"); + let slang = + shipped_symbol_at(&host, file_id, markers["name"]).expect("slang answers the member"); + + let mut times = Vec::new(); + let mut hits = 0usize; + for _ in 0..40 { + let started = Instant::now(); + let hover = host.make_analysis().hover(pos).unwrap(); + times.push(started.elapsed().as_secs_f64() * 1000.0); + if hover.as_ref().is_some_and(|h| h.info.as_str().contains("string")) { + hits += 1; + } + } + times.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let p95 = times[((times.len() * 95) / 100).min(times.len() - 1)]; + + let (matched, compared) = + source_ast_ids_agree(UVM_OBJECT, "uvm_object.svh", "uvm_object.svh"); + let id_ok = compared > 0 && matched == compared; + println!("t4.slang\t{}", slang.type_name); + println!("t4.p95_ms\t{p95:.3}"); + println!("t4.section_3_7\t{matched}/{compared} {}", if id_ok { "pass" } else { "fail" }); + println!("t4.slang_hits\t{hits}/{}", times.len()); + println!("t4.gate\tp95<50ms={} §3.7={}", p95 < 50.0, id_ok); + assert!(hits == times.len(), "hover must answer every request"); + assert!(id_ok, "§3.7 must hold"); + } +} diff --git a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_keeps_tied_duplicates_ambiguous.sv.snap b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_keeps_tied_duplicates_ambiguous.sv.snap index b729a0d58..78b996175 100644 --- a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_keeps_tied_duplicates_ambiguous.sv.snap +++ b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_keeps_tied_duplicates_ambiguous.sv.snap @@ -1,6 +1,7 @@ --- source: crates/ide/src/module_resolution.rs +assertion_line: 537 expression: fixture_snapshot(fixture) input_file: crates/ide/src/module_resolution/fixtures/best_effort_keeps_tied_duplicates_ambiguous.sv --- -Ambiguous kind=BestEffortTie candidates=["/project/a/child.sv", "/project/b/child.sv"] +Ambiguous candidates=["/project/a/child.sv", "/project/b/child.sv"] diff --git a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_selects_nearest_duplicate_module.sv.snap b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_selects_nearest_duplicate_module.sv.snap index 2df85b165..f42863f89 100644 --- a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_selects_nearest_duplicate_module.sv.snap +++ b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@best_effort_selects_nearest_duplicate_module.sv.snap @@ -1,6 +1,7 @@ --- source: crates/ide/src/module_resolution.rs +assertion_line: 537 expression: fixture_snapshot(fixture) input_file: crates/ide/src/module_resolution/fixtures/best_effort_selects_nearest_duplicate_module.sv --- -BestEffortProximity selected=/project/a/child.sv candidates=["/project/a/child.sv", "/project/b/child.sv"] +Ambiguous candidates=["/project/a/child.sv", "/project/b/child.sv"] diff --git a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@configured_root_keeps_duplicates_ambiguous.sv.snap b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@configured_root_keeps_duplicates_ambiguous.sv.snap index 572f2f1db..b282c834d 100644 --- a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@configured_root_keeps_duplicates_ambiguous.sv.snap +++ b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@configured_root_keeps_duplicates_ambiguous.sv.snap @@ -1,6 +1,7 @@ --- source: crates/ide/src/module_resolution.rs +assertion_line: 537 expression: fixture_snapshot(fixture) input_file: crates/ide/src/module_resolution/fixtures/configured_root_keeps_duplicates_ambiguous.sv --- -Ambiguous kind=Strict candidates=["/project/a/child.sv", "/project/b/child.sv"] +Ambiguous candidates=["/project/a/child.sv", "/project/b/child.sv"] diff --git a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_param_uses_nearest_duplicate_module.sv.snap b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_param_uses_nearest_duplicate_module.sv.snap index ba0a05e76..4cb736a66 100644 --- a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_param_uses_nearest_duplicate_module.sv.snap +++ b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_param_uses_nearest_duplicate_module.sv.snap @@ -1,6 +1,7 @@ --- source: crates/ide/src/module_resolution.rs +assertion_line: 537 expression: fixture_snapshot(fixture) input_file: crates/ide/src/module_resolution/fixtures/named_param_uses_nearest_duplicate_module.sv --- -ParamDecl module=/project/a/child.sv +Ambiguous candidates=["/project/a/child.sv", "/project/b/child.sv"] diff --git a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_port_uses_nearest_duplicate_module.sv.snap b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_port_uses_nearest_duplicate_module.sv.snap index fe20b80b5..2acd50c0b 100644 --- a/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_port_uses_nearest_duplicate_module.sv.snap +++ b/crates/ide/src/snapshots/ide__module_resolution__tests__module_resolution_fixtures@named_port_uses_nearest_duplicate_module.sv.snap @@ -1,6 +1,7 @@ --- source: crates/ide/src/module_resolution.rs +assertion_line: 537 expression: fixture_snapshot(fixture) input_file: crates/ide/src/module_resolution/fixtures/named_port_uses_nearest_duplicate_module.sv --- -AnsiPort module=/project/a/child.sv +Ambiguous candidates=["/project/a/child.sv", "/project/b/child.sv"] diff --git a/crates/ide/src/snapshots/ide__verilog_2005__ambiguous_instantiation_hover_lists_locations_without_expanding_signatures.snap b/crates/ide/src/snapshots/ide__verilog_2005__ambiguous_instantiation_hover_lists_locations_without_expanding_signatures.snap index eb16dfeac..52e6585bf 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__ambiguous_instantiation_hover_lists_locations_without_expanding_signatures.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__ambiguous_instantiation_hover_lists_locations_without_expanding_signatures.snap @@ -1,20 +1,24 @@ --- source: crates/ide/src/verilog_2005.rs +assertion_line: 2819 expression: normalize_hover_snapshot(hover.info.as_str()) --- -Module reference `child` - ```systemverilog -child +module child(input logic a); ``` --- -ambiguous reference, 2 candidates +from [feature.v]() + +--- + +```systemverilog +module child(output logic y); +``` + --- -Candidates -- [feature.v]() -- [feature.v]() +from [feature.v]() diff --git a/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_package_scoped_names_support_ide_features__package_hover.snap b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_package_scoped_names_support_ide_features__package_hover.snap index 81306c276..18f56596a 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_package_scoped_names_support_ide_features__package_hover.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__systemverilog_package_scoped_names_support_ide_features__package_hover.snap @@ -1,9 +1,10 @@ --- source: crates/ide/src/verilog_2005.rs +assertion_line: 3035 expression: normalize_hover_snapshot(package_hover.info.as_str()) --- ```systemverilog -package pkg () +package pkg; ``` diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_after_truncation_uses_current_syntax_context.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_after_truncation_uses_current_syntax_context.snap index 7b2ef8ce6..a88e042ac 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_after_truncation_uses_current_syntax_context.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_after_truncation_uses_current_syntax_context.snap @@ -3,12 +3,7 @@ source: crates/ide/src/verilog_2005.rs expression: normalize_hover_snapshot(hover.info.as_str()) --- ```systemverilog -module axi_addr_miter ( - i_last_addr, - i_size, - i_burst, - i_len -) +module axi_addr_miter(i_last_addr, i_size, i_burst, i_len); ``` diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_covers_all_definition_kinds__module_ref.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_covers_all_definition_kinds__module_ref.snap index 0290fed54..8b24abb00 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_covers_all_definition_kinds__module_ref.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_covers_all_definition_kinds__module_ref.snap @@ -1,12 +1,10 @@ --- source: crates/ide/src/verilog_2005.rs +assertion_line: 3224 expression: normalize_hover_snapshot(hover.info.as_str()) --- ```systemverilog -module child ( - input wire logic a, - output wire logic y -) +module child(input wire a, output wire y); ``` diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_def.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_def.snap index 31994d1eb..6fb78afee 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_def.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_def.snap @@ -3,11 +3,9 @@ source: crates/ide/src/verilog_2005.rs expression: normalize_hover_snapshot(module_hover.info.as_str()) --- ```systemverilog -module child #( - parameter logic WIDTH = 8 -) ( - input wire logic clk -) +module child #(parameter WIDTH = 8) ( + input wire clk +); ``` diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_ref.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_ref.snap index d9be422e2..131c931a5 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_ref.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_hover_uses_symbol_specific_renderers__module_ref.snap @@ -1,13 +1,12 @@ --- source: crates/ide/src/verilog_2005.rs +assertion_line: 2465 expression: normalize_hover_snapshot(inst_module_hover.info.as_str()) --- ```systemverilog -module child #( - parameter logic WIDTH = 8 -) ( - input wire logic clk -) +module child #(parameter WIDTH = 8) ( + input wire clk +); ``` diff --git a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_module_definition_names_support_references.snap b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_module_definition_names_support_references.snap index 63662a452..6ae8c2232 100644 --- a/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_module_definition_names_support_references.snap +++ b/crates/ide/src/snapshots/ide__verilog_2005__verilog_2005_module_definition_names_support_references.snap @@ -3,12 +3,7 @@ source: crates/ide/src/verilog_2005.rs expression: normalize_hover_snapshot(hover.info.as_str()) --- ```systemverilog -module mux2X1 ( - in0, - in1, - sel, - out -) +module mux2X1(in0, in1, sel, out); ``` diff --git a/crates/ide/src/token.rs b/crates/ide/src/token.rs index 9747d4b6f..f7aa1bdb0 100644 --- a/crates/ide/src/token.rs +++ b/crates/ide/src/token.rs @@ -29,8 +29,9 @@ pub(crate) fn hover_precedence(kind: TokenKind) -> usize { } } -/// Precedence for the semantic index build: only name-like tokens are -/// indexed, so the function is a boolean predicate. +/// Precedence for on-demand name walks: only name-like tokens are +/// candidates, so the function is a boolean predicate. +#[cfg(test)] pub(crate) fn name_precedence(kind: TokenKind) -> usize { usize::from(kind.name_like()) } diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 344a66282..eec96d6e2 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -15,6 +15,7 @@ use base_db::{ use hir_semantics::semantics::Semantics; use insta::assert_snapshot; use preproc_expand::preproc::{IncludeTarget, include_directive_at}; +use syntax::{SyntaxNodeExt, has_text_range::HasTextRange}; use triomphe::Arc; use utils::{ test_support::TestDir, @@ -110,7 +111,7 @@ fn parsed_file_nodes_survive_parse_lru_eviction() { let mut db = RootDb::new(Some(1)); db.apply_change(change); - let sema = Semantics::new(&db); + let sema = Semantics::new_with_context(&db, hir_def::unit::test_resolution(&db)); let parsed_file = sema.parse_file(FileId::from_raw(0)); let root = parsed_file.root().expect("a.sv should parse"); let child_count = root.child_count(); @@ -1227,7 +1228,7 @@ endmodule let (host, file_id, _clean_text, markers) = setup_marked_with_predefines(text, vec!["USE_IMPL=1".to_owned()]); - let include = include_directive_at(host.raw_db(), file_id, markers["active"]) + let include = include_directive_at(host.ctx().db, file_id, markers["active"]) .unwrap() .expect("active include should be queryable"); let IncludeTarget::Literal { path, .. } = include.target else { @@ -1235,7 +1236,7 @@ endmodule }; assert_eq!(path.as_str(), "active.svh"); - assert!(include_directive_at(host.raw_db(), file_id, markers["inactive"]).unwrap().is_none()); + assert!(include_directive_at(host.ctx().db, file_id, markers["inactive"]).unwrap().is_none()); } #[test] @@ -2870,6 +2871,64 @@ endmodule ); } +#[test] +fn design_unit_references_join_graph_candidates() { + let (host, files) = setup_marked_files(&[ + ( + "/shared_pkg.sv", + r#" +package /*marker:pkg*/shared; +endpackage +"#, + ), + ( + "/shared_mod.sv", + r#" +module /*marker:mod*/shared; +endmodule +"#, + ), + ( + "/top.sv", + r#" +module top; + shared u(); +endmodule +"#, + ), + ]); + let [(pkg_file, _, pkg_markers), (mod_file, _, mod_markers), (top_file, _, _)] = + files.as_slice() + else { + panic!("expected three fixture files"); + }; + let analysis = host.make_analysis(); + let workspace = ReferencesConfig::new(ScopeVisibility::Public, None); + + let module_refs = analysis + .references(position(*mod_file, mod_markers, "mod"), workspace.clone()) + .unwrap() + .expect("module candidate should join instantiations"); + let module_ref_files: Vec<_> = + module_refs.iter().flat_map(|refs| refs.refs.keys().copied()).collect(); + assert_eq!( + module_ref_files, + vec![*top_file], + "only the UnitCatalog module candidate owns the instantiation: {module_refs:?}" + ); + + let package_refs = analysis + .references(position(*pkg_file, pkg_markers, "pkg"), workspace) + .unwrap() + .unwrap_or_default(); + let package_ref_files: Vec<_> = + package_refs.iter().flat_map(|refs| refs.refs.keys().copied()).collect(); + assert!( + !package_ref_files.contains(top_file), + "a package is not an instantiable UnitCatalog candidate: {package_refs:?}" + ); +} + #[test] fn systemverilog_program_definition_names_support_navigation_and_hover() { let text = r#" @@ -2893,8 +2952,8 @@ endmodule "program instantiation should navigate to the program declaration: {nav:?}" ); assert!( - nav.info.iter().all(|target| target.kind == Some(DefKind::Module)), - "program navigation targets should retain module symbol metadata: {nav:?}" + nav.info.iter().all(|target| target.kind == Some(DefKind::Program)), + "program navigation targets should keep program kind: {nav:?}" ); let hover = analysis @@ -2944,9 +3003,9 @@ endmodule .expect("package definition expected"); assert!( package_nav.info.iter().any(|target| { - target.focus_range == Some(package_def_range) && target.kind == Some(DefKind::Module) + target.focus_range == Some(package_def_range) && target.kind == Some(DefKind::Package) }), - "package navigation target should retain module symbol metadata: {package_nav:?}" + "package navigation target should keep package kind: {package_nav:?}" ); let type_def_range = marked_range(&markers, "type_def", TextSize::of("exported_t")); @@ -3019,62 +3078,50 @@ endmodule panic!("expected two fixture files"); }; - let module_index = crate::db::workspace_symbol_index_db::source_root_module_index_for_root( - host.raw_db(), - SourceRootId(0), - ); - let index = crate::db::workspace_symbol_index_db::source_root_semantic_index_for_root( - host.raw_db(), - SourceRootId(0), - ); - - let modules = module_index.module_definitions(&"mod_a".into()); - 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"); + let graph = host.ctx().unit_catalog(); + let modules = graph.modules_named("mod_a").into_vec(); + assert_eq!(modules.len(), 1, "graph should contain mod_a exactly once"); + assert_eq!(modules[0].file, *file_a); + assert_eq!(modules[0].name, "mod_a"); + let interfaces = graph.modules_named("bus_if").into_vec(); + assert_eq!(interfaces.len(), 1, "graph should contain bus_if exactly once"); + assert_eq!(interfaces[0].file, *file_a); + assert_eq!(interfaces[0].name, "bus_if"); let a_def = marked_range(markers_a, "a_shared_def", 6); let a_ref = marked_range(markers_a, "a_shared_ref", 6); - let group_a = groups - .iter() - .find(|group| { - group - .definition_ranges - .iter() - .any(|range| range.file_id == *file_a && range.range == a_def) - }) - .expect("shared definition in a.sv should have a reference group"); - let refs_a = group_a - .references - .iter() - .map(|reference| (reference.file_id, reference.range)) - .collect::>(); - assert_eq!(refs_a, vec![(*file_a, a_ref)]); - let b_def = marked_range(markers_b, "b_shared_def", 6); let b_ref = marked_range(markers_b, "b_shared_ref", 6); - let group_b = groups - .iter() - .find(|group| { - group - .definition_ranges - .iter() - .any(|range| range.file_id == *file_b && range.range == b_def) - }) - .expect("shared definition in b.sv should have a reference group"); - let refs_b = group_b - .references - .iter() - .map(|reference| (reference.file_id, reference.range)) - .collect::>(); - assert_eq!(refs_b, vec![(*file_b, b_ref)]); + + let db = host.ctx(); + let refs_of = |file_id: FileId, range: TextRange| { + let tree = db.parse(preproc_expand::file::HirFileId::from(file_id)); + let token = tree + .root() + .token_at_offset(range.start()) + .find(|token| token.text_range() == Some(range)) + .expect("definition token"); + let crate::definitions::DefinitionClass::Definition(def) = + crate::definitions::DefinitionClass::resolve(&db, file_id.into(), token) + .unique() + .expect("unique def") + else { + panic!("expected a plain definition"); + }; + let scope = crate::references::search::SearchScope::new( + db.db, + &def, + ReferencesConfig::new(ScopeVisibility::Public, None), + ); + crate::references::search::search_references(&db, &def, scope) + .into_iter() + .flat_map(|(file_id, tokens)| { + tokens.into_iter().map(move |token| (file_id, token.range())) + }) + .collect::>() + }; + assert_eq!(refs_of(*file_a, a_def), vec![(*file_a, a_ref)]); + assert_eq!(refs_of(*file_b, b_def), vec![(*file_b, b_ref)]); } #[test] @@ -3120,7 +3167,7 @@ endmodule let leaf_call = marked_range(child_markers, "leaf_call", 4); let top_outgoing = - crate::semantic_index::outgoing_module_edges(host.raw_db(), *top_file, top_def); + crate::reference_support::outgoing_module_edges(&host.ctx(), *top_file, top_def); assert_eq!(top_outgoing.len(), 1); assert_eq!(top_outgoing[0].caller.file_id, *top_file); assert_eq!(top_outgoing[0].caller.name_range, top_def); @@ -3129,14 +3176,14 @@ endmodule assert_eq!(top_outgoing[0].call_range, child_call); let child_outgoing = - crate::semantic_index::outgoing_module_edges(host.raw_db(), *child_file, child_def); + crate::reference_support::outgoing_module_edges(&host.ctx(), *child_file, child_def); assert_eq!(child_outgoing.len(), 1); assert_eq!(child_outgoing[0].callee.file_id, *leaf_file); assert_eq!(child_outgoing[0].callee.name_range, leaf_def); assert_eq!(child_outgoing[0].call_range, leaf_call); let child_incoming = - crate::semantic_index::incoming_module_edges(host.raw_db(), *child_file, child_def); + crate::reference_support::incoming_module_edges(&host.ctx(), *child_file, child_def); assert_eq!(child_incoming.len(), 1); assert_eq!(child_incoming[0].caller.file_id, *top_file); assert_eq!(child_incoming[0].call_range, child_call); @@ -3760,7 +3807,7 @@ endmodule stmts.values().any(|stmt| matches_kind(&stmt.kind)) } - let db = host.raw_db(); + let db = host.ctx(); let hir_file_id = HirFileId::File(file_id); let hir_file = db.body_with_source_map(db.owner_table(hir_file_id).file_owner().expect("file owner")); diff --git a/crates/ide/src/workspace_symbols.rs b/crates/ide/src/workspace_symbols.rs index 36e4516f0..95460209f 100644 --- a/crates/ide/src/workspace_symbols.rs +++ b/crates/ide/src/workspace_symbols.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, collections::BinaryHeap}; use base_db::{source_db::SourceRootDb, source_root::SourceRootId}; use fst::{IntoStreamer, Streamer}; -use hir_ty::db::TyDb; +use hir_def::db::HirDefDb; use triomphe::Arc; use utils::line_index::TextRange; use vfs::FileId; @@ -237,7 +237,7 @@ impl SymbolIndex { } } -pub(crate) fn file_symbols(db: &dyn TyDb, file_id: FileId) -> Arc<[WorkspaceSymbol]> { +pub(crate) fn file_symbols(db: &dyn HirDefDb, file_id: FileId) -> Arc<[WorkspaceSymbol]> { if db.file_kind(file_id).is_project_manifest() { return crate::manifest::workspace_symbols(db, &[file_id], "").into(); } diff --git a/crates/preproc-expand/Cargo.toml b/crates/preproc-expand/Cargo.toml index 63e251620..3b230d9bd 100644 --- a/crates/preproc-expand/Cargo.toml +++ b/crates/preproc-expand/Cargo.toml @@ -8,6 +8,7 @@ base-db.workspace = true preproc.workspace = true rustc-hash.workspace = true salsa.workspace = true +serde.workspace = true smol_str.workspace = true syntax.workspace = true toml.workspace = true @@ -16,3 +17,6 @@ tracing.workspace = true triomphe.workspace = true utils.workspace = true vfs.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/preproc-expand/src/compilation_plan.rs b/crates/preproc-expand/src/compilation_plan.rs index 49ed79b44..d4d062b05 100644 --- a/crates/preproc-expand/src/compilation_plan.rs +++ b/crates/preproc-expand/src/compilation_plan.rs @@ -6,22 +6,62 @@ use base_db::{ use preproc::source::{ MacroIncludeTarget, SourceIncludeDirective, SourcePreprocError, SourcePreprocModel, }; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions}; use utils::{ path_identity::PathIdentityIndex, - paths::{AbsPathBuf, Utf8Path, Utf8PathBuf}, + paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf}, }; use vfs::FileId; +use crate::db::PreprocDb; + +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +struct IncludeScanQueryKey { + file_id: FileId, + predefines: triomphe::Arc<[String]>, +} + +/// A resolved literal `` `include ``. `slang_path` is the cache key slang will +/// use for this edge (`parent(from) / literal` when that join is the target +/// file, otherwise the target's VFS path for an include-dir hit). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeEdge { + pub from: FileId, + pub to: FileId, + pub literal: String, + pub slang_path: AbsPathBuf, +} + +/// A compilation-unit root. Only SystemVerilog and library maps are legal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CompilationRoot { + pub file_id: FileId, + pub kind: CompilationRootKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompilationRootKind { + SystemVerilog, + LibraryMap, +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct CompilationPlan { pub source_roots: Vec, - pub roots: Vec, + pub roots: Vec, /// Files reached through literal SystemVerilog include directives. They are /// made available to slang through include buffers, but are not added /// as standalone semantic roots. pub include_only: FxHashSet, + /// Direct resolved include edges, keyed by the including file. + pub include_dependencies: FxHashMap>, + /// Resolved include edges with the slang lookup spelling for each use. + pub include_edges: Vec, + /// Files with a non-literal include target. Their exact dependency cannot + /// be known without the authoritative preprocessor, so they are treated as + /// affected by every source edit. + pub dynamic_include_files: FxHashSet, pub include_dirs: Vec, pub top_modules: Vec, pub predefines: Vec, @@ -40,16 +80,47 @@ pub enum IncludeScanIssueReason { } impl CompilationPlan { + pub fn root_file_ids(&self) -> impl Iterator + '_ { + self.roots.iter().map(|root| root.file_id) + } + + pub fn has_root(&self, file_id: FileId) -> bool { + self.roots.iter().any(|root| root.file_id == file_id) + } + /// Every file the plan compiles: semantic roots plus include-only files, /// in stable order without duplicates. pub fn all_file_ids(&self) -> Vec { - let mut file_ids = self.roots.clone(); + let mut file_ids: Vec<_> = self.root_file_ids().collect(); file_ids.extend(self.include_only.iter().copied()); file_ids.sort_unstable_by_key(|file_id| file_id.index()); file_ids.dedup(); file_ids } + /// Exact transitive include closure when every visited directive resolved + /// statically. Dynamic or currently missing include targets return `None`, + /// which tells the parser to retain the conservative profile-wide buffer + /// set for correctness. + pub fn include_closure(&self, root: FileId) -> Option> { + let mut closure = FxHashSet::default(); + let mut pending = vec![root]; + while let Some(file_id) = pending.pop() { + if self.dynamic_include_files.contains(&file_id) { + return None; + } + let Some(dependencies) = self.include_dependencies.get(&file_id) else { + continue; + }; + for &dependency in dependencies { + if closure.insert(dependency) { + pending.push(dependency); + } + } + } + Some(closure) + } + /// Whether a file should be made available to slang as an include buffer: /// include headers reachable through the configured include paths. pub fn is_include_header_in_include_paths( @@ -63,7 +134,7 @@ impl CompilationPlan { }) } - pub fn for_source_root(db: &dyn SourceRootDb, source_root_id: SourceRootId) -> Self { + pub fn for_source_root(db: &dyn PreprocDb, source_root_id: SourceRootId) -> Self { let project_config = db.project_config(); let profile_id = project_config.profile_for_root(source_root_id); // Profile-backed plans are the normal project path. A compile-capable @@ -79,7 +150,7 @@ impl CompilationPlan { Self::from_inputs(db, source_roots, top_modules, include_dirs, predefines) } - pub fn for_profile(db: &dyn SourceRootDb, profile_id: Option) -> Self { + pub fn for_profile(db: &dyn PreprocDb, profile_id: Option) -> Self { let project_config = db.project_config(); let (source_roots, top_modules, include_dirs, predefines) = profile_inputs(&project_config, None, profile_id); @@ -89,38 +160,110 @@ impl CompilationPlan { } fn from_inputs( - db: &dyn SourceRootDb, + db: &dyn PreprocDb, source_roots: Vec, top_modules: Vec, include_dirs: Vec, predefines: Vec, ) -> Self { - let (include_only, include_scan_issues) = - include_targets_for_source_roots(db, &source_roots, &include_dirs, &predefines); + let mut starts = Vec::new(); + for root in &source_roots { + starts.extend(db.source_root(*root).iter()); + } + let scan = scan_include_graph(db, starts, &include_dirs, &predefines); + let (include_only, include_dependencies) = include_projections(&scan.edges); let roots = compile_roots_for_source_roots(db, &source_roots, &include_only); CompilationPlan { source_roots, roots, include_only, + include_dependencies, + include_edges: scan.edges, + dynamic_include_files: scan.dynamic_files, include_dirs, top_modules, predefines, - include_scan_issues, + include_scan_issues: scan.issues, } } } pub fn include_buffers_for_plan( - db: &dyn SourceRootDb, + db: &dyn PreprocDb, plan: &CompilationPlan, ) -> Vec { include_buffers_for_plan_with_roots(db, plan, false) + .into_iter() + .map(|buffer| SyntaxTreeBuffer { path: buffer.path, text: buffer.text }) + .collect() +} + +/// A source buffer we hand to slang, keyed by the spelling slang will look up. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AssignedIncludeBuffer { + pub file_id: FileId, + pub path: String, + pub text: String, +} + +/// Transitive literal includes of one file, walking only that file's +/// include graph. Dynamic or unresolved directives make the closure +/// [`Partial`](StaticIncludeClosure::Partial); resolved files are still +/// returned. This never expands to the whole profile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StaticIncludeClosure { + Complete(Vec), + Partial(Vec), +} + +impl StaticIncludeClosure { + pub fn files(&self) -> &[FileId] { + match self { + Self::Complete(files) | Self::Partial(files) => files, + } + } + + pub fn is_complete(&self) -> bool { + matches!(self, Self::Complete(_)) + } +} + +/// Include buffers needed by one standalone compilation unit. +/// +/// Each resolved include is registered under that edge's `slang_path` only. A +/// dynamic or unresolved include does **not** load every header in the +/// profile. +pub fn include_buffers_for_file(db: &dyn PreprocDb, file_id: FileId) -> Vec { + assigned_include_buffers_for_file(db, file_id) + .into_iter() + .map(|buffer| SyntaxTreeBuffer { path: buffer.path, text: buffer.text }) + .collect() +} + +pub fn assigned_include_buffers_for_file( + db: &dyn PreprocDb, + file_id: FileId, +) -> Vec { + buffers_from_edges(db, &scan_includes_from_file(db, file_id).edges) +} + +/// Walk literal `` `include `` directives from `file_id` only. +pub fn static_include_closure(db: &dyn PreprocDb, file_id: FileId) -> StaticIncludeClosure { + let scan = scan_includes_from_file(db, file_id); + let (include_only, _) = include_projections(&scan.edges); + let mut files = include_only.into_iter().collect::>(); + files.sort_unstable_by_key(|file_id| file_id.index()); + if scan.complete { + StaticIncludeClosure::Complete(files) + } else { + StaticIncludeClosure::Partial(files) + } } pub fn compilation_source_buffers_for_plan( - db: &dyn SourceRootDb, + db: &dyn PreprocDb, plan: &CompilationPlan, -) -> Vec { +) -> Vec { include_buffers_for_plan_with_roots(db, plan, true) } @@ -145,12 +288,12 @@ fn synthetic_source_buffer_path(file_id: FileId) -> AbsPathBuf { } fn include_buffers_for_plan_with_roots( - db: &dyn SourceRootDb, + db: &dyn PreprocDb, plan: &CompilationPlan, include_roots: bool, -) -> Vec { +) -> Vec { let root_files = if include_roots { - plan.roots.iter().copied().collect::>() + plan.root_file_ids().collect::>() } else { FxHashSet::default() }; @@ -190,13 +333,97 @@ fn include_buffers_for_plan_with_roots( let path = path.to_string(); if seen_buffer_paths.insert(path.clone()) { - buffers.push(SyntaxTreeBuffer { path, text: db.file_text(file_id).to_string() }); + buffers.push(AssignedIncludeBuffer { + file_id, + path, + text: db.file_text(file_id).to_string(), + }); + } + } + + for buffer in buffers_from_edges(db, &plan.include_edges) { + if seen_buffer_paths.insert(buffer.path.clone()) { + buffers.push(buffer); } } buffers } +/// FFI path → [`FileId`] for one standalone parse: the root's VFS spelling plus +/// each reachable include edge's `slang_path`. +pub(crate) fn source_buffer_file_ids_for_file( + db: &dyn PreprocDb, + file_id: FileId, +) -> PathIdentityIndex { + let mut index = PathIdentityIndex::default(); + index.insert_path(source_buffer_path(db, file_id).as_path(), file_id); + for edge in scan_includes_from_file(db, file_id).edges { + index.insert_path(edge.slang_path.as_path(), edge.to); + } + index +} + +fn scan_includes_from_file(db: &dyn PreprocDb, file_id: FileId) -> IncludeScan { + let preprocess = + db.project_config().preprocess_for_profile(db.file_compilation_profile(file_id)); + scan_include_graph(db, [file_id], &preprocess.include_dirs, &preprocess.predefine_strings()) +} + +fn buffers_from_edges(db: &dyn PreprocDb, edges: &[IncludeEdge]) -> Vec { + let mut seen_paths = FxHashSet::default(); + let mut buffers = Vec::new(); + for edge in edges { + if db.file_is_project_ignored(edge.to) { + continue; + } + let path = edge.slang_path.to_string(); + if !seen_paths.insert(path.clone()) { + continue; + } + buffers.push(AssignedIncludeBuffer { + file_id: edge.to, + path, + text: db.file_text(edge.to).to_string(), + }); + } + buffers +} + +fn include_projections( + edges: &[IncludeEdge], +) -> (FxHashSet, FxHashMap>) { + let mut include_only = FxHashSet::default(); + let mut include_dependencies = FxHashMap::>::default(); + for edge in edges { + include_only.insert(edge.to); + include_dependencies.entry(edge.from).or_default().insert(edge.to); + } + (include_only, include_dependencies) +} + +/// Slang's first include lookup key when `disableProximatePaths` is set: +/// `parent(includer) / include-literal`, with no `.`/`..` collapse. +pub(crate) fn slang_local_include_lookup_path( + includer: &AbsPath, + literal: &str, +) -> Option { + let include = Utf8Path::new(literal); + if include.is_absolute() { + return AbsPathBuf::try_from(include.to_path_buf()).ok(); + } + let dir = includer.parent()?; + AbsPathBuf::try_from(Utf8Path::new(dir.as_str()).join(include)).ok() +} + +/// The spelling to hand slang for one resolved include. +fn slang_path_for_include(includer: &AbsPath, literal: &str, target_vfs: &AbsPath) -> AbsPathBuf { + let Some(local) = slang_local_include_lookup_path(includer, literal) else { + return target_vfs.to_path_buf(); + }; + if local.normalize() == target_vfs.normalize() { local } else { target_vfs.to_path_buf() } +} + fn profile_inputs( project_config: &ProjectConfig, root_scoped_source_root: Option, @@ -233,7 +460,7 @@ fn compile_roots_for_source_roots( db: &dyn SourceRootDb, roots: &[SourceRootId], include_only: &FxHashSet, -) -> Vec { +) -> Vec { let mut files = Vec::new(); let mut visited = FxHashSet::default(); @@ -246,15 +473,17 @@ fn compile_roots_for_source_roots( if db.file_is_project_ignored(file_id) { continue; } - if !db.file_kind(file_id).is_semantic_compilation_unit() { - continue; - } - if matches!(db.file_kind(file_id), SourceFileKind::SystemVerilog) - && include_only.contains(&file_id) - { - continue; - } - files.push(file_id); + let kind = match db.file_kind(file_id) { + SourceFileKind::SystemVerilog => { + if include_only.contains(&file_id) { + continue; + } + CompilationRootKind::SystemVerilog + } + SourceFileKind::LibraryMap => CompilationRootKind::LibraryMap, + SourceFileKind::IncludeHeader | SourceFileKind::ProjectManifest => continue, + }; + files.push(CompilationRoot { file_id, kind }); } } @@ -274,20 +503,28 @@ fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex { index } -fn include_targets_for_source_roots( - db: &dyn SourceRootDb, - roots: &[SourceRootId], +struct IncludeScan { + edges: Vec, + dynamic_files: FxHashSet, + issues: Vec, + complete: bool, +} + +fn scan_include_graph( + db: &dyn PreprocDb, + starts: impl IntoIterator, include_dirs: &[AbsPathBuf], predefines: &[String], -) -> (FxHashSet, Vec) { +) -> IncludeScan { let path_file_ids = path_file_ids(db); - let mut included = FxHashSet::default(); + let predefines = triomphe::Arc::<[String]>::from(predefines.to_vec()); + let mut edges = Vec::new(); + let mut seen_edges = FxHashSet::default(); + let mut dynamic_files = FxHashSet::default(); let mut issues = Vec::new(); + let mut complete = true; let mut scanned = FxHashSet::default(); - let mut pending = Vec::new(); - for root_id in roots { - pending.extend(db.source_root(*root_id).iter()); - } + let mut pending = starts.into_iter().collect::>(); while let Some(file_id) = pending.pop() { if !scanned.insert(file_id) { @@ -303,38 +540,75 @@ fn include_targets_for_source_roots( continue; } - let Some(includer_path) = db.file_path(file_id) else { - continue; - }; + let includer_path = + db.file_path(file_id).unwrap_or_else(|| source_buffer_path(db, file_id)); - let include_targets = match literal_include_targets(db, file_id, predefines) { + let include_targets = match literal_include_targets( + db, + IncludeScanQueryKey::new(db, file_id, predefines.clone()), + ) { Ok(targets) => targets, Err(issue) => { + complete = false; + dynamic_files.insert(file_id); issues.push(issue); continue; } }; for include in include_targets { let MacroIncludeTarget::Literal { path, .. } = &include.target else { + complete = false; + dynamic_files.insert(file_id); continue; }; - if let Some(included_file_id) = + let Some(to) = resolve_include_target(path.as_str(), &includer_path, include_dirs, &path_file_ids) - && included.insert(included_file_id) - { - pending.push(included_file_id); + else { + complete = false; + dynamic_files.insert(file_id); + continue; + }; + pending.push(to); + if db.file_is_project_ignored(to) { + continue; + } + let target_vfs = source_buffer_path(db, to); + let slang_path = slang_path_for_include( + includer_path.as_path(), + path.as_str(), + target_vfs.as_path(), + ); + if seen_edges.insert((file_id, to, slang_path.clone())) { + edges.push(IncludeEdge { + from: file_id, + to, + literal: path.to_string(), + slang_path, + }); } } } - (included, issues) + IncludeScan { edges, dynamic_files, issues, complete } } +/// U3: include-closure scan. Profile predefines, no include expansion, +/// and a preprocessor `Trace`. +/// +/// This cannot share U1 (`source_model`): U1 uses empty predefines so a +/// profile edit does not invalidate the file-local editor model. It cannot +/// share U2 (`file_facts_query`): U2 must stay a cheap fact extract and +/// must not build a `Trace`. U3 does not compute +/// [`syntax::preprocessor_independent`] — the scan only needs include +/// directives after `ifdef` evaluation. The boolean lives on U1 and U2 +/// via that one function and cannot diverge between them. +#[salsa::tracked(returns(clone))] fn literal_include_targets( - db: &dyn SourceRootDb, - file_id: FileId, - predefines: &[String], + db: &dyn PreprocDb, + key: IncludeScanQueryKey, ) -> Result, IncludeScanIssue> { + let file_id = *key.file_id(db); + let predefines = key.predefines(db); if !matches!( db.file_kind(file_id), SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader @@ -348,6 +622,7 @@ fn literal_include_targets( predefines: predefines.to_vec(), ..SyntaxTreeOptions::without_include_expansion() }; + syntax::record_unexpanded_parse("include_scan"); let parsed = SyntaxTree::from_file_in_memory_with_options_and_trace( &db.file_text(file_id), &name, @@ -355,7 +630,7 @@ fn literal_include_targets( &options, ); let trace = parsed.preprocessor_trace; - let model = SourcePreprocModel::from_trace(trace) + let model = SourcePreprocModel::from_trace(&trace) .map_err(|err| IncludeScanIssue { file_id, reason: IncludeScanIssueReason::Model(err) })?; Ok(model.include_graph().directives().to_vec()) } @@ -393,6 +668,84 @@ fn resolve_include_target( mod tests { use super::*; + #[test] + fn slang_path_uses_local_join_when_it_names_the_target() { + let includer = if cfg!(windows) { + AbsPathBuf::assert(r"C:\repo\rtl\darkcache.v".into()) + } else { + AbsPathBuf::assert("/repo/rtl/darkcache.v".into()) + }; + let target = if cfg!(windows) { + AbsPathBuf::assert(r"C:\repo\rtl\config.vh".into()) + } else { + AbsPathBuf::assert("/repo/rtl/config.vh".into()) + }; + let path = slang_path_for_include(includer.as_path(), "../rtl/config.vh", target.as_path()); + let path = path.to_string().replace('\\', "/"); + assert!( + path.ends_with("rtl/../rtl/config.vh"), + "same-file local join is the slang lookup key: {path}" + ); + } + + #[test] + fn slang_path_uses_vfs_path_when_include_dirs_resolve_the_target() { + let includer = if cfg!(windows) { + AbsPathBuf::assert(r"C:\repo\rtl\top.v".into()) + } else { + AbsPathBuf::assert("/repo/rtl/top.v".into()) + }; + let target = if cfg!(windows) { + AbsPathBuf::assert(r"C:\repo\include\defs.vh".into()) + } else { + AbsPathBuf::assert("/repo/include/defs.vh".into()) + }; + let path = slang_path_for_include(includer.as_path(), "defs.vh", target.as_path()); + assert_eq!(path.as_path(), target.as_path()); + } + + #[test] + fn slang_local_include_lookup_keeps_parent_segments() { + let includer = if cfg!(windows) { + AbsPathBuf::assert(r"C:\repo\rtl\darkcache.v".into()) + } else { + AbsPathBuf::assert("/repo/rtl/darkcache.v".into()) + }; + let lookup = slang_local_include_lookup_path(includer.as_path(), "../rtl/config.vh") + .expect("relative include must produce a lookup path"); + let lookup = lookup.to_string().replace('\\', "/"); + assert!( + lookup.ends_with("rtl/../rtl/config.vh"), + "slang lookup key must keep the include join: {lookup}" + ); + } + + #[test] + fn include_closure_contains_only_transitive_dependencies() { + let root = FileId::from_raw(0); + let direct = FileId::from_raw(1); + let transitive = FileId::from_raw(2); + let unrelated = FileId::from_raw(3); + let mut plan = CompilationPlan::default(); + plan.include_dependencies.insert(root, FxHashSet::from_iter([direct])); + plan.include_dependencies.insert(direct, FxHashSet::from_iter([transitive])); + plan.include_dependencies.insert(unrelated, FxHashSet::default()); + + let closure = plan.include_closure(root).unwrap(); + + assert_eq!(closure, FxHashSet::from_iter([direct, transitive])); + assert!(!closure.contains(&unrelated)); + } + + #[test] + fn dynamic_include_forces_conservative_manifest() { + let root = FileId::from_raw(0); + let mut plan = CompilationPlan::default(); + plan.dynamic_include_files.insert(root); + + assert_eq!(plan.include_closure(root), None); + } + #[test] fn synthetic_source_buffer_paths_are_absolute() { let path = synthetic_source_buffer_path(FileId::from_raw(0)); diff --git a/crates/preproc-expand/src/context.rs b/crates/preproc-expand/src/context.rs index 6b1d166d0..d344d3fb8 100644 --- a/crates/preproc-expand/src/context.rs +++ b/crates/preproc-expand/src/context.rs @@ -60,17 +60,6 @@ pub fn macro_context_at(db: &dyn PreprocDb, file_id: FileId, offset: TextSize) - pub(crate) fn file_macro_coverage_query(db: &dyn PreprocDb, file_id: FileId) -> Arc { let contexts = db.source_preproc_contexts_for_file(file_id); - if let crate::source_db::SourcePreprocContextStatus::Partial { skipped_models } = - contexts.status - { - tracing::warn!( - ?file_id, - skipped_models, - "macro coverage unavailable because preprocessor contexts are partial" - ); - return Arc::new(MacroCoverage::default()); - } - let mut model_file_ids = vec![file_id]; for model_file_id in &contexts.model_file_ids { if !model_file_ids.contains(model_file_id) { @@ -93,8 +82,7 @@ pub(crate) fn file_macro_coverage_query(db: &dyn PreprocDb, file_id: FileId) -> return Arc::new(MacroCoverage::default()); } }; - let parsed = db.parsed_compilation_unit(model_file); - if parsed.preprocessor_trace.is_none() { + if db.preproc_trace(model_file).is_none() { tracing::warn!( ?file_id, ?model_file, diff --git a/crates/preproc-expand/src/db.rs b/crates/preproc-expand/src/db.rs index 94c4cbe20..f7fccbb65 100644 --- a/crates/preproc-expand/src/db.rs +++ b/crates/preproc-expand/src/db.rs @@ -1,3 +1,5 @@ +use std::hash::{Hash, Hasher}; + use base_db::{ analysis_snapshot::CompilationContext, diagnostics_config::{DiagnosticSource, DiagnosticsConfig}, @@ -5,12 +7,11 @@ use base_db::{ source_db::{SourceFileKind, SourceRootDb}, source_root::SourceRootId, }; -use rustc_hash::FxHashMap; +use rustc_hash::FxHasher; use syntax::{ - SyntaxTree, SyntaxTreeBuffer, - compilation::Compilation, + SyntaxTree, diagnostics::{ParserExpectedSyntax, SyntaxDiagnostic}, - preproc::{SyntaxTreeBufferIds, Trace}, + preproc::Trace, }; use triomphe::Arc; use utils::{line_index::TextSize, path_identity::PathIdentityIndex}; @@ -50,6 +51,13 @@ pub(crate) struct PreprocProfileQueryKey { pub profile_id: Option, } +/// Singleton key for the workspace-global path index (one per database). +#[salsa::interned(unsafe(no_lifetime), revisions = usize::MAX, debug)] +pub(crate) struct WorkspacePathIndexKey { + #[returns(copy)] + pub unit: (), +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct CompilationDiagnostic { /// File attribution after mapping slang source buffers back to VFS files. @@ -62,19 +70,72 @@ pub struct CompilationDiagnostic { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParsedCompilationUnit { pub syntax_tree: SyntaxTree, - pub preprocessor_trace: Option, + pub preprocessor_trace: Option>, } -pub type ParsedProfileUnits = Arc<[(FileId, ParsedCompilationUnit, SyntaxTreeBufferIds)]>; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CompilationUnitId { + pub root_file: FileId, + pub profile: Option, +} #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParsedProfile { - pub units: ParsedProfileUnits, +pub struct CompilationDependencyManifest { + pub files: Arc<[FileId]>, } +/// Immutable identity of every input that can affect one standalone Slang +/// parse. The fingerprint is diagnostic; Salsa keys the compiler artifact by a +/// tracked input containing the complete value, so hash collisions cannot +/// alias compiler artifacts. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct CompilationProfileDiagnostics { - pub diagnostics: Arc<[CompilationDiagnostic]>, +pub struct CompilationUnitSnapshot { + pub id: CompilationUnitId, + pub fingerprint: u64, + pub dependencies: CompilationDependencyManifest, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct CompilationUnitInputs { + id: CompilationUnitId, + kind: SourceFileKind, + name: String, + path: String, + text: Arc, + options: Arc, + dependencies: Arc<[FileId]>, +} + +#[salsa::tracked(debug)] +struct CompilationUnitArtifactInput<'db> { + #[returns(clone)] + inputs: Arc, +} + +/// A strictly single-file source model for editor-local operations. +/// +/// Unlike [`ParsedCompilationUnit`], this model never expands includes or +/// reads profile predefines. Its complete dependency set is the file text, +/// file kind, and display identity, so edits elsewhere cannot invalidate it. +/// +/// # Why this unexpanded parse is not U2 or U3 +/// +/// This is U1. Empty predefines, `expand_includes = false`. It cannot +/// share a tree with [`design_graph::file_facts_query`] (U2: profile +/// predefines so gated units exist in the name catalog) or +/// [`crate::compilation_plan::literal_include_targets`] (U3: profile +/// predefines plus a preprocessor `Trace` for the include graph). Sharing +/// U1 with either would make a profile edit invalidate every file-local +/// preprocessor query. +/// +/// `preprocessor_independent` is [`syntax::preprocessor_independent`] — +/// the same directive-trivia walk U2 uses. It does not depend on +/// predefines and does not materialize a `Trace`. The boolean therefore +/// cannot diverge from U2; the trees can. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceModel { + pub syntax_tree: SyntaxTree, + pub preprocessor_independent: bool, } fn source_file_identity(db: &dyn SourceRootDb, file_id: FileId) -> SourceFileIdentity { @@ -84,7 +145,36 @@ fn source_file_identity(db: &dyn SourceRootDb, file_id: FileId) -> SourceFileIde SourceFileIdentity { name, path } } -pub(crate) fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex { +#[salsa::tracked(lru = 128, returns(clone))] +fn source_model(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> Arc { + let file_id = key.file_id(db); + let text = db.file_text(file_id); + let identity = source_file_identity(db, file_id); + let syntax_tree = match db.file_kind(file_id) { + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader => { + syntax::record_unexpanded_parse("source_model"); + SyntaxTree::from_file_in_memory_with_options( + &text, + &identity.name, + &identity.path, + &syntax::SyntaxTreeOptions::without_include_expansion(), + ) + } + SourceFileKind::LibraryMap => { + SyntaxTree::from_library_map_text(&text, &identity.name, &identity.path) + } + SourceFileKind::ProjectManifest => SyntaxTree::from_text("", "", ""), + }; + let preprocessor_independent = syntax::preprocessor_independent(&syntax_tree); + Arc::new(SourceModel { syntax_tree, preprocessor_independent }) +} + +/// Workspace-global path-spelling → [`FileId`] index, memoized per revision. +#[salsa::tracked(returns(clone))] +fn path_file_ids( + db: &dyn PreprocDb, + _key: WorkspacePathIndexKey, +) -> Arc> { let mut index = PathIdentityIndex::default(); for file_id in db.files().iter().copied() { if db.file_is_project_ignored(file_id) { @@ -93,21 +183,7 @@ pub(crate) fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex let path = compilation_plan::source_buffer_path(db, file_id); index.insert_path(&path, file_id); } - index -} - -fn insert_buffer_file_ids( - buffer_file_ids: &mut FxHashMap, - path_file_ids: &PathIdentityIndex, - buffers: SyntaxTreeBufferIds, - root_file_id: FileId, -) { - buffer_file_ids.insert(buffers.root_buffer_id, root_file_id); - for buffer in buffers.source_buffers { - if let Some(file_id) = path_file_ids.get(&buffer.path) { - buffer_file_ids.insert(buffer.buffer_id, file_id); - } - } + Arc::new(index) } pub(crate) fn syntax_tree_options_for_file( @@ -116,206 +192,188 @@ pub(crate) fn syntax_tree_options_for_file( ) -> syntax::SyntaxTreeOptions { let _span = tracing::info_span!("slang.syntax_tree_options.file", ?file_id).entered(); let profile_id = db.file_compilation_profile(file_id); - let context = db.compilation_context_for_file(file_id); + let preprocess = db.project_config().preprocess_for_profile(profile_id); let identity = source_file_identity(db, file_id); - let include_buffers = db - .include_buffers_for_profile(profile_id) - .iter() + let include_buffers = compilation_plan::include_buffers_for_file(db, file_id) + .into_iter() .filter(|buffer| buffer.path != identity.path) - .cloned() .collect(); syntax::SyntaxTreeOptions { - predefines: context.predefines.to_vec(), - include_paths: context.include_dirs.iter().map(ToString::to_string).collect(), + predefines: preprocess.predefine_strings(), + include_paths: preprocess.include_dir_strings(), include_buffers, ..syntax::SyntaxTreeOptions::default() } } -fn syntax_tree_options_for_profile(context: &CompilationContext) -> syntax::SyntaxTreeOptions { - syntax::SyntaxTreeOptions { - predefines: context.predefines.to_vec(), - include_paths: context.include_dirs.iter().map(ToString::to_string).collect(), - include_buffers: Vec::new(), - ..syntax::SyntaxTreeOptions::default() - } -} - -fn syntax_tree_options_for_library_map() -> syntax::SyntaxTreeOptions { - syntax::SyntaxTreeOptions::default() -} - fn syntax_tree_options_for_parser_cursor( db: &dyn PreprocDb, file_id: FileId, ) -> syntax::SyntaxTreeOptions { + syntax_tree_options_for_file(db, file_id) +} + +#[salsa::tracked(lru = 128, returns(clone))] +fn compilation_unit_inputs( + db: &dyn PreprocDb, + key: PreprocFileQueryKey, +) -> Arc { + let file_id = key.file_id(db); let profile_id = db.file_compilation_profile(file_id); - let context = db.compilation_context_for_file(file_id); + let text = db.file_text(file_id); let identity = source_file_identity(db, file_id); - let include_buffers = if db.file_kind(file_id).is_semantic_compilation_unit() { - let plan = db.compilation_plan_for_profile(profile_id); - compilation_plan::compilation_source_buffers_for_plan(db, &plan) - } else { - db.include_buffers_for_profile(profile_id).as_ref().clone() + let kind = db.file_kind(file_id); + let options = match kind { + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader => { + // Profile predefines + this file's static include closure. + // Predecessor `$unit` macros are not injected here: that walk + // builds the profile include plan and re-parses every earlier + // root. This file's own includes carry the macros it uses. + syntax_tree_options_for_file(db, file_id) + } + SourceFileKind::LibraryMap | SourceFileKind::ProjectManifest => { + syntax::SyntaxTreeOptions::default() + } }; - syntax::SyntaxTreeOptions { - predefines: context.predefines.to_vec(), - include_paths: context.include_dirs.iter().map(ToString::to_string).collect(), - include_buffers: include_buffers + let mut dependencies = vec![file_id]; + dependencies.extend( + compilation_plan::assigned_include_buffers_for_file(db, file_id) .into_iter() - .filter(|buffer| buffer.path != identity.path) - .collect(), - ..syntax::SyntaxTreeOptions::default() - } + .map(|buffer| buffer.file_id), + ); + dependencies.sort_unstable_by_key(|dependency| dependency.index()); + dependencies.dedup(); + Arc::new(CompilationUnitInputs { + id: CompilationUnitId { root_file: file_id, profile: profile_id }, + kind, + name: identity.name, + path: identity.path, + text, + options: Arc::new(options), + dependencies: Arc::from(dependencies), + }) } #[salsa::tracked(lru = 128, returns(clone))] -fn parsed_compilation_unit(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> ParsedCompilationUnit { - let file_id = key.file_id(db); - let profile_id = db.file_compilation_profile(file_id); - let plan = db.compilation_plan_for_profile(profile_id); - if plan.roots.contains(&file_id) { - let parsed_profile = db.parsed_profile(profile_id); - let Some((_, parsed, _)) = - parsed_profile.units.iter().find(|(root_file_id, _, _)| *root_file_id == file_id) - else { - panic!( - "compilation root {file_id:?} is missing from authoritative parse for profile {profile_id:?}" - ); - }; - tracing::debug!( - ?profile_id, - ?file_id, - root_count = plan.roots.len(), - parse_mode = "authoritative", - "reusing profile root syntax tree" - ); - return parsed.clone(); - } +fn compilation_unit_snapshot( + db: &dyn PreprocDb, + key: PreprocFileQueryKey, +) -> Arc { + let inputs = compilation_unit_inputs(db, key); + let mut hasher = FxHasher::default(); + inputs.hash(&mut hasher); + let fingerprint = hasher.finish(); + Arc::new(CompilationUnitSnapshot { + id: inputs.id, + fingerprint, + dependencies: CompilationDependencyManifest { files: inputs.dependencies.clone() }, + }) +} +#[salsa::tracked] +fn compilation_unit_artifact_input<'db>( + db: &'db dyn PreprocDb, + key: PreprocFileQueryKey, +) -> CompilationUnitArtifactInput<'db> { + CompilationUnitArtifactInput::new(db, compilation_unit_inputs(db, key)) +} + +/// Content-addressed Slang artifact store. Salsa interns the complete immutable +/// input value and memoizes this parse by that identity across revisions. +#[salsa::tracked(lru = 128, returns(clone))] +fn compilation_unit_artifact( + db: &dyn PreprocDb, + key: CompilationUnitArtifactInput<'_>, +) -> Arc { + let inputs = key.inputs(db); let _span = tracing::info_span!( - "slang.parse_for_compilation", - ?profile_id, - ?file_id, - parse_mode = "authoritative" + "slang.compilation_unit_artifact", + file_id = ?inputs.id.root_file, + profile_id = ?inputs.id.profile, + include_buffer_count = inputs.options.include_buffers.len(), + bytes = inputs.text.len(), ) .entered(); - let text = { - let _span = - tracing::info_span!("slang.parse_for_compilation.file_text", ?file_id).entered(); - db.file_text(file_id) - }; - let identity = source_file_identity(db, file_id); - - match db.file_kind(file_id) { + let (syntax_tree, preprocessor_trace) = match inputs.kind { SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader => { - let options = syntax_tree_options_for_file(db, file_id); - let include_buffer_count = options.include_buffers.len(); - let _span = tracing::info_span!( - "slang.parse_for_compilation.from_text", - ?file_id, - bytes = text.len(), - include_buffer_count - ) - .entered(); let parsed = SyntaxTree::from_file_in_memory_with_options_and_trace( - &text, - &identity.name, - &identity.path, - &options, + &inputs.text, + &inputs.name, + &inputs.path, + &inputs.options, ); - ParsedCompilationUnit { - syntax_tree: parsed.tree, - preprocessor_trace: Some(parsed.preprocessor_trace), - } + (parsed.tree, Some(parsed.preprocessor_trace)) } - SourceFileKind::LibraryMap => ParsedCompilationUnit { - syntax_tree: SyntaxTree::from_library_map_text(&text, &identity.name, &identity.path), - preprocessor_trace: None, - }, - SourceFileKind::ProjectManifest => ParsedCompilationUnit { - syntax_tree: SyntaxTree::from_text("", "", ""), - preprocessor_trace: None, - }, - } + SourceFileKind::LibraryMap => { + (SyntaxTree::from_library_map_text(&inputs.text, &inputs.name, &inputs.path), None) + } + SourceFileKind::ProjectManifest => (SyntaxTree::from_text("", "", ""), None), + }; + Arc::new(ParsedCompilationUnit { syntax_tree, preprocessor_trace }) } +fn parse_tree(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> SyntaxTree { + let input = compilation_unit_artifact_input(db, key); + compilation_unit_artifact(db, *input).syntax_tree.clone() +} + +/// Preprocessor trace of one file, split from [`parse_tree`] so a syntax-only +/// edit (e.g. a comment) re-parses the tree without invalidating the trace or +/// the downstream preprocessor model and `$unit` macro chain. #[salsa::tracked(lru = 128, returns(clone))] -fn parsed_profile(db: &dyn PreprocDb, key: PreprocProfileQueryKey) -> Arc { - let profile_id = key.profile_id(db); - let context = db.compilation_context(profile_id); - let plan = db.compilation_plan_for_profile(profile_id); - let source_buffers = compilation_plan::compilation_source_buffers_for_plan(db, &plan); - let root_count = plan.roots.len(); - let _span = tracing::info_span!( - "slang.profile_parse", - ?profile_id, - root_count, - parse_mode = "authoritative" - ) - .entered(); +fn preproc_trace(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> Option> { + let input = compilation_unit_artifact_input(db, key); + compilation_unit_artifact(db, *input).preprocessor_trace.clone() +} - let mut session = Compilation::new_with_top_modules(&context.top_modules); - session.register_source_buffers(&source_buffers); - let mut units = Vec::with_capacity(root_count); - for file_id in plan.roots.iter().copied() { - let identity = source_file_identity(db, file_id); - let (syntax_tree, preprocessor_trace) = match db.file_kind(file_id) { - SourceFileKind::SystemVerilog => { - let options = syntax_tree_options_for_profile(&context); - let syntax_tree = - session.parse_syntax_tree_from_buffer(&identity.name, &identity.path, &options); - let preprocessor_trace = Some(syntax_tree.preprocessor_trace()); - (syntax_tree, preprocessor_trace) - } - SourceFileKind::LibraryMap => { - let options = syntax_tree_options_for_library_map(); - ( - session.parse_library_map_syntax_tree_from_buffer( - &identity.name, - &identity.path, - &options, - ), - None, - ) - } - SourceFileKind::IncludeHeader | SourceFileKind::ProjectManifest => { - panic!("non-compilation unit {file_id:?} appeared in profile roots") - } - }; - let buffer_ids = syntax_tree.buffer_ids(); - tracing::debug!( - ?profile_id, - ?file_id, - root_count, - parse_mode = "authoritative", - "profile root syntax tree parsed" - ); - units.push(( - file_id, - ParsedCompilationUnit { syntax_tree, preprocessor_trace }, - buffer_ids, - )); - } +/// Files actually consumed by one authoritative standalone parse. +/// +/// The preprocessor's emitted include edges are the dependency identity. This +/// deliberately does not infer reverse dependencies from source text or from +/// the profile-wide include plan. +fn parsed_compilation_dependencies(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> Arc<[FileId]> { + let file_id = key.file_id(db); + let input = compilation_unit_artifact_input(db, key); + let parsed = compilation_unit_artifact(db, *input); + dependencies_from_parsed_compilation(db, file_id, &parsed) +} - tracing::debug!( - ?profile_id, - root_count = units.len(), - parse_mode = "authoritative", - "profile authoritative parse complete" - ); - Arc::new(ParsedProfile { units: Arc::from(units) }) +fn dependencies_from_parsed_compilation( + db: &dyn PreprocDb, + file_id: FileId, + parsed: &ParsedCompilationUnit, +) -> Arc<[FileId]> { + let mut dependencies = vec![file_id]; + if let Some(trace) = &parsed.preprocessor_trace { + let source_buffer_file_ids = compilation_plan::source_buffer_file_ids_for_file(db, file_id); + dependencies.extend(trace.include_edges.iter().filter_map(|edge| { + let buffer = trace + .source_buffers + .iter() + .find(|buffer| buffer.buffer_id == edge.included_buffer_id)?; + source_buffer_file_ids.get(&buffer.path) + })); + } + dependencies.sort_unstable_by_key(|dependency| dependency.index()); + dependencies.dedup(); + Arc::from(dependencies) } #[salsa::tracked(lru = 128, returns(clone))] fn parse_src_for_compilation(db: &dyn PreprocDb, key: PreprocFileQueryKey) -> SyntaxTree { let file_id = key.file_id(db); - db.parsed_compilation_unit(file_id).syntax_tree.clone() + db.parse_tree(file_id) } pub fn set_parse_lru_capacity(db: &mut dyn PreprocDb, capacity: usize) { - parsed_profile::set_lru_capacity(db, capacity); parse_src_for_compilation::set_lru_capacity(db, capacity); + compilation_unit_inputs::set_lru_capacity(db, capacity); + compilation_unit_snapshot::set_lru_capacity(db, capacity); + compilation_unit_artifact::set_lru_capacity(db, capacity); + preproc_trace::set_lru_capacity(db, capacity); + crate::source_db::set_source_preproc_model_lru_capacity(db, capacity); + crate::macro_file::set_macro_expansion_lru_capacity(db, capacity); + crate::macro_file::set_trace_index_lru_capacity(db, capacity); } /// Parser expectations at one cursor offset. @@ -432,6 +490,13 @@ impl dyn PreprocDb + '_ { compilation_plan_for_profile(self, PreprocProfileQueryKey::new(self, profile_id)) } + pub fn static_include_closure( + &self, + file_id: FileId, + ) -> compilation_plan::StaticIncludeClosure { + compilation_plan::static_include_closure(self, file_id) + } + pub fn compilation_context( &self, profile_id: Option, @@ -443,20 +508,6 @@ impl dyn PreprocDb + '_ { compilation_context_for_file(self, file_id) } - pub fn compilation_profile_diagnostics( - &self, - profile_id: CompilationProfileId, - ) -> Arc { - compilation_profile_diagnostics(self, Some(profile_id)) - } - - pub fn include_buffers_for_profile( - &self, - profile_id: Option, - ) -> Arc> { - include_buffers_for_profile(self, profile_id) - } - pub fn source_preproc_model( &self, file_id: FileId, @@ -478,12 +529,36 @@ impl dyn PreprocDb + '_ { source_preproc_contexts_for_file(self, file_id) } - pub fn parsed_compilation_unit(&self, file_id: FileId) -> ParsedCompilationUnit { - parsed_compilation_unit(self, PreprocFileQueryKey::new(self, file_id)) + pub fn parse_tree(&self, file_id: FileId) -> SyntaxTree { + parse_tree(self, PreprocFileQueryKey::new(self, file_id)) + } + + pub fn compilation_unit_snapshot(&self, file_id: FileId) -> Arc { + compilation_unit_snapshot(self, PreprocFileQueryKey::new(self, file_id)) + } + + pub fn source_model(&self, file_id: FileId) -> Arc { + source_model(self, PreprocFileQueryKey::new(self, file_id)) + } + + pub fn preproc_trace(&self, file_id: FileId) -> Option> { + preproc_trace(self, PreprocFileQueryKey::new(self, file_id)) + } + + pub fn parsed_compilation_dependencies(&self, file_id: FileId) -> Arc<[FileId]> { + parsed_compilation_dependencies(self, PreprocFileQueryKey::new(self, file_id)) + } + + pub fn parse_src_with_dependencies(&self, file_id: FileId) -> (SyntaxTree, Arc<[FileId]>) { + let key = PreprocFileQueryKey::new(self, file_id); + let input = compilation_unit_artifact_input(self, key); + let parsed = compilation_unit_artifact(self, *input); + let dependencies = dependencies_from_parsed_compilation(self, file_id, &parsed); + (parsed.syntax_tree.clone(), dependencies) } - pub fn parsed_profile(&self, profile_id: Option) -> Arc { - parsed_profile(self, PreprocProfileQueryKey::new(self, profile_id)) + pub fn path_file_ids(&self) -> Arc> { + path_file_ids(self, WorkspacePathIndexKey::new(self, ())) } pub fn parse_src_for_compilation(&self, file_id: FileId) -> SyntaxTree { @@ -502,21 +577,6 @@ impl dyn PreprocDb + '_ { parse_diagnostics(self, file_id) } - pub fn file_compilation_diagnostics(&self, file_id: FileId) -> Arc<[CompilationDiagnostic]> { - file_compilation_diagnostics(self, file_id) - } - - pub fn semantic_diagnostics(&self, file_id: FileId) -> Arc<[SyntaxDiagnostic]> { - semantic_diagnostics(self, file_id) - } - - pub fn source_root_semantic_diagnostics( - &self, - file_id: FileId, - ) -> Arc<[(FileId, SyntaxDiagnostic)]> { - source_root_semantic_diagnostics(self, file_id) - } - pub fn macro_expansion(&self, macro_file: MacroFileId) -> Arc> { macro_file::macro_expansion_query(self, macro_file) } @@ -533,6 +593,10 @@ impl dyn PreprocDb + '_ { file_macro_coverage_query(self, file_id) } + pub fn source_semantic_map(&self, file_id: FileId) -> Arc { + macro_file::source_semantic_map_query(self, PreprocFileQueryKey::new(self, file_id)) + } + pub fn macro_reference_index_for_profile( &self, profile_id: Option, @@ -582,12 +646,14 @@ fn compilation_context( let library_maps = plan .roots .iter() - .copied() - .filter(|file_id| matches!(db.file_kind(*file_id), SourceFileKind::LibraryMap)) + .filter(|root| { + matches!(root.kind, crate::compilation_plan::CompilationRootKind::LibraryMap) + }) + .map(|root| root.file_id) .collect::>(); Arc::new(CompilationContext::new( profile_id, - plan.roots.clone(), + plan.root_file_ids().collect::>(), plan.include_dirs.clone(), plan.predefines.clone(), library_maps, @@ -600,185 +666,6 @@ fn compilation_context_for_file(db: &dyn PreprocDb, file_id: FileId) -> Arc, -) -> Arc { - let profile_id = profile_id.expect("compilation diagnostics require a concrete profile"); - let config = db.diagnostics_config(); - let _span = - tracing::info_span!("slang.profile_compilation", ?profile_id, parse_mode = "authoritative") - .entered(); - if !config.enabled { - return Arc::new(CompilationProfileDiagnostics { diagnostics: Arc::from(Vec::new()) }); - } - - let context = db.compilation_context(Some(profile_id)); - let parsed_profile = db.parsed_profile(Some(profile_id)); - let mut compilation = Compilation::new_with_top_modules(&context.top_modules); - let mut buffer_file_ids = FxHashMap::default(); - let path_file_ids = path_file_ids(db); - - for (file_id, parsed_unit, buffer_ids) in parsed_profile.units.iter() { - compilation.add_syntax_tree(&parsed_unit.syntax_tree); - let buffer_ids_for_map = buffer_ids.clone(); - insert_buffer_file_ids(&mut buffer_file_ids, &path_file_ids, buffer_ids_for_map, *file_id); - } - - let diagnostics = - compilation_diagnostics_from_compilation(&config, &compilation, &buffer_file_ids); - Arc::new(CompilationProfileDiagnostics { diagnostics }) -} - -fn compilation_diagnostics_from_compilation( - config: &DiagnosticsConfig, - compilation: &Compilation, - buffer_file_ids: &FxHashMap, -) -> Arc<[CompilationDiagnostic]> { - if !config.enabled || (!config.parse.enabled && !config.semantic.enabled) { - return Arc::from(Vec::::new()); - } - - let mut diagnostics = Vec::new(); - if config.parse.enabled { - let raw_diagnostics = { - let _span = tracing::info_span!("slang.semantic.parse_diagnostics").entered(); - compilation.parse_diagnostics_with_options(&slang_warning_options(config)) - }; - let raw_diagnostic_count = raw_diagnostics.len(); - let mut unmapped_buffer_count = 0usize; - let mut ignored_diagnostic_count = 0usize; - { - let _span = - tracing::info_span!("slang.semantic.map_parse_diagnostics", raw_diagnostic_count) - .entered(); - diagnostics.extend(raw_diagnostics.into_iter().filter_map(|diag| { - let diag_file_id = match diag - .buffer_id - .and_then(|buffer_id| buffer_file_ids.get(&buffer_id).copied()) - { - Some(file_id) => file_id, - None => { - unmapped_buffer_count += 1; - return None; - } - }; - let diag = match config.apply_rules(DiagnosticSource::Parse, diag) { - Some(diag) => diag, - None => { - ignored_diagnostic_count += 1; - return None; - } - }; - Some(CompilationDiagnostic { - file_id: diag_file_id, - source: DiagnosticSource::Parse, - diagnostic: diag, - }) - })); - } - tracing::info!( - raw_diagnostic_count, - unmapped_buffer_count, - ignored_diagnostic_count, - diagnostic_count = diagnostics.len(), - "compilation parse diagnostics complete" - ); - } - - if config.semantic.enabled { - let raw_semantic_diagnostics = { - let _span = tracing::info_span!("slang.semantic.raw_diagnostics").entered(); - compilation.semantic_diagnostics_with_options(&slang_warning_options(config)) - }; - let raw_semantic_diagnostic_count = raw_semantic_diagnostics.len(); - let mut unmapped_semantic_buffer_count = 0usize; - let mut ignored_semantic_diagnostic_count = 0usize; - { - let _span = tracing::info_span!( - "slang.semantic.map_diagnostics", - raw_semantic_diagnostic_count - ) - .entered(); - diagnostics.extend(raw_semantic_diagnostics.into_iter().filter_map(|diag| { - let diag_file_id = - diag.buffer_id.and_then(|buffer_id| buffer_file_ids.get(&buffer_id).copied()); - let Some(diag_file_id) = diag_file_id else { - unmapped_semantic_buffer_count += 1; - return None; - }; - let Some(diag) = config.apply_rules(DiagnosticSource::Semantic, diag) else { - ignored_semantic_diagnostic_count += 1; - return None; - }; - Some(CompilationDiagnostic { - file_id: diag_file_id, - source: DiagnosticSource::Semantic, - diagnostic: diag, - }) - })); - } - tracing::info!( - raw_semantic_diagnostic_count, - unmapped_semantic_buffer_count, - ignored_semantic_diagnostic_count, - diagnostic_count = diagnostics.len(), - "semantic diagnostics complete" - ); - } - - Arc::from(diagnostics) -} - -fn include_buffers_for_profile( - db: &dyn PreprocDb, - profile_id: Option, -) -> Arc> { - let plan = db.compilation_plan_for_profile(profile_id); - Arc::new(compilation_plan::include_buffers_for_plan(db, &plan)) -} - -fn semantic_diagnostics(db: &dyn PreprocDb, file_id: FileId) -> Arc<[SyntaxDiagnostic]> { - Arc::from( - db.source_root_semantic_diagnostics(file_id) - .iter() - .filter_map(|(diag_file_id, diag)| (*diag_file_id == file_id).then_some(diag.clone())) - .collect::>(), - ) -} - -fn file_compilation_diagnostics( - db: &dyn PreprocDb, - file_id: FileId, -) -> Arc<[CompilationDiagnostic]> { - let source_root_id = db.source_root_id(file_id); - let config = db.diagnostics_config(); - if !config.enabled || db.file_is_project_ignored(file_id) { - return Arc::from(Vec::::new()); - } - - let project_config = db.project_config(); - let Some(profile_id) = project_config.profile_for_root(source_root_id) else { - return Arc::from(Vec::::new()); - }; - db.compilation_profile_diagnostics(profile_id).diagnostics.clone() -} - -fn source_root_semantic_diagnostics( - db: &dyn PreprocDb, - file_id: FileId, -) -> Arc<[(FileId, SyntaxDiagnostic)]> { - Arc::from( - db.file_compilation_diagnostics(file_id) - .iter() - .filter_map(|diag| { - (diag.source == DiagnosticSource::Semantic) - .then_some((diag.file_id, diag.diagnostic.clone())) - }) - .collect::>(), - ) -} - #[cfg(test)] mod tests { use std::fmt; @@ -794,7 +681,7 @@ mod tests { }; use rustc_hash::FxHashSet; use syntax::{ - SyntaxTreeOptions, + SyntaxTreeBuffer, SyntaxTreeOptions, preproc::{SourceBufferId, SourceBufferOrigin, Trace}, }; use utils::{ @@ -967,6 +854,38 @@ mod tests { assert!(!kind.is_slang_parse_unit()); } + #[test] + fn source_model_preprocessor_independent_uses_directive_trivia() { + let mut db = db_with_root_file(); + assert!(db.source_model(TOP).preprocessor_independent); + + db.set_file_text_with_durability( + TOP, + Arc::from("`define W 8\nmodule top;\nendmodule\n"), + Durability::LOW, + ); + assert!(!db.source_model(TOP).preprocessor_independent); + + db.set_file_text_with_durability( + TOP, + Arc::from("module top;\n logic [`UNKNOWN-1:0] x;\nendmodule\n"), + Durability::LOW, + ); + assert!(!db.source_model(TOP).preprocessor_independent); + } + + #[test] + fn source_model_never_expands_includes() { + let db = db_with_macro_included_root(); + + let source = db.source_model(TOP); + let trace = source.syntax_tree.preprocessor_trace(); + + assert!(trace.include_edges.is_empty()); + let included_path = abs_path("rtl/included.sv").to_string(); + assert!(trace.source_buffers.iter().all(|buffer| buffer.path != included_path)); + } + #[test] fn systemverilog_sources_remain_parse_diagnostic_units() { let kind = SourceFileKind::from_path(&VfsPath::new_virtual_path("/rtl/top.sv".into())); @@ -975,17 +894,6 @@ mod tests { assert!(kind.is_slang_parse_unit()); } - #[test] - fn parsed_profile_uses_the_compilation_context() { - let mut db = db_with_root_file(); - db.set_project_config_with_durability(Arc::new(ProjectConfig::default()), Durability::LOW); - let profile = db.parsed_profile(None); - assert_eq!(profile.units.len(), 1); - let tree = profile.units[0].1.syntax_tree.clone(); - let root = tree.root(); - assert!(root.children().next().is_some()); - } - #[test] fn parser_expectations_are_cursor_scoped_outside_authoritative_tree() { let mut db = db_with_root_file(); @@ -993,41 +901,25 @@ mod tests { db.set_file_text_with_durability(TOP, Arc::from(text), Durability::LOW); db.set_project_config_with_durability(Arc::new(ProjectConfig::default()), Durability::LOW); - let tree = db.parsed_profile(None).units[0].1.syntax_tree.clone(); + let tree = db.parse_tree(TOP); assert!(tree.expected_syntax_at(28).is_empty()); assert!(!db.parser_expected_syntax(TOP, TextSize::from(28)).is_empty()); } #[test] - fn profile_registers_root_buffers_before_macro_include_resolution() { - let db = db_with_macro_included_root(); - let profile = db.parsed_profile(None); - let top = profile - .units - .iter() - .find(|(file_id, _, _)| *file_id == TOP) - .expect("top root should be in the profile"); - let trace = top.1.preprocessor_trace.as_ref().expect("top root should have a trace"); - - assert!(trace.source_buffers.iter().any(|buffer| { - abs_path("rtl/included.sv") == buffer.path - && buffer.text.as_deref() == Some("module included; endmodule\n") - })); - } - - #[test] - fn root_scoped_compilation_units_reuse_the_authoritative_parse() { + fn root_scoped_compilation_units_parse_standalone() { let mut db = db_with_root_file(); db.set_project_config_with_durability(Arc::new(ProjectConfig::default()), Durability::LOW); - let profile_tree = db.parsed_profile(None).units[0].1.syntax_tree.clone(); - let compilation_tree = db.parsed_compilation_unit(TOP).syntax_tree; + let compilation_tree = db.parse_tree(TOP); - assert_eq!(profile_tree, compilation_tree); + // Roots parse standalone now; the tree must still be a non-empty + // compilation unit rather than sharing the profile's buffer identity. + assert!(compilation_tree.root().children().next().is_some()); } #[test] - fn profile_compilation_units_reuse_the_authoritative_profile_parse() { + fn profile_compilation_units_parse_standalone() { let mut db = db_with_root_file(); db.set_project_config_with_durability( Arc::new(ProjectConfig::new( @@ -1041,11 +933,114 @@ mod tests { Durability::LOW, ); - let profile_tree = - db.parsed_profile(Some(CompilationProfileId(0))).units[0].1.syntax_tree.clone(); - let compilation_tree = db.parsed_compilation_unit(TOP).syntax_tree; + let compilation_tree = db.parse_tree(TOP); - assert_eq!(profile_tree, compilation_tree); + assert!(compilation_tree.root().children().next().is_some()); + } + + #[test] + fn compilation_plan_updates_when_one_files_include_directives_change() { + let mut db = db_with_macro_included_root(); + db.set_file_text_with_durability( + TOP, + Arc::from("`include \"included.sv\"\nmodule top; endmodule\n"), + Durability::LOW, + ); + + let before = db.compilation_plan_for_profile(None); + assert!(before.include_only.contains(&INCLUDED)); + assert!(!before.has_root(INCLUDED)); + + db.set_file_text_with_durability( + TOP, + Arc::from("module top; endmodule\n"), + Durability::LOW, + ); + + let after = db.compilation_plan_for_profile(None); + assert!(!after.include_only.contains(&INCLUDED)); + assert!(after.has_root(INCLUDED)); + } + + #[test] + fn compilation_unit_fingerprint_covers_include_contents() { + let mut db = db_with_macro_included_root(); + db.set_file_text_with_durability( + TOP, + Arc::from("`include \"included.sv\"\nmodule top; endmodule\n"), + Durability::LOW, + ); + let before = db.compilation_unit_snapshot(TOP); + + db.set_file_text_with_durability( + INCLUDED, + Arc::from("module included_changed; endmodule\n"), + Durability::LOW, + ); + let after = db.compilation_unit_snapshot(TOP); + + assert_ne!(before.fingerprint, after.fingerprint); + assert!(after.dependencies.files.contains(&INCLUDED)); + } + + #[test] + fn standalone_parse_registers_only_the_static_include_closure() { + let mut db = db_with_macro_included_root(); + db.set_file_text_with_durability( + TOP, + Arc::from("`include \"included.sv\"\nmodule top; endmodule\n"), + Durability::LOW, + ); + + let closure = db.static_include_closure(TOP); + assert!(closure.is_complete(), "{closure:?}"); + assert_eq!(closure.files(), &[INCLUDED]); + + let options = syntax_tree_options_for_file(&db, TOP); + assert_eq!(options.include_buffers.len(), 1); + assert!( + options.include_buffers[0].path.ends_with("included.sv"), + "{}", + options.include_buffers[0].path + ); + } + + #[test] + fn parsed_dependencies_follow_emitted_include_edges() { + let mut db = db_with_macro_included_root(); + db.set_file_text_with_durability( + TOP, + Arc::from("`include \"included.sv\"\nmodule top; endmodule\n"), + Durability::LOW, + ); + + let _ = db.parse_src_for_compilation(TOP); + let dependencies = db.parsed_compilation_dependencies(TOP); + + assert_eq!(dependencies.as_ref(), &[TOP, INCLUDED]); + } + + #[test] + fn dynamic_include_does_not_load_the_profile_as_buffers() { + let db = db_with_macro_included_root(); + let closure = db.static_include_closure(TOP); + assert!(!closure.is_complete(), "{closure:?}"); + assert!(closure.files().is_empty(), "{closure:?}"); + + let options = syntax_tree_options_for_file(&db, TOP); + assert!( + options.include_buffers.is_empty(), + "dynamic include must not register every profile file: {:?}", + options.include_buffers + ); + } + + #[test] + fn compilation_plan_records_dynamic_includes_for_authoritative_resolution() { + let db = db_with_macro_included_root(); + let plan = db.compilation_plan_for_profile(None); + + assert!(plan.dynamic_include_files.contains(&TOP)); } #[test] @@ -1100,7 +1095,18 @@ mod tests { assert!(db.parse_diagnostics(MANIFEST).is_empty()); let plan = db.compilation_plan_for_root(ROOT); - assert_eq!(plan.roots, vec![TOP]); + assert_eq!(plan.root_file_ids().collect::>(), vec![TOP]); + assert!( + plan.roots.iter().all(|root| { + matches!( + root.kind, + crate::compilation_plan::CompilationRootKind::SystemVerilog + | crate::compilation_plan::CompilationRootKind::LibraryMap + ) + }), + "{plan:?}" + ); + assert!(!plan.has_root(MANIFEST)); assert!(!plan.include_only.contains(&MANIFEST)); let preproc_model_files = @@ -1508,4 +1514,104 @@ mod tests { VfsPath::new_virtual_path("/__vide/preproc/default/predefines.sv".to_owned()) ); } + + fn db_with_abs_file(path: AbsPathBuf, text: &str) -> TestDb { + let mut file_set = FileSet::default(); + file_set.insert(TOP, VfsPath::from(path)); + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + let mut files = FxHashSet::default(); + files.insert(TOP); + let mut db = TestDb::default(); + db.set_files_with_durability(files, Durability::HIGH); + db.set_project_config_with_durability( + Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![ROOT], + top_modules: Vec::new(), + preprocess: PreprocessConfig::default(), + }], + )), + Durability::HIGH, + ); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::LOW, + ); + 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_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); + db.set_file_text_with_durability(TOP, Arc::from(text), Durability::LOW); + db + } + + fn unique_sv_path(label: &str) -> (std::path::PathBuf, AbsPathBuf) { + static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let id = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "vide-profile-ipc-{}-{}-{label}.sv", + std::process::id(), + id + )); + let abs = AbsPathBuf::assert(Utf8PathBuf::from_path_buf(path.clone()).expect("utf8 temp")); + (path, abs) + } + + #[test] + fn clean_profile_job_omits_source_text() { + let disk = "module clean;\nendmodule\n"; + let (path, abs) = unique_sv_path("clean"); + std::fs::write(&path, disk).unwrap(); + let db = db_with_abs_file(abs, disk); + let job = + crate::profile_compiler::build_profile_compilation_job(&db, CompilationProfileId(0)); + assert!( + job.buffers.iter().all(|buffer| buffer.text.is_none()), + "clean files must be path-only: {job:?}" + ); + let encoded = serde_json::to_string(&job).unwrap(); + assert!( + !encoded.contains("module clean"), + "clean-file JSON must not include source text: {encoded}" + ); + let output = crate::profile_compiler::run_profile_compilation(job); + let _ = std::fs::remove_file(&path); + assert!( + !output.diagnostics.iter().any(|diagnostic| diagnostic.file_id == TOP.index()), + "worker must compile the on-disk clean file: {output:?}" + ); + } + + #[test] + fn dirty_overlay_is_compiled_instead_of_disk() { + let disk = "module clean;\nendmodule\n"; + let overlay = "module broken(;\nendmodule\n"; + let (path, abs) = unique_sv_path("dirty"); + std::fs::write(&path, disk).unwrap(); + let mut db = db_with_abs_file(abs, overlay); + let job = + crate::profile_compiler::build_profile_compilation_job(&db, CompilationProfileId(0)); + assert_eq!( + job.buffers + .iter() + .find(|buffer| buffer.file_id == TOP.index()) + .map(|b| b.text.as_deref()), + Some(Some(overlay)), + "dirty overlay must be sent: {job:?}" + ); + let output = crate::profile_compiler::run_profile_compilation(job.clone()); + assert!( + output.diagnostics.iter().any(|diagnostic| diagnostic.file_id == TOP.index()), + "overlay syntax error must compile: {output:?}" + ); + + std::fs::write(&path, "module rewritten;\nendmodule\n").unwrap(); + let output = crate::profile_compiler::run_profile_compilation(job); + let _ = std::fs::remove_file(&path); + assert!( + output.diagnostics.iter().any(|diagnostic| diagnostic.file_id == TOP.index()), + "a disk rewrite the VFS has not applied must not be compiled: {output:?}" + ); + db.set_file_text_with_durability(TOP, Arc::from(disk), Durability::LOW); + } } diff --git a/crates/preproc-expand/src/lib.rs b/crates/preproc-expand/src/lib.rs index 50345dcdf..c5393b38b 100755 --- a/crates/preproc-expand/src/lib.rs +++ b/crates/preproc-expand/src/lib.rs @@ -12,4 +12,5 @@ pub mod db; pub mod file; pub mod macro_file; pub mod preproc; +pub mod profile_compiler; pub mod source_db; diff --git a/crates/preproc-expand/src/macro_file.rs b/crates/preproc-expand/src/macro_file.rs index c5c46c6d6..22fa1d40a 100644 --- a/crates/preproc-expand/src/macro_file.rs +++ b/crates/preproc-expand/src/macro_file.rs @@ -13,7 +13,7 @@ use utils::line_index::{TextRange, TextSize}; use vfs::FileId; use crate::{ - db::PreprocDb, + db::{PreprocDb, PreprocFileQueryKey}, preproc::{MacroDefinition, map_macro_definition}, source_db::{MappedSourcePreprocModel, SourcePreprocQueryError}, }; @@ -154,6 +154,34 @@ pub struct MacroFileCallSite { pub call_range: TextRange, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceSemanticAnchor { + pub source_range: TextRange, + pub expansion: MacroFileId, +} + +/// Per-source-file mapping from raw invocation ranges to their expanded +/// semantic files. Built once per preprocessor revision so caret and rename +/// queries do not repeatedly rediscover macro files by offset. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceSemanticMap { + complete: bool, + anchors: Box<[SourceSemanticAnchor]>, +} + +impl SourceSemanticMap { + pub fn macro_origin_for_range(&self, range: TextRange) -> Option { + self.complete.then(|| self.anchors.iter().any(|anchor| anchor.source_range == range)) + } + + pub fn expansions_at(&self, offset: TextSize) -> impl Iterator + '_ { + self.anchors + .iter() + .filter(move |anchor| anchor.source_range.contains(offset)) + .map(|anchor| anchor.expansion) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroFileExpansion { pub call_file_id: FileId, @@ -191,8 +219,7 @@ pub fn macro_files_at_offset( return Vec::new(); } }; - let parsed = db.parsed_compilation_unit(model_file); - if parsed.preprocessor_trace.is_none() { + if db.preproc_trace(model_file).is_none() { tracing::warn!( ?file_id, ?model_file, @@ -263,8 +290,7 @@ pub fn macro_files_for_file(db: &dyn PreprocDb, file_id: FileId) -> Vec Vec Option> { let contexts = db.source_preproc_contexts_for_file(file_id); - if let crate::source_db::SourcePreprocContextStatus::Partial { skipped_models } = - contexts.status - { - tracing::warn!( - ?file_id, - skipped_models, - "macro expansion query unavailable because preprocessor contexts are partial" - ); - return None; - } - let mut model_file_ids = vec![file_id]; for model_file_id in &contexts.model_file_ids { if !model_file_ids.contains(model_file_id) { @@ -337,6 +352,48 @@ fn relevant_model_files(db: &dyn PreprocDb, file_id: FileId) -> Option Arc { + let file_id = key.file_id(db); + let Some(model_file_ids) = relevant_model_files(db, file_id) else { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + }; + let mut anchors = Vec::new(); + for model_file in model_file_ids { + let mapped = db.source_preproc_model(model_file); + let Ok(mapped) = mapped.as_ref() else { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + }; + for call in mapped.model.macro_calls().iter() { + let Ok(call_file) = mapped.source_map.file_id(call.call_range.source) else { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + }; + if call_file != file_id { + continue; + } + let Some(trace_call) = call.trace_call else { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + }; + if db.trace_index(model_file).emitted_range_for_call(trace_call).is_none() { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + } + let Ok(source_range) = mapped.source_map.map_range(call.call_range) else { + return Arc::new(SourceSemanticMap { complete: false, anchors: Box::new([]) }); + }; + anchors.push(SourceSemanticAnchor { + source_range, + expansion: MacroFileId::new(db, MacroCallLoc { model_file, trace_call }), + }); + } + } + anchors.sort_unstable_by_key(|anchor| (anchor.source_range.start(), anchor.source_range.end())); + anchors.dedup(); + Arc::new(SourceSemanticMap { complete: true, anchors: anchors.into_boxed_slice() }) +} + pub fn macro_file_call_site( db: &dyn PreprocDb, macro_file: MacroFileId, @@ -400,8 +457,8 @@ pub fn macro_file_expansion( tracing::warn!(?macro_file, "macro expansion has no source call for its trace identity"); return None; }; - let parsed = db.parsed_compilation_unit(call_loc.model_file); - let Some(trace) = parsed.preprocessor_trace.as_ref() else { + let trace_opt = db.preproc_trace(call_loc.model_file); + let Some(trace) = trace_opt.as_ref() else { tracing::warn!(?macro_file, "macro expansion has no preprocessor trace"); return None; }; @@ -430,6 +487,10 @@ pub(crate) fn macro_expansion_query( Arc::new(macro_expansion(db, macro_file)) } +pub(crate) fn set_macro_expansion_lru_capacity(db: &mut dyn PreprocDb, capacity: usize) { + macro_expansion_query::set_lru_capacity(db, capacity); +} + fn macro_expansion(db: &dyn PreprocDb, macro_file: MacroFileId) -> ExpandResult { let call_loc = macro_file.loc(db); let mapped = db.source_preproc_model(call_loc.model_file); @@ -450,8 +511,8 @@ fn macro_expansion(db: &dyn PreprocDb, macro_file: MacroFileId) -> ExpandResult< ExpandErrorKind::MissingTraceCall { trace_call: call_loc.trace_call }, ); }; - let parsed = db.parsed_compilation_unit(call_loc.model_file); - let Some(trace) = parsed.preprocessor_trace.as_ref() else { + let trace_opt = db.preproc_trace(call_loc.model_file); + let Some(trace) = trace_opt.as_ref() else { return expansion_error( String::new(), ExpansionSourceMap::empty(), @@ -620,13 +681,16 @@ pub(crate) fn trace_index_query( key: crate::db::PreprocFileQueryKey, ) -> Arc { let model_file = key.file_id(db); - let parsed = db.parsed_compilation_unit(model_file); - match parsed.preprocessor_trace.as_ref() { + match db.preproc_trace(model_file).as_ref() { Some(trace) => Arc::new(TraceIndex::new(trace)), None => Arc::new(TraceIndex::default()), } } +pub(crate) fn set_trace_index_lru_capacity(db: &mut dyn PreprocDb, capacity: usize) { + trace_index_query::set_lru_capacity(db, capacity); +} + /// Parent-expansion links. Slang records them on each emitted token's origin /// (the expansion chain of the token), not on the usage events, so the map is /// built from token origins. diff --git a/crates/preproc-expand/src/macro_file/tests.rs b/crates/preproc-expand/src/macro_file/tests.rs index e781bd4c6..8f0cdc8cd 100644 --- a/crates/preproc-expand/src/macro_file/tests.rs +++ b/crates/preproc-expand/src/macro_file/tests.rs @@ -254,8 +254,8 @@ fn trace_macro_argument_origin_indices_are_exact() { let db = db_with_root_text( "`define PICK(a, b) b\nmodule top; wire x = `PICK(first, second); endmodule\n", ); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); assert!(trace.emitted_tokens.iter().any(|token| { matches!( @@ -378,8 +378,8 @@ fn macro_expansion_reports_preproc_model_failure() { #[test] fn expansion_text_reports_missing_emitted_token() { let db = db_with_root_text("`define ONE 1\n`ONE\n"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); let missing = SourceEmittedTokenId::new(trace.emitted_tokens.len()); let expansion = @@ -396,8 +396,8 @@ fn expansion_source_map_reports_missing_trace_token() { let db = db_with_root_text("`define ONE 1\n`ONE\n"); let mapped = db.source_preproc_model(TOP); let mapped = mapped.as_ref().as_ref().expect("preproc model should be available"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); let missing = SourceEmittedTokenId::new(trace.emitted_tokens.len()); let expansion = ExpansionSourceMap::from_trace_range( @@ -420,8 +420,8 @@ fn expansion_source_map_reports_missing_trace_token() { #[test] fn expansion_text_validates_zero_length_range_start() { let db = db_with_root_text("`define EMPTY\n`EMPTY\n"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); let table_len = trace.emitted_tokens.len(); let valid_start = SourceEmittedTokenId::new(table_len); @@ -446,8 +446,8 @@ fn expansion_source_map_validates_zero_length_range_start() { let db = db_with_root_text("`define EMPTY\n`EMPTY\n"); let mapped = db.source_preproc_model(TOP); let mapped = mapped.as_ref().as_ref().expect("preproc model should be available"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); let table_len = trace.emitted_tokens.len(); let valid_start = SourceEmittedTokenId::new(table_len); @@ -485,8 +485,8 @@ fn expansion_source_map_preserves_valid_prefix_before_missing_trace_token() { let db = db_with_root_text("`define ONE 1\n`ONE\n"); let mapped = db.source_preproc_model(TOP); let mapped = mapped.as_ref().as_ref().expect("preproc model should be available"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); let table_len = trace.emitted_tokens.len(); assert!(table_len > 0, "fixture should emit at least one token"); let missing = SourceEmittedTokenId::new(table_len); @@ -513,8 +513,8 @@ fn expansion_info_preserves_source_map_when_text_extraction_fails() { let db = db_with_root_text("`define ONE 1\n`ONE\n"); let mapped = db.source_preproc_model(TOP); let mapped = mapped.as_ref().as_ref().expect("preproc model should be available"); - let parsed = db.parsed_compilation_unit(TOP); - let trace = parsed.preprocessor_trace.as_ref().expect("preprocessor trace should be available"); + let trace_opt = db.preproc_trace(TOP); + let trace = trace_opt.as_ref().expect("preprocessor trace should be available"); assert!(!trace.emitted_tokens.is_empty(), "fixture should emit at least one token"); let source_map = ExpansionSourceMap::from_trace_range( &db, diff --git a/crates/preproc-expand/src/preproc.rs b/crates/preproc-expand/src/preproc.rs index dbe9ccf1c..11c82da8a 100644 --- a/crates/preproc-expand/src/preproc.rs +++ b/crates/preproc-expand/src/preproc.rs @@ -19,8 +19,8 @@ pub(crate) use self::reference_index::macro_reference_index_for_profile_query; use crate::{ db::PreprocDb, source_db::{ - MappedSourcePreprocModel, PreprocSourceMapping, SourcePreprocContextStatus, - SourcePreprocQueryError, workspace_preproc_model_file_ids, + MappedSourcePreprocModel, PreprocSourceMapping, SourcePreprocQueryError, + workspace_preproc_model_file_ids, }, }; diff --git a/crates/preproc-expand/src/preproc/helpers/context.rs b/crates/preproc-expand/src/preproc/helpers/context.rs index b587cd0f3..dd509211f 100644 --- a/crates/preproc-expand/src/preproc/helpers/context.rs +++ b/crates/preproc-expand/src/preproc/helpers/context.rs @@ -9,16 +9,6 @@ pub(in crate::preproc) fn mapped_result( #[derive(Debug, Clone, PartialEq, Eq)] pub(in crate::preproc) struct SourcePreprocQueryContexts { pub(in crate::preproc) model_file_ids: Vec, - pub(in crate::preproc) status: SourcePreprocContextStatus, -} - -impl SourcePreprocQueryContexts { - fn partial_error(&self) -> Option { - let SourcePreprocContextStatus::Partial { skipped_models } = self.status else { - return None; - }; - Some(PreprocError::PartialPreprocContextIndex { skipped_models }) - } } pub(in crate::preproc) fn source_preproc_single_query_contexts( @@ -43,20 +33,17 @@ pub(in crate::preproc) fn source_preproc_single_query_contexts( for model_file_id in relevant.model_file_ids.iter().copied() { file_ids.push_unique(model_file_id); } - SourcePreprocQueryContexts { model_file_ids: file_ids.into_vec(), status: relevant.status } + SourcePreprocQueryContexts { model_file_ids: file_ids.into_vec() } } pub(in crate::preproc) fn finish_empty_single_query( - contexts: &SourcePreprocQueryContexts, + _contexts: &SourcePreprocQueryContexts, first_error: Option, ) -> PreprocResult<()> { - if let Some(error) = first_error { - return Err(error); + match first_error { + Some(error) => Err(error), + None => Ok(()), } - if let Some(error) = contexts.partial_error() { - return Err(error); - } - Ok(()) } pub(in crate::preproc) fn record_first_error( @@ -118,14 +105,6 @@ impl ContextQuery { ); return Err(error); } - if let Some(error) = self.contexts.partial_error() { - tracing::warn!( - ?self.file_id, - ?error, - "preprocessor query uses a partial context index" - ); - return Err(error); - } Ok(()) } } diff --git a/crates/preproc-expand/src/preproc/helpers/diagnostics.rs b/crates/preproc-expand/src/preproc/helpers/diagnostics.rs index d0fe6482c..cf1041269 100644 --- a/crates/preproc-expand/src/preproc/helpers/diagnostics.rs +++ b/crates/preproc-expand/src/preproc/helpers/diagnostics.rs @@ -17,8 +17,8 @@ pub(in crate::preproc) fn diagnostic_target_for_call( let Some(trace_call) = source_call.trace_call else { return Ok(None); }; - let parsed = db.parsed_compilation_unit(model_file); - let Some(trace) = parsed.preprocessor_trace.as_ref() else { + let trace_opt = db.preproc_trace(model_file); + let Some(trace) = trace_opt.as_ref() else { return Ok(None); }; let Some(emitted_range) = db.trace_index(model_file).emitted_range_for_call(trace_call) else { diff --git a/crates/preproc-expand/src/preproc/tests.rs b/crates/preproc-expand/src/preproc/tests.rs index 104d0d5b5..5039e4d78 100644 --- a/crates/preproc-expand/src/preproc/tests.rs +++ b/crates/preproc-expand/src/preproc/tests.rs @@ -20,6 +20,7 @@ use vfs::{AnchoredPath, FileId, FileSet, VfsPath}; use super::*; use crate::{ + compilation_plan::include_buffers_for_file, db::PreprocDb, macro_file::{MacroFileId, macro_files_at_offset}, }; diff --git a/crates/preproc-expand/src/preproc/tests/include_context.rs b/crates/preproc-expand/src/preproc/tests/include_context.rs index 4bc533d23..1336ea126 100644 --- a/crates/preproc-expand/src/preproc/tests/include_context.rs +++ b/crates/preproc-expand/src/preproc/tests/include_context.rs @@ -103,9 +103,9 @@ fn preproc_include_only_sv_query_uses_all_including_roots() { let plan = db.compilation_plan_for_profile(Some(PROFILE)); assert!(plan.include_only.contains(&HEADER), "{plan:?}"); - assert!(plan.roots.contains(&TOP), "{plan:?}"); - assert!(plan.roots.contains(&LEAF), "{plan:?}"); - assert!(!plan.roots.contains(&HEADER), "{plan:?}"); + assert!(plan.has_root(TOP), "{plan:?}"); + assert!(plan.has_root(LEAF), "{plan:?}"); + assert!(!plan.has_root(HEADER), "{plan:?}"); let contexts = source_preproc_single_query_contexts(&db, HEADER); assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); @@ -157,15 +157,3 @@ fn preproc_header_without_including_context_uses_standalone_model() { assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); assert!(!contexts.model_file_ids.contains(&TOP), "{contexts:?}"); } - -#[test] -fn preproc_partial_context_index_is_structured_unavailable() { - let contexts = SourcePreprocQueryContexts { - model_file_ids: Vec::new(), - status: SourcePreprocContextStatus::Partial { skipped_models: 2 }, - }; - - let error = finish_empty_single_query(&contexts, None).unwrap_err(); - - assert!(matches!(error, PreprocError::PartialPreprocContextIndex { skipped_models: 2 })); -} diff --git a/crates/preproc-expand/src/preproc/tests/manifest.rs b/crates/preproc-expand/src/preproc/tests/manifest.rs index 9e2796956..0c6d1d36b 100644 --- a/crates/preproc-expand/src/preproc/tests/manifest.rs +++ b/crates/preproc-expand/src/preproc/tests/manifest.rs @@ -139,3 +139,34 @@ wire active; assert_eq!(branches[0].file_id, TOP); assert!(text_at_range(root_text, branches[0].range).contains("disabled_by_header")); } + +#[test] +fn preproc_inactive_branch_uses_parent_relative_header_include() { + let root_text = concat!( + "`include \"../rtl/config.vh\"\n", + "`ifndef HEADER_FLAG\n", + "wire should_be_inactive;\n", + "`endif\n", + ); + let header_text = concat!( + "`define HEADER_FLAG\n", + "`ifdef NEVER_DEFINED\n", + "wire header_inactive;\n", + "`endif\n", + ); + let db = + db_with_entries(&[(TOP, "rtl/top.v", root_text), (HEADER, "rtl/config.vh", header_text)]); + + let buffers = include_buffers_for_file(&db, TOP); + assert_eq!(buffers.len(), 1, "one include edge issues one slang_path: {buffers:?}"); + let path = buffers[0].path.replace('\\', "/"); + assert!( + path.contains("rtl/../rtl/config.vh"), + "include buffer must be issued under slang's local join spelling: {buffers:?}" + ); + + let branches = inactive_branches(&db, TOP).unwrap(); + assert_eq!(branches.len(), 1); + assert_eq!(branches[0].file_id, TOP); + assert!(text_at_range(root_text, branches[0].range).contains("should_be_inactive")); +} diff --git a/crates/preproc-expand/src/preproc/types/common.rs b/crates/preproc-expand/src/preproc/types/common.rs index ffe85ba3a..73088c5c8 100644 --- a/crates/preproc-expand/src/preproc/types/common.rs +++ b/crates/preproc-expand/src/preproc/types/common.rs @@ -33,10 +33,6 @@ pub enum PreprocError { directive_file_id: FileId, name_file_id: FileId, }, - /// The preproc context index was partial because some compilation models - /// could not be queried; queries that ran were valid but the result is - /// not authoritative across the whole project. - PartialPreprocContextIndex { skipped_models: usize }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/preproc-expand/src/profile_compiler.rs b/crates/preproc-expand/src/profile_compiler.rs new file mode 100644 index 000000000..cbc3c4fd1 --- /dev/null +++ b/crates/preproc-expand/src/profile_compiler.rs @@ -0,0 +1,640 @@ +use base_db::{ + diagnostics_config::{ + DiagnosticRuleSeverity, DiagnosticSelector, DiagnosticSource, DiagnosticsConfig, + }, + project::CompilationProfileId, +}; +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use syntax::{ + SyntaxTreeBuffer, SyntaxTreeOptions, + compilation::Compilation, + diagnostics::{DiagnosticSeverity, SyntaxDiagnostic}, +}; +use vfs::FileId; + +use crate::{ + compilation_plan, + db::{CompilationDiagnostic, PreprocDb}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileCompilationJob { + pub profile_id: u32, + pub roots: Vec, + pub buffers: Vec, + pub top_modules: Vec, + pub include_dirs: Vec, + pub predefines: Vec, + pub diagnostics: ProfileDiagnosticsOptions, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileCompilationRoot { + pub file_id: u32, + pub kind: ProfileRootKind, + pub name: String, + pub path: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileCompilationBuffer { + pub file_id: u32, + pub path: String, + /// Dirty or virtual overlay. `None` means the worker reads `path` from + /// disk. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProfileRootKind { + SystemVerilog, + LibraryMap, +} + +impl From for ProfileRootKind { + fn from(kind: compilation_plan::CompilationRootKind) -> Self { + match kind { + compilation_plan::CompilationRootKind::SystemVerilog => Self::SystemVerilog, + compilation_plan::CompilationRootKind::LibraryMap => Self::LibraryMap, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileDiagnosticsOptions { + pub parse: bool, + pub semantic: bool, + pub warnings: Option>, + pub rules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileDiagnosticRule { + pub selector: ProfileDiagnosticSelector, + pub severity: ProfileDiagnosticRuleSeverity, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProfileDiagnosticSelector { + Code { subsystem: u16, code: u16 }, + Option(String), + Group(String), + Source(ProfileDiagnosticSource), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProfileDiagnosticRuleSeverity { + Ignore, + Info, + Warning, + Error, + Fatal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProfileDiagnosticSource { + Parse, + Semantic, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileCompilationOutput { + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProfileCompilationDiagnostic { + pub file_id: u32, + pub source: ProfileDiagnosticSource, + #[serde(with = "syntax_diagnostic_serde")] + pub diagnostic: SyntaxDiagnostic, +} + +pub fn build_profile_compilation_job( + db: &dyn PreprocDb, + profile_id: CompilationProfileId, +) -> ProfileCompilationJob { + let plan = db.compilation_plan_for_profile(Some(profile_id)); + let context = db.compilation_context(Some(profile_id)); + let config = db.diagnostics_config(); + let buffers = compilation_plan::compilation_source_buffers_for_plan(db, &plan) + .into_iter() + .map(|buffer| ProfileCompilationBuffer { + file_id: buffer.file_id.index(), + path: buffer.path.clone(), + text: overlay_text_for_compilation_buffer( + db, + buffer.file_id, + &buffer.path, + &buffer.text, + ), + }) + .collect(); + let roots = plan + .roots + .iter() + .copied() + .map(|root| { + let path = compilation_plan::source_buffer_path(db, root.file_id).to_string(); + let name = db + .file_path(root.file_id) + .map(|path| path.to_string()) + .unwrap_or_else(|| "source".to_owned()); + ProfileCompilationRoot { + file_id: root.file_id.index(), + kind: ProfileRootKind::from(root.kind), + name, + path, + } + }) + .collect(); + ProfileCompilationJob { + profile_id: profile_id.0, + roots, + buffers, + top_modules: context.top_modules.to_vec(), + include_dirs: context.include_dirs.iter().map(ToString::to_string).collect(), + predefines: context.predefines.to_vec(), + diagnostics: diagnostics_options(&config), + } +} + +pub fn run_profile_compilation(job: ProfileCompilationJob) -> ProfileCompilationOutput { + let mut compilation = Compilation::new_with_top_modules(&job.top_modules); + compilation.register_source_buffers( + &job.buffers + .iter() + .map(|buffer| SyntaxTreeBuffer { + path: buffer.path.clone(), + text: resolved_buffer_text(buffer), + }) + .collect::>(), + ); + let path_file_ids = job + .buffers + .iter() + .map(|buffer| (buffer.path.as_str(), buffer.file_id)) + .collect::>(); + let mut buffer_file_ids = FxHashMap::default(); + for root in &job.roots { + let options = match root.kind { + ProfileRootKind::SystemVerilog => SyntaxTreeOptions { + predefines: job.predefines.clone(), + include_paths: job.include_dirs.clone(), + include_buffers: Vec::new(), + ..SyntaxTreeOptions::default() + }, + ProfileRootKind::LibraryMap => SyntaxTreeOptions::default(), + }; + let tree = match root.kind { + ProfileRootKind::SystemVerilog => { + compilation.parse_syntax_tree_from_buffer(&root.name, &root.path, &options) + } + ProfileRootKind::LibraryMap => compilation + .parse_library_map_syntax_tree_from_buffer(&root.name, &root.path, &options), + }; + let buffers = tree.buffer_ids(); + buffer_file_ids.insert(buffers.root_buffer_id, root.file_id); + for source in buffers.source_buffers { + if let Some(file_id) = path_file_ids.get(source.path.as_str()) { + buffer_file_ids.insert(source.buffer_id, *file_id); + } + } + } + + let warning_options = match &job.diagnostics.warnings { + Some(options) if options.is_empty() => vec!["none".to_owned()], + Some(options) => options.clone(), + None => Vec::new(), + }; + let mut diagnostics = Vec::new(); + if job.diagnostics.parse { + collect_diagnostics( + &job.diagnostics, + ProfileDiagnosticSource::Parse, + compilation.parse_diagnostics_with_options(&warning_options), + &buffer_file_ids, + &mut diagnostics, + ); + } + if job.diagnostics.semantic { + collect_diagnostics( + &job.diagnostics, + ProfileDiagnosticSource::Semantic, + compilation.semantic_diagnostics_with_options(&warning_options), + &buffer_file_ids, + &mut diagnostics, + ); + } + ProfileCompilationOutput { diagnostics } +} + +impl ProfileCompilationOutput { + pub fn into_diagnostics(self) -> Vec { + self.diagnostics + .into_iter() + .map(|diagnostic| CompilationDiagnostic { + file_id: FileId::from_raw(diagnostic.file_id), + source: match diagnostic.source { + ProfileDiagnosticSource::Parse => DiagnosticSource::Parse, + ProfileDiagnosticSource::Semantic => DiagnosticSource::Semantic, + }, + diagnostic: diagnostic.diagnostic, + }) + .collect() + } +} + +/// Send text only when the VFS buffer is not the on-disk file. Virtual +/// paths and unreadable/mismatched disk files are overlays. +pub fn overlay_text_for_compilation_buffer( + db: &dyn PreprocDb, + file_id: vfs::FileId, + path: &str, + text: &str, +) -> Option { + let disk_path = + db.file_path(file_id).map(|path| path.to_string()).unwrap_or_else(|| path.to_owned()); + match std::fs::read_to_string(&disk_path) { + Ok(disk) if disk == text => None, + _ => Some(text.to_owned()), + } +} + +fn resolved_buffer_text(buffer: &ProfileCompilationBuffer) -> String { + match &buffer.text { + Some(text) => text.clone(), + None => std::fs::read_to_string(&buffer.path).unwrap_or_else(|error| { + panic!("compiler worker failed to read clean file {}: {error}", buffer.path) + }), + } +} + +fn diagnostics_options(config: &DiagnosticsConfig) -> ProfileDiagnosticsOptions { + ProfileDiagnosticsOptions { + parse: config.enabled && config.parse.enabled, + semantic: config.enabled && config.semantic.enabled, + warnings: config.slang.warnings.clone(), + rules: config + .slang + .rules + .iter() + .map(|rule| ProfileDiagnosticRule { + selector: match &rule.selector { + DiagnosticSelector::Code { subsystem, code } => { + ProfileDiagnosticSelector::Code { subsystem: *subsystem, code: *code } + } + DiagnosticSelector::Option(option) => { + ProfileDiagnosticSelector::Option(option.clone()) + } + DiagnosticSelector::Group(group) => { + ProfileDiagnosticSelector::Group(group.clone()) + } + DiagnosticSelector::Source(source) => { + ProfileDiagnosticSelector::Source(match source { + DiagnosticSource::Parse => ProfileDiagnosticSource::Parse, + DiagnosticSource::Semantic => ProfileDiagnosticSource::Semantic, + }) + } + }, + severity: match rule.severity { + DiagnosticRuleSeverity::Ignore => ProfileDiagnosticRuleSeverity::Ignore, + DiagnosticRuleSeverity::Info => ProfileDiagnosticRuleSeverity::Info, + DiagnosticRuleSeverity::Warning => ProfileDiagnosticRuleSeverity::Warning, + DiagnosticRuleSeverity::Error => ProfileDiagnosticRuleSeverity::Error, + DiagnosticRuleSeverity::Fatal => ProfileDiagnosticRuleSeverity::Fatal, + }, + }) + .collect(), + } +} + +fn collect_diagnostics( + options: &ProfileDiagnosticsOptions, + source: ProfileDiagnosticSource, + raw: Vec, + buffer_file_ids: &FxHashMap, + diagnostics: &mut Vec, +) { + diagnostics.extend(raw.into_iter().filter_map(|diagnostic| { + let file_id = + diagnostic.buffer_id.and_then(|buffer_id| buffer_file_ids.get(&buffer_id).copied())?; + let diagnostic = apply_rules(options, source, diagnostic)?; + Some(ProfileCompilationDiagnostic { file_id, source, diagnostic }) + })); +} + +fn apply_rules( + options: &ProfileDiagnosticsOptions, + source: ProfileDiagnosticSource, + mut diagnostic: SyntaxDiagnostic, +) -> Option { + for rule in &options.rules { + let matches = match &rule.selector { + ProfileDiagnosticSelector::Code { subsystem, code } => { + diagnostic.subsystem == *subsystem && diagnostic.code == *code + } + ProfileDiagnosticSelector::Option(option) => { + diagnostic.option_name.as_deref() == Some(option) + } + ProfileDiagnosticSelector::Group(group) => { + diagnostic.groups.iter().any(|candidate| candidate == group) + } + ProfileDiagnosticSelector::Source(rule_source) => source == *rule_source, + }; + if !matches { + continue; + } + diagnostic.severity = match rule.severity { + ProfileDiagnosticRuleSeverity::Ignore => return None, + ProfileDiagnosticRuleSeverity::Info => DiagnosticSeverity::Note, + ProfileDiagnosticRuleSeverity::Warning => DiagnosticSeverity::Warning, + ProfileDiagnosticRuleSeverity::Error => DiagnosticSeverity::Error, + ProfileDiagnosticRuleSeverity::Fatal => DiagnosticSeverity::Fatal, + }; + } + (diagnostic.severity != DiagnosticSeverity::Ignored).then_some(diagnostic) +} + +mod syntax_diagnostic_serde { + use std::ops::Range; + + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use syntax::diagnostics::{ + DiagnosticSeverity, SyntaxDiagnostic, SyntaxDiagnosticExpansion, SyntaxDiagnosticLocation, + SyntaxDiagnosticRange, + }; + + #[derive(Serialize, Deserialize)] + struct DiagnosticRepr { + code: u16, + subsystem: u16, + #[serde(with = "severity_serde")] + severity: DiagnosticSeverity, + message: String, + args: Vec, + name: String, + option_name: Option, + groups: Vec, + primary_range: Option>, + location: Option, + buffer_id: Option, + file_name: Option, + ranges: Vec, + expansion_locations: Vec, + include_stack: Vec, + diagnostic_id: u32, + parent_diagnostic_id: Option, + } + + #[derive(Serialize, Deserialize)] + struct LocationRepr { + offset: usize, + buffer_id: u32, + file_name: Option, + } + + #[derive(Serialize, Deserialize)] + struct RangeRepr { + start: usize, + end: usize, + start_buffer_id: u32, + end_buffer_id: u32, + } + + #[derive(Serialize, Deserialize)] + struct ExpansionRepr { + location: Option, + original_location: Option, + macro_name: String, + } + + mod severity_serde { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use syntax::diagnostics::DiagnosticSeverity; + + #[derive(Serialize, Deserialize)] + enum Severity { + Ignored, + Note, + Warning, + Error, + Fatal, + } + + pub fn serialize( + value: &DiagnosticSeverity, + serializer: S, + ) -> Result { + let value = match *value { + DiagnosticSeverity::Ignored => Severity::Ignored, + DiagnosticSeverity::Note => Severity::Note, + DiagnosticSeverity::Warning => Severity::Warning, + DiagnosticSeverity::Error => Severity::Error, + DiagnosticSeverity::Fatal => Severity::Fatal, + }; + value.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + Ok(match Severity::deserialize(deserializer)? { + Severity::Ignored => DiagnosticSeverity::Ignored, + Severity::Note => DiagnosticSeverity::Note, + Severity::Warning => DiagnosticSeverity::Warning, + Severity::Error => DiagnosticSeverity::Error, + Severity::Fatal => DiagnosticSeverity::Fatal, + }) + } + } + + fn location_from(location: SyntaxDiagnosticLocation) -> LocationRepr { + LocationRepr { + offset: location.offset, + buffer_id: location.buffer_id, + file_name: location.file_name, + } + } + + fn location_into(location: LocationRepr) -> SyntaxDiagnosticLocation { + SyntaxDiagnosticLocation { + offset: location.offset, + buffer_id: location.buffer_id, + file_name: location.file_name, + } + } + + pub fn serialize( + value: &SyntaxDiagnostic, + serializer: S, + ) -> Result { + DiagnosticRepr { + code: value.code, + subsystem: value.subsystem, + severity: value.severity, + message: value.message.clone(), + args: value.args.clone(), + name: value.name.clone(), + option_name: value.option_name.clone(), + groups: value.groups.clone(), + primary_range: value.primary_range.clone(), + location: value.location, + buffer_id: value.buffer_id, + file_name: value.file_name.clone(), + ranges: value + .ranges + .iter() + .map(|range| RangeRepr { + start: range.start, + end: range.end, + start_buffer_id: range.start_buffer_id, + end_buffer_id: range.end_buffer_id, + }) + .collect(), + expansion_locations: value + .expansion_locations + .iter() + .map(|expansion| ExpansionRepr { + location: expansion.location.clone().map(location_from), + original_location: expansion.original_location.clone().map(location_from), + macro_name: expansion.macro_name.clone(), + }) + .collect(), + include_stack: value.include_stack.iter().cloned().map(location_from).collect(), + diagnostic_id: value.diagnostic_id, + parent_diagnostic_id: value.parent_diagnostic_id, + } + .serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let repr = DiagnosticRepr::deserialize(deserializer)?; + Ok(SyntaxDiagnostic { + code: repr.code, + subsystem: repr.subsystem, + severity: repr.severity, + message: repr.message, + args: repr.args, + name: repr.name, + option_name: repr.option_name, + groups: repr.groups, + primary_range: repr.primary_range, + location: repr.location, + buffer_id: repr.buffer_id, + file_name: repr.file_name, + ranges: repr + .ranges + .into_iter() + .map(|range| SyntaxDiagnosticRange { + start: range.start, + end: range.end, + start_buffer_id: range.start_buffer_id, + end_buffer_id: range.end_buffer_id, + }) + .collect(), + expansion_locations: repr + .expansion_locations + .into_iter() + .map(|expansion| SyntaxDiagnosticExpansion { + location: expansion.location.map(location_into), + original_location: expansion.original_location.map(location_into), + macro_name: expansion.macro_name, + }) + .collect(), + include_stack: repr.include_stack.into_iter().map(location_into).collect(), + diagnostic_id: repr.diagnostic_id, + parent_diagnostic_id: repr.parent_diagnostic_id, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rtl_dir() -> String { + if cfg!(windows) { r"C:\rtl".to_owned() } else { "/rtl".to_owned() } + } + + fn rtl_file(name: &str) -> String { + if cfg!(windows) { format!(r"C:\rtl\{name}") } else { format!("/rtl/{name}") } + } + + fn job(text: &str) -> ProfileCompilationJob { + let top = rtl_file("top.sv"); + ProfileCompilationJob { + profile_id: 0, + roots: vec![ProfileCompilationRoot { + file_id: 0, + kind: ProfileRootKind::SystemVerilog, + name: top.clone(), + path: top.clone(), + }], + buffers: vec![ProfileCompilationBuffer { + file_id: 0, + path: top, + text: Some(text.to_owned()), + }], + top_modules: Vec::new(), + include_dirs: vec![rtl_dir()], + predefines: Vec::new(), + diagnostics: ProfileDiagnosticsOptions { + parse: true, + semantic: true, + warnings: Some(Vec::new()), + rules: Vec::new(), + }, + } + } + + #[test] + fn job_round_trips_through_json() { + let job = job("module top; endmodule\n"); + let encoded = serde_json::to_vec(&job).unwrap(); + let decoded: ProfileCompilationJob = serde_json::from_slice(&encoded).unwrap(); + assert_eq!(decoded, job); + } + + #[test] + fn parse_diagnostics_are_attributed_to_the_root() { + let output = run_profile_compilation(job("module top(;\nendmodule\n")); + assert!(output.diagnostics.iter().any(|diagnostic| diagnostic.file_id == 0), "{output:?}"); + } + + #[test] + fn source_rule_filters_worker_diagnostics() { + let mut job = job("module top(;\nendmodule\n"); + job.diagnostics.semantic = false; + job.diagnostics.rules.push(ProfileDiagnosticRule { + selector: ProfileDiagnosticSelector::Source(ProfileDiagnosticSource::Parse), + severity: ProfileDiagnosticRuleSeverity::Ignore, + }); + assert!(run_profile_compilation(job).diagnostics.is_empty()); + } + + #[test] + fn included_buffer_diagnostics_keep_their_file_identity() { + let mut job = job("`include \"defs.svh\"\nmodule top; endmodule\n"); + job.buffers.push(ProfileCompilationBuffer { + file_id: 1, + path: rtl_file("defs.svh"), + text: Some("module broken(;\nendmodule\n".to_owned()), + }); + let output = run_profile_compilation(job); + assert!(output.diagnostics.iter().any(|diagnostic| diagnostic.file_id == 1), "{output:?}"); + } + + #[test] + fn library_map_roots_use_the_profile_session() { + let mut job = job(""); + job.roots[0].kind = ProfileRootKind::LibraryMap; + job.buffers[0].text = Some("library work \"/rtl/*.sv\";\n".to_owned()); + let output = run_profile_compilation(job); + assert!(output.diagnostics.is_empty(), "{output:?}"); + } +} diff --git a/crates/preproc-expand/src/source_db.rs b/crates/preproc-expand/src/source_db.rs index 634aa86e9..ef72c1075 100644 --- a/crates/preproc-expand/src/source_db.rs +++ b/crates/preproc-expand/src/source_db.rs @@ -13,11 +13,10 @@ use triomphe::Arc; use utils::{ line_index::{TextRange, TextSize}, path_identity::PathIdentityIndex, - uniq_vec::UniqVec, }; use vfs::{FileId, VfsPath}; -use crate::db::{PreprocDb, path_file_ids, syntax_tree_options_for_file}; +use crate::db::{PreprocDb, syntax_tree_options_for_file}; mod context; mod queries; @@ -25,16 +24,13 @@ pub(crate) mod range_index; mod source_map; mod source_mapping; -pub(crate) use self::source_mapping::manifest_predefine_name_range; #[cfg(not(test))] use self::source_mapping::source_preproc_file_ids; #[cfg(test)] pub(super) use self::source_mapping::{materialized_predefine_text, source_preproc_file_ids}; use self::source_mapping::{shift_text_range, unshift_text_size}; pub use self::{ - context::{ - SourcePreprocContextIndex, SourcePreprocContextStatus, SourcePreprocRelevantContexts, - }, + context::{SourcePreprocContextIndex, SourcePreprocRelevantContexts}, queries::{SourcePreprocQueryError, workspace_preproc_model_file_ids}, range_index::MappedSourcePreprocModel, source_map::{ @@ -46,3 +42,6 @@ pub(super) use self::{ context::{source_preproc_context_index_for_profile, source_preproc_contexts_for_file}, queries::source_preproc_model, }; +pub(crate) use self::{ + queries::set_source_preproc_model_lru_capacity, source_mapping::manifest_predefine_name_range, +}; diff --git a/crates/preproc-expand/src/source_db/context.rs b/crates/preproc-expand/src/source_db/context.rs index d371e97ab..061312040 100644 --- a/crates/preproc-expand/src/source_db/context.rs +++ b/crates/preproc-expand/src/source_db/context.rs @@ -3,192 +3,76 @@ use super::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocRelevantContexts { pub model_file_ids: Vec, - pub status: SourcePreprocContextStatus, } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SourcePreprocContextIndex { contexts_by_file: FxHashMap>, - status: SourcePreprocContextStatus, } impl SourcePreprocContextIndex { fn contexts_for_file(&self, file_id: FileId) -> SourcePreprocRelevantContexts { SourcePreprocRelevantContexts { model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), - status: self.status, } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum SourcePreprocContextStatus { - #[default] - Complete, - Partial { - skipped_models: usize, - }, -} - -fn preproc_context_file_ids( - mapped: &MappedSourcePreprocModel, - model_file_id: FileId, -) -> Result, SourcePreprocQueryError> { - let mut file_ids = UniqVec::::default(); - file_ids.push_unique(model_file_id); - - for definition in mapped.model.macro_definitions().iter() { - collect_context_source_range(mapped, definition.directive_range, &mut file_ids)?; - collect_context_source_range(mapped, definition.name_range, &mut file_ids)?; - if let Some(params) = &definition.params { - for param in params { - if let Some(range) = param.name_range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - if let Some(range) = param.range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - if let Some(default) = ¶m.default { - for token in default { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - } - } - } - } - for token in &definition.body_tokens { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - } - } - - for reference in mapped.model.macro_references().iter() { - collect_context_source_range(mapped, reference.directive_range, &mut file_ids)?; - collect_context_source_range(mapped, reference.name_range, &mut file_ids)?; - } - - for call in mapped.model.macro_calls().iter() { - collect_context_source_range(mapped, call.call_range, &mut file_ids)?; - for argument in &call.arguments { - if let Some(range) = argument.argument_range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - for token in &argument.tokens { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - } - } - } - - for include in mapped.model.include_graph().directives() { - collect_context_source_range(mapped, include.directive_range, &mut file_ids)?; - if let Some(range) = include.target_range { - collect_context_source_range(mapped, range, &mut file_ids)?; - } - if let Some(source) = include.resolved_source { - collect_context_source(mapped, source, &mut file_ids)?; - } - } - - for range in mapped.model.inactive_ranges() { - collect_context_source_range(mapped, *range, &mut file_ids)?; - } - - let mut file_ids = file_ids.into_vec(); - file_ids.sort(); - Ok(file_ids) -} - -fn collect_context_source_range( - mapped: &MappedSourcePreprocModel, - range: SourceRange, - file_ids: &mut UniqVec, -) -> Result<(), SourcePreprocQueryError> { - collect_context_source(mapped, range.source, file_ids) -} - -fn collect_context_source( - mapped: &MappedSourcePreprocModel, - source: PreprocSourceId, - file_ids: &mut UniqVec, -) -> Result<(), SourcePreprocQueryError> { - match mapped.source_map.file_id(source) { - Ok(file_id) => { - file_ids.push_unique(file_id); - } - Err(SourcePreprocQueryError::DisplayOnlyVirtualSource { .. }) => {} - Err(error) => return Err(error), - } - if let Some(manifest_source) = mapped.source_map.predefine_manifest_source(source) { - file_ids.push_unique(manifest_source.file_id); - } - Ok(()) -} - +/// Which runs read each file, inverted from what those runs actually consumed. +/// +/// A run's inputs are facts, not inferences: the include edges its +/// preprocessor emitted, plus the manifest supplying its predefines. Both are +/// already memoized for other consumers, so inverting them costs one slice +/// read per root instead of a preprocessor model per root. +/// +/// This shares its identity with the invalidation model. A file's dependents +/// and the runs that can answer a query about it are the same relation, so +/// they must not be two computations. pub(crate) fn source_preproc_context_index_for_profile( db: &dyn PreprocDb, profile_id: Option, ) -> Arc { let plan = db.compilation_plan_for_profile(profile_id); - let mut contexts_by_file = FxHashMap::>::default(); - let mut skipped_models = 0usize; - - for model_file_id in plan.roots.iter().copied() { - if !matches!( - db.file_kind(model_file_id), - SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader - ) { - continue; - } - let mapped = db.source_preproc_model(model_file_id); - match mapped.as_ref() { - Ok(mapped) => match preproc_context_file_ids(mapped, model_file_id) { - Ok(file_ids) => { - for file_id in file_ids { - if file_id == model_file_id { - continue; - } - contexts_by_file.entry(file_id).or_default().push_unique(model_file_id); - } - } - Err(error) => { - tracing::warn!( - ?model_file_id, - ?error, - "failed to index source preprocessor context" - ); - skipped_models += 1; - } - }, - Err(error) => { - tracing::warn!( - ?model_file_id, - ?error, - "failed to load source preprocessor model for context index" - ); - skipped_models += 1; + let manifest_file_ids = predefine_manifest_file_ids(db, profile_id); + let mut contexts_by_file = FxHashMap::>::default(); + + for root in plan.root_file_ids() { + let inputs = db.parsed_compilation_dependencies(root); + for file_id in inputs.iter().copied().chain(manifest_file_ids.iter().copied()) { + if file_id == root { + continue; } + contexts_by_file.entry(file_id).or_default().push(root); } } - let contexts_by_file = contexts_by_file - .into_iter() - .map(|(file_id, model_file_ids)| { - let mut model_file_ids = model_file_ids.into_vec(); - model_file_ids.sort(); - (file_id, model_file_ids) - }) - .collect(); - let status = if skipped_models == 0 { - SourcePreprocContextStatus::Complete - } else { - SourcePreprocContextStatus::Partial { skipped_models } - }; - Arc::new(SourcePreprocContextIndex { contexts_by_file, status }) + for roots in contexts_by_file.values_mut() { + roots.sort_unstable_by_key(|root| root.index()); + roots.dedup(); + } + Arc::new(SourcePreprocContextIndex { contexts_by_file }) } + +/// Files whose text a profile's predefines were read from. A predefine is an +/// input to every run in the profile without being included by any of them. +fn predefine_manifest_file_ids( + db: &dyn PreprocDb, + profile_id: Option, +) -> Vec { + let path_file_ids = db.path_file_ids(); + let mut file_ids = db + .project_config() + .preprocess_for_profile(profile_id) + .predefines + .iter() + .filter_map(|predefine| predefine.source.as_ref()) + .filter_map(|source| path_file_ids.get_path(source.path.as_path())) + .collect::>(); + file_ids.sort_unstable_by_key(|file_id| file_id.index()); + file_ids.dedup(); + file_ids +} + pub(crate) fn source_preproc_contexts_for_file( db: &dyn PreprocDb, file_id: FileId, diff --git a/crates/preproc-expand/src/source_db/queries.rs b/crates/preproc-expand/src/source_db/queries.rs index df4050bb7..0240c308f 100644 --- a/crates/preproc-expand/src/source_db/queries.rs +++ b/crates/preproc-expand/src/source_db/queries.rs @@ -37,7 +37,7 @@ pub fn workspace_preproc_model_file_ids( let plan = db.compilation_plan_for_profile(profile_id); let mut file_ids = FxHashSet::default(); - for root in plan.roots.iter().copied() { + for root in plan.root_file_ids() { if matches!( db.file_kind(root), SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader @@ -87,7 +87,7 @@ pub(crate) fn source_preproc_model( let profile_id = db.file_compilation_profile(file_id); let preprocess = db.project_config().preprocess_for_profile(profile_id); let options = syntax_tree_options_for_file(db, file_id); - let Some(trace) = db.parsed_compilation_unit(file_id).preprocessor_trace.clone() else { + let Some(trace) = db.preproc_trace(file_id) else { return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; @@ -96,10 +96,14 @@ pub(crate) fn source_preproc_model( Ok(source_map) => source_map, Err(err) => return Arc::new(Err(err)), }; - let model = match SourcePreprocModel::from_trace(trace) { + let model = match SourcePreprocModel::from_trace(&trace) { Ok(model) => model, Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), }; Arc::new(Ok(MappedSourcePreprocModel::new(model, source_map))) } + +pub(crate) fn set_source_preproc_model_lru_capacity(db: &mut dyn PreprocDb, capacity: usize) { + source_preproc_model::set_lru_capacity(db, capacity); +} diff --git a/crates/preproc-expand/src/source_db/source_mapping.rs b/crates/preproc-expand/src/source_db/source_mapping.rs index 4207920f5..2be5abd3a 100644 --- a/crates/preproc-expand/src/source_db/source_mapping.rs +++ b/crates/preproc-expand/src/source_db/source_mapping.rs @@ -1,6 +1,7 @@ use base_db::project::{Predefine, PreprocessConfig}; use super::*; +use crate::compilation_plan; pub(crate) fn source_preproc_file_ids( db: &dyn PreprocDb, @@ -11,7 +12,8 @@ pub(crate) fn source_preproc_file_ids( preprocess: &PreprocessConfig, ) -> Result { let mut source_map = PreprocSourceMap::default(); - let path_file_ids = path_file_ids(db); + let path_file_ids = db.path_file_ids(); + let source_buffer_file_ids = compilation_plan::source_buffer_file_ids_for_file(db, file_id); let root_source = PreprocSourceId::from(trace.root_buffer_id); source_map.insert_real_file(root_source, file_id, db.file_text(file_id).len()); let include_buffer_texts = include_buffer_texts_by_path(options); @@ -33,7 +35,7 @@ pub(crate) fn source_preproc_file_ids( match source.origin { SourceBufferOrigin::Source => { - if let Some(mapped_file_id) = path_file_ids.get(&source.path) { + if let Some(mapped_file_id) = source_buffer_file_ids.get(&source.path) { source_map.insert_real_file( source_id, mapped_file_id, diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 6bcc3ab33..8cc0e554d 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -4,7 +4,7 @@ use super::{tables::*, types::*}; impl SourcePreprocModel { /// Build the model in a single pass from the slang preprocessor trace. - pub fn from_trace(trace: Trace) -> Result { + pub fn from_trace(trace: &Trace) -> Result { Ok(SourcePreprocModelBuilder::collect(trace)?.build()) } diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index e5725da70..8963c1220 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -19,7 +19,7 @@ fn preprocessor_trace( name: &str, path: &str, options: &SyntaxTreeOptions, -) -> Trace { +) -> std::sync::Arc { SyntaxTree::from_text_with_options_and_trace(root_text, name, path, options).preprocessor_trace } @@ -38,7 +38,7 @@ fn source_model( }; let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); - let model = SourcePreprocModel::from_trace(trace).unwrap(); + let model = SourcePreprocModel::from_trace(&trace).unwrap(); let header_source = source_by_path_suffix(&model, "defs.vh"); (model, root_source, header_source) } @@ -61,7 +61,7 @@ fn source_model_from_root( ) -> (SourcePreprocModel, PreprocSourceId) { let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); - let model = SourcePreprocModel::from_trace(trace).unwrap(); + let model = SourcePreprocModel::from_trace(&trace).unwrap(); (model, root_source) } diff --git a/crates/preproc/src/source/model/tests/include_resolution.rs b/crates/preproc/src/source/model/tests/include_resolution.rs index b79437ca5..1acf053a8 100644 --- a/crates/preproc/src/source/model/tests/include_resolution.rs +++ b/crates/preproc/src/source/model/tests/include_resolution.rs @@ -75,7 +75,7 @@ logic [`LEAF_WIDTH-1:0] data; }; let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); - let model = SourcePreprocModel::from_trace(trace).unwrap(); + let model = SourcePreprocModel::from_trace(&trace).unwrap(); let leaf_source = source_by_path_suffix(&model, "include/leaf.vh"); let reference = model @@ -130,7 +130,7 @@ fn source_model_fails_closed_when_directive_event_range_is_missing() { }; assert_eq!( - SourcePreprocModel::from_trace(trace).unwrap_err(), + SourcePreprocModel::from_trace(&trace).unwrap_err(), SourcePreprocError::MissingEventRange { source_order: 0, kind: MacroEventKind::Define } ); } diff --git a/crates/preproc/src/source/tables/builder/state.rs b/crates/preproc/src/source/tables/builder/state.rs index 22efbb27b..ad5746682 100644 --- a/crates/preproc/src/source/tables/builder/state.rs +++ b/crates/preproc/src/source/tables/builder/state.rs @@ -30,17 +30,82 @@ impl SourcePreprocModelBuilder { .collect::>(); let source_parents = self.source_parents_by_include(); + // Depth of each source in the include forest. Root, predefine, and + // detached sources have no parent and sit at depth 0. + let mut depth = BTreeMap::::new(); + for source in &self.model.sources { + let source_id = source.id; + if depth.contains_key(&source_id) { + continue; + } + let mut chain = Vec::new(); + let mut current = source_id; + loop { + if depth.contains_key(¤t) { + break; + } + match source_parents.get(¤t) { + Some(&parent) => { + chain.push(current); + current = parent; + } + None => { + depth.insert(current, 0); + break; + } + } + } + let base = depth[¤t]; + for (offset, source_id) in chain.iter().rev().enumerate() { + depth.insert(*source_id, base + offset + 1); + } + } + + // Every included source closes at `include_order + 1` when its subtree + // is empty; the stack pass below overrides this for non-empty subtrees. + let mut end_orders = BTreeMap::::new(); + for source in &self.model.sources { + if let PreprocSourceOrigin::Included { include_event_id } = source.origin { + let Some(include_order) = event_orders_by_id.get(&include_event_id).copied() else { + continue; + }; + end_orders.insert(source.id, include_order + 1); + } + } + + // The trace events are a depth-first traversal of the include forest, + // so an included source's scope ends exactly when the stream returns to + // a shallower source. A monotonic stack computes every end order in one + // O(events) pass (the old scan was O(sources * events * depth)). + let mut open = Vec::::new(); + for (source_order, event) in self.event_records.iter().enumerate() { + let source = event.range.source; + let source_depth = depth.get(&source).copied().unwrap_or(0); + while let Some(&top) = open.last() { + if top == source || depth[&top] < source_depth { + break; + } + end_orders.insert(top, source_order); + open.pop(); + } + if source_depth >= 1 && open.last() != Some(&source) { + open.push(source); + } + } + for source in open { + end_orders.insert(source, self.event_records.len()); + } + for source in &self.model.sources { let end_order = match source.origin { PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine | PreprocSourceOrigin::Detached => self.event_records.len(), - PreprocSourceOrigin::Included { include_event_id } => { - let Some(include_order) = event_orders_by_id.get(&include_event_id).copied() - else { + PreprocSourceOrigin::Included { .. } => { + let Some(&end_order) = end_orders.get(&source.id) else { continue; }; - self.included_source_end_order(source.id, include_order, &source_parents) + end_order } }; self.model @@ -74,23 +139,6 @@ impl SourcePreprocModelBuilder { .collect() } - pub(in crate::source::tables::builder) fn included_source_end_order( - &self, - source: PreprocSourceId, - include_order: usize, - source_parents: &BTreeMap, - ) -> usize { - self.event_records - .iter() - .enumerate() - .skip(include_order + 1) - .find_map(|(source_order, directive)| { - (!source_is_descendant_or_same(directive.range.source, source, source_parents)) - .then_some(source_order) - }) - .unwrap_or(self.event_records.len()) - } - pub(in crate::source::tables::builder) fn build_include_graph(&mut self) { let mut resolved_sources_by_event = BTreeMap::new(); @@ -117,19 +165,3 @@ impl SourcePreprocModelBuilder { } } } - -pub(in crate::source::tables::builder) fn source_is_descendant_or_same( - mut source: PreprocSourceId, - ancestor: PreprocSourceId, - source_parents: &BTreeMap, -) -> bool { - loop { - if source == ancestor { - return true; - } - let Some(parent) = source_parents.get(&source).copied() else { - return false; - }; - source = parent; - } -} diff --git a/crates/preproc/src/source/tables/builder/trace.rs b/crates/preproc/src/source/tables/builder/trace.rs index a7d762a78..a4b3858ba 100644 --- a/crates/preproc/src/source/tables/builder/trace.rs +++ b/crates/preproc/src/source/tables/builder/trace.rs @@ -14,7 +14,7 @@ use super::*; impl SourcePreprocModelBuilder { /// Collect the raw event projections from the preprocessor trace into the /// builder's private fields, ready for table derivation. - pub(in crate::source) fn collect(trace: Trace) -> Result { + pub(in crate::source) fn collect(trace: &Trace) -> Result { let root_source = PreprocSourceId::from(trace.root_buffer_id); let include_edges = trace .include_edges @@ -30,7 +30,7 @@ impl SourcePreprocModelBuilder { .collect::>(); let sources = trace .source_buffers - .into_iter() + .iter() .map(|source| PreprocSource { id: PreprocSourceId::from(source.buffer_id), path: source.path.to_smolstr(), @@ -71,7 +71,7 @@ impl SourcePreprocModelBuilder { current_state: BTreeMap::new(), }; - for (source_order, directive) in trace.events.into_iter().enumerate() { + for (source_order, directive) in trace.events.iter().enumerate() { builder.collect_trace_event(source_order, directive)?; } @@ -81,7 +81,7 @@ impl SourcePreprocModelBuilder { fn collect_trace_event( &mut self, source_order: usize, - directive: Event, + directive: &Event, ) -> Result<(), SourcePreprocError> { self.model.inactive_ranges.extend( directive @@ -95,12 +95,12 @@ impl SourcePreprocModelBuilder { return Ok(()); }; let event_id = SourcePreprocEventId::from(directive.event_id); - let range = required_event_range(source_order, kind, &directive)?; + let range = required_event_range(source_order, kind, directive)?; match kind { MacroEventKind::Define => { let event_index = self.defines.len(); - let define = collect_trace_define(directive, event_id, range); + let define = collect_trace_define(directive.clone(), event_id, range); self.defines.push(define); self.push_source_event_record(event_id, kind, event_index, range); } @@ -130,7 +130,12 @@ impl SourcePreprocModelBuilder { self.conditionals.push(SourceMacroConditional { event_id, kind: trace_conditional_kind(directive.kind), - expr: directive.expr_tokens.into_iter().map(macro_token_from_trace).collect(), + expr: directive + .expr_tokens + .iter() + .cloned() + .map(macro_token_from_trace) + .collect(), range, }); self.push_source_event_record(event_id, kind, event_index, range); @@ -145,7 +150,8 @@ impl SourcePreprocModelBuilder { name_range: directive.name.source_range(), arguments: directive .arguments - .into_iter() + .iter() + .cloned() .enumerate() .map(macro_actual_argument_from_trace) .collect(), diff --git a/crates/slang-sys/src/compilation.rs b/crates/slang-sys/src/compilation.rs index ccfff55d3..4550a6f61 100644 --- a/crates/slang-sys/src/compilation.rs +++ b/crates/slang-sys/src/compilation.rs @@ -13,6 +13,33 @@ pub struct Compilation { raw: UniquePtr, } +/// One elaborated instance: hierarchical path and the instantiation site. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HierInstance { + pub path: String, + pub file: String, + pub offset: usize, +} + +/// Symbol at a source offset: type and definition site. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SymbolInfo { + pub name: String, + pub type_name: String, + pub kind: String, + pub def_file: String, + pub def_offset: usize, + pub owner_class: String, + pub inheritance: Vec, +} + +/// A member of a scope or structured type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MemberInfo { + pub name: String, + pub type_name: String, +} + impl Default for Compilation { fn default() -> Self { Self::new() @@ -132,6 +159,63 @@ impl Compilation { .collect() } + /// Semantic answer for the symbol at `offset` in `path`. + /// + /// `None` means slang elaborated the compilation and the offset denotes + /// no symbol. For a class member the answer also carries the owning + /// class and its base-class chain. + pub fn lookup_symbol(&mut self, path: &str, offset: usize) -> Option { + let answer = ffi::lookup_symbol(self.raw_pin(), path, offset); + answer.found.then_some(SymbolInfo { + name: answer.name, + type_name: answer.type_name, + kind: answer.kind, + def_file: answer.def_file, + def_offset: answer.def_offset, + owner_class: answer.owner_class, + inheritance: answer.inheritance, + }) + } + + pub fn lookup_scoped(&mut self, left: &str, right: &str) -> Option { + let answer = ffi::lookup_scoped(self.raw_pin(), left, right); + answer.found.then_some(SymbolInfo { + name: answer.name, + type_name: answer.type_name, + kind: answer.kind, + def_file: answer.def_file, + def_offset: answer.def_offset, + owner_class: answer.owner_class, + inheritance: answer.inheritance, + }) + } + + pub fn list_members(&mut self, path: &str, offset: usize) -> Vec { + ffi::list_members(self.raw_pin(), path, offset) + .into_iter() + .map(|row| MemberInfo { name: row.name, type_name: row.type_name }) + .collect() + } + + pub fn list_scope_members(&mut self, name: &str) -> Vec { + ffi::list_scope_members(self.raw_pin(), name) + .into_iter() + .map(|row| MemberInfo { name: row.name, type_name: row.type_name }) + .collect() + } + + pub fn lookup_type(&mut self, path: &str, start: usize, end: usize) -> Option { + let answer = ffi::lookup_type(self.raw_pin(), path, start, end); + answer.found.then_some(answer.type_name) + } + + pub fn list_instances(&mut self) -> Vec { + ffi::list_instances(self.raw_pin()) + .into_iter() + .map(|row| HierInstance { path: row.path, file: row.file, offset: row.offset }) + .collect() + } + fn raw_pin(&mut self) -> Pin<&mut ffi::Compilation> { self.raw.as_mut().expect("Slang compilation unexpectedly null") } @@ -191,6 +275,232 @@ mod tests { assert!(compilation.parse_diagnostics_with_options(&[]).is_empty()); } + #[test] + fn uvm_shaped_class_member_has_type_class_and_inheritance() { + let src = r#" +virtual class uvm_void; +endclass +virtual class uvm_object extends uvm_void; + string m_leaf_name; + function string get_type_name(); + return ""; + endfunction +endclass +"#; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text( + src, + "uvm_object.svh", + "uvm_object.svh", + &SyntaxTreeOptions::default(), + ); + let offset = src.find("m_leaf_name").expect("property"); + let info = compilation + .lookup_symbol("uvm_object.svh", offset) + .expect("slang must see the UVM-shaped class property"); + assert_eq!(info.owner_class, "uvm_object"); + assert!(info.inheritance.iter().any(|name| name == "uvm_void"), "{info:?}"); + assert!(info.type_name.contains("string"), "{info:?}"); + } + + #[test] + fn lookup_uses_the_assigned_buffer_path() { + let src = "virtual class uvm_void; endclass\nvirtual class uvm_object extends uvm_void;\n string m_leaf_name;\nendclass\n"; + let path = "/vide-assigned/uvm_object.svh"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text( + src, + "uvm_object.svh", + path, + &SyntaxTreeOptions::default(), + ); + let offset = src.find("m_leaf_name").expect("property"); + let info = compilation + .lookup_symbol(path, offset) + .expect("lookup must hit the buffer under the path it was assigned"); + assert_eq!(info.owner_class, "uvm_object"); + assert!(info.type_name.contains("string"), "{info:?}"); + } + + #[test] + fn list_instances_reports_hierarchical_path_and_site() { + let src = "module child; endmodule\nmodule top; child u0(); endmodule\n"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text( + src, + "top", + "top.sv", + &SyntaxTreeOptions::default(), + ); + let instances = compilation.list_instances(); + assert!( + instances.iter().any(|inst| inst.path.contains("u0") && inst.file == "top.sv"), + "{instances:?}" + ); + } + + #[test] + fn list_instances_reports_the_assigned_buffer_path() { + let src = "module child; endmodule\nmodule top; child u0(); endmodule\n"; + let path = "/vide-assigned/top.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let instances = compilation.list_instances(); + let inst = instances + .iter() + .find(|inst| inst.path.contains("u0")) + .unwrap_or_else(|| panic!("missing u0: {instances:?}")); + assert_eq!(inst.file, path, "{instances:?}"); + } + + #[test] + fn lookup_symbol_answers_a_net_type_and_a_class_scope() { + let src = r#" +class env; + static int count; +endclass +module top; + logic [7:0] x; + initial env::count = x; +endmodule +"#; + let path = "/vide-assigned/top.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let x = compilation + .lookup_symbol(path, src.find("x;").expect("net")) + .expect("net at its declaration"); + assert!(x.type_name.contains("logic"), "{x:?}"); + let scoped = compilation + .lookup_symbol(path, src.find("count =").expect("class scope")) + .expect("env::count at the use"); + assert_eq!(scoped.name, "count", "{scoped:?}"); + assert!(scoped.type_name.contains("int"), "{scoped:?}"); + assert_eq!(scoped.def_file, path, "{scoped:?}"); + assert_eq!(scoped.def_offset, src.find("count;").expect("def"), "{scoped:?}"); + } + + #[test] + fn lookup_scoped_resolves_package_and_class() { + let src = r#" +package p; + typedef logic exported_t; +endpackage +class env; + static int count; +endclass +module top; + p::exported_t x; + initial env::count = 1; +endmodule +"#; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text( + src, + "top", + "/vide-assigned/top.sv", + &SyntaxTreeOptions::default(), + ); + let exported = compilation.lookup_scoped("p", "exported_t").expect("p::exported_t"); + assert_eq!(exported.name, "exported_t", "{exported:?}"); + let count = compilation.lookup_scoped("env", "count").expect("env::count"); + assert_eq!(count.name, "count", "{count:?}"); + let pkg = compilation.lookup_scoped("p", "").expect("package p"); + assert_eq!(pkg.name, "p", "{pkg:?}"); + } + + #[test] + fn list_members_of_a_package_and_a_struct() { + let src = r#" +package p; + typedef logic exported_t; + function int make(); return 1; endfunction +endpackage +module top; + typedef struct { logic [7:0] field; } packet_t; + packet_t pkt; +endmodule +"#; + let path = "/vide-assigned/members.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let pkg_members = compilation.list_members(path, src.find("p;").expect("package name")); + let names: Vec<_> = pkg_members.iter().map(|m| m.name.as_str()).collect(); + assert!(names.contains(&"exported_t"), "{pkg_members:?}"); + assert!(names.contains(&"make"), "{pkg_members:?}"); + let fields = compilation.list_members(path, src.find("pkt;").expect("pkt")); + assert!(fields.iter().any(|m| m.name == "field"), "{fields:?}"); + } + + #[test] + fn list_scope_members_of_hierarchical_instance_and_struct() { + let src = r#" +package p; + typedef logic exported_t; + function int make(); return 1; endfunction +endpackage +module leaf; + wire leaf_wire; +endmodule +module top; + leaf u0(); + typedef struct { logic [7:0] field; } packet_t; + packet_t pkt; +endmodule +"#; + let path = "/vide-assigned/hier.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let inst = compilation.list_scope_members("top.u0"); + assert!(inst.iter().any(|m| m.name == "leaf_wire"), "top.u0 members: {inst:?}"); + let nested = compilation.list_scope_members("u0"); + assert!(nested.iter().any(|m| m.name == "leaf_wire"), "u0 members: {nested:?}"); + let fields = compilation.list_scope_members("pkt"); + assert!(fields.iter().any(|m| m.name == "field"), "pkt members: {fields:?}"); + let pkg = compilation.list_scope_members("p"); + let pkg_names: Vec<_> = pkg.iter().map(|m| m.name.as_str()).collect(); + assert!(pkg_names.contains(&"exported_t"), "{pkg:?}"); + assert!(pkg_names.contains(&"make"), "{pkg:?}"); + } + + #[test] + fn lookup_type_covers_an_additive_expression() { + let src = "module top; logic [7:0] a, b, y; always_comb y = a + b; endmodule\n"; + let path = "/vide-assigned/add.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let start = src.find("a + b").expect("expr"); + let end = start + "a + b".len(); + let ty = compilation.lookup_type(path, start, end).expect("type of a + b"); + assert!(ty.contains("logic"), "{ty}"); + } + + #[test] + fn list_scope_members_of_a_select_prefix_does_not_abort() { + let src = "module top; logic [7:0] bus; initial bus[0] = 1; endmodule\n"; + let path = "/vide-assigned/select.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let members = compilation.list_scope_members("bus[0]"); + assert!(members.is_empty(), "element select is not a hierarchical name: {members:?}"); + } + + #[test] + fn lookup_type_of_mixed_width_add_is_the_sum_not_the_narrow_operand() { + let src = "module top; logic [3:0] b; logic [7:0] a, y; always_comb y = b + a; endmodule\n"; + let path = "/vide-assigned/add-mixed.sv"; + let mut compilation = Compilation::new(); + compilation.parse_syntax_tree_from_text(src, "top", path, &SyntaxTreeOptions::default()); + let start = src.find("b + a").expect("expr"); + let end = start + "b + a".len(); + let ty = compilation.lookup_type(path, start, end).expect("type of b + a"); + assert!( + ty.contains("logic") && ty.contains("7"), + "sum of logic[3:0] + logic[7:0] must be the 8-bit result, not operand b: {ty}" + ); + assert!(!ty.contains("[3:0]"), "must not return the narrow operand type: {ty}"); + } + #[test] fn empty_compilation_has_no_diagnostics() { let compilation = Compilation::new(); diff --git a/crates/slang-sys/src/compilation/ffi.rs b/crates/slang-sys/src/compilation/ffi.rs index 787682d0e..a72f9c6c8 100644 --- a/crates/slang-sys/src/compilation/ffi.rs +++ b/crates/slang-sys/src/compilation/ffi.rs @@ -12,6 +12,37 @@ pub(crate) use slang_ffi::*; #[cxx::bridge(namespace = "slang_sys::compilation")] mod slang_ffi { + #[derive(Debug, Clone, PartialEq, Eq)] + struct HierInstanceAnswer { + path: String, + file: String, + offset: usize, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct SymbolAnswer { + found: bool, + name: String, + type_name: String, + kind: String, + def_file: String, + def_offset: usize, + owner_class: String, + inheritance: Vec, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct MemberAnswer { + name: String, + type_name: String, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct TypeAnswer { + found: bool, + type_name: String, + } + #[derive(Debug, Clone, PartialEq, Eq)] struct ParseSyntaxTreeOptions { predefines: Vec, @@ -86,6 +117,29 @@ mod slang_ffi { compilation: &Compilation, warning_options: Vec, ) -> Vec; + fn lookup_symbol( + compilation: Pin<&mut Compilation>, + path: &str, + offset: usize, + ) -> SymbolAnswer; + fn lookup_scoped( + compilation: Pin<&mut Compilation>, + left: &str, + right: &str, + ) -> SymbolAnswer; + fn list_members( + compilation: Pin<&mut Compilation>, + path: &str, + offset: usize, + ) -> Vec; + fn list_scope_members(compilation: Pin<&mut Compilation>, name: &str) -> Vec; + fn lookup_type( + compilation: Pin<&mut Compilation>, + path: &str, + start: usize, + end: usize, + ) -> TypeAnswer; + fn list_instances(compilation: Pin<&mut Compilation>) -> Vec; } } diff --git a/crates/slang-sys/src/compilation/wrapper.cpp b/crates/slang-sys/src/compilation/wrapper.cpp index ecb841d06..8fc80ce14 100644 --- a/crates/slang-sys/src/compilation/wrapper.cpp +++ b/crates/slang-sys/src/compilation/wrapper.cpp @@ -1,7 +1,27 @@ #include "compilation/wrapper.h" #include "slang-sys/src/compilation/ffi.rs.h" +#include "slang/ast/ASTContext.h" +#include "slang/ast/ASTVisitor.h" +#include "slang/ast/Lookup.h" +#include "slang/ast/Scope.h" +#include "slang/ast/expressions/CallExpression.h" +#include "slang/ast/expressions/MiscExpressions.h" +#include "slang/ast/expressions/SelectExpressions.h" +#include "slang/ast/symbols/ClassSymbols.h" +#include "slang/ast/symbols/CompilationUnitSymbols.h" +#include "slang/ast/symbols/InstanceSymbols.h" +#include "slang/ast/symbols/SubroutineSymbols.h" +#include "slang/ast/symbols/VariableSymbols.h" +#include "slang/ast/types/AllTypes.h" +#include "slang/text/SourceManager.h" +#include "slang/util/String.h" +#include "slang/util/Util.h" + +#include #include +#include +#include namespace slang_sys::compilation { @@ -184,4 +204,495 @@ rust::Vec semantic_diagnostics( ); } +namespace { + +// Path we handed `assignText`. `getRawFileName` is not that: SourceSession +// sets disableProximatePaths, so cacheBuffer stores only path.filename() +// in FileData::name. FileData::fullPath is the assigned spelling. +std::string assigned_path(const slang::SourceManager& sm, slang::BufferID buffer) { + auto full = sm.getFullPath(buffer); + if (!full.empty()) + return slang::getU8Str(full); + return std::string(sm.getRawFileName(buffer)); +} + +// Resolve the query path once. Per-symbol string compares were the T4 slice +// cost; a live compilation has thousands of symbols and that does not scale. +std::optional buffer_for_path( + const slang::SourceManager& sm, + std::string_view want +) { + for (auto buffer : sm.getAllBuffers()) { + auto kind = sm.getBufferKind(buffer); + if (kind == slang::SourceManager::BufferKind::Macro || + kind == slang::SourceManager::BufferKind::MacroArg) + continue; + if (assigned_path(sm, buffer) == want) + return buffer; + } + return std::nullopt; +} + +bool in_buffer( + const slang::SourceManager& sm, + slang::SourceLocation loc, + slang::BufferID buffer +) { + if (!loc.valid()) + return false; + if (loc.buffer() == buffer) + return true; + auto original = sm.getFullyOriginalLoc(loc); + return original.valid() && original.buffer() == buffer; +} + +std::vector inheritance_of(const slang::ast::ClassType& cls) { + std::vector chain; + const slang::ast::Type* base = cls.getBaseClass(); + while (base) { + chain.emplace_back(std::string(base->name)); + if (const auto* base_cls = base->as_if()) + base = base_cls->getBaseClass(); + else + break; + } + return chain; +} + +} // namespace + +namespace { + +std::string type_of_symbol(const slang::ast::Symbol& symbol) { + if (const auto* value = symbol.as_if()) + return value->getType().toString(); + if (const auto* sub = symbol.as_if()) + return sub->getReturnType().toString(); + if (const auto* type = symbol.as_if()) + return type->toString(); + if (const auto* inst = symbol.as_if()) + return std::string(inst->getDefinition().name); + return {}; +} + +void fill_symbol( + const slang::ast::Symbol& symbol, + const slang::SourceManager& sm, + SymbolAnswer& out +) { + out.found = true; + out.name = rust::String(std::string(symbol.name)); + out.kind = rust::String(std::string(toString(symbol.kind))); + out.type_name = rust::String(type_of_symbol(symbol)); + if (symbol.location.valid()) { + out.def_file = rust::String(assigned_path(sm, symbol.location.buffer())); + out.def_offset = symbol.location.offset(); + } + if (const auto* scope = symbol.getParentScope()) { + if (const auto* cls = scope->asSymbol().as_if()) { + out.owner_class = rust::String(std::string(cls->name)); + for (auto& name : inheritance_of(*cls)) + out.inheritance.push_back(rust::String(std::move(name))); + } + } +} + +struct FindAtOffset : slang::ast::ASTVisitor< + FindAtOffset, + slang::ast::VisitFlags::AllGood | slang::ast::VisitFlags::Bad> { + const slang::SourceManager& sm; + slang::BufferID buffer; + std::size_t offset; + const slang::ast::Symbol* best = nullptr; + std::size_t best_end_dist = static_cast(-1); + std::size_t best_span = static_cast(-1); + + FindAtOffset(const slang::SourceManager& sm, slang::BufferID buffer, std::size_t offset) : + sm(sm), buffer(buffer), offset(offset) {} + + void consider(const slang::ast::Symbol& symbol, slang::SourceRange range) { + if (!range.start().valid() || !range.end().valid()) + return; + if (!in_buffer(sm, range.start(), buffer) && !in_buffer(sm, symbol.location, buffer)) + return; + auto start = range.start().offset(); + auto end = range.end().offset(); + if (offset < start || offset > end) + return; + auto span = end - start; + auto end_dist = end >= offset ? end - offset : offset - end; + if (end_dist < best_end_dist || (end_dist == best_end_dist && span < best_span)) { + best_end_dist = end_dist; + best_span = span; + best = &symbol; + } + } + + void consider_symbol(const slang::ast::Symbol& symbol) { + if (symbol.name.empty()) + return; + if (!symbol.location.valid() || !in_buffer(sm, symbol.location, buffer)) + return; + auto end = slang::SourceLocation( + symbol.location.buffer(), + symbol.location.offset() + symbol.name.size()); + consider(symbol, slang::SourceRange(symbol.location, end)); + } + + template + void handle(const T& node) { + if constexpr (std::is_same_v || + std::is_same_v) { + consider(node.symbol, node.sourceRange); + } else if constexpr (std::is_same_v) { + if (auto* sub = std::get_if(&node.subroutine)) + consider(**sub, node.sourceRange); + } else if constexpr (std::is_same_v) { + consider(node.member, node.sourceRange); + } else if constexpr (std::is_base_of_v) { + consider_symbol(node); + } + visitDefault(node); + } +}; + +} // namespace + +SymbolAnswer lookup_symbol( + Compilation& compilation, + rust::Str path, + std::size_t offset +) { + SymbolAnswer out; + out.found = false; + out.def_offset = 0; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + const auto* sm = compilation.inner->getSourceManager(); + if (!sm) + return out; + std::string path_owned(path.data(), path.size()); + auto buffer = buffer_for_path(*sm, path_owned); + if (!buffer) + return out; + FindAtOffset finder(*sm, *buffer, offset); + root.visit(finder); + if (finder.best) + fill_symbol(*finder.best, *sm, out); + return out; +} + +namespace { + +const slang::ast::ClassType* find_class(const slang::ast::Scope& scope, std::string_view name) { + for (const auto& member : scope.members()) { + if (const auto* cls = member.as_if(); cls && cls->name == name) + return cls; + if (const auto* pkg = member.as_if()) { + if (const auto* found = find_class(*pkg, name)) + return found; + } else if (const auto* cu = member.as_if()) { + if (const auto* found = find_class(*cu, name)) + return found; + } else if (const auto* inst = member.as_if()) { + if (const auto* found = find_class(inst->body, name)) + return found; + } + } + return nullptr; +} + +const slang::ast::Scope* scope_of_symbol(const slang::ast::Symbol& symbol) { + if (const auto* inst = symbol.as_if()) + return &inst->body; + if (const auto* type = symbol.as_if()) { + const auto& canon = type->getCanonicalType(); + if (const auto* scope = canon.as_if()) + return scope; + } + if (const auto* value = symbol.as_if()) { + const auto& canon = value->getType().getCanonicalType(); + if (const auto* scope = canon.as_if()) + return scope; + } + return symbol.as_if(); +} + +/// Resolve `name` in `scope`, tolerating a name that carries selectors. +/// +/// `Scope::lookupName` is the convenience wrapper and it ends in +/// `SLANG_ASSERT(result.selectors.empty())`. That does not mean "the name +/// had no selectors": `u0[0]` on an instance array resolves and leaves none. +/// It fires when a select could not be applied, as in `bus[0]` on a plain +/// net. Completion prefixes are arbitrary source text, so which case a name +/// falls into is not knowable before the lookup — using the underlying +/// `Lookup::name` and reading `selectors` is how that question gets asked +/// instead of assumed. +const slang::ast::Symbol* lookup_name(const slang::ast::Scope& scope, std::string_view name) { + slang::ast::LookupResult result; + slang::ast::ASTContext context(scope, slang::ast::LookupLocation::max); + slang::ast::Lookup::name( + scope.getCompilation().parseName(name), + context, + slang::bitmask{}, + result + ); + // An unapplied select means the name reached something the select does + // not fit. That is not the scope the caller named. + return result.selectors.empty() ? result.found : nullptr; +} + +/// Resolve `name` in an instance body, anywhere under `scope`. +/// +/// A design walk, and deliberately so. The caller is completing inside a +/// buffer that does not parse yet — `initial pkt.` has no expression for +/// slang to type — so a name is all there is to go on, and the name is +/// visible only from inside the instance it was declared in. Nothing +/// cheaper reaches it. What would remove this walk is resolving the prefix +/// expression instead, which needs the buffer to parse. +const slang::ast::Symbol* search_instance_bodies( + const slang::ast::Scope& scope, + std::string_view name +) { + for (const auto& member : scope.members()) { + const slang::ast::Scope* body = nullptr; + if (const auto* inst = member.as_if()) + body = &inst->body; + else if (const auto* nested = member.as_if()) + body = nested; + else if (const auto* pkg = member.as_if()) + body = pkg; + else if (const auto* cu = member.as_if()) + body = cu; + if (!body) + continue; + if (const auto* found = lookup_name(*body, name)) + return found; + if (const auto* found = search_instance_bodies(*body, name)) + return found; + } + return nullptr; +} + +/// The scope a name denotes. +/// +/// SystemVerilog does not disambiguate these by spelling, so each namespace +/// is asked in turn: a package, a class-like, a hierarchical path from the +/// root, and finally a name declared inside some instance body. The order is +/// cheapest first; only the last one walks. +const slang::ast::Symbol* find_named_scope( + slang::ast::Compilation& compilation, + const slang::ast::RootSymbol& root, + std::string_view name +) { + if (name.empty()) + return nullptr; + if (const auto* pkg = compilation.getPackage(name)) + return pkg; + if (const auto* cls = find_class(root, name)) + return cls; + if (const auto* found = lookup_name(root, name)) + return found; + return search_instance_bodies(root, name); +} + +void collect_members(const slang::ast::Scope& scope, rust::Vec& out) { + for (const auto& member : scope.members()) { + if (member.name.empty()) + continue; + MemberAnswer row; + row.name = rust::String(std::string(member.name)); + row.type_name = rust::String(type_of_symbol(member)); + out.push_back(std::move(row)); + } +} + +struct FindType : slang::ast::ASTVisitor< + FindType, + slang::ast::VisitFlags::AllGood | slang::ast::VisitFlags::Bad> { + const slang::SourceManager& sm; + slang::BufferID buffer; + std::size_t start; + std::size_t end; + const slang::ast::Type* covering = nullptr; + std::size_t covering_span = static_cast(-1); + const slang::ast::Type* contained = nullptr; + std::size_t contained_span = 0; + + FindType( + const slang::SourceManager& sm, + slang::BufferID buffer, + std::size_t start, + std::size_t end + ) : + sm(sm), buffer(buffer), start(start), end(end) {} + + template + void handle(const T& node) { + if constexpr (std::is_base_of_v) { + auto range = node.sourceRange; + if (range.start().valid() && range.end().valid() && + in_buffer(sm, range.start(), buffer) && node.type) { + auto rs = range.start().offset(); + auto re = range.end().offset(); + auto span = re - rs; + if (rs <= start && end <= re) { + if (span < covering_span) { + covering_span = span; + covering = node.type; + } + } else if (start <= rs && re <= end && span > contained_span) { + contained_span = span; + contained = node.type; + } + } + } + visitDefault(node); + } + + const slang::ast::Type* best() const { return covering ? covering : contained; } +}; + +} // namespace + +SymbolAnswer lookup_scoped( + Compilation& compilation, + rust::Str left, + rust::Str right +) { + SymbolAnswer out; + out.found = false; + out.def_offset = 0; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + const auto* sm = compilation.inner->getSourceManager(); + if (!sm) + return out; + // `left` and `right` are single identifier tokens from the caller's + // `ScopedName`, so neither carries selectors and `lookupName` cannot + // assert on them. + std::string left_s(left.data(), left.size()); + std::string right_s(right.data(), right.size()); + const auto* qualifier = find_named_scope(*compilation.inner, root, left_s); + if (!qualifier) + return out; + const slang::ast::Symbol* found = qualifier; + if (!right_s.empty()) { + const auto* scope = scope_of_symbol(*qualifier); + found = scope ? scope->lookupName(right_s) : nullptr; + } + if (found) + fill_symbol(*found, *sm, out); + return out; +} + +rust::Vec list_members( + Compilation& compilation, + rust::Str path, + std::size_t offset +) { + rust::Vec out; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + const auto* sm = compilation.inner->getSourceManager(); + if (!sm) + return out; + std::string path_owned(path.data(), path.size()); + auto buffer = buffer_for_path(*sm, path_owned); + if (!buffer) + return out; + FindAtOffset finder(*sm, *buffer, offset); + root.visit(finder); + if (!finder.best) + return out; + if (const auto* scope = scope_of_symbol(*finder.best)) + collect_members(*scope, out); + return out; +} + +rust::Vec list_scope_members(Compilation& compilation, rust::Str name) { + rust::Vec out; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + std::string name_s(name.data(), name.size()); + const auto* found = find_named_scope(*compilation.inner, root, name_s); + if (!found) + return out; + if (const auto* scope = scope_of_symbol(*found)) + collect_members(*scope, out); + return out; +} + +TypeAnswer lookup_type( + Compilation& compilation, + rust::Str path, + std::size_t start, + std::size_t end +) { + TypeAnswer out; + out.found = false; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + const auto* sm = compilation.inner->getSourceManager(); + if (!sm) + return out; + std::string path_owned(path.data(), path.size()); + auto buffer = buffer_for_path(*sm, path_owned); + if (!buffer) + return out; + FindType finder(*sm, *buffer, start, end); + root.visit(finder); + if (const auto* ty = finder.best()) { + out.found = true; + out.type_name = rust::String(ty->toString()); + } + return out; +} + +namespace { + +void collect_instances( + const slang::ast::Scope& scope, + const slang::SourceManager& sm, + rust::Vec& out +) { + for (const auto& member : scope.members()) { + if (const auto* inst = member.as_if()) { + HierInstanceAnswer row; + row.path = rust::String(inst->getHierarchicalPath()); + if (inst->location.valid()) { + row.file = rust::String(assigned_path(sm, inst->location.buffer())); + row.offset = inst->location.offset(); + } + out.push_back(std::move(row)); + collect_instances(inst->body, sm, out); + } else if (const auto* pkg = member.as_if()) { + collect_instances(*pkg, sm, out); + } else if (const auto* cu = member.as_if()) { + collect_instances(*cu, sm, out); + } else if (const auto* body = member.as_if()) { + collect_instances(*body, sm, out); + } + } +} + +} // namespace + +rust::Vec list_instances(Compilation& compilation) { + rust::Vec out; + if (!compilation.inner) + return out; + const auto& root = compilation.inner->getRoot(); + const auto* sm = compilation.inner->getSourceManager(); + if (!sm) + return out; + collect_instances(root, *sm, out); + return out; +} + } // namespace slang_sys::compilation diff --git a/crates/slang-sys/src/compilation/wrapper.h b/crates/slang-sys/src/compilation/wrapper.h index f6e7f4abe..46baed989 100644 --- a/crates/slang-sys/src/compilation/wrapper.h +++ b/crates/slang-sys/src/compilation/wrapper.h @@ -15,6 +15,10 @@ namespace slang_sys::compilation { struct ParseSyntaxTreeOptions; +struct SymbolAnswer; +struct MemberAnswer; +struct TypeAnswer; +struct HierInstanceAnswer; class Compilation { public: @@ -67,4 +71,30 @@ rust::Vec semantic_diagnostics( const Compilation& compilation, rust::Vec warning_options ); +SymbolAnswer lookup_symbol( + Compilation& compilation, + rust::Str path, + std::size_t offset +); +SymbolAnswer lookup_scoped( + Compilation& compilation, + rust::Str left, + rust::Str right +); +rust::Vec list_members( + Compilation& compilation, + rust::Str path, + std::size_t offset +); +rust::Vec list_scope_members( + Compilation& compilation, + rust::Str name +); +TypeAnswer lookup_type( + Compilation& compilation, + rust::Str path, + std::size_t start, + std::size_t end +); +rust::Vec list_instances(Compilation& compilation); } // namespace slang_sys::compilation diff --git a/crates/slang-sys/src/syntax/tree.rs b/crates/slang-sys/src/syntax/tree.rs index 692aaa39e..b7572d014 100644 --- a/crates/slang-sys/src/syntax/tree.rs +++ b/crates/slang-sys/src/syntax/tree.rs @@ -1,4 +1,5 @@ use std::{ + cell::Cell, fmt, sync::{Arc, OnceLock}, }; @@ -6,6 +7,20 @@ use std::{ use cxx::SharedPtr; use tracing::warn; +thread_local! { + /// Executions of an unexpanded (`expand_includes = false`) parse. + /// The three shipped sites (source_model / file_facts / include_scan) + /// record here so a cold-start count is testable. + pub static UNEXPANDED_PARSE_RUNS: Cell = const { Cell::new(0) }; +} + +/// Record one unexpanded parse at a named site. Call only from the three +/// shipped query bodies, not from ad-hoc test parses. +pub fn record_unexpanded_parse(site: &'static str) { + let _span = tracing::info_span!("unexpanded_parse", site).entered(); + UNEXPANDED_PARSE_RUNS.with(|runs| runs.set(runs.get() + 1)); +} + use super::{ ffi, syntax_node::{SyntaxNode, SyntaxToken}, @@ -24,17 +39,17 @@ use crate::{ #[derive(Clone)] pub struct SyntaxTree { pub(crate) raw: SharedPtr, - preprocessor_trace_cache: Arc>, + preprocessor_trace_cache: Arc>>, } #[derive(Debug, Clone)] pub struct SyntaxTreeWithTrace { pub tree: SyntaxTree, - pub preprocessor_trace: crate::preproc::Trace, + pub preprocessor_trace: Arc, } /// Parser options for creating a syntax tree. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SyntaxTreeOptions { pub predefines: Vec, pub include_paths: Vec, @@ -48,7 +63,7 @@ pub struct SyntaxTreeOptions { } /// In-memory source buffer that can be used for include resolution. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SyntaxTreeBuffer { pub path: String, pub text: String, @@ -140,7 +155,7 @@ impl SyntaxTree { options: &SyntaxTreeOptions, ) -> SyntaxTreeWithTrace { let tree = Self::from_file_in_memory_with_options(text, name, path, options); - let preprocessor_trace = tree.build_preprocessor_trace(); + let preprocessor_trace = tree.preprocessor_trace(); SyntaxTreeWithTrace { tree, preprocessor_trace } } @@ -151,7 +166,7 @@ impl SyntaxTree { options: &SyntaxTreeOptions, ) -> SyntaxTreeWithTrace { let tree = Self::from_text_with_options(text, name, path, options); - let preprocessor_trace = tree.build_preprocessor_trace(); + let preprocessor_trace = tree.preprocessor_trace(); SyntaxTreeWithTrace { tree, preprocessor_trace } } @@ -262,20 +277,18 @@ impl SyntaxTree { .collect() } - pub fn preprocessor_trace(&self) -> crate::preproc::Trace { + /// The trace is built once per tree and shared; every emitted token carries + /// three owned strings, so handing out copies is never affordable. + pub fn preprocessor_trace(&self) -> Arc { self.preprocessor_trace_cache .get_or_init(|| { - crate::preproc::Trace::from_raw(ffi::syntax_tree_preprocessor_trace( + Arc::new(crate::preproc::Trace::from_raw(ffi::syntax_tree_preprocessor_trace( self.raw.as_ref().expect("Slang returned a null syntax tree"), - )) + ))) }) .clone() } - fn build_preprocessor_trace(&self) -> crate::preproc::Trace { - self.preprocessor_trace() - } - pub fn buffer_id(&self) -> u32 { ffi::syntax_tree_root_buffer_id(self.raw.as_ref().expect("null Slang syntax tree")) } @@ -284,7 +297,7 @@ impl SyntaxTree { let trace = self.preprocessor_trace(); SyntaxTreeBufferIds { root_buffer_id: trace.root_buffer_id, - source_buffers: trace.source_buffers, + source_buffers: trace.source_buffers.clone(), } } } diff --git a/crates/slang-sys/src/syntax/wrapper.cpp b/crates/slang-sys/src/syntax/wrapper.cpp index 3507bc6aa..358fac04c 100644 --- a/crates/slang-sys/src/syntax/wrapper.cpp +++ b/crates/slang-sys/src/syntax/wrapper.cpp @@ -30,6 +30,12 @@ namespace slang_sys::syntax::helper { range.end().valid(); } + /// Whether a range can be reported to the trace, which addresses spans + /// inside a single source buffer. + static bool trace_range_valid(slang::SourceRange range) { + return source_range_valid(range) && range.start().buffer() == range.end().buffer(); + } + static const SyntaxNode *find_root(const SyntaxNode *node) { while (node && node->parent.get()) node = node->parent.get(); @@ -136,6 +142,29 @@ namespace slang_sys::syntax { const SyntaxNode &SyntaxTree::root() const { return tree->root(); } + + std::size_t SyntaxTokenHash::operator()(const SyntaxToken &token) const { + auto location = token.location(); + return std::hash()(static_cast(token.kind)) ^ + (std::hash()(location.buffer().getId()) << 1) ^ + (std::hash()(location.offset()) << 2); + } + + const EmittedTokenIndices &SyntaxTree::emitted_token_indices() const { + std::call_once(emitted_token_indices_once, [this] { + for (auto token : tree->getEmittedTokens()) { + if (!helper::trace_range_valid(token.range())) + continue; + // A repeated macro argument emits equal tokens more than once; + // the first position is the one the trace reports. + emitted_token_indices_cache.by_token.emplace( + token, emitted_token_indices_cache.length + ); + emitted_token_indices_cache.length++; + } + }); + return emitted_token_indices_cache; + } } // namespace slang_sys::syntax namespace slang_sys::syntax::tree { @@ -468,8 +497,7 @@ namespace slang_sys::syntax::tree { } RawTraceSourceRange trace_range(slang::SourceRange range) { - if (range == slang::SourceRange::NoLocation || !range.start().valid() || - !range.end().valid() || range.start().buffer() != range.end().buffer()) + if (!helper::trace_range_valid(range)) return empty_trace_range(); return RawTraceSourceRange { range.start().buffer().getId(), @@ -884,10 +912,9 @@ namespace slang_sys::syntax::tree { call_origin->second == slang::parsing::MacroUsageOrigin::Source && (!origin.has_body_token_index || !origin.has_argument_index || !origin.has_argument_token_index)) - throw std::logic_error( - "Slang source macro argument has incomplete token origin metadata: " + - std::to_string(call->call_id) - ); + { + origin.kind = 0; + } } else { auto token_origin = token.macroOrigin(); switch (call_origin->second) { @@ -910,10 +937,9 @@ namespace slang_sys::syntax::tree { if (macro_operation == slang::parsing::Token::MacroOperation::None && call_origin->second == slang::parsing::MacroUsageOrigin::Source && !origin.has_body_token_index) - throw std::logic_error( - "Slang source macro body has no token origin metadata: " + - std::to_string(call->call_id) - ); + { + origin.kind = 0; + } } if (macro_operation == slang::parsing::Token::MacroOperation::TokenPaste) origin.kind = 5; @@ -1067,30 +1093,32 @@ namespace slang_sys::syntax::tree { auto insertion = calls.emplace(call_key(range), TraceCallInfo { call_id, call_id, event.range }); if (!insertion.second) { - throw std::logic_error( - "Slang macro usage ranges are not unique: " + name + " at " + - std::to_string(range.buffer_id) + ":" + - std::to_string(range.range_start) + "-" + - std::to_string(range.range_end) - ); - } - if (auto usage = macro_origins.find(node); usage != macro_origins.end() && - usage->second == slang::parsing::MacroUsageOrigin::Source) { - auto definition = macro_definitions.find(node); - if (definition == macro_definitions.end() || !definition->second) - throw std::logic_error("Slang source macro usage has no definition"); - auto definition_id = definitions.find(definition->second); - if (definition_id == definitions.end()) - throw std::logic_error("Slang source macro usage definition is unindexed"); - event.macro_definition_id = definition_id->second; - event.has_macro_definition_id = true; - call_definitions[call_id] = definition_id->second; - } - if (usage.args) { - for (auto* argument : usage.args->args) - if (argument) - event.arguments.emplace_back(trace_actual_argument_with_original_ranges( - *argument, tree.session->source_manager)); + // Slang may report overlapping macro usages at the same + // source range (e.g. a macro expanding to another macro + // at the same location). Emit the event without a call + // identity; the first call's range key wins for + // token-origin lookups. + event.has_macro_call_id = false; + event.has_macro_expansion_id = false; + } else { + if (auto usage = macro_origins.find(node); usage != macro_origins.end() && + usage->second == slang::parsing::MacroUsageOrigin::Source) { + auto definition = macro_definitions.find(node); + if (definition == macro_definitions.end() || !definition->second) + throw std::logic_error("Slang source macro usage has no definition"); + auto definition_id = definitions.find(definition->second); + if (definition_id == definitions.end()) + throw std::logic_error("Slang source macro usage definition is unindexed"); + event.macro_definition_id = definition_id->second; + event.has_macro_definition_id = true; + call_definitions[call_id] = definition_id->second; + } + if (usage.args) { + for (auto* argument : usage.args->args) + if (argument) + event.arguments.emplace_back(trace_actual_argument_with_original_ranges( + *argument, tree.session->source_manager)); + } } } else if (kind == slang::syntax::SyntaxKind::IfDefDirective || kind == slang::syntax::SyntaxKind::IfNDefDirective || @@ -1182,22 +1210,17 @@ namespace slang_sys::syntax::tree { if (root != &owner.root()) throw std::invalid_argument("syntax context does not belong to its owner tree"); - std::optional match; - uint32_t emitted_index = 0; - for (auto token : owner.tree->getEmittedTokens()) { - if (!trace_range(token.range()).has_range) - continue; - if (!match && token == *target) - match = emitted_index; - emitted_index++; - } - if (trace && emitted_index != trace->emitted_tokens.size()) + const auto &indices = owner.emitted_token_indices(); + if (trace && indices.length != trace->emitted_tokens.size()) throw std::logic_error("Slang trace token sequence is inconsistent"); // Recovery and macro splicing can leave syntax-tree tokens that were // never emitted by the preprocessor. Only the requested target needs // an emitted identity; the two sequences are not required to be // positionally isomorphic. - return match; + auto match = indices.by_token.find(*target); + if (match == indices.by_token.end()) + return std::nullopt; + return match->second; } RawTraceEmittedToken trace_emitted_token_for_target( diff --git a/crates/slang-sys/src/syntax/wrapper.h b/crates/slang-sys/src/syntax/wrapper.h index abee39072..633702cc7 100644 --- a/crates/slang-sys/src/syntax/wrapper.h +++ b/crates/slang-sys/src/syntax/wrapper.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,20 @@ namespace slang_sys::syntax { // TODO: Maybe we should expose this data structure to the rust side, rather // than pretendint it as a SyntaxTree. + // Hashes the public fields that Token equality implies, so equal tokens + // always land in the same bucket and `operator==` decides identity. + struct SyntaxTokenHash { + std::size_t operator()(const SyntaxToken &token) const; + }; + + struct EmittedTokenIndices { + /// First emitted position of each distinct token. + std::unordered_map by_token; + /// Length of the emitted sequence, which repeated macro arguments make + /// longer than `by_token`. + uint32_t length = 0; + }; + class SyntaxTree { public: std::shared_ptr<::slang::syntax::SyntaxTree> tree; @@ -65,6 +80,16 @@ namespace slang_sys::syntax { ~SyntaxTree(); const SyntaxNode &root() const; + + /// Position of each emitted token that carries a source range, keyed by + /// token identity. Built once per tree: callers ask for one token at a + /// time, and rescanning the emitted stream per token is quadratic in + /// file size. + const EmittedTokenIndices &emitted_token_indices() const; + + private: + mutable std::once_flag emitted_token_indices_once; + mutable EmittedTokenIndices emitted_token_indices_cache; }; namespace tree { diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs index 72b6f1353..e16cbecc8 100644 --- a/crates/syntax/src/lib.rs +++ b/crates/syntax/src/lib.rs @@ -32,11 +32,23 @@ pub use slang_sys::{ ChildrenIter, SyntaxAncestors, SyntaxChildren, SyntaxCursor, SyntaxElemPreorder, SyntaxElement, SyntaxElementKind, SyntaxIdxChildren, SyntaxKind, SyntaxNode, SyntaxNodePreorder, SyntaxToken, SyntaxTokenWithParent, SyntaxTree, SyntaxTreeBuffer, - SyntaxTreeOptions, SyntaxTrivia, SyntaxTriviaLoc, WalkEvent, ast, + SyntaxTreeOptions, SyntaxTrivia, SyntaxTriviaLoc, UNEXPANDED_PARSE_RUNS, WalkEvent, ast, + record_unexpanded_parse, }, token::{TokenKind, TriviaKind}, }; +/// Whether this tree contains any preprocessor-directive trivia. +/// +/// This is the single computation of `preprocessor_independent`. It does +/// not depend on predefines and does not build a `Trace`. U1 (`source_model`) +/// and U2 (`file_facts`) both call this; U3 (`include_scan`) does not +/// compute the predicate. +pub fn preprocessor_independent(tree: &SyntaxTree) -> bool { + use crate::SyntaxNodeExt; + !tree.root().has_directive_trivia() +} + pub mod compilation { pub use slang_sys::compilation::Compilation; } diff --git a/crates/syntax/src/ptr.rs b/crates/syntax/src/ptr.rs index 708586465..cab046a5b 100644 --- a/crates/syntax/src/ptr.rs +++ b/crates/syntax/src/ptr.rs @@ -102,6 +102,10 @@ impl SyntaxTokenPtr { SyntaxTokenPtr { kind: token.kind(), range: token.text_range().unwrap() } } + pub fn from_kind_range(kind: TokenKind, range: TextRange) -> SyntaxTokenPtr { + SyntaxTokenPtr { kind, range } + } + pub fn from_token_in(context: SyntaxNode, token: SyntaxToken) -> SyntaxTokenPtr { SyntaxTokenPtr::from_token(SyntaxTokenWithParent { parent: context, tok: token }) } diff --git a/crates/syntax/src/slang_ext/node.rs b/crates/syntax/src/slang_ext/node.rs index 4d634b9ce..d03658dcf 100644 --- a/crates/syntax/src/slang_ext/node.rs +++ b/crates/syntax/src/slang_ext/node.rs @@ -4,7 +4,7 @@ use either::Either; use slang_sys::{ syntax::{ ChildrenIter, SyntaxAncestors, SyntaxElement, SyntaxNode, SyntaxTokenWithParent, - SyntaxTrivia, ast::AstNode, + SyntaxTrivia, WalkEvent, ast::AstNode, }, token::TriviaKind, }; @@ -39,6 +39,11 @@ pub trait SyntaxNodeExt<'a> { fn trivias_with_range( &self, ) -> impl ChildrenIter<(TextRange, SyntaxTrivia<'a>)> + use<'a, Self>; + /// Whether any token in this subtree carries `TriviaKind::DIRECTIVE`. + /// + /// [`crate::preprocessor_independent`] is the single caller of this + /// walk. It does not build a `Trace`. + fn has_directive_trivia(&self) -> bool; } impl<'a> SyntaxNodeExt<'a> for SyntaxNode<'a> { @@ -411,4 +416,14 @@ impl<'a> SyntaxNodeExt<'a> for SyntaxNode<'a> { Either::Left(iter::empty()) } } + + fn has_directive_trivia(&self) -> bool { + self.elem_preorder().any(|event| { + matches!( + event, + WalkEvent::Enter(SyntaxElement::Token(token)) + if token.trivias().any(|trivia| trivia.kind() == TriviaKind::DIRECTIVE) + ) + }) + } } diff --git a/crates/syntax/src/slang_ext/tests.rs b/crates/syntax/src/slang_ext/tests.rs index a9fb4f79c..30f146fda 100644 --- a/crates/syntax/src/slang_ext/tests.rs +++ b/crates/syntax/src/slang_ext/tests.rs @@ -32,3 +32,22 @@ endmodule }; assert_eq!(tok.kind(), TokenKind::INTEGER_LITERAL); } + +fn tree(text: &str) -> SyntaxTree { + SyntaxTree::from_file_in_memory(text, "t.sv", "t.sv") +} + +#[test] +fn plain_module_has_no_directive_trivia() { + assert!(!tree("module m;\nendmodule\n").root().has_directive_trivia()); +} + +#[test] +fn define_include_ifdef_and_macro_use_have_directive_trivia() { + assert!(tree("`define W 8\nmodule m;\nendmodule\n").root().has_directive_trivia()); + assert!(tree("`include \"a.svh\"\nmodule m;\nendmodule\n").root().has_directive_trivia()); + assert!(tree("`ifdef W\nmodule m;\nendmodule\n`endif\n").root().has_directive_trivia()); + assert!( + tree("module m;\n logic [`UNKNOWN-1:0] x;\nendmodule\n").root().has_directive_trivia() + ); +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 5acb640c9..236d14af6 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -10,7 +10,6 @@ anyhow.workspace = true camino.workspace = true ra_ap_paths = "0.0.347" crossbeam-channel.workspace = true -dunce.workspace = true itertools.workspace = true jod-thread = "0.1.2" la-arena.workspace = true @@ -26,7 +25,6 @@ tracing.workspace = true triomphe.workspace = true [target.'cfg(windows)'.dependencies] -winapi-util = "0.1.11" winapi = { version = "0.3.9", features = ["jobapi2", "handleapi", "winnt"] } [features] diff --git a/crates/utils/src/path_identity.rs b/crates/utils/src/path_identity.rs index cd6399e83..71620f487 100644 --- a/crates/utils/src/path_identity.rs +++ b/crates/utils/src/path_identity.rs @@ -2,9 +2,9 @@ use std::path::Path; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::paths::{AbsPath, AbsPathBuf}; +use crate::paths::AbsPath; -/// Normalized path spelling key used before filesystem identity is available. +/// Normalized path spelling key for paths that cross process or FFI boundaries. #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct PathKey(String); @@ -23,178 +23,52 @@ impl PathKey { } } -/// Returns proven path spellings for a path crossing a process or FFI boundary. +/// Maps path spellings to a caller-owned value. /// -/// This is intentionally not a total "canonical path" function. The raw path -/// key is always registered first, because it is the only identity we can -/// preserve without doing IO. The filesystem canonical path is added only when -/// the OS can prove one for the current path. Canonicalization goes through -/// `dunce` so Windows extended-length paths are converted back to ordinary path -/// spelling where possible. When canonicalization fails, for example because -/// the file does not exist yet or the filesystem rejects the lookup, we do not -/// invent another spelling. -/// -/// These strings are safe to hand to external parsers as alternate names for -/// the same path spelling identity. Callers that need filesystem-object -/// identity can use [`FileIdentityKey`] separately. -pub fn path_alias_paths(path: &AbsPath) -> Vec { - let mut paths = vec![path.to_path_buf()]; - - if let Some(canonical) = canonical_path(path) - && !paths.contains(&canonical) - { - paths.push(canonical); - } - - paths -} - -pub fn path_alias_keys(path: &AbsPath) -> Vec { - path_alias_paths(path).iter().map(|path| PathKey::from_abs_path(path)).collect() -} - -/// Value identity for an existing filesystem object. -/// -/// Unlike `same_file::Handle`, this key does not keep the file open after it is -/// computed. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub struct FileIdentityKey(FileIdentityKeyRepr); - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum FileIdentityKeyRepr { - #[cfg(unix)] - Unix { dev: u64, ino: u64 }, - #[cfg(windows)] - Windows { volume: u64, index: u64 }, -} - -impl FileIdentityKey { - /// Returns a stable value identity for an existing filesystem path. - /// - /// The path may be opened or statted while computing the key, but the key - /// itself does not retain any OS file handle. - pub fn from_path(path: &AbsPath) -> Option { - platform_file_identity_key(path.as_ref()) - } -} - -/// Maps filesystem identity evidence to a caller-owned value. -/// -/// Raw and canonical path aliases cover stable path spellings. OS identity keys -/// cover aliases that only the filesystem can prove, such as links. Callers -/// should insert a path again when a formerly missing file is created, because -/// identity evidence may become available later. +/// A path identity is the spelling we handed out, not something the filesystem +/// is asked to prove: every path that crosses a boundary leaves through this +/// index, so it comes back as the same spelling. Lookups therefore never touch +/// the filesystem, which matters because the include search probes far more +/// paths than exist. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct PathIdentityIndex { - aliases: FxHashMap, - identities: FxHashMap, + paths: FxHashMap, } impl Default for PathIdentityIndex { fn default() -> Self { - Self { aliases: FxHashMap::default(), identities: FxHashMap::default() } + Self { paths: FxHashMap::default() } } } impl PathIdentityIndex { - /// Registers every path spelling and OS file identity that can be proven. - /// - /// Later inserts for the same alias replace earlier values. This mirrors - /// the previous `PathKey -> FileId` map behavior and keeps collisions - /// visible to the caller's insertion order instead of guessing which - /// spelling is more correct. + /// Later inserts for the same spelling replace earlier values. pub fn insert_path(&mut self, path: &AbsPath, value: T) { - for key in path_alias_keys(path) { - self.aliases.insert(key, value); - } - self.insert_identity(path, value); + self.paths.insert(PathKey::from_abs_path(path), value); } pub fn get(&self, path: impl AsRef) -> Option { - let path = path.as_ref(); - self.aliases.get(&PathKey::new(path)).copied().or_else(|| self.get_path(Path::new(path))) + self.paths.get(&PathKey::new(path.as_ref())).copied() } pub fn get_path(&self, path: impl AsRef) -> Option { - let path = path.as_ref(); - if let Some(path) = path.to_str() - && let Some(value) = self.aliases.get(&PathKey::new(path)).copied() - { - return Some(value); - } - - if let Some(canonical) = canonical_path(path) - && let Some(value) = - self.aliases.get(&PathKey::from_abs_path(canonical.as_path())).copied() - { - return Some(value); - } - - let identity = platform_file_identity_key(path)?; - self.identities.get(&identity).copied() - } - - fn insert_identity(&mut self, path: &AbsPath, value: T) { - if let Some(identity) = FileIdentityKey::from_path(path) { - self.identities.insert(identity, value); - } + self.get(path.as_ref().to_str()?) } } -/// Deduplicates paths by the same evidence model as [`PathIdentityIndex`]. +/// Deduplicates paths by the same spelling identity as [`PathIdentityIndex`]. #[derive(Default)] pub struct PathIdentitySet { - aliases: FxHashSet, - identities: FxHashSet, + paths: FxHashSet, } impl PathIdentitySet { - /// Inserts all known aliases and returns whether none of them had been - /// seen. + /// Returns whether this spelling had not been seen. pub fn insert_path(&mut self, path: &AbsPath) -> bool { - let keys = path_alias_keys(path); - let identity = FileIdentityKey::from_path(path); - let is_new = keys.iter().all(|key| !self.aliases.contains(key)) - && identity.as_ref().is_none_or(|identity| !self.identities.contains(identity)); - - self.aliases.extend(keys); - if let Some(identity) = identity { - self.identities.insert(identity); - } - - is_new + self.paths.insert(PathKey::from_abs_path(path)) } } -fn canonical_path(path: impl AsRef) -> Option { - // `dunce` wraps `std::fs::canonicalize` but smooths over Windows - // extended-length path spelling. It is still only an optional, OS-proven - // spelling; file identity checks use a value key derived from metadata. - dunce::canonicalize(path).ok().and_then(crate::paths::abs_path_buf_from_path_buf) -} - -#[cfg(unix)] -fn platform_file_identity_key(path: &Path) -> Option { - use std::os::unix::fs::MetadataExt; - - let metadata = std::fs::metadata(path).ok()?; - Some(FileIdentityKey(FileIdentityKeyRepr::Unix { dev: metadata.dev(), ino: metadata.ino() })) -} - -#[cfg(windows)] -fn platform_file_identity_key(path: &Path) -> Option { - let handle = winapi_util::Handle::from_path_any(path).ok()?; - let info = winapi_util::file::information(&handle).ok()?; - Some(FileIdentityKey(FileIdentityKeyRepr::Windows { - volume: info.volume_serial_number(), - index: info.file_index(), - })) -} - -#[cfg(not(any(unix, windows)))] -fn platform_file_identity_key(_path: &Path) -> Option { - None -} - fn normalize_path_key(path: &str) -> String { let mut path = path.replace('\\', "/"); @@ -215,6 +89,7 @@ fn normalize_path_key(path: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::paths::AbsPathBuf; #[test] fn path_key_normalizes_separators() { @@ -233,48 +108,49 @@ mod tests { } #[test] - fn path_alias_paths_include_raw_path() { + fn path_identity_index_resolves_the_spelling_it_was_given() { let cwd = AbsPathBuf::assert_utf8(std::env::current_dir().unwrap()); + let mut index = PathIdentityIndex::default(); - assert!(path_alias_paths(cwd.as_path()).contains(&cwd)); - } - - #[test] - fn path_alias_paths_do_not_invent_canonical_path_for_missing_path() { - let dir = crate::test_support::TestDir::new("missing-path-alias"); - let missing = dir.join("missing.sv"); - let missing_path: &std::path::Path = missing.as_ref(); + index.insert_path(cwd.as_path(), 1); - assert!(!missing_path.exists()); - assert_eq!(path_alias_paths(missing.as_path()), vec![missing]); + assert_eq!(index.get(cwd.to_string()), Some(1)); } #[test] - fn path_identity_index_resolves_raw_path() { - let cwd = AbsPathBuf::assert_utf8(std::env::current_dir().unwrap()); + fn path_identity_index_keeps_parent_directory_segments() { let mut index = PathIdentityIndex::default(); + let path = if cfg!(windows) { + AbsPathBuf::assert("C:\\repo\\rtl\\config.vh".into()) + } else { + AbsPathBuf::assert("/repo/rtl/config.vh".into()) + }; + index.insert_path(path.as_path(), 1); - index.insert_path(cwd.as_path(), 1); - - assert_eq!(index.get(cwd.to_string()), Some(1)); + let slang_path = if cfg!(windows) { + r"C:\repo\rtl\..\rtl\config.vh" + } else { + "/repo/rtl/../rtl/config.vh" + }; + assert_eq!(index.get(slang_path), None); + assert_eq!(index.get(path.to_string()), Some(1)); } #[test] - fn path_identity_index_resolves_existing_path_by_file_identity() { - let dir = crate::test_support::TestDir::new("file-identity"); - let path = dir.write("source.sv", "module top; endmodule\n"); - let alias = dir.join("alias.sv"); + fn path_identity_index_resolves_a_path_that_does_not_exist() { + let dir = crate::test_support::TestDir::new("unwritten-path-identity"); + let missing = dir.join("missing.sv"); + let missing_path: &std::path::Path = missing.as_ref(); let mut index = PathIdentityIndex::default(); - index.insert_path(path.as_path(), 1); - - std::fs::hard_link(&path, &alias).unwrap(); + index.insert_path(missing.as_path(), 1); - assert_eq!(index.get_path(alias.as_path()), Some(1)); + assert!(!missing_path.exists()); + assert_eq!(index.get_path(missing_path), Some(1)); } #[test] - fn path_identity_set_detects_duplicate_raw_path() { + fn path_identity_set_detects_duplicate_path() { let cwd = AbsPathBuf::assert_utf8(std::env::current_dir().unwrap()); let mut set = PathIdentitySet::default(); diff --git a/crates/utils/src/process.rs b/crates/utils/src/process.rs index 40e2f9205..95766efa3 100644 --- a/crates/utils/src/process.rs +++ b/crates/utils/src/process.rs @@ -2,19 +2,43 @@ use std::{ io::{Read, Write}, process::{Child, Command, ExitStatus, Output}, thread::{self, JoinHandle}, - time::Duration, + time::{Duration, Instant}, }; use anyhow::{Context, Result}; use crate::cancellation::{CancellationError, CancellationToken}; +/// The child did not exit before the configured deadline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProcessTimeout { + pub timeout: Duration, + pub pid: u32, +} + +impl std::fmt::Display for ProcessTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "process timed out after {:?} (pid={})", self.timeout, self.pid) + } +} + +impl std::error::Error for ProcessTimeout {} + pub fn configure_process_tree(command: &mut Command) { imp::configure_process_tree(command); } pub fn wait_with_cancellation(child: &mut Child, cancel: &CancellationToken) -> Result { + wait_with_cancellation_and_timeout(child, cancel, None) +} + +pub fn wait_with_cancellation_and_timeout( + child: &mut Child, + cancel: &CancellationToken, + timeout: Option, +) -> Result { let process_tree = imp::ProcessTree::attach(child); + let deadline = timeout.map(|timeout| Instant::now() + timeout); loop { if cancel.is_cancelled() { process_tree.kill(child); @@ -22,6 +46,15 @@ pub fn wait_with_cancellation(child: &mut Child, cancel: &CancellationToken) -> return Err(CancellationError.into()); } + if let Some(timeout) = timeout + && deadline.is_some_and(|deadline| Instant::now() >= deadline) + { + let pid = child.id(); + process_tree.kill(child); + let _ = child.wait(); + return Err(ProcessTimeout { timeout, pid }.into()); + } + if let Some(status) = child.try_wait().context("failed to poll child process")? { return Ok(status); } @@ -184,7 +217,7 @@ pub fn wait_with_output_and_cancellation( child: Child, cancel: &CancellationToken, ) -> Result { - wait_with_stdio_and_cancellation(child, None, cancel) + wait_with_stdio_and_cancellation(child, None, cancel, None) } pub fn wait_with_input_and_output_and_cancellation( @@ -192,13 +225,23 @@ pub fn wait_with_input_and_output_and_cancellation( input: Vec, cancel: &CancellationToken, ) -> Result { - wait_with_stdio_and_cancellation(child, Some(input), cancel) + wait_with_stdio_and_cancellation(child, Some(input), cancel, None) +} + +pub fn wait_with_input_and_output_and_cancellation_and_timeout( + child: Child, + input: Vec, + cancel: &CancellationToken, + timeout: Duration, +) -> Result { + wait_with_stdio_and_cancellation(child, Some(input), cancel, Some(timeout)) } fn wait_with_stdio_and_cancellation( mut child: Child, input: Option>, cancel: &CancellationToken, + timeout: Option, ) -> Result { let stdin = input.map(|input| { child @@ -210,7 +253,7 @@ fn wait_with_stdio_and_cancellation( let stdout = child.stdout.take().map(read_to_end); let stderr = child.stderr.take().map(read_to_end); - let status = wait_with_cancellation(&mut child, cancel); + let status = wait_with_cancellation_and_timeout(&mut child, cancel, timeout); let stdin = match stdin.transpose() { Ok(handle) => join_input(handle), Err(error) => Err(error), @@ -280,8 +323,8 @@ mod tests { use crate::{ cancellation::{CancellationError, CancellationToken}, process::{ - configure_process_tree, wait_with_cancellation, - wait_with_input_and_output_and_cancellation, + ProcessTimeout, configure_process_tree, wait_with_cancellation, + wait_with_cancellation_and_timeout, wait_with_input_and_output_and_cancellation, }, }; @@ -294,6 +337,7 @@ mod tests { .stderr(Stdio::null()) .spawn() .expect("sleep command should spawn"); + let pid = child.id(); let token = CancellationToken::new(); token.cancel(); @@ -301,6 +345,32 @@ mod tests { assert!(error.is::(), "{error:#}"); assert!(child.try_wait().expect("child status should be available").is_some()); + assert!(!pid_is_alive(pid), "cancelled child pid {pid} must not stay alive"); + } + + #[test] + fn timeout_kills_child_process() { + let mut command = sleeper_command(); + configure_process_tree(&mut command); + let mut child = command + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("sleep command should spawn"); + let pid = child.id(); + let token = CancellationToken::new(); + + let error = wait_with_cancellation_and_timeout( + &mut child, + &token, + Some(Duration::from_millis(200)), + ) + .unwrap_err(); + + let timeout = error.downcast_ref::().unwrap_or_else(|| panic!("{error:#}")); + assert_eq!(timeout.pid, pid); + assert!(child.try_wait().expect("child status should be available").is_some()); + assert!(!pid_is_alive(pid), "timed-out child pid {pid} must not stay alive"); } #[test] @@ -403,4 +473,25 @@ Start-Sleep -Seconds 30 command.arg(child).arg(grandchild).arg(marker); command } + + #[cfg(unix)] + fn pid_is_alive(pid: u32) -> bool { + unsafe { libc::kill(pid as libc::pid_t, 0) == 0 } + } + + #[cfg(windows)] + fn pid_is_alive(pid: u32) -> bool { + use winapi::um::{ + handleapi::CloseHandle, processthreadsapi::OpenProcess, + winnt::PROCESS_QUERY_LIMITED_INFORMATION, + }; + let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; + if handle.is_null() { + return false; + } + unsafe { + CloseHandle(handle); + } + true + } } diff --git a/crates/workspace-model/src/source_db.rs b/crates/workspace-model/src/source_db.rs index a16fbdf06..bc4e9f3af 100644 --- a/crates/workspace-model/src/source_db.rs +++ b/crates/workspace-model/src/source_db.rs @@ -1,6 +1,6 @@ use vfs::VfsPath; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum SourceFileKind { #[default] SystemVerilog, diff --git a/src/compiler_worker.rs b/src/compiler_worker.rs new file mode 100644 index 000000000..0bdbc3917 --- /dev/null +++ b/src/compiler_worker.rs @@ -0,0 +1,252 @@ +#[cfg(not(test))] +use std::process::{Command, Stdio}; +use std::{ + io::{BufReader, BufWriter, Write}, + sync::{Condvar, Mutex, OnceLock}, + time::Duration, +}; + +use anyhow::Context; +#[cfg(not(test))] +use anyhow::bail; +use preproc_expand::profile_compiler::{ + ProfileCompilationJob, ProfileCompilationOutput, run_profile_compilation, +}; +use utils::cancellation::CancellationToken; + +const DEFAULT_WORKER_TIMEOUT: Duration = Duration::from_secs(30); +const DEFAULT_WORKER_JOBS: usize = 2; + +pub fn run_stdio() -> anyhow::Result<()> { + let input = std::io::stdin(); + let output = std::io::stdout(); + run(BufReader::new(input.lock()), BufWriter::new(output.lock())) +} + +fn run(input: impl std::io::Read, mut output: impl Write) -> anyhow::Result<()> { + let job: ProfileCompilationJob = + serde_json::from_reader(input).context("invalid compiler job")?; + let result = run_profile_compilation(job); + serde_json::to_writer(&mut output, &result).context("failed to encode compiler result")?; + output.flush().context("failed to flush compiler result") +} + +pub(crate) fn compile( + job: &ProfileCompilationJob, + cancellation: &CancellationToken, +) -> anyhow::Result { + cancellation.check()?; + let _slot = acquire_worker_slot(cancellation)?; + compile_with_timeout(job, cancellation, worker_timeout()) +} + +fn compile_with_timeout( + job: &ProfileCompilationJob, + cancellation: &CancellationToken, + timeout: Duration, +) -> anyhow::Result { + #[cfg(test)] + { + let _ = timeout; + cancellation.check()?; + Ok(run_profile_compilation(job.clone())) + } + + #[cfg(not(test))] + { + compile_in_child(job, cancellation, timeout) + } +} + +#[cfg(not(test))] +fn compile_in_child( + job: &ProfileCompilationJob, + cancellation: &CancellationToken, + timeout: Duration, +) -> anyhow::Result { + let executable = std::env::current_exe().context("failed to locate vide executable")?; + let mut command = Command::new(executable); + utils::process::configure_process_tree(&mut command); + let child = command + .arg("--compiler-worker") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to start compiler worker")?; + let input = serde_json::to_vec(job).context("failed to encode compiler job")?; + let output = match utils::process::wait_with_input_and_output_and_cancellation_and_timeout( + child, + input, + cancellation, + timeout, + ) { + Ok(output) => output, + Err(error) if error.is::() => { + return Err(error); + } + Err(error) => { + if let Some(timeout) = error.downcast_ref::() { + bail!("{}", timeout_message(job, timeout.timeout, timeout.pid)); + } + return Err(error); + } + }; + if !output.status.success() { + bail!( + "compiler worker exited with {}: {}", + output.status, + String::from_utf8_lossy(&output.stderr).trim() + ); + } + serde_json::from_slice(&output.stdout).context("invalid compiler worker result") +} + +fn worker_timeout() -> Duration { + std::env::var("VIDE_COMPILER_WORKER_TIMEOUT_SECS") + .ok() + .and_then(|value| value.parse().ok()) + .map(Duration::from_secs) + .unwrap_or(DEFAULT_WORKER_TIMEOUT) +} + +fn worker_job_limit() -> usize { + std::env::var("VIDE_COMPILER_WORKER_JOBS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(DEFAULT_WORKER_JOBS) + .max(1) +} + +fn timeout_message(job: &ProfileCompilationJob, timeout: Duration, pid: u32) -> String { + let bytes: usize = + job.buffers.iter().map(|buffer| buffer.text.as_deref().map(str::len).unwrap_or(0)).sum(); + format!( + "compiler worker timed out after {timeout:?} (pid={pid}, roots={}, buffers={}, bytes={bytes})", + job.roots.len(), + job.buffers.len(), + ) +} + +struct WorkerLimiter { + max: usize, + in_flight: Mutex, + ready: Condvar, +} + +struct WorkerSlot { + limiter: &'static WorkerLimiter, +} + +fn limiter() -> &'static WorkerLimiter { + static LIMITER: OnceLock = OnceLock::new(); + LIMITER.get_or_init(|| WorkerLimiter { + max: worker_job_limit(), + in_flight: Mutex::new(0), + ready: Condvar::new(), + }) +} + +fn acquire_worker_slot(cancellation: &CancellationToken) -> anyhow::Result { + let limiter = limiter(); + let mut in_flight = limiter.in_flight.lock().unwrap_or_else(|poison| poison.into_inner()); + loop { + cancellation.check()?; + if *in_flight < limiter.max { + *in_flight += 1; + return Ok(WorkerSlot { limiter }); + } + let (guard, _) = limiter + .ready + .wait_timeout(in_flight, Duration::from_millis(50)) + .unwrap_or_else(|poison| poison.into_inner()); + in_flight = guard; + } +} + +impl Drop for WorkerSlot { + fn drop(&mut self) { + let mut in_flight = + self.limiter.in_flight.lock().unwrap_or_else(|poison| poison.into_inner()); + *in_flight = in_flight.saturating_sub(1); + self.limiter.ready.notify_one(); + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use preproc_expand::profile_compiler::{ + ProfileCompilationBuffer, ProfileCompilationRoot, ProfileDiagnosticsOptions, + ProfileRootKind, + }; + + use super::*; + + #[test] + fn malformed_job_fails_before_compilation() { + let error = super::run("not json".as_bytes(), Vec::new()).unwrap_err(); + assert!(error.to_string().contains("invalid compiler job")); + } + + #[test] + fn timeout_error_reports_job_scale() { + let job = ProfileCompilationJob { + profile_id: 0, + roots: vec![ProfileCompilationRoot { + file_id: 0, + kind: ProfileRootKind::SystemVerilog, + name: "top.sv".to_owned(), + path: "/top.sv".to_owned(), + }], + buffers: vec![ProfileCompilationBuffer { + file_id: 0, + path: "/top.sv".to_owned(), + text: Some("module top; endmodule\n".to_owned()), + }], + top_modules: Vec::new(), + include_dirs: Vec::new(), + predefines: Vec::new(), + diagnostics: ProfileDiagnosticsOptions { + parse: true, + semantic: true, + warnings: None, + rules: Vec::new(), + }, + }; + let message = timeout_message(&job, Duration::from_secs(30), 4242); + assert!(message.contains("pid=4242"), "{message}"); + assert!(message.contains("roots=1"), "{message}"); + assert!(message.contains("buffers=1"), "{message}"); + assert!( + message.contains(&format!( + "bytes={}", + job.buffers[0].text.as_deref().map(str::len).unwrap_or(0) + )), + "{message}" + ); + } + + #[test] + fn compile_propagates_cancellation_before_work() { + let job = ProfileCompilationJob { + profile_id: 0, + roots: Vec::new(), + buffers: Vec::new(), + top_modules: Vec::new(), + include_dirs: Vec::new(), + predefines: Vec::new(), + diagnostics: ProfileDiagnosticsOptions { + parse: true, + semantic: true, + warnings: None, + rules: Vec::new(), + }, + }; + let cancellation = CancellationToken::new(); + cancellation.cancel(); + let error = compile(&job, &cancellation).unwrap_err(); + assert!(error.to_string().contains("cancelled"), "{error:#}"); + } +} diff --git a/src/global_state.rs b/src/global_state.rs index 0c887427e..c61c2e1f3 100644 --- a/src/global_state.rs +++ b/src/global_state.rs @@ -39,8 +39,8 @@ pub(crate) use self::workspace_state::{ }; use self::{ diagnostics::{ - DiagnosticCommitFreshness, DiagnosticFileRevision, DiagnosticPublishFreshness, - DiagnosticSource, publisher::DiagnosticPublishKey, + DiagnosticFileRevision, DiagnosticPublishFreshness, DiagnosticSource, + publisher::DiagnosticPublishKey, }, mem_docs::MemDocs, snapshot::GlobalStateSnapshot, @@ -94,6 +94,11 @@ pub(crate) struct DiagnosticsState { // text. Keep those target changes explicit so push diagnostics converge at // the normal change-processing boundary. pub(crate) pending_document_diagnostic_targets: FxHashSet, + /// Last isolated slang profile compile, keyed by analysis file. + /// Vide diagnostics are computed at publish time, not stored here. + /// URI-only didOpen/didClose republishes slang from here and adds live + /// Vide. + pub(crate) cached_slang_diagnostics: FxHashMap>, pub(crate) diagnostics_revision: u64, pub(crate) diagnostic_target_revision: u64, pub(crate) diagnostic_file_revisions: FxHashMap, @@ -223,6 +228,7 @@ impl GlobalState { diagnostics: DiagnosticsState { published_diagnostics: FxHashMap::default(), pending_document_diagnostic_targets: FxHashSet::default(), + cached_slang_diagnostics: FxHashMap::default(), diagnostics_revision: 0, diagnostic_target_revision: 0, diagnostic_file_revisions: FxHashMap::default(), @@ -274,6 +280,12 @@ impl GlobalState { qihe::with_global_ctx(self, |qihe, ctx| qihe.handle(task, ctx)); } + pub(crate) fn cancel_semantic_compiler(&mut self) { + semantic_compiler::with_global_ctx(self, |semantic_compiler, _ctx| { + semantic_compiler.cancel_active(); + }); + } + pub(crate) fn schedule_semantic_compiler(&mut self, profile_ids: Vec) { semantic_compiler::with_global_ctx(self, |semantic_compiler, ctx| { semantic_compiler.schedule(profile_ids, ctx) @@ -305,7 +317,13 @@ impl GlobalState { #[derive(Debug, Clone, Default)] pub(crate) struct QiheDiagnosticState { - pub(crate) freshness: DiagnosticCommitFreshness, + pub(crate) captured_snapshot: base_db::analysis_snapshot::AnalysisSnapshotId, pub(crate) generation: u64, - pub(crate) diagnostics: Vec, + pub(crate) items: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct AnchoredQiheDiagnostic { + pub(crate) ast_id: Option, + pub(crate) diagnostic: lsp_types::Diagnostic, } diff --git a/src/global_state/diagnostics.rs b/src/global_state/diagnostics.rs index 5b3f8f188..dc33bdd92 100644 --- a/src/global_state/diagnostics.rs +++ b/src/global_state/diagnostics.rs @@ -54,6 +54,18 @@ pub(crate) trait DiagnosticSource: Send + Sync { Vec::new() } + fn lsp_diagnostics_projected( + &self, + file_id: FileId, + freshness: &DiagnosticCommitFreshness, + analysis: &ide::analysis::AnalysisSnapshot, + i18n: crate::i18n::I18n, + line_info: Option<&utils::lines::LineInfo>, + ) -> Vec { + let _ = (analysis, i18n, line_info); + self.lsp_diagnostics(file_id, freshness) + } + fn external_revision( &self, file_id: FileId, diff --git a/src/global_state/event_loop.rs b/src/global_state/event_loop.rs index 8d9a46769..e80840aef 100644 --- a/src/global_state/event_loop.rs +++ b/src/global_state/event_loop.rs @@ -194,6 +194,7 @@ impl GlobalState { Event::Lsp(msg) => match msg { Message::Request(request) => { self.client.register_incoming(loop_start, &request); + self.commit_pending_vfs(); self.dispatch_request(router, request); } Message::Notification(notification) => { @@ -358,6 +359,15 @@ impl GlobalState { } } + /// Apply queued VFS messages before a request snapshot is taken, so the + /// request does not start on a revision that the next turn will write. + fn commit_pending_vfs(&mut self) { + while let Ok(msg) = self.workspace.vfs_loader.receiver.try_recv() { + self.process_vfs_msg(msg); + } + let _ = self.process_changes(); + } + fn handle_vfs_msg(&mut self, msg: vfs_loader::Message) { self.process_vfs_msg(msg); diff --git a/src/global_state/handlers/request/navigation.rs b/src/global_state/handlers/request/navigation.rs index 092e0bf90..d679b756d 100644 --- a/src/global_state/handlers/request/navigation.rs +++ b/src/global_state/handlers/request/navigation.rs @@ -1,6 +1,6 @@ use ide::{ - DefKind, FileRange, navigation_target::NavTarget, references::References, - semantic_index::ModuleCallItem, + DefKind, FileRange, navigation_target::NavTarget, reference_support::ModuleCallItem, + references::References, }; use itertools::Itertools; diff --git a/src/global_state/process_changes.rs b/src/global_state/process_changes.rs index e0b3e9153..dc43ab11c 100644 --- a/src/global_state/process_changes.rs +++ b/src/global_state/process_changes.rs @@ -20,6 +20,9 @@ use crate::{config::user_config::DiagnosticsUpdateUserConfig, lsp_ext::to_proto} pub(crate) enum DiagnosticInvalidation { FileChanges(FxHashSet), WorkspaceChanged, + /// Open/close URI set changed; analysis text did not. Republish cached + /// diagnostics for those files — do not compile the profile again. + PublishTargets(FxHashSet), } // Apply changes @@ -39,7 +42,9 @@ impl GlobalState { std::mem::drop(read_guard); if !pending_diagnostic_targets.is_empty() { self.diagnostics.diagnostic_target_revision += 1; - self.request_diagnostics(pending_diagnostic_targets.into_iter().collect()); + self.invalidate_diagnostics(DiagnosticInvalidation::PublishTargets( + pending_diagnostic_targets, + )); } return false; }; @@ -83,6 +88,7 @@ impl GlobalState { std::mem::drop(write_guard); + self.cancel_semantic_compiler(); self.analysis.analysis_host.apply_change(change); self.diagnostics.diagnostics_revision += 1; for file_id in &content_changed_file_ids { @@ -114,11 +120,13 @@ impl GlobalState { } } if !pending_diagnostic_targets.is_empty() - && (has_structure_changes - || self.config_state.config.user_config.diagnostics.update - != DiagnosticsUpdateUserConfig::OnType) + && !has_structure_changes + && self.config_state.config.user_config.diagnostics.update + != DiagnosticsUpdateUserConfig::OnType { - self.request_diagnostics(pending_diagnostic_targets.into_iter().collect()); + self.invalidate_diagnostics(DiagnosticInvalidation::FileChanges( + pending_diagnostic_targets, + )); } true @@ -138,12 +146,17 @@ impl GlobalState { return; } + if let DiagnosticInvalidation::PublishTargets(file_ids) = &invalidation { + self.republish_cached_slang_diagnostics(file_ids); + return; + } + let semantic_profile_ids = self.semantic_compiler_profiles_for_invalidation(&invalidation); let semantic_compilation_scheduled = !semantic_profile_ids.is_empty(); self.schedule_semantic_compiler(semantic_profile_ids); - if self.config_state.config.cli_pull_diagnostics_support() - && semantic_compilation_scheduled - && matches!(&invalidation, DiagnosticInvalidation::FileChanges(_)) + if semantic_compilation_scheduled + && (!self.config_state.config.cli_pull_diagnostics_support() + || matches!(&invalidation, DiagnosticInvalidation::FileChanges(_))) { return; } @@ -153,6 +166,7 @@ impl GlobalState { && match &invalidation { DiagnosticInvalidation::FileChanges(file_ids) => !file_ids.is_empty(), DiagnosticInvalidation::WorkspaceChanged => true, + DiagnosticInvalidation::PublishTargets(_) => false, } { self.client.request_ignore::(()); @@ -166,6 +180,7 @@ impl GlobalState { .into_iter() .collect(), DiagnosticInvalidation::WorkspaceChanged => self.open_mem_doc_file_ids(), + DiagnosticInvalidation::PublishTargets(file_ids) => file_ids.into_iter().collect(), }; self.request_diagnostics(file_ids); } @@ -190,16 +205,14 @@ impl GlobalState { match invalidation { DiagnosticInvalidation::WorkspaceChanged => profile_ids, + DiagnosticInvalidation::PublishTargets(_) => Vec::new(), DiagnosticInvalidation::FileChanges(changed_file_ids) => profile_ids .into_iter() .filter(|profile_id| { - snapshot.analysis.compilation_profile_file_ids(*profile_id).is_ok_and( - |profile_file_ids| { - profile_file_ids - .iter() - .any(|file_id| changed_file_ids.contains(file_id)) - }, - ) + changed_file_ids.iter().any(|file_id| { + snapshot.analysis.file_compilation_profile(*file_id).ok().flatten() + == Some(*profile_id) + }) }) .collect(), } @@ -335,6 +348,55 @@ impl GlobalState { Some(changed_file) } + fn republish_cached_slang_diagnostics(&mut self, file_ids: &FxHashSet) { + if file_ids.is_empty() || self.diagnostics.cached_slang_diagnostics.is_empty() { + return; + } + if self.config_state.config.cli_pull_diagnostics_support() { + if self.config_state.config.cli_workspace_diagnostic_refresh_support() { + self.client.request_ignore::(()); + } + return; + } + + let snapshot = self.make_snapshot(); + let mut results = Vec::new(); + let mut touched_file_ids = FxHashSet::default(); + for &file_id in file_ids { + let Ok(targets) = snapshot.diagnostic_publish_targets(file_id) else { + continue; + }; + if targets.is_empty() { + touched_file_ids.insert(file_id); + continue; + } + let slang = self + .diagnostics + .cached_slang_diagnostics + .get(&file_id) + .cloned() + .unwrap_or_default(); + let vide = snapshot.analysis.file_vide_diagnostics(file_id).unwrap_or_default(); + let diagnostics = super::semantic_compiler::with_vide_diagnostics(slang, vide); + let Ok(lsp_diagnostics) = snapshot.lsp_diagnostics_from_ide(file_id, diagnostics) + else { + continue; + }; + touched_file_ids.insert(file_id); + results.extend(targets.into_iter().map(|target| { + PublishDiagnosticsTask::from_target(target, lsp_diagnostics.clone()) + })); + } + if touched_file_ids.is_empty() { + return; + } + self.publish_diagnostics_tasks(PublishDiagnosticsBatch::for_touched_files( + touched_file_ids, + results, + snapshot.diagnostic_publish_freshness, + )); + } + pub(crate) fn request_diagnostics(&mut self, files: Vec) { if files.is_empty() { return; @@ -484,6 +546,48 @@ mod tests { ); } + #[test] + fn uri_only_did_open_does_not_schedule_semantic_compiler() { + use super::super::handlers::notification::handle_did_open_text_document; + + let root = TestDir::new("uri-only-did-open-no-recompile"); + let root_path = root.path().to_path_buf(); + let mut state = test_state(root_path); + let file_path = root.join("top.sv"); + let text = "module top;\nendmodule\n"; + state + .workspace + .vfs + .write() + .0 + .set_file_contents(VfsPath::from(file_path.clone()), Some(text.as_bytes().to_vec())); + assert!(state.process_changes()); + let generation = state.semantic_compiler.run_generation(); + + handle_did_open_text_document( + &mut state, + lsp_types::DidOpenTextDocumentParams { + text_document: lsp_types::TextDocumentItem { + uri: lsp_types::Url::from_file_path(file_path.as_path()).unwrap(), + language_id: "systemverilog".to_owned(), + version: 1, + text: text.to_owned(), + }, + }, + ) + .unwrap(); + assert!(!state.process_changes()); + assert_eq!( + state.semantic_compiler.run_generation(), + generation, + "didOpen of an already-loaded file must not start a new profile compile" + ); + assert!( + !state.semantic_compiler.has_pending_profiles(), + "didOpen of an already-loaded file must not queue another profile compile" + ); + } + #[test] fn unchanged_external_manifest_does_not_request_workspace_reload() { let root = TestDir::new("unchanged-external-manifest-no-reload"); diff --git a/src/global_state/protocol.rs b/src/global_state/protocol.rs index 6d085de82..7af07f53d 100644 --- a/src/global_state/protocol.rs +++ b/src/global_state/protocol.rs @@ -53,15 +53,15 @@ pub(crate) fn router() -> LspRouter { InlayHintRequest => RequestPolicy::WORKER_NO_RETRY, request::handle_inlay_hint; CodeLensRequest => RequestPolicy::WORKER_NO_RETRY, request::handle_code_lens; CodeLensResolve => RequestPolicy::WORKER_NO_RETRY, request::handle_code_lens_resolve; - HoverRequest => RequestPolicy::WORKER_NO_RETRY, request::handle_hover; - GotoDefinition => RequestPolicy::WORKER_NO_RETRY, request::handle_goto_definition; - GotoDeclaration => RequestPolicy::WORKER_NO_RETRY, request::handle_goto_declaration; - GotoTypeDefinition => RequestPolicy::WORKER_NO_RETRY, request::handle_goto_type_definition; + HoverRequest => RequestPolicy::LATENCY_SENSITIVE, request::handle_hover; + GotoDefinition => RequestPolicy::LATENCY_SENSITIVE, request::handle_goto_definition; + GotoDeclaration => RequestPolicy::LATENCY_SENSITIVE, request::handle_goto_declaration; + GotoTypeDefinition => RequestPolicy::LATENCY_SENSITIVE, request::handle_goto_type_definition; CallHierarchyPrepare => RequestPolicy::WORKER_NO_RETRY, request::handle_prepare_call_hierarchy; CallHierarchyIncomingCalls => RequestPolicy::WORKER_NO_RETRY, request::handle_call_hierarchy_incoming; CallHierarchyOutgoingCalls => RequestPolicy::WORKER_NO_RETRY, request::handle_call_hierarchy_outgoing; DocumentHighlightRequest => RequestPolicy::WORKER_NO_RETRY, request::handle_document_highlight; - References => RequestPolicy::WORKER_NO_RETRY, request::handle_references; + References => RequestPolicy::LATENCY_SENSITIVE, request::handle_references; PrepareRenameRequest => RequestPolicy::WORKER_NO_RETRY, request::handle_prepare_rename; Rename => RequestPolicy::WORKER_NO_RETRY, request::handle_rename; Formatting => RequestPolicy::LATENCY_SENSITIVE_NO_RETRY, request::handle_formatting; diff --git a/src/global_state/qihe.rs b/src/global_state/qihe.rs index 0cea53218..084b625fb 100644 --- a/src/global_state/qihe.rs +++ b/src/global_state/qihe.rs @@ -34,8 +34,8 @@ use utils::{ use vfs::FileId; use super::{ - AnalysisState, ConfigState, DiagnosticsState, GlobalState, LspClient, QiheDiagnosticState, - TaskState, WorkspaceState, + AnalysisState, AnchoredQiheDiagnostic, ConfigState, DiagnosticsState, GlobalState, LspClient, + QiheDiagnosticState, TaskState, WorkspaceState, diagnostics::{ DiagnosticCommitFreshness, DiagnosticExternalRevision, DiagnosticOwner, DiagnosticPublishFreshness, DiagnosticSource, @@ -85,6 +85,54 @@ impl QiheDiagnostics { fn lock(&self) -> MutexGuard<'_, FxHashMap> { self.states.lock() } + + fn projected( + &self, + file_id: FileId, + current_snapshot: base_db::analysis_snapshot::AnalysisSnapshotId, + analysis: Option<&ide::analysis::AnalysisSnapshot>, + i18n: crate::i18n::I18n, + line_info: Option<&utils::lines::LineInfo>, + ) -> Vec { + let mut cache = self.lock(); + let Some(state) = cache.get_mut(&file_id) else { + return Vec::new(); + }; + if let (Some(analysis), Some(line_info)) = (analysis, line_info) { + for item in &mut state.items { + if item.ast_id.is_none() + && let Ok(range) = from_proto::text_range(line_info, item.diagnostic.range) + { + item.ast_id = analysis.ast_id_at_range(file_id, range).ok().flatten(); + } + } + } + let state = state.clone(); + drop(cache); + let edits_ago = current_snapshot.get().saturating_sub(state.captured_snapshot.get()); + state + .items + .into_iter() + .map(|item| { + let mut diagnostic = item.diagnostic; + if let (Some(analysis), Some(ast_id), Some(line_info)) = + (analysis, item.ast_id, line_info) + && let Ok(Some(origin)) = analysis + .project_anchor(ide::anchor::Anchor::Definition { file: file_id, ast_id }) + { + diagnostic.range = to_proto::range(line_info, origin.range); + } + if edits_ago > 0 { + let note = + i18n.format(keys::QIHE_BASED_ON_EDITS, [("n", edits_ago.to_string())]); + if !diagnostic.message.contains(¬e) { + diagnostic.message = format!("{}\n{note}", diagnostic.message); + } + } + diagnostic + }) + .collect() + } } impl DiagnosticSource for QiheDiagnostics { @@ -93,11 +141,18 @@ impl DiagnosticSource for QiheDiagnostics { file_id: FileId, freshness: &DiagnosticCommitFreshness, ) -> Vec { - self.lock() - .get(&file_id) - .filter(|state| state.freshness == *freshness) - .map(|state| state.diagnostics.clone()) - .unwrap_or_default() + self.projected(file_id, freshness.snapshot_id(), None, crate::i18n::I18n::default(), None) + } + + fn lsp_diagnostics_projected( + &self, + file_id: FileId, + freshness: &DiagnosticCommitFreshness, + analysis: &ide::analysis::AnalysisSnapshot, + i18n: crate::i18n::I18n, + line_info: Option<&utils::lines::LineInfo>, + ) -> Vec { + self.projected(file_id, freshness.snapshot_id(), Some(analysis), i18n, line_info) } fn external_revision( @@ -105,7 +160,8 @@ impl DiagnosticSource for QiheDiagnostics { file_id: FileId, freshness: &DiagnosticCommitFreshness, ) -> Option { - self.lock().get(&file_id).filter(|state| state.freshness == *freshness).map(|state| { + let _ = freshness; + self.lock().get(&file_id).map(|state| { DiagnosticExternalRevision::new( DiagnosticOwner::External { source: QIHE, file: file_id }, state.generation, @@ -284,20 +340,9 @@ impl Qihe { self.end_current(progress_token, "end", message.clone(), message, ctx); return; } - let current_freshness = ctx.diagnostic_commit_freshness(); - if update.freshness != current_freshness { - tracing::debug!( - ?run_id, - freshness = ?update.freshness, - current = ?current_freshness, - "stale qihe diagnostics ignored" - ); - let message = ctx.i18n_text(QiheI18nKey::Stale).to_owned(); - self.end_current(progress_token, "end", message.clone(), message, ctx); - return; - } let summary = update.summary.clone(); - let changed_files = self.replace_diagnostics(update.by_file, current_freshness); + let captured = update.freshness.snapshot_id(); + let changed_files = self.replace_diagnostics(update.by_file, captured); self.publish_diagnostics(changed_files, ctx); self.end_current(progress_token, "end", summary.clone(), summary, ctx); } @@ -381,12 +426,12 @@ impl Qihe { fn replace_diagnostics( &mut self, mut by_file: FxHashMap>, - freshness: DiagnosticCommitFreshness, + captured_snapshot: base_db::analysis_snapshot::AnalysisSnapshotId, ) -> FxHashSet { let mut cache = self.diagnostics.lock(); let mut changed_files = cache .iter() - .filter_map(|(&file_id, state)| (!state.diagnostics.is_empty()).then_some(file_id)) + .filter_map(|(&file_id, state)| (!state.items.is_empty()).then_some(file_id)) .collect::>(); changed_files.extend(by_file.keys().copied()); @@ -394,7 +439,11 @@ impl Qihe { let diagnostics = by_file.remove(file_id).unwrap_or_default(); let generation = cache.get(file_id).map_or(1, |state| state.generation.saturating_add(1)); - cache.insert(*file_id, QiheDiagnosticState { freshness, generation, diagnostics }); + let items = diagnostics + .into_iter() + .map(|diagnostic| AnchoredQiheDiagnostic { ast_id: None, diagnostic }) + .collect(); + cache.insert(*file_id, QiheDiagnosticState { captured_snapshot, generation, items }); } changed_files @@ -403,7 +452,6 @@ impl Qihe { pub(crate) trait QiheCtx { fn i18n_text(&self, key: QiheI18nKey) -> &str; - fn diagnostic_commit_freshness(&self) -> DiagnosticCommitFreshness; fn make_snapshot(&self, cancellation: CancellationToken) -> GlobalStateSnapshot; fn spawn_qihe_task(&mut self, task: F) where @@ -425,7 +473,6 @@ pub(crate) trait QiheCtx { pub(crate) enum QiheI18nKey { ProgressTitle, Cancelled, - Stale, Failed, } @@ -469,16 +516,11 @@ impl QiheCtx for QiheGlobalCtx<'_> { let key = match key { QiheI18nKey::ProgressTitle => keys::QIHE_PROGRESS_TITLE, QiheI18nKey::Cancelled => keys::QIHE_CANCELLED, - QiheI18nKey::Stale => keys::QIHE_STALE, QiheI18nKey::Failed => keys::QIHE_FAILED, }; self.config_state.config.i18n.text(key) } - fn diagnostic_commit_freshness(&self) -> DiagnosticCommitFreshness { - self.diagnostic_publish_freshness().commit() - } - fn make_snapshot(&self, cancellation: CancellationToken) -> GlobalStateSnapshot { super::make_snapshot( &self.config_state.config, @@ -781,9 +823,8 @@ fn qihe_compile_input( let plan = snapshot.analysis.compilation_plan(active_file_id).map_err(|_| CancellationError)?; cancellation.check()?; let files = plan - .roots - .iter() - .filter_map(|file_id| snapshot.file_path(*file_id).map(PathBuf::from)) + .root_file_ids() + .filter_map(|file_id| snapshot.file_path(file_id).map(PathBuf::from)) .collect::>(); Ok(qihe_compile_input_from_plan(&plan, files, active_path, manifest_file_name)) diff --git a/src/global_state/qihe/tests.rs b/src/global_state/qihe/tests.rs index 20a5fc4cf..0bdc99897 100644 --- a/src/global_state/qihe/tests.rs +++ b/src/global_state/qihe/tests.rs @@ -159,7 +159,14 @@ fn stale_qihe_result_does_not_replace_current_diagnostics() { let freshness = state.diagnostic_publish_freshness().commit(); state.qihe.diagnostics.lock().insert( file_id, - QiheDiagnosticState { freshness, generation: 1, diagnostics: vec![current.clone()] }, + QiheDiagnosticState { + captured_snapshot: freshness.snapshot_id(), + generation: 1, + items: vec![crate::global_state::AnchoredQiheDiagnostic { + ast_id: None, + diagnostic: current.clone(), + }], + }, ); state.handle_qihe_task(QiheTask::Finished { @@ -172,7 +179,16 @@ fn stale_qihe_result_does_not_replace_current_diagnostics() { progress_token: "old".to_owned(), }); - let stored = state.qihe.diagnostics.lock().get(&file_id).unwrap().diagnostics.clone(); + let stored = state + .qihe + .diagnostics + .lock() + .get(&file_id) + .unwrap() + .items + .iter() + .map(|item| item.diagnostic.clone()) + .collect::>(); assert_eq!(stored, vec![current]); } @@ -277,7 +293,7 @@ fn work_done_progress_cancel_ignores_stale_qihe_run_token() { } #[test] -fn qihe_diagnostics_are_scoped_to_diagnostic_commit_freshness() { +fn qihe_diagnostics_survive_a_freshness_advance() { let root = TestDir::new("qihe-diagnostic-freshness"); let config = config::Config::new( Opt { @@ -306,7 +322,14 @@ fn qihe_diagnostics_are_scoped_to_diagnostic_commit_freshness() { let freshness = state.diagnostic_publish_freshness().commit(); state.qihe.diagnostics.lock().insert( file_id, - QiheDiagnosticState { freshness, generation: 1, diagnostics: vec![diagnostic.clone()] }, + QiheDiagnosticState { + captured_snapshot: freshness.snapshot_id(), + generation: 1, + items: vec![crate::global_state::AnchoredQiheDiagnostic { + ast_id: None, + diagnostic: diagnostic.clone(), + }], + }, ); let snapshot = state.make_snapshot(); @@ -321,19 +344,43 @@ fn qihe_diagnostics_are_scoped_to_diagnostic_commit_freshness() { state.diagnostics.diagnostics_revision += 1; let snapshot = state.make_snapshot(); let freshness = snapshot.diagnostic_commit_freshness(); + let after_edit = snapshot + .external_sources + .iter() + .flat_map(|source| source.lsp_diagnostics(file_id, &freshness)) + .collect::>(); + assert_eq!( + after_edit.iter().map(|diag| diag.message.as_str()).collect::>(), + vec!["current"], + "an edit must not drop the last qihe result" + ); + + let later = crate::global_state::diagnostics::DiagnosticCommitFreshness::for_snapshot( + ide::AnalysisSnapshotId::new(3), + 0, + 0, + ); + let labeled = snapshot + .external_sources + .iter() + .flat_map(|source| source.lsp_diagnostics(file_id, &later)) + .collect::>(); assert!( - snapshot - .external_sources - .iter() - .flat_map(|source| source.lsp_diagnostics(file_id, &freshness)) - .collect::>() - .is_empty() + labeled.iter().any(|diag| diag.message.contains("edit")), + "a later snapshot must label how many edits the analysis predates:\n{labeled:?}" ); } #[test] -fn qihe_result_with_stale_diagnostic_freshness_does_not_commit() { +fn qihe_result_that_lands_after_an_edit_still_commits() { let root = TestDir::new("stale-qihe-freshness"); + let caps = lsp_types::ClientCapabilities { + text_document: Some(TextDocumentClientCapabilities { + diagnostic: Some(DiagnosticClientCapabilities::default()), + ..TextDocumentClientCapabilities::default() + }), + ..lsp_types::ClientCapabilities::default() + }; let config = config::Config::new( Opt { process_name: "vide-test".to_string(), @@ -342,7 +389,7 @@ fn qihe_result_with_stale_diagnostic_freshness_does_not_commit() { profile_trace: None, }, root.path().to_path_buf(), - lsp_types::ClientCapabilities::default(), + caps, vec![root.path().to_path_buf()], I18n::default(), UserConfig::default(), @@ -374,7 +421,10 @@ fn qihe_result_with_stale_diagnostic_freshness_does_not_commit() { progress_token: "current".to_owned(), }); - assert!(state.qihe.diagnostics.lock().is_empty()); + assert!( + !state.qihe.diagnostics.lock().is_empty(), + "results that land after an edit stay and reproject; they are not discarded" + ); assert_eq!(state.qihe.active_progress_token, None); } diff --git a/src/global_state/semantic_compiler.rs b/src/global_state/semantic_compiler.rs index 9710a5f33..9709f0380 100644 --- a/src/global_state/semantic_compiler.rs +++ b/src/global_state/semantic_compiler.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::{Context, Result}; use base_db::project::CompilationProfileId; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use utils::{ cancellation::{CancellationError, CancellationToken}, thread::ThreadIntent, @@ -16,7 +16,7 @@ use super::{ AnalysisState, ConfigState, DiagnosticsState, GlobalState, LspClient, TaskState, WorkspaceState, diagnostics::{ - DiagnosticPublishFreshness, DiagnosticSource, + DiagnosticCommitFreshness, DiagnosticPublishFreshness, DiagnosticSource, publisher::{DiagnosticsPublisher, PublishDiagnosticsBatch, PublishDiagnosticsTask}, }, snapshot::GlobalStateSnapshot, @@ -28,7 +28,10 @@ pub(crate) struct SemanticCompilerUpdate { delivery: SemanticDiagnosticsDelivery, touched_files: FxHashSet, diagnostic_count: usize, - freshness: DiagnosticPublishFreshness, + /// Commit freshness only. URI-set (didOpen/didClose) changes must not + /// discard a compile whose analysis inputs are still current. + freshness: DiagnosticCommitFreshness, + by_file: FxHashMap>, } #[derive(Debug)] @@ -71,6 +74,22 @@ impl SemanticCompiler { } } + #[cfg(test)] + pub(crate) fn run_generation(&self) -> u64 { + self.run_generation.0 + } + + #[cfg(test)] + pub(crate) fn has_pending_profiles(&self) -> bool { + !self.pending_profiles.is_empty() + } + + pub(crate) fn cancel_active(&mut self) { + if let Some(token) = &self.active_cancel_token { + token.cancel(); + } + } + pub(crate) fn schedule( &mut self, profile_ids: Vec, @@ -112,7 +131,7 @@ impl SemanticCompiler { } self.active_cancel_token = None; - let current_freshness = ctx.diagnostic_publish_freshness(); + let current_freshness = ctx.diagnostic_publish_freshness().commit(); if update.freshness != current_freshness { tracing::debug!( ?run_id, @@ -124,6 +143,7 @@ impl SemanticCompiler { return; } + ctx.store_profile_diagnostics(update.by_file.clone()); let SemanticCompilerUpdate { delivery, touched_files, .. } = update; match delivery { SemanticDiagnosticsDelivery::PullRefresh => { @@ -212,6 +232,10 @@ pub(crate) trait SemanticCompilerCtx { fn task_cancel_token(&self) -> CancellationToken; fn refresh_semantic_diagnostics(&mut self, changed_files: FxHashSet); fn publish_semantic_diagnostics(&mut self, batch: PublishDiagnosticsBatch); + fn store_profile_diagnostics( + &mut self, + by_file: FxHashMap>, + ); } pub(super) struct SemanticCompilerGlobalCtx<'a> { @@ -278,6 +302,13 @@ impl SemanticCompilerCtx for SemanticCompilerGlobalCtx<'_> { self.refresh_pull_diagnostics(changed_files); } + fn store_profile_diagnostics( + &mut self, + by_file: FxHashMap>, + ) { + self.diagnostics.cached_slang_diagnostics = by_file; + } + fn publish_semantic_diagnostics(&mut self, batch: PublishDiagnosticsBatch) { if batch.touched_file_count() == 0 { return; @@ -336,6 +367,19 @@ fn run_semantic_compiler_task( } } +fn open_file_vide_diagnostics( + snapshot: &GlobalStateSnapshot, + profile_id: CompilationProfileId, +) -> Result> { + let mut diagnostics = Vec::new(); + for file_id in snapshot.mem_docs.file_ids() { + if snapshot.analysis.file_compilation_profile(file_id)? == Some(profile_id) { + diagnostics.extend(snapshot.analysis.file_vide_diagnostics(file_id)?); + } + } + Ok(diagnostics) +} + fn collect_semantic_diagnostics( snapshot: GlobalStateSnapshot, profile_ids: Vec, @@ -343,61 +387,118 @@ fn collect_semantic_diagnostics( ) -> Result { let freshness = snapshot.diagnostic_publish_freshness; let mut touched_files = FxHashSet::default(); - let mut diagnostic_count = 0; + let mut profiles = Vec::with_capacity(profile_ids.len()); let profile_count = profile_ids.len(); + let pull_diagnostics = snapshot.config.cli_pull_diagnostics_support(); for profile_id in profile_ids { cancellation.check()?; touched_files.extend(snapshot.analysis.compilation_profile_file_ids(profile_id)?); - let diagnostics = snapshot.analysis.compilation_profile_diagnostics(profile_id)?; - diagnostic_count += diagnostics.len(); + if !pull_diagnostics { + profiles.push(( + snapshot.analysis.compilation_profile_job(profile_id)?, + open_file_vide_diagnostics(&snapshot, profile_id)?, + )); + } cancellation.check()?; } - cancellation.check()?; + if pull_diagnostics { + drop(snapshot); + return Ok(SemanticCompilerUpdate { + delivery: SemanticDiagnosticsDelivery::PullRefresh, + touched_files, + diagnostic_count: 0, + freshness: freshness.commit(), + by_file: FxHashMap::default(), + }); + } + + let i18n = snapshot.config.i18n; + let mut publish_files = FxHashMap::default(); + for file_id in touched_files.iter().copied() { + cancellation.check()?; + let targets = snapshot + .diagnostic_publish_targets(file_id) + .with_context(|| format!("failed to resolve diagnostic targets for {file_id:?}"))?; + let line_info = snapshot.line_info(file_id)?; + let external = snapshot.external_lsp_diagnostics(file_id)?; + publish_files.insert(file_id, (targets, line_info, external)); + } + drop(snapshot); + let mut slang_by_file = FxHashMap::>::default(); + let mut vide_by_file = FxHashMap::>::default(); + let mut diagnostic_count = 0; + for (job, vide_diagnostics) in profiles { + cancellation.check()?; + let output = crate::compiler_worker::compile(&job, cancellation)?; + for diagnostic in + ide::diagnostics::materialize_compiler_diagnostics(output.into_diagnostics()) + { + diagnostic_count += 1; + slang_by_file.entry(diagnostic.file_id).or_default().push(diagnostic); + } + for diagnostic in vide_diagnostics { + diagnostic_count += 1; + vide_by_file.entry(diagnostic.file_id).or_default().push(diagnostic); + } + cancellation.check()?; + } + let cached = slang_by_file.clone(); + let mut diagnostics_by_file = merge_slang_and_vide(slang_by_file, vide_by_file); + let delivery = SemanticDiagnosticsDelivery::Push(materialize_semantic_publish_batch( + publish_files, + &touched_files, + &mut diagnostics_by_file, + freshness, + i18n, + cancellation, + )?); tracing::debug!( - snapshot_id = ?snapshot.analysis_snapshot_id(), profile_count, root_file_count = touched_files.len(), diagnostic_count, - "semantic compiler prewarmed profile diagnostics" + "semantic compiler completed isolated profile diagnostics" ); - let delivery = if snapshot.config.cli_pull_diagnostics_support() { - SemanticDiagnosticsDelivery::PullRefresh - } else { - SemanticDiagnosticsDelivery::Push(materialize_semantic_publish_batch( - &snapshot, - &touched_files, - cancellation, - )?) - }; - drop(snapshot); - - Ok(SemanticCompilerUpdate { delivery, touched_files, diagnostic_count, freshness }) + Ok(SemanticCompilerUpdate { + delivery, + touched_files, + diagnostic_count, + freshness: freshness.commit(), + by_file: cached, + }) } fn materialize_semantic_publish_batch( - snapshot: &GlobalStateSnapshot, + mut publish_files: FxHashMap< + FileId, + ( + Vec, + utils::lines::LineInfo, + Vec, + ), + >, changed_files: &FxHashSet, + diagnostics_by_file: &mut FxHashMap>, + freshness: DiagnosticPublishFreshness, + i18n: crate::i18n::I18n, cancellation: &CancellationToken, ) -> Result { let mut publish_tasks = Vec::with_capacity(changed_files.len()); let mut touched_file_ids = FxHashSet::default(); for file_id in changed_files.iter().copied() { cancellation.check()?; - let targets = snapshot - .diagnostic_publish_targets(file_id) - .with_context(|| format!("failed to resolve diagnostic targets for {file_id:?}"))?; - let diagnostics = match snapshot.lsp_diagnostics(file_id) { - Ok(diagnostics) => diagnostics, - Err(error) if error.is::() => return Err(CancellationError.into()), - Err(error) => { - return Err(error.context(format!( - "failed to materialize semantic diagnostics for {file_id:?}" - ))); - } - }; + let (targets, line_info, mut external) = publish_files + .remove(&file_id) + .expect("every touched file must have prepared publish metadata"); + let mut diagnostics = diagnostics_by_file + .remove(&file_id) + .unwrap_or_default() + .into_iter() + .map(|diagnostic| crate::lsp_ext::to_proto::diagnostic(i18n, &line_info, diagnostic)) + .collect::>(); + diagnostics.append(&mut external); touched_file_ids.insert(file_id); publish_tasks.extend( targets @@ -407,11 +508,28 @@ fn materialize_semantic_publish_batch( } cancellation.check()?; - Ok(PublishDiagnosticsBatch::for_touched_files( - touched_file_ids, - publish_tasks, - snapshot.diagnostic_publish_freshness, - )) + Ok(PublishDiagnosticsBatch::for_touched_files(touched_file_ids, publish_tasks, freshness)) +} + +/// Join slang profile output with live Vide diagnostics. The two sources +/// have different lifetimes; callers must not store the result as "profile". +pub(crate) fn with_vide_diagnostics( + mut slang: Vec, + vide: Vec, +) -> Vec { + slang.extend(vide); + slang +} + +pub(crate) fn merge_slang_and_vide( + mut slang_by_file: FxHashMap>, + vide_by_file: FxHashMap>, +) -> FxHashMap> { + for (file_id, vide) in vide_by_file { + let slang = slang_by_file.remove(&file_id).unwrap_or_default(); + slang_by_file.insert(file_id, with_vide_diagnostics(slang, vide)); + } + slang_by_file } fn normalize_profile_ids(mut profile_ids: Vec) -> Vec { @@ -490,4 +608,66 @@ mod tests { "semantic compiler task retained an analysis snapshot and blocked the next change" ); } + + fn test_diagnostic( + file_id: FileId, + source: ide::diagnostics::DiagnosticSource, + ) -> ide::diagnostics::Diagnostic { + ide::diagnostics::Diagnostic { + file_id, + code: 1, + subsystem: 0, + name: "test".to_owned(), + option_name: None, + groups: Vec::new(), + source, + range: utils::text_edit::TextRange::empty(utils::text_edit::TextSize::new(0)), + severity: syntax::diagnostics::DiagnosticSeverity::Warning, + message: "test".to_owned(), + args: Vec::new(), + message_key: None, + message_args: Vec::new(), + tags: Vec::new(), + } + } + + #[test] + fn slang_cache_plus_live_vide_is_not_doubled() { + let file = FileId::from_raw(0); + let slang = test_diagnostic(file, ide::diagnostics::DiagnosticSource::SlangSemantic); + let vide = test_diagnostic(file, ide::diagnostics::DiagnosticSource::Vide); + let mut cache = FxHashMap::default(); + cache.insert(file, vec![slang.clone()]); + assert!( + cache + .values() + .flatten() + .all(|diag| diag.source != ide::diagnostics::DiagnosticSource::Vide), + "slang cache must not contain Vide diagnostics: {cache:?}" + ); + + let first = with_vide_diagnostics( + cache.get(&file).cloned().unwrap_or_default(), + vec![vide.clone()], + ); + let republish = + with_vide_diagnostics(cache.get(&file).cloned().unwrap_or_default(), vec![vide]); + assert_eq!( + first + .iter() + .filter(|diag| diag.source == ide::diagnostics::DiagnosticSource::Vide) + .count(), + 1 + ); + assert_eq!( + republish + .iter() + .filter(|diag| diag.source == ide::diagnostics::DiagnosticSource::Vide) + .count(), + first + .iter() + .filter(|diag| diag.source == ide::diagnostics::DiagnosticSource::Vide) + .count() + ); + } } diff --git a/src/global_state/snapshot.rs b/src/global_state/snapshot.rs index 2b4827991..8235d8ca3 100644 --- a/src/global_state/snapshot.rs +++ b/src/global_state/snapshot.rs @@ -143,23 +143,22 @@ impl GlobalStateSnapshot { pub(crate) fn diagnostics( &self, file_id: FileId, - ) -> Cancellable> { + ) -> anyhow::Result> { if !self.document_diagnostics_enabled(file_id) { return Ok(Vec::new()); } if self.open_file_syntax_diagnostics_for_disabled_root(file_id) { - return self.analysis.parse_diagnostics(file_id); + return Ok(self.analysis.parse_diagnostics(file_id)?); } if let Some(DiagnosticOwner::CompilationProfile(profile_id)) = self.diagnostic_owner(file_id, DiagnosticRequestScope::Document) { - let diagnostics = self.analysis.compilation_profile_diagnostics(profile_id)?; - return Ok(diagnostics.into_iter().filter(|diag| diag.file_id == file_id).collect()); + return self.compilation_profile_file_diagnostics(profile_id, file_id); } - self.analysis.diagnostics(file_id) + Ok(self.analysis.diagnostics(file_id)?) } pub(crate) fn lsp_diagnostics( @@ -176,6 +175,14 @@ impl GlobalStateSnapshot { } let diagnostics = self.diagnostics(file_id)?; + self.lsp_diagnostics_from_ide(file_id, diagnostics) + } + + pub(crate) fn lsp_diagnostics_from_ide( + &self, + file_id: FileId, + diagnostics: Vec, + ) -> anyhow::Result> { let line_info = self.line_info(file_id)?; let mut diagnostics = diagnostics .into_iter() @@ -185,6 +192,48 @@ impl GlobalStateSnapshot { Ok(diagnostics) } + pub(crate) fn compilation_profile_diagnostics( + &self, + profile_id: base_db::project::CompilationProfileId, + ) -> anyhow::Result> { + let mut diagnostics = self.compilation_profile_slang_diagnostics(profile_id)?; + for file_id in self.mem_docs.file_ids() { + if self.analysis.file_compilation_profile(file_id)? == Some(profile_id) { + diagnostics.extend(self.analysis.file_vide_diagnostics(file_id)?); + } + } + Ok(diagnostics) + } + + /// Slang diagnostics of the profile plus Vide checks of this file. + /// Does not lower every compilation-unit body to answer one document. + fn compilation_profile_file_diagnostics( + &self, + profile_id: base_db::project::CompilationProfileId, + file_id: FileId, + ) -> anyhow::Result> { + let config = self.config.diagnostics_config(); + if config.enabled && config.semantic.enabled { + let mut diagnostics = self + .compilation_profile_slang_diagnostics(profile_id)? + .into_iter() + .filter(|diagnostic| diagnostic.file_id == file_id) + .collect::>(); + diagnostics.extend(self.analysis.file_vide_diagnostics(file_id)?); + return Ok(diagnostics); + } + Ok(self.analysis.diagnostics(file_id)?) + } + + fn compilation_profile_slang_diagnostics( + &self, + profile_id: base_db::project::CompilationProfileId, + ) -> anyhow::Result> { + let job = self.analysis.compilation_profile_job(profile_id)?; + let output = crate::compiler_worker::compile(&job, &self.cancellation)?; + Ok(ide::diagnostics::materialize_compiler_diagnostics(output.into_diagnostics())) + } + pub(crate) fn external_diagnostics( &self, file_id: FileId, @@ -207,7 +256,14 @@ impl GlobalStateSnapshot { let line_info = self.line_info(diagnostic.file_id)?; diagnostics.push(to_proto::diagnostic(self.config.i18n, &line_info, diagnostic)); } - diagnostics.extend(source.lsp_diagnostics(file_id, &freshness)); + let line_info = self.line_info(file_id).ok(); + diagnostics.extend(source.lsp_diagnostics_projected( + file_id, + &freshness, + &self.analysis, + self.config.i18n, + line_info.as_ref(), + )); } Ok(diagnostics) } @@ -403,15 +459,15 @@ impl GlobalStateSnapshot { pub(crate) fn workspace_diagnostics_for_producer( &self, producer: &DiagnosticWorkspaceProducer, - ) -> Cancellable> { + ) -> anyhow::Result> { match producer.owner() { DiagnosticOwner::CompilationProfile(profile_id) => { - self.analysis.compilation_profile_diagnostics(profile_id) + self.compilation_profile_diagnostics(profile_id) } DiagnosticOwner::SourceRoot(_) => { - self.analysis.source_root_diagnostics(producer.representative_file_id()) + Ok(self.analysis.source_root_diagnostics(producer.representative_file_id())?) } - DiagnosticOwner::File(file_id) => self.diagnostics(file_id), + DiagnosticOwner::File(file_id) => Ok(self.diagnostics(file_id)?), DiagnosticOwner::External { .. } => Ok(Vec::new()), } } diff --git a/src/i18n.rs b/src/i18n.rs index 758d6e0e4..4f416fa76 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -53,7 +53,7 @@ pub(crate) mod keys { pub(crate) const QIHE_FINISHED: &str = "qihe.finished"; pub(crate) const QIHE_FAILED: &str = "qihe.failed"; pub(crate) const QIHE_CANCELLED: &str = "qihe.cancelled"; - pub(crate) const QIHE_STALE: &str = "qihe.stale"; + pub(crate) const QIHE_BASED_ON_EDITS: &str = "qihe.based_on_edits"; pub(crate) const QIHE_LOCATION: &str = "qihe.location"; pub(crate) const QIHE_CONVERT_DIAGNOSTIC_FAILED: &str = "qihe.convert_diagnostic_failed"; pub(crate) const QIHE_PREPARE_WORKSPACE_FAILED: &str = "qihe.prepare_workspace_failed"; diff --git a/src/i18n/en.toml b/src/i18n/en.toml index 00deba0eb..9e5122ad2 100644 --- a/src/i18n/en.toml +++ b/src/i18n/en.toml @@ -8,6 +8,7 @@ finished = "Qihe analysis finished with {total} diagnostic(s)." failed = "Qihe analysis failed" cancelled = "qihe analysis cancelled" stale = "Qihe analysis result discarded because the workspace changed." +based_on_edits = "Based on analysis from {n} edit(s) ago." location = "Location: {primary_element}" convert_diagnostic_failed = "failed to convert qihe diagnostic" prepare_workspace_failed = "failed to prepare qihe workspace" diff --git a/src/i18n/zh-CN.toml b/src/i18n/zh-CN.toml index 00ed547f6..f89e03ddd 100644 --- a/src/i18n/zh-CN.toml +++ b/src/i18n/zh-CN.toml @@ -8,6 +8,7 @@ finished = "Qihe 分析完成,共 {total} 条诊断。" failed = "Qihe 分析失败" cancelled = "Qihe 分析已取消" stale = "工作区已变化,已丢弃本次 Qihe 分析结果。" +based_on_edits = "基于 {n} 次编辑前的分析。" location = "位置:{primary_element}" convert_diagnostic_failed = "无法转换 Qihe 诊断" prepare_workspace_failed = "无法准备 Qihe 工作区" diff --git a/src/lib.rs b/src/lib.rs index 90cc05fa2..4afdaed8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use crate::{ }; pub mod browser; +pub mod compiler_worker; mod config; mod global_state; mod i18n; diff --git a/src/main.rs b/src/main.rs index 1a4129e7e..7c927d090 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "base_db=trace,", "hir_semantics=trace,", "hir_def=trace,", - "hir_ty=trace,", "ide=trace,", "project_model=trace,", "preproc_expand=trace,", @@ -121,6 +120,10 @@ fn main() -> anyhow::Result<()> { } } + if env::args_os().nth(1).as_deref() == Some(std::ffi::OsStr::new("--compiler-worker")) { + return vide::compiler_worker::run_stdio(); + } + let opt = Opt::parse(); let _profile_guard = setup_logging(&opt)?; run_server(opt)?; diff --git a/src/snapshots/vide__i18n__tests__i18n_matrix.snap b/src/snapshots/vide__i18n__tests__i18n_matrix.snap index 03b0e325c..740e77d2d 100644 --- a/src/snapshots/vide__i18n__tests__i18n_matrix.snap +++ b/src/snapshots/vide__i18n__tests__i18n_matrix.snap @@ -1,5 +1,6 @@ --- source: src/i18n.rs +assertion_line: 246 expression: report --- locale mapping: @@ -13,7 +14,7 @@ message lookup: formatting: Qihe 分析完成,共 3 条诊断。 locale table keys: - en: 81 - zh-CN: 81 + en: 82 + zh-CN: 82 only en: [] only zh-CN: [] diff --git a/src/tests/code_actions.rs b/src/tests/code_actions.rs index 63e521081..791568877 100644 --- a/src/tests/code_actions.rs +++ b/src/tests/code_actions.rs @@ -114,13 +114,30 @@ endmodule fn code_action_request_returns_extract_variable_for_selected_expression() { let text = "\ module top; + logic [7:0] y, a, b; always_comb begin y = a + b; end endmodule "; let (_temp_dir, client, server_thread, uri) = - setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + setup_configured_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + let diagnostics_id = lsp_server::RequestId::from(198); + client + .sender + .send(Message::Request(Request::new( + diagnostics_id.clone(), + DocumentDiagnosticRequest::METHOD.to_string(), + DocumentDiagnosticParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + identifier: None, + previous_result_id: None, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + let _ = recv_document_diagnostics(&client, diagnostics_id); let actions = request_code_actions_with_range( &client, @@ -146,19 +163,30 @@ endmodule #[test] fn code_action_request_returns_extract_variable_for_selected_continuous_assign_rhs() { let text = "\ -module top ( - c, - led0 -); - input wire c; - output led0; - reg led0; - +module top; + logic c; + logic led0; assign led0 = c * 2 + c; endmodule "; let (_temp_dir, client, server_thread, uri) = - setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + setup_configured_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + let diagnostics_id = lsp_server::RequestId::from(197); + client + .sender + .send(Message::Request(Request::new( + diagnostics_id.clone(), + DocumentDiagnosticRequest::METHOD.to_string(), + DocumentDiagnosticParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + identifier: None, + previous_result_id: None, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + let _ = recv_document_diagnostics(&client, diagnostics_id); let actions = request_code_actions_with_range( &client, @@ -193,7 +221,7 @@ module top; endmodule "; let (_temp_dir, client, server_thread, uri) = - setup_configured_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); let (_result_id, mut diagnostics) = request_document_diagnostics_until( &client, diff --git a/src/tests/diagnostics.rs b/src/tests/diagnostics.rs index 561025c2e..742ee2ccd 100644 --- a/src/tests/diagnostics.rs +++ b/src/tests/diagnostics.rs @@ -1,5 +1,104 @@ use super::*; +fn vide_diagnostics(diagnostics: &[lsp_types::Diagnostic]) -> Vec<&lsp_types::Diagnostic> { + diagnostics.iter().filter(|diagnostic| diagnostic.source.as_deref() == Some("vide")).collect() +} + +fn recv_publish_diagnostics_until( + client: &Connection, + uri: &Url, + pred: impl Fn(&[lsp_types::Diagnostic]) -> bool, + context: &str, +) -> Vec { + let deadline = Instant::now() + LSP_TEST_TIMEOUT; + let mut last = None; + while let Some(message) = recv_lsp_message_until(client, deadline, context) { + match message { + Message::Notification(notification) + if notification.method == lsp_types::notification::PublishDiagnostics::METHOD => + { + let params = + serde_json::from_value::(notification.params) + .unwrap(); + if ¶ms.uri == uri { + if pred(¶ms.diagnostics) { + return params.diagnostics; + } + last = Some(params.diagnostics); + } + } + Message::Notification(notification) + if notification.method == lsp_types::notification::Progress::METHOD => {} + Message::Request(request) => handle_test_server_request(client, request, context), + _ => {} + } + } + panic!("{context}: matching publishDiagnostics not received; last={last:?}"); +} + +fn drain_publish_diagnostics_for_uri( + client: &Connection, + uri: &Url, + window: Duration, +) -> Vec> { + let deadline = Instant::now() + window; + let mut extras = Vec::new(); + while let Some(message) = recv_lsp_message_until(client, deadline, "drain publishDiagnostics") { + match message { + Message::Notification(notification) + if notification.method == lsp_types::notification::PublishDiagnostics::METHOD => + { + let params = + serde_json::from_value::(notification.params) + .unwrap(); + if ¶ms.uri == uri { + extras.push(params.diagnostics); + } + } + Message::Notification(notification) + if notification.method == lsp_types::notification::Progress::METHOD => {} + Message::Request(request) => { + handle_test_server_request(client, request, "drain publishDiagnostics") + } + _ => {} + } + } + extras +} + +#[test] +fn did_open_after_semantic_compile_does_not_duplicate_vide_diagnostics() { + let text = "`ifdef NEVER\nwire hidden;\n`endif\nmodule top;\nendmodule\n"; + let (_temp_dir, client, server_thread, uri) = setup_configured_diagnostics_test( + ClientCapabilities::default(), + UserConfig::default(), + text, + ); + + let first = recv_publish_diagnostics_until( + &client, + &uri, + |diagnostics| !vide_diagnostics(diagnostics).is_empty(), + "first semantic compile vide diagnostic", + ); + let first_vide = vide_diagnostics(&first).len(); + assert!(first_vide >= 1, "expected a Vide diagnostic before republish: {first:?}"); + + // A second didOpen of the same file republishes the cached profile + // diagnostics without compiling again. + open_test_document(&client, uri.clone(), text); + let extras = drain_publish_diagnostics_for_uri(&client, &uri, Duration::from_secs(5)); + for extra in &extras { + assert_eq!( + vide_diagnostics(extra).len(), + first_vide, + "didOpen must not duplicate Vide diagnostics: first={first:?} extra={extra:?}" + ); + } + + shutdown_test_server(&client, server_thread); +} + #[test] fn default_diagnostics_warn_on_port_width_mismatch() { let text = "\ diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c871cc616..55601fd11 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -8,5 +8,6 @@ edition.workspace = true anyhow.workspace = true clap.workspace = true project-model = { workspace = true, features = ["manifest-schema"] } +regex.workspace = true serde_json.workspace = true user-config.workspace = true diff --git a/xtask/src/include_shape.rs b/xtask/src/include_shape.rs new file mode 100644 index 000000000..27146c18f --- /dev/null +++ b/xtask/src/include_shape.rs @@ -0,0 +1,423 @@ +//! Classify `` `include `` targets as MacrosOnly / Balanced / Unbalanced. +//! +//! Port of `scripts/include_shape.py`. Error direction is conservative: +//! anything that cannot be proved balanced is `Unbalanced`. Never classify +//! an unbalanced file as `Balanced`. + +use std::{ + collections::BTreeMap, + fmt::Write as _, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, bail}; +use regex::Regex; + +const EXTS: &[&str] = &["sv", "v", "svh", "vh", "svi", "inc", "h", "vi"]; + +const OPENERS: &[&str] = &[ + "module", + "macromodule", + "class", + "package", + "interface", + "program", + "function", + "task", + "generate", + "checker", + "property", + "sequence", + "covergroup", + "clocking", + "config", + "primitive", + "specify", + "table", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IncludeShape { + MacrosOnly, + Balanced, + Unbalanced, + Unresolved, + Unreadable, +} + +impl IncludeShape { + fn label(self) -> &'static str { + match self { + Self::MacrosOnly => "MacrosOnly", + Self::Balanced => "Balanced", + Self::Unbalanced => "Unbalanced", + Self::Unresolved => "Unresolved", + Self::Unreadable => "unreadable", + } + } +} + +#[derive(Debug, Default)] +pub struct ShapeReport { + pub root: PathBuf, + pub file_count: usize, + pub distinct_targets: usize, + pub include_sites: usize, + pub unresolved_targets: usize, + pub unresolved_sites: usize, + pub shape_files: BTreeMap, + pub shape_sites: BTreeMap, + pub top_included: Vec<(usize, IncludeShape, String, usize)>, +} + +impl ShapeReport { + pub fn site_pct(&self, shape: IncludeShape) -> f64 { + if self.include_sites == 0 { + return 0.0; + } + let n = *self.shape_sites.get(&shape).unwrap_or(&0); + 100.0 * n as f64 / self.include_sites as f64 + } + + pub fn render(&self) -> String { + let mut out = String::new(); + let _ = writeln!(out, "corpus: {}", self.root.display()); + let _ = writeln!(out, "total SV files: {}", self.file_count); + let _ = writeln!( + out, + "distinct include targets: {} total include sites: {}", + self.distinct_targets, self.include_sites + ); + let _ = writeln!( + out, + "unresolved targets: {} ({} sites)", + self.unresolved_targets, self.unresolved_sites + ); + out.push('\n'); + + let _ = writeln!(out, "=== by distinct included file ==="); + let tot: usize = self.shape_files.values().sum(); + let mut files: Vec<_> = self.shape_files.iter().collect(); + files.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0))); + for (shape, count) in files { + let pct = if tot == 0 { 0.0 } else { 100.0 * *count as f64 / tot as f64 }; + let _ = writeln!(out, " {:12} {:5} {:5.1}%", shape.label(), count, pct); + } + + let _ = writeln!( + out, + "\n=== weighted by include sites (this is what matters for invalidation) ===" + ); + let tot: usize = self.shape_sites.values().sum(); + let mut sites: Vec<_> = self.shape_sites.iter().collect(); + sites.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0))); + for (shape, count) in sites { + let pct = if tot == 0 { 0.0 } else { 100.0 * *count as f64 / tot as f64 }; + let _ = writeln!(out, " {:12} {:6} {:5.1}%", shape.label(), count, pct); + } + + let unbalanced = self.site_pct(IncludeShape::Unbalanced); + let _ = writeln!( + out, + "\nT8 gate (site-weighted Unbalanced): {unbalanced:.1}% {}", + if unbalanced <= 5.0 { + "<= 5% — T8 may proceed later" + } else { + "> 5% — T8 must be redesigned, not silently skipped" + } + ); + + let _ = writeln!(out, "\n=== top 25 most-included files ==="); + for (nsites, shape, target, size) in self.top_included.iter().take(25) { + let _ = writeln!( + out, + " {nsites:5} sites {:11} {target} (residue tokens: {size})", + shape.label() + ); + } + out + } +} + +pub fn classify_corpus(roots: &[PathBuf]) -> Result { + if roots.is_empty() { + bail!("at least one corpus directory is required"); + } + for root in roots { + if !root.is_dir() { + bail!("corpus is not a directory: {}", root.display()); + } + } + + let files: Vec = roots.iter().flat_map(|root| collect_sv_files(root)).collect(); + let mut by_name: BTreeMap> = BTreeMap::new(); + for path in &files { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + by_name.entry(name.to_owned()).or_default().push(path.clone()); + } + } + + let include_re = include_regex(); + let mut edges: BTreeMap = BTreeMap::new(); + let mut unresolved: BTreeMap = BTreeMap::new(); + for path in &files { + let Ok(text) = fs::read_to_string(path) else { + continue; + }; + for cap in include_re.captures_iter(&text) { + let Some(target) = cap.get(1).map(|m| Path::new(m.as_str())) else { + continue; + }; + let Some(name) = target.file_name().and_then(|n| n.to_str()) else { + continue; + }; + *edges.entry(name.to_owned()).or_default() += 1; + if !by_name.contains_key(name) { + *unresolved.entry(name.to_owned()).or_default() += 1; + } + } + } + + let mut report = ShapeReport { + root: if roots.len() == 1 { + roots[0].clone() + } else { + PathBuf::from( + roots.iter().map(|p| p.display().to_string()).collect::>().join("+"), + ) + }, + file_count: files.len(), + distinct_targets: edges.len(), + include_sites: edges.values().sum(), + unresolved_targets: unresolved.len(), + unresolved_sites: unresolved.values().sum(), + ..ShapeReport::default() + }; + + let mut detail = Vec::new(); + for (target, nsites) in &edges { + let Some(cands) = by_name.get(target) else { + *report.shape_files.entry(IncludeShape::Unresolved).or_default() += 1; + *report.shape_sites.entry(IncludeShape::Unresolved).or_default() += nsites; + continue; + }; + let (shape, size) = classify_path(&cands[0]); + *report.shape_files.entry(shape).or_default() += 1; + *report.shape_sites.entry(shape).or_default() += nsites; + detail.push((*nsites, shape, target.clone(), size)); + } + detail.sort_by(|a, b| b.0.cmp(&a.0).then(a.2.cmp(&b.2))); + report.top_included = detail; + Ok(report) +} + +pub fn run(roots: &[PathBuf]) -> Result<()> { + let report = classify_corpus(roots).with_context(|| { + format!( + "classify {}", + roots.iter().map(|p| p.display().to_string()).collect::>().join(" ") + ) + })?; + print!("{}", report.render()); + Ok(()) +} + +fn collect_sv_files(root: &Path) -> Vec { + let mut out = Vec::new(); + fn rec(dir: &Path, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + let mut ents: Vec<_> = entries.flatten().collect(); + ents.sort_by_key(|e| e.file_name()); + for ent in ents { + let path = ent.path(); + if path.is_dir() { + if path.file_name().and_then(|n| n.to_str()) == Some(".git") { + continue; + } + rec(&path, out); + } else if is_sv(&path) { + out.push(path); + } + } + } + rec(root, &mut out); + out +} + +fn is_sv(path: &Path) -> bool { + path.extension().and_then(|e| e.to_str()).is_some_and(|ext| EXTS.contains(&ext)) +} + +fn include_regex() -> Regex { + Regex::new(r#"(?m)^\s*`include\s+[<"]([^">]+)[">]"#).expect("static include regex") +} + +fn directive_regex() -> Regex { + Regex::new( + r"^\s*`(define|ifdef|ifndef|elsif|else|endif|undef|include|timescale|default_nettype|line|pragma|celldefine|endcelldefine|resetall|unconnected_drive|nounconnected_drive|begin_keywords|end_keywords)\b", + ) + .expect("static directive regex") +} + +fn ident_regex() -> Regex { + Regex::new(r"\b[A-Za-z_][A-Za-z0-9_$]*\b").expect("static ident regex") +} + +fn closer_for(opener: &str) -> String { + // SV closers. `covergroup` pairs with `endgroup` (the old `end`+opener + // table invented `endcovergroup` and treated every covergroup as + // Unbalanced). Still conservative: unmatched covergroups stay Unbalanced. + match opener { + "generate" => "endgenerate".to_owned(), + "specify" => "endspecify".to_owned(), + "table" => "endtable".to_owned(), + "covergroup" => "endgroup".to_owned(), + other => format!("end{other}"), + } +} + +pub fn classify_source(raw: &str) -> (IncludeShape, usize) { + let body = strip_macro_bodies(&strip_comments(raw)); + let residue = body.trim(); + if residue.is_empty() { + return (IncludeShape::MacrosOnly, 0); + } + let ident_re = ident_regex(); + let toks: Vec<&str> = ident_re.find_iter(residue).map(|m| m.as_str()).collect(); + if toks.is_empty() { + return (IncludeShape::MacrosOnly, 0); + } + + // T8 redesign: `typedef class` and `extern function/task` are not + // openers. Counting them as Unbalanced was lexical noise on UVM. + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + let mut index = 0; + while index < toks.len() { + if toks[index] == "typedef" && toks.get(index + 1) == Some(&"class") { + index += 2; + continue; + } + if toks[index] == "extern" && matches!(toks.get(index + 1), Some(&"function" | &"task")) { + index += 2; + continue; + } + *counts.entry(toks[index]).or_default() += 1; + index += 1; + } + let mut imbalance = 0usize; + for op in OPENERS { + let closer = closer_for(op); + let open = *counts.get(op).unwrap_or(&0); + let close = *counts.get(closer.as_str()).unwrap_or(&0); + imbalance += open.abs_diff(close); + } + for (a, b) in [('(', ')'), ('[', ']'), ('{', '}')] { + imbalance += residue + .chars() + .filter(|&c| c == a) + .count() + .abs_diff(residue.chars().filter(|&c| c == b).count()); + } + if imbalance == 0 { + (IncludeShape::Balanced, toks.len()) + } else { + (IncludeShape::Unbalanced, toks.len()) + } +} + +fn classify_path(path: &Path) -> (IncludeShape, usize) { + match fs::read_to_string(path) { + Ok(raw) => classify_source(&raw), + Err(_) => (IncludeShape::Unreadable, 0), + } +} + +fn strip_comments(s: &str) -> String { + let block = Regex::new(r"(?s)/\*.*?\*/").expect("block comment regex"); + let without_block = block.replace_all(s, " "); + let line = Regex::new(r"//[^\n]*").expect("line comment regex"); + line.replace_all(&without_block, " ").into_owned() +} + +fn strip_macro_bodies(s: &str) -> String { + let directive = directive_regex(); + let mut out = Vec::new(); + let lines: Vec<&str> = s.split('\n').collect(); + let mut i = 0; + while i < lines.len() { + let mut line = lines[i]; + if directive.is_match(line) { + while line.trim_end().ends_with('\\') && i + 1 < lines.len() { + i += 1; + line = lines[i]; + } + i += 1; + continue; + } + out.push(line); + i += 1; + } + out.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_defines_are_macros_only() { + let src = "`define WIDTH 8\n`define DEPTH 4\n"; + assert_eq!(classify_source(src).0, IncludeShape::MacrosOnly); + } + + #[test] + fn a_closed_class_is_balanced() { + let src = "class foo extends uvm_object;\n `uvm_object_utils(foo)\nendclass\n"; + assert_eq!(classify_source(src).0, IncludeShape::Balanced); + } + + #[test] + fn an_unclosed_module_is_unbalanced_not_balanced() { + let src = "module foo;\n wire x;\n"; + assert_eq!(classify_source(src).0, IncludeShape::Unbalanced); + assert_ne!(classify_source(src).0, IncludeShape::Balanced); + } + + #[test] + fn unmatched_paren_is_unbalanced() { + let src = "function int f;\n return (1;\nendfunction\n"; + assert_eq!(classify_source(src).0, IncludeShape::Unbalanced); + } + + #[test] + fn classify_never_promotes_unbalanced_to_balanced() { + // Conservative direction: we may call a balanced file Unbalanced, + // but never the reverse. This source opens a class and a module + // and closes neither. + let src = "class c;\nmodule m;\n"; + assert_eq!(classify_source(src).0, IncludeShape::Unbalanced); + } + + #[test] + fn typedef_class_and_extern_function_are_not_openers() { + let src = "\ +typedef class uvm_component; +virtual class uvm_object extends uvm_void; + extern function string get_name(); + function string get_type_name(); + return \"\"; + endfunction +endclass +"; + assert_eq!(classify_source(src).0, IncludeShape::Balanced); + } + + #[test] + fn covergroup_pairs_with_endgroup() { + let src = "class c;\n covergroup g;\n endgroup\nendclass\n"; + assert_eq!(classify_source(src).0, IncludeShape::Balanced); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 53e3964f7..590356f43 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,5 +1,7 @@ #![recursion_limit = "512"] +mod include_shape; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::{ @@ -27,6 +29,8 @@ fn main() -> Result<()> { Some(XtaskCommand::CheckSchemas) => check_schemas(&workspace_root), Some(XtaskCommand::Server(server)) => run_server_command(&workspace_root, server), Some(XtaskCommand::Vscode(vscode)) => run_vscode_command(&workspace_root, vscode), + Some(XtaskCommand::BenchIde) => run_ide_benches(&workspace_root), + Some(XtaskCommand::IncludeShape(args)) => include_shape::run(&args.corpus), None => { Cli::command().print_help()?; eprintln!(); @@ -52,6 +56,18 @@ enum XtaskCommand { CheckSchemas, Server(ServerArgs), Vscode(VscodeArgs), + /// Synthetic design-graph fold and post-edit request benches. + BenchIde, + /// Classify `` `include `` targets as MacrosOnly / Balanced / Unbalanced. + IncludeShape(IncludeShapeArgs), +} + +#[derive(Debug, Args)] +struct IncludeShapeArgs { + /// Corpus roots (files under these trees with + /// .sv/.v/.svh/.vh/.svi/.inc/.h/.vi). + #[arg(required = true, num_args = 1..)] + corpus: Vec, } #[derive(Debug, Args)] @@ -148,6 +164,30 @@ fn run_vscode_command(workspace_root: &Path, args: VscodeArgs) -> Result<()> { } } +fn run_ide_benches(workspace_root: &Path) -> Result<()> { + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); + let status = ProcessCommand::new(cargo) + .current_dir(workspace_root) + .args([ + "test", + "-p", + "ide", + "--release", + "--lib", + "incrementality_benches", + "--", + "--ignored", + "--nocapture", + "--test-threads=1", + ]) + .status() + .context("failed to spawn cargo test for ide incrementality benches")?; + if !status.success() { + bail!("ide incrementality benches failed with {status}"); + } + Ok(()) +} + fn run_server_command(workspace_root: &Path, args: ServerArgs) -> Result<()> { match args.command { ServerCommand::Build(args) => { @@ -525,6 +565,15 @@ mod tests { check_schemas(&workspace_root().unwrap()).unwrap(); } + #[test] + fn parses_include_shape_command_with_clap() { + let cli = Cli::try_parse_from(["xtask", "include-shape", "/tmp/corpus"]).unwrap(); + let Some(XtaskCommand::IncludeShape(args)) = cli.command else { + panic!("expected include-shape command"); + }; + assert_eq!(args.corpus, vec![PathBuf::from("/tmp/corpus")]); + } + #[test] fn parses_vscode_prepare_server_command_with_clap() { let cli = Cli::try_parse_from([