From de90dbd173216a25948ef87bdf2cb2a900ea3b6b Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Thu, 20 Aug 2026 17:22:06 +0800 Subject: [PATCH] feat(ide): replace hir-ty TypeSystem with resident slang elaboration Slang's Compilation is not incremental, so types, class members, and `::` navigation come from a resident worker rather than salsa inference. Hover, member completion, and extract-variable ask that worker; HIR display stays as pretty-printing of lowered syntax and moves into ide. The hir-ty crate is removed. ModuleIndex and the preprocessor are unchanged. --- Cargo.toml | 2 - crates/hir-ty/Cargo.toml | 18 - crates/hir-ty/src/compatibility.rs | 173 ----- crates/hir-ty/src/db.rs | 30 - crates/hir-ty/src/infer.rs | 513 ------------- crates/hir-ty/src/lib.rs | 16 - crates/hir-ty/src/members.rs | 136 ---- crates/hir-ty/src/ty.rs | 64 -- crates/hir-ty/src/type_system.rs | 152 ---- crates/hir-ty/tests/type_system.rs | 501 ------------- crates/ide/Cargo.toml | 2 +- crates/ide/src/analysis.rs | 52 +- crates/ide/src/analysis_host.rs | 99 ++- crates/ide/src/code_action/context.rs | 10 +- crates/ide/src/code_action/engine.rs | 9 +- ...tract_variable_allows_selection_padding.sv | 2 +- .../extract_variable_continuous_assign.sv | 2 +- ...variable_inserts_local_before_statement.sv | 2 +- .../extract_variable_mixed_width_add.sv | 2 + .../handlers/convert_port_declarations.rs | 9 +- .../code_action/handlers/extract_variable.rs | 42 +- ..._variable_allows_selection_padding.sv.snap | 2 +- ...extract_variable_continuous_assign.sv.snap | 2 +- ...ble_inserts_local_before_statement.sv.snap | 2 +- ...s@extract_variable_mixed_width_add.sv.snap | 7 + ..._variable_uses_assignment_lhs_type.sv.snap | 2 +- ...le_uses_continuous_assign_lhs_type.sv.snap | 2 +- crates/ide/src/code_action/tests.rs | 48 +- crates/ide/src/completion.rs | 3 +- crates/ide/src/completion/context.rs | 7 +- crates/ide/src/completion/engine.rs | 10 +- crates/ide/src/completion/engine/expr.rs | 168 +---- .../src/completion/engine/instantiation.rs | 16 +- crates/ide/src/completion/engine/keywords.rs | 6 +- crates/ide/src/completion/engine/member.rs | 191 ++--- crates/ide/src/completion/engine/named.rs | 72 +- .../ide/src/completion/engine/paren_list.rs | 60 +- crates/ide/src/completion/engine/plan.rs | 6 +- crates/ide/src/completion/engine/port_list.rs | 26 +- crates/ide/src/completion/engine/preproc.rs | 6 +- .../src/completion/engine/sensitivity_list.rs | 14 +- ...t_ordered_param_assign_at_token_end.v.snap | 12 + ..._ordered_param_assign_expr_by_width.v.snap | 12 + ...dered_port_connection_expr_by_width.v.snap | 23 + ...ers_assignment_rhs_by_expected_type.v.snap | 12 + ...ializer_expression_by_expected_type.v.snap | 12 + ...rs_named_param_assign_expr_by_width.v.snap | 12 + ...named_port_connection_expr_by_width.v.snap | 23 + ...ers_subroutine_calls_by_return_type.v.snap | 17 + ...port_expr_fallback_for_unknown_type.v.snap | 15 +- ...prefers_data_decl_for_non_ansi_port.v.snap | 12 + crates/ide/src/completion/engine/tests.rs | 7 +- .../ide/src/completion/engine/typed_filter.rs | 74 +- crates/ide/src/db/root_db.rs | 4 - .../ide/src/db/workspace_symbol_index_db.rs | 7 +- crates/ide/src/definitions.rs | 30 + crates/ide/src/document_symbols.rs | 74 +- crates/ide/src/elaboration.rs | 676 ++++++++++++++++++ crates/ide/src/goto_definition.rs | 53 +- crates/ide/src/hier.rs | 32 + crates/ide/src/hover.rs | 62 +- crates/ide/src/index_benchmarks.rs | 2 +- crates/ide/src/lib.rs | 3 + crates/ide/src/navigation_target.rs | 4 +- crates/ide/src/references/search.rs | 4 +- crates/ide/src/render.rs | 14 +- .../src/render/hir_display.rs} | 148 +--- crates/ide/src/render/hir_display/tests.rs | 222 ++++++ crates/ide/src/semantic_index.rs | 13 +- crates/ide/src/semantic_tokens/port.rs | 4 +- crates/ide/src/signature_help.rs | 3 +- crates/ide/src/slang_class.rs | 248 +++++++ crates/ide/src/workspace_symbols.rs | 4 +- crates/slang-sys/src/compilation.rs | 310 ++++++++ crates/slang-sys/src/compilation/ffi.rs | 54 ++ crates/slang-sys/src/compilation/wrapper.cpp | 511 +++++++++++++ crates/slang-sys/src/compilation/wrapper.h | 30 + src/main.rs | 1 - 78 files changed, 2903 insertions(+), 2327 deletions(-) delete mode 100644 crates/hir-ty/Cargo.toml delete mode 100644 crates/hir-ty/src/compatibility.rs delete mode 100644 crates/hir-ty/src/db.rs delete mode 100644 crates/hir-ty/src/infer.rs delete mode 100644 crates/hir-ty/src/lib.rs delete mode 100644 crates/hir-ty/src/members.rs delete mode 100644 crates/hir-ty/src/ty.rs delete mode 100644 crates/hir-ty/src/type_system.rs delete mode 100644 crates/hir-ty/tests/type_system.rs create mode 100644 crates/ide/src/code_action/fixtures/code_actions/extract_variable_mixed_width_add.sv create mode 100644 crates/ide/src/code_action/snapshots/ide__code_action__tests__code_action_edit_fixtures@extract_variable_mixed_width_add.sv.snap create mode 100644 crates/ide/src/elaboration.rs create mode 100644 crates/ide/src/hier.rs rename crates/{hir-ty/src/display.rs => ide/src/render/hir_display.rs} (88%) create mode 100644 crates/ide/src/render/hir_display/tests.rs create mode 100644 crates/ide/src/slang_class.rs diff --git a/Cargo.toml b/Cargo.toml index d5a7bbe73..aaed9d28a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "crates/base-db", "crates/hir-def", "crates/hir-semantics", - "crates/hir-ty", "crates/ide", "crates/preproc", "crates/preproc-expand", @@ -80,7 +79,6 @@ triomphe.workspace = true base-db = { path = "./crates/base-db/", 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/hir-ty/Cargo.toml b/crates/hir-ty/Cargo.toml deleted file mode 100644 index 0a3ddb6f5..000000000 --- a/crates/hir-ty/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "hir-ty" -version = "0.0.0" -edition.workspace = true - -[dependencies] -base-db.workspace = true -hir-def.workspace = true -rustc-hash.workspace = true -salsa.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/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..3792b6d82 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -15,7 +15,6 @@ fst = "0.4.7" # 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 @@ -27,6 +26,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..65d353c7a 100644 --- a/crates/ide/src/analysis.rs +++ b/crates/ide/src/analysis.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::ops::{Deref, Range}; use base_db::{ Cancelled, @@ -26,6 +26,7 @@ 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, @@ -46,6 +47,37 @@ use crate::{ pub struct AnalysisSnapshot { pub(crate) db: RootDb, pub(crate) snapshot_id: AnalysisSnapshotId, + pub(crate) elab: ElaborationService, +} + +/// Read view of one IDE request: the Salsa database and the resident +/// elaboration service. +pub(crate) struct AnalysisContext<'a> { + pub(crate) db: &'a RootDb, + 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, + elab: &'a ElaborationService, + revision: ElabRevision, + ) -> AnalysisContext<'a> { + AnalysisContext { db, elab, revision } + } + + pub(crate) fn semantics(&self) -> hir_semantics::semantics::Semantics<'_, RootDb> { + hir_semantics::semantics::Semantics::new(self.db) + } } impl AnalysisSnapshot { @@ -61,6 +93,14 @@ impl AnalysisSnapshot { Cancelled::catch(|| f(&self.db)) } + fn with_ctx(&self, f: F) -> Cancellable + where + F: FnOnce(&AnalysisContext<'_>) -> T + std::panic::UnwindSafe, + { + let _span = tracing::debug_span!("ide.analysis", snapshot_id = ?self.snapshot_id).entered(); + Cancelled::catch(|| f(&AnalysisContext::new(&self.db, &self.elab, self.snapshot_id))) + } + pub fn line_index(&self, file_id: FileId) -> Cancellable> { self.with_db(|db| db.line_index(file_id)) } @@ -146,7 +186,7 @@ impl AnalysisSnapshot { &self, position: FilePosition, ) -> Cancellable>>> { - self.with_db(|db| goto_definition::goto_definition(db, position)) + self.with_ctx(|ctx| goto_definition::goto_definition(ctx, position)) } pub fn goto_declaration( @@ -279,7 +319,7 @@ impl AnalysisSnapshot { } pub fn hover(&self, position: FilePosition) -> Cancellable>> { - self.with_db(|db| hover::hover(db, position)) + self.with_ctx(|ctx| hover::hover(ctx, position)) } pub fn inlay_hint( @@ -321,7 +361,7 @@ impl AnalysisSnapshot { position: FilePosition, trigger: Option, ) -> Cancellable> { - self.with_db(|db| crate::completion::completions(db, position, trigger)) + self.with_ctx(|ctx| crate::completion::completions(ctx, position, trigger)) } pub fn code_action( @@ -331,8 +371,8 @@ impl AnalysisSnapshot { diagnostics: &[crate::diagnostics::Diagnostic], resolve_strategy: CodeActionResolveStrategy, ) -> Cancellable> { - self.with_db(|db| { - code_action::code_action(db, file_id, range, diagnostics, resolve_strategy) + self.with_ctx(|ctx| { + code_action::code_action(ctx, file_id, range, diagnostics, resolve_strategy) }) } } diff --git a/crates/ide/src/analysis_host.rs b/crates/ide/src/analysis_host.rs index 73a7f507f..9edd872cc 100644 --- a/crates/ide/src/analysis_host.rs +++ b/crates/ide/src/analysis_host.rs @@ -1,29 +1,68 @@ +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, +}; pub struct AnalysisHost { db: RootDb, snapshot_id: AnalysisSnapshotId, + elab: ElaborationService, + elab_worker: Option>, + prewarm: 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), + snapshot_id: AnalysisSnapshotId::default(), + elab, + elab_worker: Some(elab_worker), + prewarm: None, + } } pub fn make_analysis(&self) -> AnalysisSnapshot { + self.signal_foreground_request(); let db = self.db.clone(); - AnalysisSnapshot { db, snapshot_id: self.snapshot_id } + AnalysisSnapshot { db, snapshot_id: self.snapshot_id, elab: self.elab.clone() } } pub fn apply_change(&mut self, change: Change) { + self.cancel_prewarm(); self.db.apply_change(change); self.advance_revision(); + self.start_prewarm(); + #[cfg(test)] + self.await_prewarm(); + } + + #[cfg(test)] + fn await_prewarm(&mut self) { + if let Some(task) = self.prewarm.take() { + let _ = task.worker.join(); + } } pub fn set_diagnostics_config(&mut self, config: Arc) { @@ -35,13 +74,67 @@ impl AnalysisHost { self.snapshot_id = self.snapshot_id.next(); } + fn start_prewarm(&mut self) { + let db = self.db.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-elaboration-prewarm".to_owned()) + .spawn(move || { + if !worker_cancel.load(Ordering::Acquire) { + let _ = elab.prewarm(&db, revision); + } + }) + .expect("failed to spawn elaboration 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); + } + + 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.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 { 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/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/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..67e61577e 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 @@ -3,5 +3,5 @@ source: crates/ide/src/code_action/tests.rs 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..471ea0008 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 @@ -3,5 +3,5 @@ source: crates/ide/src/code_action/tests.rs 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..230ec601f 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 @@ -3,5 +3,5 @@ source: crates/ide/src/code_action/tests.rs 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..68ac3a0f9 --- /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,7 @@ +--- +source: crates/ide/src/code_action/tests.rs +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..6325fcaa9 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 @@ -3,5 +3,5 @@ source: crates/ide/src/code_action/tests.rs 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..c79066f99 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 @@ -3,5 +3,5 @@ source: crates/ide/src/code_action/tests.rs 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/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..07c7bfa87 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 { @@ -89,11 +88,11 @@ struct CompletionWord { } pub(crate) fn completion_context( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, trigger: Option, ) -> CompletionContext { - 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 CompletionContext { 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..d2e07caea 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, 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..6ac5c7218 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, position.file_id, 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, position.file_id, 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..3fd3c4166 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, instantiation: ast::HierarchyInstantiation<'_>, ) -> Option { - resolve_instantiation_target(db, from_file, instantiation).unique() + resolve_instantiation_target(db.db, from_file, 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..653438512 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: 188 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..b6c117b0f 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: 188 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..d0b3c66ae 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: 188 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..7d34670e6 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: 188 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..e4c51c6f0 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: 188 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..cc5f3019b 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: 188 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..b44df1c90 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: 188 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..b3b18a517 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: 188 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..cd08f1704 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: 188 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..75b1ebf29 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: 188 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/root_db.rs b/crates/ide/src/db/root_db.rs index 207a8d4ce..81a91995d 100644 --- a/crates/ide/src/db/root_db.rs +++ b/crates/ide/src/db/root_db.rs @@ -7,7 +7,6 @@ use base_db::{ source_db::{FileLoader, SourceDb, SourceRootDb}, }; use hir_def::db::HirDefDb; -use hir_ty::db::TyDb; use preproc_expand::db::PreprocDb; use triomphe::Arc; use vfs::{AnchoredPath, FileId}; @@ -35,9 +34,6 @@ impl PreprocDb for RootDb {} #[salsa::db] impl HirDefDb for RootDb {} -#[salsa::db] -impl TyDb for RootDb {} - #[salsa::db] impl LineIndexDb for RootDb {} diff --git a/crates/ide/src/db/workspace_symbol_index_db.rs b/crates/ide/src/db/workspace_symbol_index_db.rs index c273d098f..a14cea4a0 100644 --- a/crates/ide/src/db/workspace_symbol_index_db.rs +++ b/crates/ide/src/db/workspace_symbol_index_db.rs @@ -1,8 +1,7 @@ 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, def_id::DefId}; use triomphe::Arc; use vfs::FileId; @@ -15,11 +14,11 @@ use crate::{ }; #[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 diff --git a/crates/ide/src/definitions.rs b/crates/ide/src/definitions.rs index d4dd501dc..81e8abd5f 100644 --- a/crates/ide/src/definitions.rs +++ b/crates/ide/src/definitions.rs @@ -353,6 +353,36 @@ fn name_context_for_token(parent: syntax::SyntaxNode<'_>) -> NameContext { } } +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; + } + let scoped = SyntaxAncestors::start_from(tp.parent).find_map(ast::ScopedName::cast)?; + if scoped_uses_dot(scoped) { + return None; + } + 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 scoped_right_token(scoped: ast::ScopedName<'_>) -> Option> { use ast::Name::*; match scoped.right() { 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..45a34b835 --- /dev/null +++ b/crates/ide/src/elaboration.rs @@ -0,0 +1,676 @@ +//! Resident slang elaboration service. +//! +//! This is a backend worker, not a cache. Slang's `Compilation` is not +//! incremental, so the value does not belong in salsa. 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, compilation_source_buffers_for_plan}; +use rustc_hash::FxHashMap; +use slang_sys::compilation::{Compilation, HierInstance, MemberInfo, SymbolInfo}; +use syntax::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)); + + for file_id in &plan.roots { + let path = compilation_plan::source_buffer_path(db, *file_id).to_string(); + let name = + db.file_path(*file_id).map(|path| path.to_string()).unwrap_or_else(|| path.clone()); + match db.file_kind(*file_id) { + base_db::source_db::SourceFileKind::LibraryMap => { + compilation.parse_library_map_syntax_tree_from_buffer( + &name, + &path, + &SyntaxTreeOptions::default(), + ); + } + _ => { + compilation.parse_syntax_tree_from_buffer( + &name, + &path, + &SyntaxTreeOptions { + predefines: context.predefines.to_vec(), + include_paths: include_paths.clone(), + ..SyntaxTreeOptions::default() + }, + ); + } + } + } + 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/goto_definition.rs b/crates/ide/src/goto_definition.rs index aa9406584..1fd1964fc 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -11,6 +11,7 @@ use vfs::FileId; use crate::{ FilePosition, RangeInfo, + analysis::AnalysisContext, db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, @@ -21,13 +22,13 @@ use crate::{ }; pub(crate) fn goto_definition( - db: &RootDb, + db: &AnalysisContext<'_>, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { - let sema = Semantics::new(db); + let sema = Semantics::new(db.db); let parsed_file = sema.parse_file(file_id); let target = resolve_semantic_target( - db, + db.db, file_id, offset, parsed_file.root(), @@ -37,7 +38,7 @@ pub(crate) fn goto_definition( } fn render_definition_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, sema: &Semantics, target: TargetResolution<'_>, @@ -47,8 +48,8 @@ fn render_definition_target( for target in target.targets_for_intent(TargetIntent::Navigate) { let target = match 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::Include(includes) => render_include_definition_target(db.db, includes), + SemanticTarget::Manifest(target) => crate::manifest::definition_target(db.db, target), SemanticTarget::Source(target) => { render_source_definition_target(db, file_id, sema, target) } @@ -66,7 +67,7 @@ fn render_definition_target( } fn render_source_definition_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, sema: &Semantics, target: SourceTarget<'_>, @@ -87,7 +88,7 @@ fn render_source_definition_target( } fn nav_targets_for_token( - db: &RootDb, + db: &AnalysisContext<'_>, sema: &Semantics, hir_file_id: HirFileId, token: SyntaxTokenWithParent, @@ -96,14 +97,44 @@ fn nav_targets_for_token( let navs = DefinitionClass::resolve(sema.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)) .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::slang_class::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 render_preproc_definition_target( target: PreprocMacroTarget, ) -> Option>> { 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..728cfc0fd 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,18 +49,19 @@ 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 sema = Semantics::new(db.db); 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_hover_target(db, file_id, offset, &sema, target) } fn render_hover_target( - db: &RootDb, + db: &AnalysisContext<'_>, file_id: FileId, offset: TextSize, sema: &Semantics, @@ -72,13 +74,13 @@ fn render_hover_target( 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) + hover_for_source_target(db, sema, file_id.into(), target) } }?; ranges.push(hover.range); @@ -88,22 +90,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 +115,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 +165,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,6 +191,7 @@ fn merge_hover_results(markups: Vec) -> Option { } fn handle_definition( + db: &AnalysisContext<'_>, sema: &Semantics, file_id: HirFileId, tp: SyntaxTokenWithParent, @@ -238,10 +244,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/index_benchmarks.rs b/crates/ide/src/index_benchmarks.rs index ad7b31db2..e1610a353 100644 --- a/crates/ide/src/index_benchmarks.rs +++ b/crates/ide/src/index_benchmarks.rs @@ -191,7 +191,7 @@ fn index_benchmarks_real_file() { ); let position = FilePosition { file_id, offset: probe_offset }; - let (nav, goto_cost) = timed(|| goto_definition::goto_definition(db, position)); + let (nav, goto_cost) = timed(|| goto_definition::goto_definition(&host.ctx(), position)); eprintln!( "goto definition on first module ({probe}): {goto_cost:?} ({} targets)", nav.map_or(0, |info| info.info.len()) diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 5ff12756e..5873a85b0 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -12,6 +12,7 @@ pub type Cancellable = Result; pub mod analysis; pub mod analysis_host; pub mod definitions; +pub mod hier; pub(crate) mod manifest; pub mod markup; pub(crate) mod module_resolution; @@ -26,6 +27,7 @@ pub mod db; 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; @@ -44,6 +46,7 @@ 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/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/references/search.rs b/crates/ide/src/references/search.rs index b6e5df71f..d098ff5c1 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -1,13 +1,13 @@ 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 nohash_hasher::IntMap; use preproc_expand::{file::HirFileId, macro_file::macro_file_call_site}; use rustc_hash::FxHashMap; @@ -271,7 +271,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)> { diff --git a/crates/ide/src/render.rs b/crates/ide/src/render.rs index 935c6e510..c3accaaab 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 { 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..a9856a437 --- /dev/null +++ b/crates/ide/src/render/hir_display/tests.rs @@ -0,0 +1,222 @@ +//! 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 { + db.unit_index() + .module_ids(&ident(name)) + .unique() + .unwrap_or_else(|| panic!("{name} should be a unique module owner")) +} + +#[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_of_kind(hir_def::owner::OwnerKind::Covergroup) + .find(|owner| owner.name == "cg") + .expect("covergroup should project") + .id; + 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/semantic_index.rs b/crates/ide/src/semantic_index.rs index ab43b849b..145fd345f 100644 --- a/crates/ide/src/semantic_index.rs +++ b/crates/ide/src/semantic_index.rs @@ -1,6 +1,7 @@ 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 hir_def::{ + Ident, container::InFile, db::HirDefDb, def_id::DefId, item_tree::ModuleHeader, owner::OwnerId, +}; use preproc_expand::{db::PreprocDb, file::HirFileId, macro_file::macro_files_for_file}; use rustc_hash::FxHashMap; use syntax::{ @@ -244,7 +245,7 @@ impl ModuleIndex { } impl SemanticModuleDefinition { - fn new(db: &dyn TyDb, module_id: OwnerId) -> Option { + fn new(db: &dyn HirDefDb, module_id: OwnerId) -> Option { let source_file = module_id.file(db); let header = db .item_tree(source_file) @@ -253,7 +254,11 @@ impl SemanticModuleDefinition { Self::from_header(db, source_file, header) } - fn from_header(db: &dyn TyDb, source_file: HirFileId, header: ModuleHeader) -> Option { + fn from_header( + db: &dyn HirDefDb, + 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) = 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..925a4ce4e 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::{ @@ -28,7 +27,7 @@ use utils::text_edit::{TextRange, TextSize}; use crate::{ FilePosition, db::root_db::RootDb, markup::Markup, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_instantiation_target, render::hir_display::HirDisplay, }; #[derive(Debug)] diff --git a/crates/ide/src/slang_class.rs b/crates/ide/src/slang_class.rs new file mode 100644 index 000000000..1039d0973 --- /dev/null +++ b/crates/ide/src/slang_class.rs @@ -0,0 +1,248 @@ +//! 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}; +use vfs::FileId; + +use crate::{analysis::AnalysisContext, elaboration::ElabResult}; + +pub fn file_id_for_slang_path(db: &dyn SourceRootDb, slang_file: &str) -> FileId { + for &file_id in db.files().iter() { + if compilation_plan::source_buffer_path(db, file_id).as_str() == slang_file { + return file_id; + } + if db.file_path(file_id).is_some_and(|path| path.as_str() == slang_file) { + return file_id; + } + } + panic!("elaboration reported a buffer path that was not assigned: {slang_file}") +} + +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 collect = |text: &str| { + let (host, file_id) = crate::test_utils::setup_with_path(text, &format!("/{path}")); + let db = host.raw_db(); + let tree = db.parse_src_for_compilation(file_id); + let map = db.ast_id_map(file_id.into()); + 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 = collect(text); + let b = collect(text); + 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/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/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/src/main.rs b/src/main.rs index 1a4129e7e..9311d21bf 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,",