From e75211659de28646876bb5e7b265f8a26f328016 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 18:01:06 +0800 Subject: [PATCH 01/80] fix(preproc): separate macro definition ranges --- crates/hir/src/preproc.rs | 100 ++++++++++++++++++++++-------- crates/ide/src/goto_definition.rs | 6 +- crates/ide/src/hover.rs | 2 +- crates/ide/src/references.rs | 4 +- 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index fcc70848..c0e74deb 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -25,7 +25,17 @@ pub type PreprocResult = Result; pub enum PreprocError { SourceQuery(SourcePreprocQueryError), MissingRootSource, - UnmappedSource { buffer_id: u32 }, + UnmappedSource { + buffer_id: u32, + }, + MismatchedDefinitionRangeFiles { + event_id: u32, + directive_file_id: FileId, + name_file_id: FileId, + }, + MissingDefinitionNameRange { + event_id: u32, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -34,8 +44,8 @@ pub struct MacroDefinition { pub name: SmolStr, pub define_index: usize, pub event_id: u32, - pub event_range: TextRange, - pub range: TextRange, + pub directive_range: TextRange, + pub name_range: TextRange, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -58,8 +68,8 @@ pub struct MacroUsageResolution { pub struct MacroDefinitionProvenance { pub event_id: u32, pub file_id: FileId, - pub range: TextRange, - pub name_range: Option, + pub directive_range: TextRange, + pub name_range: TextRange, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -101,8 +111,8 @@ impl MacroDefinitionKey { fn from_definition(definition: &MacroDefinition) -> Self { Self { file_id: definition.file_id, - range_start: definition.range.start(), - range_end: definition.range.end(), + range_start: definition.name_range.start(), + range_end: definition.name_range.end(), name: definition.name.clone(), } } @@ -264,9 +274,12 @@ pub fn macro_definition_at( let mapped = mapped_result(mapped.as_ref())?; for (define_index, define) in mapped.model.defines().iter().enumerate() { - let (define_file_id, event_range) = map_source_range(mapped, define.range)?; - let (_, range) = map_source_range(mapped, define.name_range.unwrap_or(define.range))?; - if define_file_id == file_id && range_contains_offset(range, offset) { + let Some(source_name_range) = define.name_range else { + continue; + }; + let (define_file_id, directive_range, name_range) = + map_definition_ranges(mapped, define.event_id.raw(), define.range, source_name_range)?; + if define_file_id == file_id && range_contains_offset(name_range, offset) { return Ok(Some(MacroDefinition { file_id: define_file_id, name: match define.name.clone() { @@ -275,8 +288,8 @@ pub fn macro_definition_at( }, define_index, event_id: define.event_id.raw(), - event_range, - range, + directive_range, + name_range, })); } } @@ -531,14 +544,23 @@ fn map_definition_provenance( provenance: &SourcePreprocProvenance, ) -> PreprocResult { let (file_id, range) = map_source_range(mapped, provenance.range)?; - let name_range = provenance - .name_range - .map(|source_range| map_source_range(mapped, source_range).map(|(_, range)| range)) - .transpose()?; + let Some(source_name_range) = provenance.name_range else { + return Err(PreprocError::MissingDefinitionNameRange { + event_id: provenance.event_id.raw(), + }); + }; + let (name_file_id, name_range) = map_source_range(mapped, source_name_range)?; + if name_file_id != file_id { + return Err(PreprocError::MismatchedDefinitionRangeFiles { + event_id: provenance.event_id.raw(), + directive_file_id: file_id, + name_file_id, + }); + } Ok(MacroDefinitionProvenance { event_id: provenance.event_id.raw(), file_id, - range, + directive_range: range, name_range, }) } @@ -566,22 +588,46 @@ fn map_binding_definition( mapped: &MappedSourcePreprocModel, binding: SourceMacroBinding<'_>, ) -> PreprocResult> { - let (file_id, event_range) = map_source_range(mapped, binding.define.range)?; - let (_, range) = - map_source_range(mapped, binding.define.name_range.unwrap_or(binding.define.range))?; let Some(name) = binding.define.name.clone() else { return Ok(None); }; + let Some(source_name_range) = binding.define.name_range else { + return Ok(None); + }; + let (file_id, directive_range, name_range) = map_definition_ranges( + mapped, + binding.event_id.raw(), + binding.define.range, + source_name_range, + )?; Ok(Some(MacroDefinition { file_id, name, define_index: binding.define_index, event_id: binding.event_id.raw(), - event_range, - range, + directive_range, + name_range, })) } +fn map_definition_ranges( + mapped: &MappedSourcePreprocModel, + event_id: u32, + directive_source_range: SourceRange, + name_source_range: SourceRange, +) -> PreprocResult<(FileId, TextRange, TextRange)> { + let (directive_file_id, directive_range) = map_source_range(mapped, directive_source_range)?; + let (name_file_id, name_range) = map_source_range(mapped, name_source_range)?; + if directive_file_id != name_file_id { + return Err(PreprocError::MismatchedDefinitionRangeFiles { + event_id, + directive_file_id, + name_file_id, + }); + } + Ok((directive_file_id, directive_range, name_range)) +} + fn push_unique_macro_reference(refs: &mut Vec, reference: MacroReference) { if refs.iter().any(|existing| { existing.file_id == reference.file_id @@ -599,7 +645,7 @@ fn push_unique_macro_definition( ) { if definitions.iter().any(|existing| { existing.file_id == definition.file_id - && existing.range == definition.range + && existing.name_range == definition.name_range && existing.name == definition.name }) { return; @@ -865,7 +911,7 @@ endmodule assert_eq!(resolution.usage.file_id, TOP); assert_eq!(resolution.definition.file_id, HEADER); assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); - assert!(text_at_range(header_text, resolution.definition.range).contains("HEADER_WIDTH")); + assert_eq!(text_at_range(header_text, resolution.definition.name_range), "HEADER_WIDTH"); let include = include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); @@ -997,7 +1043,7 @@ localparam int ENABLED = `HEADER_FLAG; assert_eq!(text_at_range(root_text, definitions.reference.range), "`HEADER_FLAG"); assert!(definitions.definitions.iter().any(|indexed| { indexed.file_id == HEADER - && indexed.range == definition.range + && indexed.name_range == definition.name_range && indexed.name == definition.name })); } @@ -1018,7 +1064,7 @@ localparam int ENABLED = `HEADER_FLAG; assert_eq!(resolution.reference.file_id, HEADER); let definition = resolution.definitions.iter().find(|definition| definition.file_id == HEADER).unwrap(); - assert_eq!(text_at_range(header_text, definition.range), "HEADER_FLAG"); + assert_eq!(text_at_range(header_text, definition.name_range), "HEADER_FLAG"); let refs = macro_references(&db, HEADER, definition).unwrap(); assert!(refs.iter().any(|reference| { @@ -1044,7 +1090,7 @@ localparam int ENABLED = `HEADER_FLAG; assert_eq!(resolution.reference.file_id, HEADER); assert!(resolution.definitions.iter().any(|definition| { definition.file_id == HEADER - && text_at_range(header_text, definition.range) == "HEADER_FLAG" + && text_at_range(header_text, definition.name_range) == "HEADER_FLAG" })); } } diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index ac32f760..7f1e615e 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -61,7 +61,7 @@ fn handle_preproc_macro( offset: TextSize, ) -> Option>> { if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { - return Some(RangeInfo::new(definition.range, vec![macro_nav_target(definition)])); + return Some(RangeInfo::new(definition.name_range, vec![macro_nav_target(definition)])); } let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; @@ -73,8 +73,8 @@ fn handle_preproc_macro( fn macro_nav_target(definition: MacroDefinition) -> NavTarget { NavTarget { file_id: definition.file_id, - full_range: definition.range, - focus_range: Some(definition.range), + full_range: definition.name_range, + focus_range: Some(definition.name_range), name: Some(definition.name), kind: None, container_name: None, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 63d4042f..d22c8d8e 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -91,7 +91,7 @@ fn handle_preproc_macro( offset: TextSize, ) -> Option> { if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { - return Some(RangeInfo::new(definition.range, macro_definition_markup(&definition))); + return Some(RangeInfo::new(definition.name_range, macro_definition_markup(&definition))); } let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 929f5717..2c469b75 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -139,8 +139,8 @@ fn macro_references_for_definition( fn macro_nav_target(definition: MacroDefinition) -> NavTarget { NavTarget { file_id: definition.file_id, - full_range: definition.range, - focus_range: Some(definition.range), + full_range: definition.name_range, + focus_range: Some(definition.name_range), name: Some(definition.name), kind: None, container_name: None, From c86a0f5580578fc78867fc3e7922652e742c8695 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 18:14:39 +0800 Subject: [PATCH 02/80] feat(preproc): add expansion provenance model skeleton --- crates/preproc/src/source.rs | 4 +- crates/preproc/src/source/model.rs | 120 ++- crates/preproc/src/source/provenance.rs | 958 ++++++++++++++++++++++++ crates/preproc/src/source/references.rs | 10 +- crates/preproc/src/source/types.rs | 5 +- 5 files changed, 1085 insertions(+), 12 deletions(-) create mode 100644 crates/preproc/src/source/provenance.rs diff --git a/crates/preproc/src/source.rs b/crates/preproc/src/source.rs index 15443b6e..067ef442 100644 --- a/crates/preproc/src/source.rs +++ b/crates/preproc/src/source.rs @@ -1,7 +1,9 @@ mod model; +mod provenance; mod references; mod trace; mod types; -pub use references::{SourceMacroReferenceResolution, SourceMacroReferenceSite}; +pub use provenance::*; +pub use references::SourceMacroReferenceResolution; pub use types::*; diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 31ebcb5b..bd9ffcaf 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -3,11 +3,12 @@ use std::collections::BTreeMap; use smol_str::SmolStr; use syntax::PreprocessorTrace; -use super::types::*; +use super::{provenance::*, types::*}; impl SourcePreprocModel { pub fn new(index: SourcePreprocIndex) -> Self { - Self { index } + let tables = SourcePreprocTables::from_index(&index); + Self { index, tables } } pub fn from_trace(trace: PreprocessorTrace) -> Result { @@ -22,6 +23,46 @@ impl SourcePreprocModel { self.index } + pub fn provenance_tables(&self) -> &SourcePreprocTables { + &self.tables + } + + pub fn macro_definitions(&self) -> &SourceMacroDefinitionTable { + &self.tables.macro_definitions + } + + pub fn macro_references(&self) -> &SourceMacroReferenceTable { + &self.tables.macro_references + } + + pub fn macro_calls(&self) -> &SourceMacroCallTable { + &self.tables.macro_calls + } + + pub fn macro_expansions(&self) -> &SourceMacroExpansionTable { + &self.tables.macro_expansions + } + + pub fn emitted_tokens(&self) -> &SourceEmittedTokenTable { + &self.tables.emitted_tokens + } + + pub fn token_provenance(&self) -> &SourceTokenProvenanceTable { + &self.tables.token_provenance + } + + pub fn include_graph(&self) -> &SourceIncludeGraph { + &self.tables.include_graph + } + + pub fn state_timeline(&self) -> &SourceMacroStateTimeline { + &self.tables.state_timeline + } + + pub fn capabilities(&self) -> &SourcePreprocCapabilities { + &self.tables.capabilities + } + pub fn root_source(&self) -> Option { self.index.root_source } @@ -563,6 +604,81 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(resolution.definition_include_chain[0].included_source, header_source); } + #[test] + fn source_model_exposes_expansion_provenance_skeleton_tables() { + let root_text = r#"`include "defs.vh" +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let definition = model + .macro_definitions() + .iter() + .find(|definition| definition.name.as_str() == "HEADER_WIDTH") + .expect("definition table should include precise macro definition"); + assert_eq!(definition.directive_range.source, header_source); + assert_eq!(definition.name_range.source, header_source); + assert_ne!(definition.directive_range.range, definition.name_range.range); + assert_eq!(text_at_range(header_text, definition.name_range.range), "HEADER_WIDTH"); + + let reference = model + .macro_references() + .iter() + .find(|reference| { + reference.name.as_str() == "HEADER_WIDTH" + && matches!(reference.site, SourceMacroReferenceSite::Usage { usage_index: _ }) + }) + .expect("reference table should include resolved macro usage"); + assert_eq!(reference.name_range.source, root_source); + assert_eq!(reference.directive_range.source, root_source); + let SourceMacroResolution::Resolved { + definition: resolved_definition, + reason, + include_chain, + } = &reference.resolution + else { + panic!("macro usage should resolve to included definition"); + }; + assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); + assert_eq!(include_chain.len(), 1); + assert_eq!( + model.macro_definitions().get(*resolved_definition).unwrap().name.as_str(), + "HEADER_WIDTH" + ); + + assert_eq!(model.include_graph().directives().len(), 1); + assert!(matches!( + &model.include_graph().directives()[0].status, + SourceIncludeStatus::Resolved { source } if *source == header_source + )); + assert!(!model.state_timeline().checkpoints().is_empty()); + assert!(model.macro_calls().is_empty()); + assert!(model.macro_expansions().is_empty()); + assert!(model.emitted_tokens().is_empty()); + assert!(model.token_provenance().is_empty()); + assert!(matches!( + &model.capabilities().macro_calls, + CapabilityStatus::Unavailable(SourcePreprocUnavailable::MacroCallAuthorityUnavailable) + )); + assert!(matches!( + &model.capabilities().macro_expansions, + CapabilityStatus::Unavailable(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) + )); + assert!(matches!( + &model.capabilities().emitted_tokens, + CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + ) + )); + assert!(matches!( + &model.capabilities().emitted_token_provenance, + CapabilityStatus::Unavailable( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable + ) + )); + } + #[test] fn source_model_resolves_conditional_tokens_to_visible_defines() { let root_text = r#"`include "defs.vh" diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs new file mode 100644 index 00000000..ee2335b8 --- /dev/null +++ b/crates/preproc/src/source/provenance.rs @@ -0,0 +1,958 @@ +use std::collections::BTreeMap; + +use smol_str::SmolStr; + +use super::types::*; + +macro_rules! source_table_id { + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(usize); + + impl $name { + pub fn new(raw: usize) -> Self { + Self(raw) + } + + pub fn raw(self) -> usize { + self.0 + } + } + }; +} + +source_table_id!(SourceMacroDefinitionId); +source_table_id!(SourceMacroReferenceId); +source_table_id!(SourceIncludeDirectiveId); +source_table_id!(SourceMacroStateId); +source_table_id!(SourceMacroCallId); +source_table_id!(SourceMacroExpansionId); +source_table_id!(SourceEmittedTokenId); +source_table_id!(SourceTokenProvenanceId); + +pub trait HasDirectiveRange { + fn directive_range(&self) -> SourceRange; +} + +pub trait HasNameRange { + fn name_range(&self) -> Option; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourceMacroReferenceSite { + Usage { usage_index: usize }, + ConditionalToken { conditional_index: usize, token_index: usize }, + IncludeGuardIfNDef { conditional_index: usize, token_index: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroDefinition { + pub id: SourceMacroDefinitionId, + pub event_id: SourcePreprocEventId, + pub name: SmolStr, + pub name_range: SourceRange, + pub directive_range: SourceRange, + pub params: Option>, + pub body_tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroReference { + pub id: SourceMacroReferenceId, + pub event_id: SourcePreprocEventId, + pub site: SourceMacroReferenceSite, + pub name: SmolStr, + pub name_range: SourceRange, + pub directive_range: SourceRange, + pub resolution: SourceMacroResolution, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceMacroResolution { + Resolved { + definition: SourceMacroDefinitionId, + reason: SourceMacroResolutionReason, + include_chain: Vec, + }, + Undefined, + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourceMacroResolutionReason { + VisibleDefinition, + IncludeGuardIfNDef, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceIncludeGraph { + directives: Vec, + edges: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceIncludeDirective { + pub id: SourceIncludeDirectiveId, + pub event_id: SourcePreprocEventId, + pub directive_range: SourceRange, + pub target: MacroIncludeTarget, + pub target_range: Option, + pub resolved_source: Option, + pub status: SourceIncludeStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceIncludeStatus { + Resolved { source: PreprocSourceId }, + Unresolved, + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroStateTimeline { + states: Vec, + checkpoints: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroState { + pub id: SourceMacroStateId, + pub definitions: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroStateCheckpoint { + pub source_order: usize, + pub boundary: SourcePosition, + pub state: SourceMacroStateId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroCall { + pub id: SourceMacroCallId, + pub reference: SourceMacroReferenceId, + pub call_range: SourceRange, + pub callee: SourceMacroResolution, + pub arguments: Vec, + pub expansion: Option, + pub status: SourceMacroCallStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroArgument { + pub argument_index: usize, + pub argument_range: Option, + pub tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceMacroCallStatus { + ExpansionAvailable, + ExpansionUnavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroExpansion { + pub id: SourceMacroExpansionId, + pub call: SourceMacroCallId, + pub definition: SourceMacroDefinitionId, + pub emitted_token_range: SourceEmittedTokenRange, + pub child_calls: Vec, + pub status: SourceMacroExpansionStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceMacroExpansionStatus { + Complete, + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceEmittedTokenRange { + pub start: SourceEmittedTokenId, + pub len: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceEmittedToken { + pub id: SourceEmittedTokenId, + pub text: SmolStr, + pub kind: SourceTokenKind, + pub emitted_range: SourceEmittedTokenRange, + pub provenance: SourceTokenProvenanceId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceTokenKind { + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceTokenProvenance { + Source { + token_range: SourceRange, + }, + MacroBody { + definition: SourceMacroDefinitionId, + body_token_range: SourceRange, + call: SourceMacroCallId, + }, + MacroArgument { + call: SourceMacroCallId, + argument_index: usize, + argument_token_range: SourceRange, + }, + TokenPaste { + call: SourceMacroCallId, + parts: Vec, + }, + Stringification { + call: SourceMacroCallId, + argument_index: usize, + }, + Predefine { + source: PreprocSourceId, + }, + Builtin { + name: SmolStr, + }, + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocTables { + pub macro_definitions: SourceMacroDefinitionTable, + pub macro_references: SourceMacroReferenceTable, + pub macro_calls: SourceMacroCallTable, + pub macro_expansions: SourceMacroExpansionTable, + pub emitted_tokens: SourceEmittedTokenTable, + pub token_provenance: SourceTokenProvenanceTable, + pub include_graph: SourceIncludeGraph, + pub state_timeline: SourceMacroStateTimeline, + pub capabilities: SourcePreprocCapabilities, + pub issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceMacroDefinitionTable { + definitions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceMacroReferenceTable { + references: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceMacroCallTable { + calls: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceMacroExpansionTable { + expansions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceEmittedTokenTable { + tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourceTokenProvenanceTable { + provenance: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocCapabilities { + pub source_events: CapabilityStatus, + pub definition_name_ranges: CapabilityStatus, + pub include_edges: CapabilityStatus, + pub inactive_ranges: CapabilityStatus, + pub macro_reference_resolution: CapabilityStatus, + pub macro_calls: CapabilityStatus, + pub macro_expansions: CapabilityStatus, + pub emitted_tokens: CapabilityStatus, + pub emitted_token_provenance: CapabilityStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapabilityStatus { + Complete, + Partial, + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourcePreprocUnavailable { + MissingDefinitionName { event_id: SourcePreprocEventId }, + MissingDefinitionNameRange { event_id: SourcePreprocEventId }, + MissingReferenceName { event_id: SourcePreprocEventId }, + MissingReferenceNameRange { event_id: SourcePreprocEventId }, + IncludeChainUnavailable { source: PreprocSourceId }, + MacroCallAuthorityUnavailable, + EmittedTokenAuthorityUnavailable, + TokenProvenanceAuthorityUnavailable, + ExpansionAuthorityUnavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourcePreprocFactIssue { + MissingDefinitionName { event_id: SourcePreprocEventId }, + MissingDefinitionNameRange { event_id: SourcePreprocEventId }, + MissingReferenceName { event_id: SourcePreprocEventId }, + MissingReferenceNameRange { event_id: SourcePreprocEventId }, + IncludeChainUnavailable { source: PreprocSourceId }, +} + +impl SourcePreprocTables { + pub fn from_index(index: &SourcePreprocIndex) -> Self { + let mut builder = SourcePreprocTableBuilder::new(index); + builder.build(); + builder.tables + } + + pub fn capabilities(&self) -> &SourcePreprocCapabilities { + &self.capabilities + } +} + +impl Default for SourcePreprocTables { + fn default() -> Self { + Self { + macro_definitions: SourceMacroDefinitionTable::default(), + macro_references: SourceMacroReferenceTable::default(), + macro_calls: SourceMacroCallTable::default(), + macro_expansions: SourceMacroExpansionTable::default(), + emitted_tokens: SourceEmittedTokenTable::default(), + token_provenance: SourceTokenProvenanceTable::default(), + include_graph: SourceIncludeGraph::default(), + state_timeline: SourceMacroStateTimeline::default(), + capabilities: SourcePreprocCapabilities::unavailable(), + issues: Vec::new(), + } + } +} + +impl Default for SourceIncludeGraph { + fn default() -> Self { + Self { directives: Vec::new(), edges: Vec::new() } + } +} + +impl SourceIncludeGraph { + pub fn directives(&self) -> &[SourceIncludeDirective] { + &self.directives + } + + pub fn edges(&self) -> &[SourceIncludeEdge] { + &self.edges + } +} + +impl SourceMacroStateTimeline { + pub fn states(&self) -> &[SourceMacroState] { + &self.states + } + + pub fn checkpoints(&self) -> &[SourceMacroStateCheckpoint] { + &self.checkpoints + } +} + +impl Default for SourceMacroStateTimeline { + fn default() -> Self { + Self { states: Vec::new(), checkpoints: Vec::new() } + } +} + +impl SourcePreprocCapabilities { + pub fn unavailable() -> Self { + Self { + source_events: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + definition_name_ranges: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + include_edges: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + inactive_ranges: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + macro_reference_resolution: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + macro_calls: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::MacroCallAuthorityUnavailable, + ), + macro_expansions: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + emitted_tokens: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ), + emitted_token_provenance: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + ), + } + } +} + +impl SourceMacroDefinitionTable { + pub fn get(&self, id: SourceMacroDefinitionId) -> Option<&SourceMacroDefinition> { + self.definitions.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.definitions.iter() + } + + pub fn len(&self) -> usize { + self.definitions.len() + } + + pub fn is_empty(&self) -> bool { + self.definitions.is_empty() + } + + fn push(&mut self, definition: SourceMacroDefinition) { + self.definitions.push(definition); + } +} + +impl SourceMacroReferenceTable { + pub fn get(&self, id: SourceMacroReferenceId) -> Option<&SourceMacroReference> { + self.references.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.references.iter() + } + + pub fn len(&self) -> usize { + self.references.len() + } + + pub fn is_empty(&self) -> bool { + self.references.is_empty() + } + + fn push(&mut self, reference: SourceMacroReference) { + self.references.push(reference); + } +} + +impl SourceMacroCallTable { + pub fn get(&self, id: SourceMacroCallId) -> Option<&SourceMacroCall> { + self.calls.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.calls.iter() + } + + pub fn len(&self) -> usize { + self.calls.len() + } + + pub fn is_empty(&self) -> bool { + self.calls.is_empty() + } +} + +impl SourceMacroExpansionTable { + pub fn get(&self, id: SourceMacroExpansionId) -> Option<&SourceMacroExpansion> { + self.expansions.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.expansions.iter() + } + + pub fn len(&self) -> usize { + self.expansions.len() + } + + pub fn is_empty(&self) -> bool { + self.expansions.is_empty() + } +} + +impl SourceEmittedTokenTable { + pub fn get(&self, id: SourceEmittedTokenId) -> Option<&SourceEmittedToken> { + self.tokens.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.tokens.iter() + } + + pub fn len(&self) -> usize { + self.tokens.len() + } + + pub fn is_empty(&self) -> bool { + self.tokens.is_empty() + } +} + +impl SourceTokenProvenanceTable { + pub fn get(&self, id: SourceTokenProvenanceId) -> Option<&SourceTokenProvenance> { + self.provenance.get(id.raw()) + } + + pub fn iter(&self) -> impl Iterator { + self.provenance.iter() + } + + pub fn len(&self) -> usize { + self.provenance.len() + } + + pub fn is_empty(&self) -> bool { + self.provenance.is_empty() + } +} + +impl HasDirectiveRange for SourceMacroDefinition { + fn directive_range(&self) -> SourceRange { + self.directive_range + } +} + +impl HasNameRange for SourceMacroDefinition { + fn name_range(&self) -> Option { + Some(self.name_range) + } +} + +impl HasDirectiveRange for SourceMacroReference { + fn directive_range(&self) -> SourceRange { + self.directive_range + } +} + +impl HasNameRange for SourceMacroReference { + fn name_range(&self) -> Option { + Some(self.name_range) + } +} + +impl HasDirectiveRange for SourceIncludeDirective { + fn directive_range(&self) -> SourceRange { + self.directive_range + } +} + +struct SourcePreprocTableBuilder<'a> { + index: &'a SourcePreprocIndex, + tables: SourcePreprocTables, + definition_ids_by_define_index: BTreeMap, + current_state: BTreeMap, + definition_ranges_partial: bool, + references_partial: bool, +} + +impl<'a> SourcePreprocTableBuilder<'a> { + fn new(index: &'a SourcePreprocIndex) -> Self { + Self { + index, + tables: SourcePreprocTables::default(), + definition_ids_by_define_index: BTreeMap::new(), + current_state: BTreeMap::new(), + definition_ranges_partial: false, + references_partial: false, + } + } + + fn build(&mut self) { + self.build_definition_table(); + self.build_include_graph(); + self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); + self.scan_references_and_state(); + self.tables.capabilities = SourcePreprocCapabilities { + source_events: CapabilityStatus::Complete, + definition_name_ranges: partial_status(self.definition_ranges_partial), + include_edges: CapabilityStatus::Complete, + inactive_ranges: CapabilityStatus::Complete, + macro_reference_resolution: partial_status(self.references_partial), + macro_calls: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::MacroCallAuthorityUnavailable, + ), + macro_expansions: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + emitted_tokens: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ), + emitted_token_provenance: CapabilityStatus::Unavailable( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + ), + }; + } + + fn build_definition_table(&mut self) { + for (define_index, define) in self.index.defines.iter().enumerate() { + let Some(name) = define.name.clone() else { + self.definition_ranges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionName { + event_id: define.event_id, + }); + continue; + }; + let Some(name_range) = define.name_range else { + self.definition_ranges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionNameRange { + event_id: define.event_id, + }); + continue; + }; + + let id = SourceMacroDefinitionId::new(self.tables.macro_definitions.len()); + self.tables.macro_definitions.push(SourceMacroDefinition { + id, + event_id: define.event_id, + name, + name_range, + directive_range: define.range, + params: define.params.clone(), + body_tokens: define.body.clone(), + }); + self.definition_ids_by_define_index.insert(define_index, id); + } + } + + fn build_include_graph(&mut self) { + let resolved_sources_by_event = self + .index + .include_edges + .iter() + .map(|edge| (edge.include_event_id, edge.included_source)) + .collect::>(); + + self.tables.include_graph.edges = self.index.include_edges.clone(); + for include in &self.index.includes { + let id = SourceIncludeDirectiveId::new(self.tables.include_graph.directives.len()); + let resolved_source = resolved_sources_by_event.get(&include.event_id).copied(); + let status = resolved_source + .map(|source| SourceIncludeStatus::Resolved { source }) + .unwrap_or(SourceIncludeStatus::Unresolved); + self.tables.include_graph.directives.push(SourceIncludeDirective { + id, + event_id: include.event_id, + directive_range: include.range, + target: include.target.clone(), + target_range: include.target_range, + resolved_source, + status, + }); + } + } + + fn scan_references_and_state(&mut self) { + for (source_order, directive) in self.index.event_records.iter().enumerate() { + match directive.kind { + MacroEventKind::Define => self.apply_define(source_order, directive), + MacroEventKind::Undef => self.apply_undef(source_order, directive), + MacroEventKind::Conditional => self.record_conditional_references(directive), + MacroEventKind::Usage => self.record_usage_reference(directive), + MacroEventKind::Include | MacroEventKind::Branch => {} + } + } + } + + fn apply_define(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { + if let Some(definition_id) = self.definition_ids_by_define_index.get(&directive.index) { + let definition = self + .tables + .macro_definitions + .get(*definition_id) + .expect("definition id should point at inserted definition"); + self.current_state.insert(definition.name.clone(), *definition_id); + self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); + } + } + + fn apply_undef(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { + let Some(undef) = self.index.undefs.get(directive.index) else { + return; + }; + if let Some(name) = undef.name.as_ref() { + self.current_state.remove(name.as_str()); + self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); + } + } + + fn record_usage_reference(&mut self, directive: &SourcePreprocEventRecord) { + let Some(usage) = self.index.usages.get(directive.index) else { + return; + }; + let Some(name) = usage.name.clone() else { + self.record_missing_reference_name(usage.event_id); + return; + }; + let Some(name_range) = usage.name_range else { + self.record_missing_reference_name_range(usage.event_id); + return; + }; + let event_id = usage.event_id; + let directive_range = usage.range; + let resolution = self.resolve_visible_reference(name.as_str()); + self.push_reference( + event_id, + SourceMacroReferenceSite::Usage { usage_index: directive.index }, + name, + name_range, + directive_range, + resolution, + ); + } + + fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { + let Some(conditional) = self.index.conditionals.get(directive.index) else { + return; + }; + let event_id = conditional.event_id; + let directive_range = conditional.range; + let tokens = conditional + .expr + .iter() + .enumerate() + .filter_map(|(token_index, token)| { + Some((token_index, token.value.clone(), token.range?)) + }) + .collect::>(); + + for (token_index, name, name_range) in tokens { + let (site, resolution) = + if let Some(definition) = self.current_state.get(name.as_str()).copied() { + ( + SourceMacroReferenceSite::ConditionalToken { + conditional_index: directive.index, + token_index, + }, + self.resolve_definition( + definition, + SourceMacroResolutionReason::VisibleDefinition, + ), + ) + } else if let Some(definition) = + self.include_guard_definition_after_ifndef(directive.index, name.as_str()) + { + ( + SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: directive.index, + token_index, + }, + self.resolve_definition( + definition, + SourceMacroResolutionReason::IncludeGuardIfNDef, + ), + ) + } else { + ( + SourceMacroReferenceSite::ConditionalToken { + conditional_index: directive.index, + token_index, + }, + SourceMacroResolution::Undefined, + ) + }; + self.push_reference(event_id, site, name, name_range, directive_range, resolution); + } + } + + fn push_reference( + &mut self, + event_id: SourcePreprocEventId, + site: SourceMacroReferenceSite, + name: SmolStr, + name_range: SourceRange, + directive_range: SourceRange, + resolution: SourceMacroResolution, + ) { + let id = SourceMacroReferenceId::new(self.tables.macro_references.len()); + self.tables.macro_references.push(SourceMacroReference { + id, + event_id, + site, + name, + name_range, + directive_range, + resolution, + }); + } + + fn resolve_visible_reference(&mut self, name: &str) -> SourceMacroResolution { + let Some(definition) = self.current_state.get(name).copied() else { + return SourceMacroResolution::Undefined; + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + + fn resolve_definition( + &mut self, + definition: SourceMacroDefinitionId, + reason: SourceMacroResolutionReason, + ) -> SourceMacroResolution { + let definition_source = self + .tables + .macro_definitions + .get(definition) + .expect("definition id should point at inserted definition") + .directive_range + .source; + match include_chain_for_source(self.index, definition_source) { + Ok(include_chain) => { + SourceMacroResolution::Resolved { definition, reason, include_chain } + } + Err(_) => { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { + source: definition_source, + }); + SourceMacroResolution::Unavailable( + SourcePreprocUnavailable::IncludeChainUnavailable { source: definition_source }, + ) + } + } + } + + fn include_guard_definition_after_ifndef( + &self, + conditional_index: usize, + name: &str, + ) -> Option { + let conditional = self.index.conditionals.get(conditional_index)?; + if conditional.kind != MacroConditionalKind::IfNDef { + return None; + } + + let source = conditional.range.source; + let (conditional_order, _) = + self.index.event_records.iter().enumerate().find(|(_, directive)| { + directive.kind == MacroEventKind::Conditional + && directive.index == conditional_index + })?; + for directive in self.index.event_records.iter().skip(conditional_order + 1) { + if directive.range.source != source { + continue; + } + match directive.kind { + MacroEventKind::Define => { + let define = self.index.defines.get(directive.index)?; + if define.name.as_deref() == Some(name) { + return self.definition_ids_by_define_index.get(&directive.index).copied(); + } + } + MacroEventKind::Branch => break, + MacroEventKind::Undef + | MacroEventKind::Include + | MacroEventKind::Conditional + | MacroEventKind::Usage => {} + } + } + None + } + + fn record_missing_reference_name(&mut self, event_id: SourcePreprocEventId) { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceName { event_id }); + } + + fn record_missing_reference_name_range(&mut self, event_id: SourcePreprocEventId) { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceNameRange { event_id }); + } + + fn record_state_checkpoint(&mut self, source_order: usize, boundary: SourcePosition) { + let id = SourceMacroStateId::new(self.tables.state_timeline.states.len()); + self.tables + .state_timeline + .states + .push(SourceMacroState { id, definitions: self.current_state.clone() }); + self.tables.state_timeline.checkpoints.push(SourceMacroStateCheckpoint { + source_order, + boundary, + state: id, + }); + } +} + +impl SourcePosition { + fn from_first_event(index: &SourcePreprocIndex) -> Self { + index + .event_records + .first() + .map(|record| SourcePosition { + source: record.range.source, + offset: record.range.range.start(), + }) + .unwrap_or(SourcePosition { + source: index.root_source.unwrap_or_else(|| PreprocSourceId::new(0)), + offset: 0.into(), + }) + } +} + +fn boundary_after(directive_range: SourceRange) -> SourcePosition { + SourcePosition { source: directive_range.source, offset: directive_range.range.end() } +} + +fn partial_status(is_partial: bool) -> CapabilityStatus { + if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } +} + +fn include_chain_for_source( + index: &SourcePreprocIndex, + source: PreprocSourceId, +) -> Result, SourcePreprocError> { + let mut chain = Vec::new(); + let mut current = source; + let mut visited = BTreeMap::new(); + + loop { + if visited.insert(current, ()).is_some() { + return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); + } + + let Some(source) = index.sources.iter().find(|candidate| candidate.id == current) else { + return Err(SourcePreprocError::MissingIncludedSource { + include_event_id: 0, + source: current.raw(), + }); + }; + + match source.origin { + PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, + PreprocSourceOrigin::Detached => { + return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); + } + PreprocSourceOrigin::Included { include_event_id } => { + let directive = index + .event_records + .iter() + .find(|directive| directive.event_id == include_event_id) + .ok_or(SourcePreprocError::MissingIncludeEvent { + include_event_id: include_event_id.raw(), + })?; + if directive.kind != MacroEventKind::Include { + return Err(SourcePreprocError::IncludeEdgeNotInclude { + include_event_id: include_event_id.raw(), + }); + } + chain.push(SourceIncludeChainEntry { + include_event_id, + include_range: directive.range, + included_source: current, + }); + current = directive.range.source; + } + } + } + + chain.reverse(); + Ok(chain) +} diff --git a/crates/preproc/src/source/references.rs b/crates/preproc/src/source/references.rs index c105fc83..c1765f16 100644 --- a/crates/preproc/src/source/references.rs +++ b/crates/preproc/src/source/references.rs @@ -2,12 +2,6 @@ use smol_str::SmolStr; use super::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SourceMacroReferenceSite { - Usage { usage_index: usize }, - ConditionalToken { conditional_index: usize, token_index: usize }, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroReferenceResolution<'a> { pub site: SourceMacroReferenceSite, @@ -22,7 +16,7 @@ impl SourcePreprocModel { pub fn definition_for_usage( &self, usage_index: usize, - ) -> Result>, SourcePreprocError> { + ) -> Result>, SourcePreprocError> { let Some(usage) = self.index.usages.get(usage_index) else { return Ok(None); }; @@ -50,7 +44,7 @@ impl SourcePreprocModel { .provenance(SourcePreprocEntity::Define(define_index)) .ok_or(SourcePreprocError::MissingEvent { event_id: define.event_id.raw() })?; let definition_include_chain = self.include_chain_for_source(define.range.source)?; - Ok(Some(SourceMacroResolution { + Ok(Some(SourceMacroUsageResolution { usage_index, usage, definition, diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index de2bf8cf..c61ab863 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -3,6 +3,8 @@ use std::collections::BTreeMap; use smol_str::SmolStr; use utils::line_index::{TextRange, TextSize}; +use super::provenance::SourcePreprocTables; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PreprocSourceId(u32); @@ -156,6 +158,7 @@ pub struct SourceMacroToken { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocModel { pub(super) index: SourcePreprocIndex, + pub(super) tables: SourcePreprocTables, } #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -172,7 +175,7 @@ pub struct SourceMacroBinding<'a> { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceMacroResolution<'a> { +pub struct SourceMacroUsageResolution<'a> { pub usage_index: usize, pub usage: &'a SourceMacroUsage, pub definition: SourceMacroBinding<'a>, From 34a946f4e106646e72c150a373570480c008428d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 18:31:19 +0800 Subject: [PATCH 03/80] feat(preproc): build resolved provenance model --- crates/preproc/src/source/model.rs | 135 +++++++--------- crates/preproc/src/source/provenance.rs | 161 +++++++++++++++---- crates/preproc/src/source/references.rs | 205 ++++++++++++------------ 3 files changed, 292 insertions(+), 209 deletions(-) diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index bd9ffcaf..5026e846 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -12,7 +12,8 @@ impl SourcePreprocModel { } pub fn from_trace(trace: PreprocessorTrace) -> Result { - SourcePreprocIndex::from_trace(trace).map(Self::new) + let index = SourcePreprocIndex::from_trace(trace)?; + Ok(Self::new(index)) } pub fn index(&self) -> &SourcePreprocIndex { @@ -92,7 +93,7 @@ impl SourcePreprocModel { } pub fn inactive_ranges(&self) -> &[SourceRange] { - &self.index.inactive_ranges + &self.tables.inactive_ranges } pub fn events(&self) -> impl Iterator> + '_ { @@ -104,12 +105,10 @@ impl SourcePreprocModel { } pub fn macro_environment_at(&self, position: SourcePosition) -> SourceMacroEnvironment { - let mut environment = SourceMacroEnvironment::default(); let end_order = self.source_order_at_position(position); - for directive in self.index.event_records.iter().take(end_order) { - self.apply_macro_state(directive, &mut environment); - } - environment + self.state_at_source_order(end_order) + .map(|state| self.environment_for_state(state)) + .unwrap_or_default() } pub fn visible_macros_at(&self, position: SourcePosition) -> Vec> { @@ -225,17 +224,6 @@ impl SourcePreprocModel { self.index.event_records.iter().find(|directive| directive.event_id == event_id) } - pub(super) fn event_record_for_entity( - &self, - entity: SourcePreprocEntity, - ) -> Option<(usize, &SourcePreprocEventRecord)> { - self.index - .event_records - .iter() - .enumerate() - .find(|(_, directive)| source_event_matches_entity(directive, entity)) - } - fn source_order_at_position(&self, position: SourcePosition) -> usize { self.index .event_records @@ -249,18 +237,28 @@ impl SourcePreprocModel { .unwrap_or(self.index.event_records.len()) } - pub(super) fn macro_environment_before( - &self, - entity: SourcePreprocEntity, - ) -> Option { - let mut environment = SourceMacroEnvironment::default(); - for directive in &self.index.event_records { - if source_event_matches_entity(directive, entity) { - return Some(environment); - } - self.apply_macro_state(directive, &mut environment); + fn state_at_source_order(&self, source_order: usize) -> Option<&SourceMacroState> { + let checkpoint = self + .tables + .state_timeline + .checkpoints() + .iter() + .rev() + .find(|checkpoint| checkpoint.source_order <= source_order)?; + self.tables.state_timeline.states().get(checkpoint.state.raw()) + } + + fn environment_for_state(&self, state: &SourceMacroState) -> SourceMacroEnvironment { + SourceMacroEnvironment { + definitions: state + .definitions + .iter() + .filter_map(|(name, definition_id)| { + self.define_index_for_definition_id(*definition_id) + .map(|define_index| (name.clone(), define_index)) + }) + .collect(), } - None } fn bindings_for_environment( @@ -285,31 +283,21 @@ impl SourcePreprocModel { Some(SourceMacroBinding { name, event_id: define.event_id, define_index, define }) } - fn apply_macro_state( + pub(super) fn binding_for_definition_id( &self, - directive: &SourcePreprocEventRecord, - environment: &mut SourceMacroEnvironment, - ) { - match directive.kind { - MacroEventKind::Define => { - if let Some(define) = self.index.defines.get(directive.index) - && let Some(name) = define.name.as_ref() - { - environment.definitions.insert(name.clone(), directive.index); - } - } - MacroEventKind::Undef => { - if let Some(undef) = self.index.undefs.get(directive.index) - && let Some(name) = undef.name.as_ref() - { - environment.definitions.remove(name.as_str()); - } - } - MacroEventKind::Include - | MacroEventKind::Conditional - | MacroEventKind::Branch - | MacroEventKind::Usage => {} - } + definition_id: SourceMacroDefinitionId, + ) -> Option> { + let definition = self.tables.macro_definitions.get(definition_id)?; + let define_index = self.define_index_for_definition_id(definition_id)?; + self.binding_for_define_index(definition.name.clone(), define_index) + } + + fn define_index_for_definition_id( + &self, + definition_id: SourceMacroDefinitionId, + ) -> Option { + let definition = self.tables.macro_definitions.get(definition_id)?; + self.index.defines.iter().position(|define| define.event_id == definition.event_id) } fn event_from_record( @@ -376,25 +364,6 @@ impl SourcePreprocModel { } } -fn source_event_matches_entity( - directive: &SourcePreprocEventRecord, - entity: SourcePreprocEntity, -) -> bool { - match (directive.kind, entity) { - (MacroEventKind::Define, SourcePreprocEntity::Define(index)) - | (MacroEventKind::Undef, SourcePreprocEntity::Undef(index)) - | (MacroEventKind::Usage, SourcePreprocEntity::Usage(index)) - | (MacroEventKind::Include, SourcePreprocEntity::Include(index)) => { - directive.index == index - } - ( - MacroEventKind::Conditional | MacroEventKind::Branch, - SourcePreprocEntity::Conditional(index), - ) => directive.index == index, - _ => false, - } -} - #[cfg(test)] mod tests { use syntax::{ @@ -743,7 +712,7 @@ wire active; assert!(references.iter().any(|reference| { matches!( reference.site, - SourceMacroReferenceSite::ConditionalToken { + SourceMacroReferenceSite::IncludeGuardIfNDef { conditional_index: site_conditional_index, token_index: 0, } if site_conditional_index == conditional_index @@ -751,6 +720,26 @@ wire active; && reference.range.source == header_source && reference.definition.define.name_range.unwrap().source == header_source })); + let reference = model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: site_conditional_index, + token_index: 0, + } if site_conditional_index == conditional_index + ) + }) + .expect("include guard token should be modeled as a resolved reference"); + assert!(matches!( + reference.resolution, + SourceMacroResolution::Resolved { + reason: SourceMacroResolutionReason::IncludeGuardIfNDef, + .. + } + )); } #[test] diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index ee2335b8..388edb23 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -228,6 +228,7 @@ pub struct SourcePreprocTables { pub emitted_tokens: SourceEmittedTokenTable, pub token_provenance: SourceTokenProvenanceTable, pub include_graph: SourceIncludeGraph, + pub inactive_ranges: Vec, pub state_timeline: SourceMacroStateTimeline, pub capabilities: SourcePreprocCapabilities, pub issues: Vec, @@ -289,7 +290,11 @@ pub enum SourcePreprocUnavailable { MissingDefinitionNameRange { event_id: SourcePreprocEventId }, MissingReferenceName { event_id: SourcePreprocEventId }, MissingReferenceNameRange { event_id: SourcePreprocEventId }, + MissingIncludedSource { include_event_id: SourcePreprocEventId, source: PreprocSourceId }, + MissingIncludeEvent { include_event_id: SourcePreprocEventId }, + IncludeEdgeNotInclude { include_event_id: SourcePreprocEventId }, IncludeChainUnavailable { source: PreprocSourceId }, + DetachedSource { source: PreprocSourceId }, MacroCallAuthorityUnavailable, EmittedTokenAuthorityUnavailable, TokenProvenanceAuthorityUnavailable, @@ -302,14 +307,16 @@ pub enum SourcePreprocFactIssue { MissingDefinitionNameRange { event_id: SourcePreprocEventId }, MissingReferenceName { event_id: SourcePreprocEventId }, MissingReferenceNameRange { event_id: SourcePreprocEventId }, + MissingIncludedSource { include_event_id: SourcePreprocEventId, source: PreprocSourceId }, + MissingIncludeEvent { include_event_id: SourcePreprocEventId }, + IncludeEdgeNotInclude { include_event_id: SourcePreprocEventId }, IncludeChainUnavailable { source: PreprocSourceId }, + DetachedSource { source: PreprocSourceId }, } impl SourcePreprocTables { pub fn from_index(index: &SourcePreprocIndex) -> Self { - let mut builder = SourcePreprocTableBuilder::new(index); - builder.build(); - builder.tables + SourcePreprocModelBuilder::new(index).build() } pub fn capabilities(&self) -> &SourcePreprocCapabilities { @@ -327,6 +334,7 @@ impl Default for SourcePreprocTables { emitted_tokens: SourceEmittedTokenTable::default(), token_provenance: SourceTokenProvenanceTable::default(), include_graph: SourceIncludeGraph::default(), + inactive_ranges: Vec::new(), state_timeline: SourceMacroStateTimeline::default(), capabilities: SourcePreprocCapabilities::unavailable(), issues: Vec::new(), @@ -546,28 +554,35 @@ impl HasDirectiveRange for SourceIncludeDirective { } } -struct SourcePreprocTableBuilder<'a> { +pub struct SourcePreprocModelBuilder<'a> { index: &'a SourcePreprocIndex, tables: SourcePreprocTables, definition_ids_by_define_index: BTreeMap, current_state: BTreeMap, definition_ranges_partial: bool, + include_edges_partial: bool, references_partial: bool, } -impl<'a> SourcePreprocTableBuilder<'a> { - fn new(index: &'a SourcePreprocIndex) -> Self { +impl<'a> SourcePreprocModelBuilder<'a> { + pub fn new(index: &'a SourcePreprocIndex) -> Self { Self { index, tables: SourcePreprocTables::default(), definition_ids_by_define_index: BTreeMap::new(), current_state: BTreeMap::new(), definition_ranges_partial: false, + include_edges_partial: false, references_partial: false, } } - fn build(&mut self) { + pub fn build(mut self) -> SourcePreprocTables { + self.build_tables(); + self.tables + } + + fn build_tables(&mut self) { self.build_definition_table(); self.build_include_graph(); self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); @@ -575,7 +590,7 @@ impl<'a> SourcePreprocTableBuilder<'a> { self.tables.capabilities = SourcePreprocCapabilities { source_events: CapabilityStatus::Complete, definition_name_ranges: partial_status(self.definition_ranges_partial), - include_edges: CapabilityStatus::Complete, + include_edges: partial_status(self.include_edges_partial), inactive_ranges: CapabilityStatus::Complete, macro_reference_resolution: partial_status(self.references_partial), macro_calls: CapabilityStatus::Unavailable( @@ -625,20 +640,41 @@ impl<'a> SourcePreprocTableBuilder<'a> { } fn build_include_graph(&mut self) { - let resolved_sources_by_event = self - .index - .include_edges - .iter() - .map(|edge| (edge.include_event_id, edge.included_source)) - .collect::>(); + self.tables.inactive_ranges = self.index.inactive_ranges.clone(); + let mut resolved_sources_by_event = BTreeMap::new(); + let mut unavailable_by_event = BTreeMap::new(); + let mut valid_edges = Vec::new(); + + for edge in &self.index.include_edges { + if let Some(unavailable) = self.validate_include_edge(edge) { + unavailable_by_event.insert(edge.include_event_id, unavailable); + continue; + } + + valid_edges.push(*edge); + resolved_sources_by_event.insert(edge.include_event_id, edge.included_source); + } + + for source in &self.index.sources { + if source.origin == PreprocSourceOrigin::Detached { + self.include_edges_partial = true; + self.tables + .issues + .push(SourcePreprocFactIssue::DetachedSource { source: source.id }); + } + } - self.tables.include_graph.edges = self.index.include_edges.clone(); + self.tables.include_graph.edges = valid_edges; for include in &self.index.includes { let id = SourceIncludeDirectiveId::new(self.tables.include_graph.directives.len()); let resolved_source = resolved_sources_by_event.get(&include.event_id).copied(); - let status = resolved_source - .map(|source| SourceIncludeStatus::Resolved { source }) - .unwrap_or(SourceIncludeStatus::Unresolved); + let status = match resolved_source { + Some(source) => SourceIncludeStatus::Resolved { source }, + None => unavailable_by_event + .remove(&include.event_id) + .map(SourceIncludeStatus::Unavailable) + .unwrap_or(SourceIncludeStatus::Unresolved), + }; self.tables.include_graph.directives.push(SourceIncludeDirective { id, event_id: include.event_id, @@ -651,6 +687,50 @@ impl<'a> SourcePreprocTableBuilder<'a> { } } + fn validate_include_edge( + &mut self, + edge: &SourceIncludeEdge, + ) -> Option { + if !self.index.sources.iter().any(|source| source.id == edge.included_source) { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingIncludedSource { + include_event_id: edge.include_event_id, + source: edge.included_source, + }); + return Some(SourcePreprocUnavailable::MissingIncludedSource { + include_event_id: edge.include_event_id, + source: edge.included_source, + }); + } + + let Some(directive) = self + .index + .event_records + .iter() + .find(|directive| directive.event_id == edge.include_event_id) + else { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingIncludeEvent { + include_event_id: edge.include_event_id, + }); + return Some(SourcePreprocUnavailable::MissingIncludeEvent { + include_event_id: edge.include_event_id, + }); + }; + + if directive.kind != MacroEventKind::Include { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::IncludeEdgeNotInclude { + include_event_id: edge.include_event_id, + }); + return Some(SourcePreprocUnavailable::IncludeEdgeNotInclude { + include_event_id: edge.include_event_id, + }); + } + + None + } + fn scan_references_and_state(&mut self) { for (source_order, directive) in self.index.event_records.iter().enumerate() { match directive.kind { @@ -716,16 +796,12 @@ impl<'a> SourcePreprocTableBuilder<'a> { }; let event_id = conditional.event_id; let directive_range = conditional.range; - let tokens = conditional - .expr - .iter() - .enumerate() - .filter_map(|(token_index, token)| { - Some((token_index, token.value.clone(), token.range?)) - }) - .collect::>(); - - for (token_index, name, name_range) in tokens { + for (token_index, token) in conditional.expr.iter().enumerate() { + let name = token.value.clone(); + let Some(name_range) = token.range else { + self.record_missing_reference_name_range(event_id); + continue; + }; let (site, resolution) = if let Some(definition) = self.current_state.get(name.as_str()).copied() { ( @@ -810,16 +886,33 @@ impl<'a> SourcePreprocTableBuilder<'a> { } Err(_) => { self.references_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { - source: definition_source, - }); - SourceMacroResolution::Unavailable( - SourcePreprocUnavailable::IncludeChainUnavailable { source: definition_source }, - ) + if self.source_is_detached(definition_source) { + self.tables + .issues + .push(SourcePreprocFactIssue::DetachedSource { source: definition_source }); + SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { + source: definition_source, + }) + } else { + self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { + source: definition_source, + }); + SourceMacroResolution::Unavailable( + SourcePreprocUnavailable::IncludeChainUnavailable { + source: definition_source, + }, + ) + } } } } + fn source_is_detached(&self, source: PreprocSourceId) -> bool { + self.index.sources.iter().any(|candidate| { + candidate.id == source && candidate.origin == PreprocSourceOrigin::Detached + }) + } + fn include_guard_definition_after_ifndef( &self, conditional_index: usize, diff --git a/crates/preproc/src/source/references.rs b/crates/preproc/src/source/references.rs index c1765f16..2418bb56 100644 --- a/crates/preproc/src/source/references.rs +++ b/crates/preproc/src/source/references.rs @@ -20,36 +20,26 @@ impl SourcePreprocModel { let Some(usage) = self.index.usages.get(usage_index) else { return Ok(None); }; - let Some(name) = usage.name.as_ref() else { + let Some(reference) = self.reference_for_usage(usage_index) else { return Ok(None); }; - let Some(environment) = - self.macro_environment_before(SourcePreprocEntity::Usage(usage_index)) + let SourceMacroResolution::Resolved { definition, include_chain, .. } = + &reference.resolution else { - return Ok(None); - }; - let Some(define_index) = environment.define_index(name.as_str()) else { - return Ok(None); + return unavailable_reference_result(&reference.resolution); }; - let Some(define) = self.index.defines.get(define_index) else { + let Some(definition) = self.binding_for_definition_id(*definition) else { return Ok(None); }; - let definition = SourceMacroBinding { - name: name.clone(), - event_id: define.event_id, - define_index, - define, - }; let definition_provenance = self - .provenance(SourcePreprocEntity::Define(define_index)) - .ok_or(SourcePreprocError::MissingEvent { event_id: define.event_id.raw() })?; - let definition_include_chain = self.include_chain_for_source(define.range.source)?; + .provenance(SourcePreprocEntity::Define(definition.define_index)) + .ok_or(SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() })?; Ok(Some(SourceMacroUsageResolution { usage_index, usage, definition, definition_provenance, - definition_include_chain, + definition_include_chain: include_chain.clone(), })) } @@ -58,16 +48,11 @@ impl SourcePreprocModel { conditional_index: usize, token_index: usize, ) -> Option> { - let conditional = self.index.conditionals.get(conditional_index)?; - let token = conditional.expr.get(token_index)?; - token.range?; - let environment = - self.macro_environment_before(SourcePreprocEntity::Conditional(conditional_index))?; - if let Some(define_index) = environment.define_index(token.value.as_str()) { - return self.binding_for_define_index(token.value.clone(), define_index); - } - - self.include_guard_definition_after_ifndef(conditional_index, token.value.as_str()) + let reference = self.reference_for_conditional_token(conditional_index, token_index)?; + let SourceMacroResolution::Resolved { definition, .. } = reference.resolution else { + return None; + }; + self.binding_for_definition_id(definition) } pub fn resolved_macro_references( @@ -75,94 +60,110 @@ impl SourcePreprocModel { ) -> Result>, SourcePreprocError> { let mut references = Vec::new(); - for (usage_index, usage) in self.index.usages.iter().enumerate() { - let Some(resolution) = self.definition_for_usage(usage_index)? else { + for reference in self.tables.macro_references.iter() { + let SourceMacroResolution::Resolved { definition, include_chain, .. } = + &reference.resolution + else { + if let Some(error) = source_error_for_unavailable_resolution(&reference.resolution) + { + return Err(error); + } continue; }; - let Some(name) = usage.name.clone() else { + let Some(definition) = self.binding_for_definition_id(*definition) else { continue; }; + let definition_provenance = self + .provenance(SourcePreprocEntity::Define(definition.define_index)) + .ok_or(SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() })?; references.push(SourceMacroReferenceResolution { - site: SourceMacroReferenceSite::Usage { usage_index }, - name, - range: usage.range, - definition: resolution.definition, - definition_provenance: resolution.definition_provenance, - definition_include_chain: resolution.definition_include_chain, + site: reference.site, + name: reference.name.clone(), + range: reference.name_range, + definition, + definition_provenance, + definition_include_chain: include_chain.clone(), }); } - for (conditional_index, conditional) in self.index.conditionals.iter().enumerate() { - for (token_index, token) in conditional.expr.iter().enumerate() { - let Some(range) = token.range else { - continue; - }; - let Some(definition) = - self.definition_for_conditional_token(conditional_index, token_index) - else { - continue; - }; - let definition_provenance = - self.provenance(SourcePreprocEntity::Define(definition.define_index)).ok_or( - SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, - )?; - let definition_include_chain = - self.include_chain_for_source(definition.define.range.source)?; - references.push(SourceMacroReferenceResolution { - site: SourceMacroReferenceSite::ConditionalToken { - conditional_index, - token_index, - }, - name: token.value.clone(), - range, - definition, - definition_provenance, - definition_include_chain, - }); - } - } - Ok(references) } - fn include_guard_definition_after_ifndef( + fn reference_for_usage(&self, usage_index: usize) -> Option<&SourceMacroReference> { + self.tables.macro_references.iter().find(|reference| { + matches!(reference.site, SourceMacroReferenceSite::Usage { + usage_index: site_usage_index, + } if site_usage_index == usage_index) + }) + } + + fn reference_for_conditional_token( &self, conditional_index: usize, - name: &str, - ) -> Option> { - let conditional = self.index.conditionals.get(conditional_index)?; - if conditional.kind != MacroConditionalKind::IfNDef { - return None; - } + token_index: usize, + ) -> Option<&SourceMacroReference> { + self.tables.macro_references.iter().find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::ConditionalToken { + conditional_index: site_conditional_index, + token_index: site_token_index, + } | SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: site_conditional_index, + token_index: site_token_index, + } if site_conditional_index == conditional_index && site_token_index == token_index + ) + }) + } +} - // Include guards are intentional forward references: at `ifndef GUARD`, - // normal macro visibility says GUARD is not defined yet. For navigation - // we model only the canonical same-source guard shape by binding that - // token to the following same-name `define` before any branch boundary. - // This is collected into the resolved-reference model, not used as a - // path, text, or IDE-layer fallback. - let source = conditional.range.source; - let (conditional_order, _) = - self.event_record_for_entity(SourcePreprocEntity::Conditional(conditional_index))?; - for directive in self.index.event_records.iter().skip(conditional_order + 1) { - if directive.range.source != source { - continue; - } - match directive.kind { - MacroEventKind::Define => { - let define = self.index.defines.get(directive.index)?; - if define.name.as_deref() == Some(name) { - return self.binding_for_define_index(SmolStr::new(name), directive.index); - } - } - MacroEventKind::Branch => break, - MacroEventKind::Undef - | MacroEventKind::Include - | MacroEventKind::Conditional - | MacroEventKind::Usage => {} - } - } +fn unavailable_reference_result<'a>( + resolution: &SourceMacroResolution, +) -> Result>, SourcePreprocError> { + if let Some(error) = source_error_for_unavailable_resolution(resolution) { + return Err(error); + } - None + Ok(None) +} + +fn source_error_for_unavailable_resolution( + resolution: &SourceMacroResolution, +) -> Option { + let SourceMacroResolution::Unavailable(unavailable) = resolution else { + return None; + }; + + match unavailable { + SourcePreprocUnavailable::MissingIncludedSource { include_event_id, source } => { + Some(SourcePreprocError::MissingIncludedSource { + include_event_id: include_event_id.raw(), + source: source.raw(), + }) + } + SourcePreprocUnavailable::MissingIncludeEvent { include_event_id } => { + Some(SourcePreprocError::MissingIncludeEvent { + include_event_id: include_event_id.raw(), + }) + } + SourcePreprocUnavailable::IncludeEdgeNotInclude { include_event_id } => { + Some(SourcePreprocError::IncludeEdgeNotInclude { + include_event_id: include_event_id.raw(), + }) + } + SourcePreprocUnavailable::IncludeChainUnavailable { source } + | SourcePreprocUnavailable::DetachedSource { source } => { + Some(SourcePreprocError::MissingIncludeEdge { source: source.raw() }) + } + SourcePreprocUnavailable::MissingDefinitionName { event_id } + | SourcePreprocUnavailable::MissingDefinitionNameRange { event_id } + | SourcePreprocUnavailable::MissingReferenceName { event_id } + | SourcePreprocUnavailable::MissingReferenceNameRange { event_id } => { + Some(SourcePreprocError::MissingEvent { event_id: event_id.raw() }) + } + SourcePreprocUnavailable::MacroCallAuthorityUnavailable + | SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + | SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable + | SourcePreprocUnavailable::ExpansionAuthorityUnavailable => None, } } From de5de1ae685a743a11362c43ad23460b61671a29 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 18:40:57 +0800 Subject: [PATCH 04/80] feat(preproc): query macro visibility from state timeline --- crates/hir/src/preproc.rs | 26 ++++ crates/preproc/src/source/model.rs | 193 ++++++++++++++---------- crates/preproc/src/source/provenance.rs | 64 +++++++- 3 files changed, 204 insertions(+), 79 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index c0e74deb..f028bcc6 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -997,6 +997,32 @@ endmodule assert!(names.iter().any(|name| name == "A005_MAGIC"), "{names:?}"); } + #[test] + fn preproc_visible_macro_names_follow_define_undef_boundaries() { + let root_text = r#"`define A005_LOCAL 1 +`undef A005_LOCAL +`define A005_NEXT 2 +module top; +localparam int W = `A005_; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let names_after_define = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_LOCAL 1\n")) + .unwrap(); + let names_after_undef = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`undef A005_LOCAL\n")) + .unwrap(); + let names_after_next = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_NEXT 2\n")) + .unwrap(); + + assert!(names_after_define.iter().any(|name| name == "A005_LOCAL")); + assert!(!names_after_undef.iter().any(|name| name == "A005_LOCAL")); + assert!(names_after_next.iter().any(|name| name == "A005_NEXT")); + } + #[test] fn preproc_inactive_branch_uses_header_define() { let root_text = r#"`include "defs.vh" diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 5026e846..45922072 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -104,16 +104,12 @@ impl SourcePreprocModel { .filter_map(|(source_order, directive)| self.event_from_record(source_order, directive)) } - pub fn macro_environment_at(&self, position: SourcePosition) -> SourceMacroEnvironment { - let end_order = self.source_order_at_position(position); - self.state_at_source_order(end_order) - .map(|state| self.environment_for_state(state)) - .unwrap_or_default() - } - pub fn visible_macros_at(&self, position: SourcePosition) -> Vec> { - let environment = self.macro_environment_at(position); - self.bindings_for_environment(&environment) + self.tables + .state_timeline + .state_at_position(position) + .map(|state| self.bindings_for_state(state)) + .unwrap_or_default() } pub fn provenance(&self, entity: SourcePreprocEntity) -> Option { @@ -224,52 +220,13 @@ impl SourcePreprocModel { self.index.event_records.iter().find(|directive| directive.event_id == event_id) } - fn source_order_at_position(&self, position: SourcePosition) -> usize { - self.index - .event_records - .iter() - .enumerate() - .find(|(_, directive)| { - directive.range.source == position.source - && directive.range.range.end() > position.offset - }) - .map(|(source_order, _)| source_order) - .unwrap_or(self.index.event_records.len()) - } - - fn state_at_source_order(&self, source_order: usize) -> Option<&SourceMacroState> { - let checkpoint = self - .tables - .state_timeline - .checkpoints() - .iter() - .rev() - .find(|checkpoint| checkpoint.source_order <= source_order)?; - self.tables.state_timeline.states().get(checkpoint.state.raw()) - } - - fn environment_for_state(&self, state: &SourceMacroState) -> SourceMacroEnvironment { - SourceMacroEnvironment { - definitions: state - .definitions - .iter() - .filter_map(|(name, definition_id)| { - self.define_index_for_definition_id(*definition_id) - .map(|define_index| (name.clone(), define_index)) - }) - .collect(), - } - } - - fn bindings_for_environment( - &self, - environment: &SourceMacroEnvironment, - ) -> Vec> { - environment + fn bindings_for_state(&self, state: &SourceMacroState) -> Vec> { + state .definitions .iter() - .filter_map(|(name, define_index)| { - self.binding_for_define_index(name.clone(), *define_index) + .filter_map(|(name, definition_id)| { + let define_index = self.define_index_for_definition_id(*definition_id)?; + self.binding_for_define_index(name.clone(), define_index) }) .collect() } @@ -437,6 +394,30 @@ mod tests { &text[usize::from(range.start())..usize::from(range.end())] } + fn visible_macro_names( + model: &SourcePreprocModel, + source: PreprocSourceId, + offset: TextSize, + ) -> Vec { + model + .visible_macros_at(SourcePosition { source, offset }) + .into_iter() + .map(|binding| binding.name) + .collect() + } + + fn visible_macro_binding<'a>( + model: &'a SourcePreprocModel, + source: PreprocSourceId, + offset: TextSize, + name: &str, + ) -> Option> { + model + .visible_macros_at(SourcePosition { source, offset }) + .into_iter() + .find(|binding| binding.name == name) + } + #[test] fn source_model_applies_include_define_after_include_point_only() { let root_text = r#"`include "defs.vh" @@ -445,17 +426,20 @@ logic [`HEADER_WIDTH-1:0] data; let header_text = "`define HEADER_WIDTH 8\n"; let (model, root_source, header_source) = source_model(root_text, header_text); - let before_include = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_before(root_text, "`include"), - }); - assert!(!before_include.contains("HEADER_WIDTH")); + assert!( + !visible_macro_names(&model, root_source, offset_before(root_text, "`include")) + .iter() + .any(|name| name == "HEADER_WIDTH") + ); - let after_include = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`include \"defs.vh\"\n"), - }); - assert_eq!(after_include.define_index("HEADER_WIDTH"), Some(0)); + let after_include = visible_macro_binding( + &model, + root_source, + offset_after(root_text, "`include \"defs.vh\"\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_include.define_index, 0); let binding = model .visible_macros_at(SourcePosition { @@ -477,18 +461,25 @@ logic [`HEADER_WIDTH-1:0] data; let header_text = "`define HEADER_WIDTH 8\n"; let (model, root_source, header_source) = source_model(root_text, header_text); - let after_include = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`include \"defs.vh\"\n"), - }); - assert_eq!(after_include.define_index("HEADER_WIDTH"), Some(0)); + let after_include = visible_macro_binding( + &model, + root_source, + offset_after(root_text, "`include \"defs.vh\"\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_include.define_index, 0); assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); - let after_undef = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`undef HEADER_WIDTH\n"), - }); - assert!(!after_undef.contains("HEADER_WIDTH")); + assert!( + visible_macro_binding( + &model, + root_source, + offset_after(root_text, "`undef HEADER_WIDTH\n"), + "HEADER_WIDTH", + ) + .is_none() + ); assert_eq!(model.undefs()[0].name.as_deref(), Some("HEADER_WIDTH")); assert_eq!(model.undefs()[0].name_range.unwrap().source, root_source); } @@ -505,11 +496,14 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); assert_eq!(model.defines()[1].name_range.unwrap().source, root_source); - let after_override = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`define HEADER_WIDTH 16\n"), - }); - assert_eq!(after_override.define_index("HEADER_WIDTH"), Some(1)); + let after_override = visible_macro_binding( + &model, + root_source, + offset_after(root_text, "`define HEADER_WIDTH 16\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_override.define_index, 1); let binding = model .visible_macros_at(SourcePosition { @@ -523,6 +517,49 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(binding.define.name_range.unwrap().source, root_source); } + #[test] + fn visible_macro_query_reads_timeline_without_event_records() { + let root_text = r#"`define A 1 +`undef A +`define B 2 +"#; + let trace = SyntaxTree::preprocessor_trace( + root_text, + "source", + ROOT_PATH, + &SyntaxTreeOptions::default(), + ) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let mut model = SourcePreprocModel::from_trace(trace).unwrap(); + + let names_after_define = + visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")); + let names_after_undef = + visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")); + let names_after_second_define = + visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")); + + assert_eq!(names_after_define, vec![SmolStr::new("A")]); + assert!(names_after_undef.is_empty(), "{names_after_undef:?}"); + assert_eq!(names_after_second_define, vec![SmolStr::new("B")]); + + model.index.event_records.clear(); + + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")), + names_after_define + ); + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")), + names_after_undef + ); + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")), + names_after_second_define + ); + } + #[test] fn source_model_preserves_inactive_range_sources() { let root_text = r#"`include "defs.vh" diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 388edb23..853a8d6c 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -112,6 +112,8 @@ pub enum SourceIncludeStatus { pub struct SourceMacroStateTimeline { states: Vec, checkpoints: Vec, + source_order_boundaries: BTreeMap>, + final_source_order: usize, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -127,6 +129,12 @@ pub struct SourceMacroStateCheckpoint { pub state: SourceMacroStateId, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceMacroStatePositionBoundary { + source_order: usize, + boundary: SourcePosition, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroCall { pub id: SourceMacroCallId, @@ -370,7 +378,41 @@ impl SourceMacroStateTimeline { impl Default for SourceMacroStateTimeline { fn default() -> Self { - Self { states: Vec::new(), checkpoints: Vec::new() } + Self { + states: Vec::new(), + checkpoints: Vec::new(), + source_order_boundaries: BTreeMap::new(), + final_source_order: 0, + } + } +} + +impl SourceMacroStateTimeline { + pub fn state_at_position(&self, position: SourcePosition) -> Option<&SourceMacroState> { + let source_order = self.source_order_at_position(position); + self.state_at_source_order(source_order) + } + + fn source_order_at_position(&self, position: SourcePosition) -> usize { + let Some(boundaries) = self.source_order_boundaries.get(&position.source) else { + return self.final_source_order; + }; + let index = + boundaries.partition_point(|boundary| boundary.boundary.offset <= position.offset); + boundaries + .get(index) + .map(|boundary| boundary.source_order) + .unwrap_or(self.final_source_order) + } + + fn state_at_source_order(&self, source_order: usize) -> Option<&SourceMacroState> { + let index = + self.checkpoints.partition_point(|checkpoint| checkpoint.source_order <= source_order); + if index == 0 { + return None; + } + let checkpoint = &self.checkpoints[index - 1]; + self.states.get(checkpoint.state.raw()) } } @@ -585,6 +627,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn build_tables(&mut self) { self.build_definition_table(); self.build_include_graph(); + self.record_position_boundaries(); self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); self.scan_references_and_state(); self.tables.capabilities = SourcePreprocCapabilities { @@ -639,6 +682,25 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } + fn record_position_boundaries(&mut self) { + self.tables.state_timeline.final_source_order = self.index.event_records.len(); + for (source_order, directive) in self.index.event_records.iter().enumerate() { + self.tables + .state_timeline + .source_order_boundaries + .entry(directive.range.source) + .or_default() + .push(SourceMacroStatePositionBoundary { + source_order, + boundary: boundary_after(directive.range), + }); + } + + for boundaries in self.tables.state_timeline.source_order_boundaries.values_mut() { + boundaries.sort_by_key(|boundary| (boundary.boundary.offset, boundary.source_order)); + } + } + fn build_include_graph(&mut self) { self.tables.inactive_ranges = self.index.inactive_ranges.clone(); let mut resolved_sources_by_event = BTreeMap::new(); From e505df9e9c8e4bee8236bfd77f839f6f3f40d45d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 19:01:44 +0800 Subject: [PATCH 05/80] refactor(preproc): expose mapped provenance results --- crates/hir/src/base_db/source_db.rs | 46 +- crates/hir/src/preproc.rs | 730 ++++++++++++++++++++-------- crates/ide/src/goto_definition.rs | 3 + crates/ide/src/references.rs | 4 + 4 files changed, 562 insertions(+), 221 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index ee9e5a68..c5db88f7 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -127,7 +127,33 @@ pub struct CompilationDiagnostic { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MappedSourcePreprocModel { pub model: SourcePreprocModel, - pub source_file_ids: FxHashMap, + pub source_map: PreprocSourceMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PreprocSourceMap { + entries: FxHashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PreprocSourceMapping { + RealFile(FileId), +} + +impl PreprocSourceMap { + pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId) { + self.entries.insert(source, PreprocSourceMapping::RealFile(file_id)); + } + + pub fn get(&self, source: PreprocSourceId) -> Option { + self.entries.get(&source).copied() + } + + pub fn file_id(&self, source: PreprocSourceId) -> Option { + match self.get(source)? { + PreprocSourceMapping::RealFile(file_id) => Some(file_id), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -175,15 +201,15 @@ fn source_preproc_file_ids( db: &dyn SourceRootDb, file_id: FileId, trace: &PreprocessorTrace, -) -> Result, SourcePreprocQueryError> { - let mut source_file_ids = FxHashMap::default(); +) -> Result { + let mut source_map = PreprocSourceMap::default(); let path_file_ids = path_file_ids(db); - source_file_ids.insert(PreprocSourceId::from(trace.root_buffer_id), file_id); + source_map.insert_real_file(PreprocSourceId::from(trace.root_buffer_id), file_id); for source in &trace.source_buffers { let source_id = PreprocSourceId::from(source.buffer_id); if source_id == PreprocSourceId::from(trace.root_buffer_id) { - source_file_ids.insert(source_id, file_id); + source_map.insert_real_file(source_id, file_id); continue; } @@ -195,13 +221,13 @@ fn source_preproc_file_ids( path: source.path.clone(), }); }; - source_file_ids.insert(source_id, mapped_file_id); + source_map.insert_real_file(source_id, mapped_file_id); } SourceBufferOrigin::Predefine => {} } } - Ok(source_file_ids) + Ok(source_map) } fn syntax_tree_options_for_file( @@ -457,8 +483,8 @@ fn source_preproc_model( return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; - let source_file_ids = match source_preproc_file_ids(db, file_id, &trace) { - Ok(source_file_ids) => source_file_ids, + let source_map = match source_preproc_file_ids(db, file_id, &trace) { + Ok(source_map) => source_map, Err(err) => return Arc::new(Err(err)), }; let model = match SourcePreprocModel::from_trace(trace) { @@ -466,7 +492,7 @@ fn source_preproc_model( Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), }; - Arc::new(Ok(MappedSourcePreprocModel { model, source_file_ids })) + Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) } fn macro_reference_index_for_profile( diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index f028bcc6..c5500756 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,15 +1,18 @@ use std::collections::BTreeMap; use preproc::source::{ - MacroIncludeTarget, SourceIncludeChainEntry, SourceMacroBinding, SourcePosition, - SourcePreprocError, SourcePreprocProvenance, SourceRange, + CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceIncludeChainEntry, + SourceIncludeDirectiveId, SourceIncludeStatus, SourceMacroBinding, + SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, + SourceMacroReference as SourceMacroReferenceFact, SourceMacroReferenceId, + SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, + SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePosition, + SourcePreprocError, SourcePreprocUnavailable, SourceRange, }; use rustc_hash::FxHashSet; use smol_str::SmolStr; use utils::{ line_index::{TextRange, TextSize}, - path_identity::PathIdentityIndex, - paths::{AbsPathBuf, Utf8Path}, uniq_vec::UniqVec, }; use vfs::FileId; @@ -33,13 +36,95 @@ pub enum PreprocError { directive_file_id: FileId, name_file_id: FileId, }, + MismatchedReferenceRangeFiles { + event_id: u32, + directive_file_id: FileId, + name_file_id: FileId, + }, MissingDefinitionNameRange { event_id: u32, }, + Unavailable { + reason: PreprocUnavailable, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocUnavailable { + Source(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocAvailability { + Complete, + Partial, + Unavailable(PreprocUnavailable), +} + +macro_rules! mapped_preproc_id { + ($name:ident, $core:ty) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct $name($core); + + impl $name { + pub fn raw(self) -> usize { + self.0.raw() + } + } + + impl From<$core> for $name { + fn from(value: $core) -> Self { + Self(value) + } + } + }; +} + +mapped_preproc_id!(MacroDefinitionId, SourceMacroDefinitionId); +mapped_preproc_id!(MacroReferenceId, SourceMacroReferenceId); +mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); + +impl MacroDefinitionId { + fn core_id(self) -> SourceMacroDefinitionId { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MappedPreprocSource { + RealFile { file_id: FileId }, +} + +impl MappedPreprocSource { + pub fn file_id(self) -> FileId { + match self { + Self::RealFile { file_id } => file_id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroResolution { + Resolved { + definition_id: MacroDefinitionId, + reason: MacroResolutionReason, + include_chain: Vec, + }, + Undefined, + Unavailable(PreprocUnavailable), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MacroResolutionReason { + VisibleDefinition, + IncludeGuardIfNDef, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroDefinition { + pub id: MacroDefinitionId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub file_id: FileId, pub name: SmolStr, pub define_index: usize, @@ -50,10 +135,15 @@ pub struct MacroDefinition { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroUsage { + pub reference_id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub file_id: FileId, pub name: SmolStr, pub usage_index: usize, + pub directive_range: TextRange, pub range: TextRange, + pub resolution: MacroResolution, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -66,6 +156,9 @@ pub struct MacroUsageResolution { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroDefinitionProvenance { + pub id: MacroDefinitionId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub event_id: u32, pub file_id: FileId, pub directive_range: TextRange, @@ -82,9 +175,14 @@ pub struct IncludeChainEntry { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroReference { + pub id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub file_id: FileId, pub name: SmolStr, + pub directive_range: TextRange, pub range: TextRange, + pub resolution: MacroResolution, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -97,6 +195,8 @@ pub struct MacroReferenceResolution { pub struct MacroReferenceDefinitions { pub reference: MacroReference, pub definitions: Vec, + pub resolution: MacroResolution, + pub capability: PreprocAvailability, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -141,6 +241,32 @@ impl MacroReferenceKey { pub struct MacroReferenceIndex { references_by_definition: BTreeMap>, definitions_by_reference: BTreeMap>, + issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReferences { + pub references: Vec, + pub status: MacroReferenceIndexStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroReferenceIndexStatus { + Complete, + Partial { issues: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroReferenceIndexIssue { + SkippedModel { + file_id: FileId, + error: PreprocError, + }, + UnavailableReference { + file_id: FileId, + reference_id: MacroReferenceId, + reason: PreprocUnavailable, + }, } impl MacroReferenceIndex { @@ -160,6 +286,14 @@ impl MacroReferenceIndex { .map(Vec::as_slice) } + pub fn status(&self) -> MacroReferenceIndexStatus { + if self.issues.is_empty() { + MacroReferenceIndexStatus::Complete + } else { + MacroReferenceIndexStatus::Partial { issues: self.issues.clone() } + } + } + fn push(&mut self, definition: MacroDefinition, reference: MacroReference) { let definition_key = MacroDefinitionKey::from_definition(&definition); let references = self.references_by_definition.entry(definition_key).or_default(); @@ -169,18 +303,30 @@ impl MacroReferenceIndex { let definitions = self.definitions_by_reference.entry(reference_key).or_default(); push_unique_macro_definition(definitions, definition); } + + fn push_issue(&mut self, issue: MacroReferenceIndexIssue) { + if !self.issues.contains(&issue) { + self.issues.push(issue); + } + } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct IncludeDirective { + pub id: IncludeDirectiveId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub file_id: FileId, pub include_index: usize, pub range: TextRange, pub target: IncludeTarget, + pub status: IncludeDirectiveStatus, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct InactiveBranch { + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, pub file_id: FileId, pub range: TextRange, } @@ -191,6 +337,13 @@ pub enum IncludeTarget { Token { raw: SmolStr }, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncludeDirectiveStatus { + Resolved { source: MappedPreprocSource }, + Unresolved, + Unavailable(PreprocUnavailable), +} + impl From for PreprocError { fn from(value: SourcePreprocQueryError) -> Self { Self::SourceQuery(value) @@ -273,24 +426,12 @@ pub fn macro_definition_at( let mapped = db.source_preproc_model(file_id); let mapped = mapped_result(mapped.as_ref())?; - for (define_index, define) in mapped.model.defines().iter().enumerate() { - let Some(source_name_range) = define.name_range else { - continue; - }; - let (define_file_id, directive_range, name_range) = - map_definition_ranges(mapped, define.event_id.raw(), define.range, source_name_range)?; - if define_file_id == file_id && range_contains_offset(name_range, offset) { - return Ok(Some(MacroDefinition { - file_id: define_file_id, - name: match define.name.clone() { - Some(name) => name, - None => return Ok(None), - }, - define_index, - event_id: define.event_id.raw(), - directive_range, - name_range, - })); + for definition in mapped.model.macro_definitions().iter() { + let mapped_definition = map_macro_definition(mapped, definition)?; + if mapped_definition.file_id == file_id + && range_contains_offset(mapped_definition.name_range, offset) + { + return Ok(Some(mapped_definition)); } } @@ -305,27 +446,51 @@ pub fn macro_usage_resolution_at( let mapped = db.source_preproc_model(file_id); let mapped = mapped_result(mapped.as_ref())?; - for (usage_index, usage) in mapped.model.usages().iter().enumerate() { - let (usage_file_id, range) = map_source_range(mapped, usage.range)?; - if usage_file_id != file_id || !range_contains_offset(range, offset) { + for reference in mapped.model.macro_references().iter() { + let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { + continue; + }; + let mapped_reference = map_macro_reference(mapped, reference)?; + if mapped_reference.file_id != file_id + || !range_contains_offset(mapped_reference.range, offset) + { continue; } - let Some(name) = usage.name.clone() else { - return Ok(None); - }; - let Some(source_resolution) = mapped.model.definition_for_usage(usage_index)? else { - return Ok(None); - }; - let Some(definition) = map_binding_definition(mapped, source_resolution.definition)? else { - return Ok(None); + let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = + &reference.resolution + else { + return match &reference.resolution { + SourceMacroResolutionFact::Undefined => Ok(None), + SourceMacroResolutionFact::Unavailable(reason) => { + Err(unavailable_error(reason.clone())) + } + SourceMacroResolutionFact::Resolved { .. } => unreachable!(), + }; }; + let definition_fact = + mapped.model.macro_definitions().get(*definition).ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )) + })?; + let definition = map_macro_definition(mapped, definition_fact)?; let definition_provenance = - map_definition_provenance(mapped, &source_resolution.definition_provenance)?; - let include_chain = map_include_chain(mapped, &source_resolution.definition_include_chain)?; + map_definition_provenance_from_definition(mapped, definition_fact)?; + let include_chain = map_include_chain(mapped, include_chain)?; return Ok(Some(MacroUsageResolution { - usage: MacroUsage { file_id: usage_file_id, name, usage_index, range }, + usage: MacroUsage { + reference_id: mapped_reference.id, + source: mapped_reference.source, + capability: mapped_reference.capability.clone(), + file_id: mapped_reference.file_id, + name: mapped_reference.name, + usage_index, + directive_range: mapped_reference.directive_range, + range: mapped_reference.range, + resolution: mapped_reference.resolution, + }, definition, definition_provenance, include_chain, @@ -343,31 +508,12 @@ pub fn macro_reference_at( let mapped = db.source_preproc_model(file_id); let mapped = mapped_result(mapped.as_ref())?; - for usage in mapped.model.usages() { - let (usage_file_id, range) = map_source_range(mapped, usage.range)?; - if usage_file_id != file_id || !range_contains_offset(range, offset) { - continue; - } - let Some(name) = usage.name.clone() else { - return Ok(None); - }; - return Ok(Some(MacroReference { file_id: usage_file_id, name, range })); - } - - for conditional in mapped.model.conditionals() { - for token in &conditional.expr { - let Some(source_range) = token.range else { - continue; - }; - let (reference_file_id, range) = map_source_range(mapped, source_range)?; - if reference_file_id != file_id || !range_contains_offset(range, offset) { - continue; - } - return Ok(Some(MacroReference { - file_id: reference_file_id, - name: token.value.clone(), - range, - })); + for reference in mapped.model.macro_references().iter() { + let mapped_reference = map_macro_reference(mapped, reference)?; + if mapped_reference.file_id == file_id + && range_contains_offset(mapped_reference.range, offset) + { + return Ok(Some(mapped_reference)); } } @@ -396,25 +542,36 @@ pub fn macro_reference_definitions_at( let Some(reference) = macro_reference_at(db, file_id, offset)? else { return Ok(None); }; - let profile_id = db.file_compilation_profile(file_id); - let index = db.macro_reference_index_for_profile(profile_id); - let definitions = index.definitions_for_reference(&reference).unwrap_or(&[]).to_vec(); - if definitions.is_empty() { - return Ok(None); - } - Ok(Some(MacroReferenceDefinitions { reference, definitions })) + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let definitions = match &reference.resolution { + MacroResolution::Resolved { definition_id, .. } => { + let Some(definition) = mapped.model.macro_definitions().get(definition_id.core_id()) + else { + return Ok(None); + }; + vec![map_macro_definition(mapped, definition)?] + } + MacroResolution::Undefined | MacroResolution::Unavailable(_) => Vec::new(), + }; + Ok(Some(MacroReferenceDefinitions { + resolution: reference.resolution.clone(), + capability: reference.capability.clone(), + reference, + definitions, + })) } pub fn macro_references( db: &dyn SourceRootDb, file_id: FileId, definition: &MacroDefinition, -) -> PreprocResult> { +) -> PreprocResult { let profile_id = db .file_compilation_profile(file_id) .or_else(|| db.file_compilation_profile(definition.file_id)); let index = db.macro_reference_index_for_profile(profile_id); - Ok(index.references_for(definition)) + Ok(MacroReferences { references: index.references_for(definition), status: index.status() }) } pub(crate) fn build_macro_reference_index( @@ -425,16 +582,17 @@ pub(crate) fn build_macro_reference_index( for model_file_id in preproc_reference_model_file_ids(db, profile_id) { let mapped = db.source_preproc_model(model_file_id); - let Ok(mapped) = mapped.as_ref() else { - continue; + let mapped = match mapped.as_ref() { + Ok(mapped) => mapped, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error: error.clone().into(), + }); + continue; + } }; - if let Err(err) = collect_macro_references_in_model(mapped, &mut index) { - tracing::debug!( - ?model_file_id, - ?err, - "failed to add preprocessor macro references to index", - ); - } + collect_macro_references_in_model(mapped, model_file_id, &mut index); } index @@ -442,20 +600,53 @@ pub(crate) fn build_macro_reference_index( fn collect_macro_references_in_model( mapped: &MappedSourcePreprocModel, + model_file_id: FileId, index: &mut MacroReferenceIndex, -) -> PreprocResult<()> { - for resolved in mapped.model.resolved_macro_references()? { - let Some(definition) = map_binding_definition(mapped, resolved.definition)? else { +) { + for reference in mapped.model.macro_references().iter() { + let SourceMacroResolutionFact::Resolved { definition, .. } = reference.resolution else { + if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { + index.push_issue(MacroReferenceIndexIssue::UnavailableReference { + file_id: model_file_id, + reference_id: reference.id.into(), + reason: PreprocUnavailable::Source(reason.clone()), + }); + } + continue; + }; + + let Some(definition) = mapped.model.macro_definitions().get(definition) else { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error: PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )), + }); continue; }; - let (reference_file_id, range) = map_source_range(mapped, resolved.range)?; - index.push( - definition, - MacroReference { file_id: reference_file_id, name: resolved.name, range }, - ); - } - Ok(()) + let definition = match map_macro_definition(mapped, definition) { + Ok(definition) => definition, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + continue; + } + }; + let reference = match map_macro_reference(mapped, reference) { + Ok(reference) => reference, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + continue; + } + }; + index.push(definition, reference); + } } pub fn include_directive_at( @@ -465,23 +656,32 @@ pub fn include_directive_at( ) -> PreprocResult> { let mapped = db.source_preproc_model(file_id); let mapped = mapped_result(mapped.as_ref())?; - for (include_index, include) in mapped.model.includes().iter().enumerate() { - let (include_file_id, range) = map_source_range(mapped, include.range)?; + for include in mapped.model.include_graph().directives() { + let (source, range) = map_mapped_source_range(mapped, include.directive_range)?; + let include_file_id = source.file_id(); if include_file_id != file_id || !range_contains_offset(range, offset) { continue; } + let status = map_include_status(mapped, &include.status)?; + let resolved_file = match &status { + IncludeDirectiveStatus::Resolved { source } => Some(source.file_id()), + IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, + }; let target = match &include.target { - MacroIncludeTarget::Literal { path, .. } => IncludeTarget::Literal { - path: path.clone(), - resolved_file: resolve_literal_include(db, include_file_id, path), - }, + MacroIncludeTarget::Literal { path, .. } => { + IncludeTarget::Literal { path: path.clone(), resolved_file } + } MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, }; return Ok(Some(IncludeDirective { + id: include.id.into(), + source, + capability: capability_status(&mapped.model.capabilities().include_edges), file_id: include_file_id, - include_index, + include_index: include.id.raw(), range, target, + status, })); } @@ -497,9 +697,15 @@ pub fn inactive_branches( let mut branches = Vec::new(); for source_range in mapped.model.inactive_ranges() { - let (branch_file_id, range) = map_source_range(mapped, *source_range)?; + let (source, range) = map_mapped_source_range(mapped, *source_range)?; + let branch_file_id = source.file_id(); if branch_file_id == file_id { - branches.push(InactiveBranch { file_id: branch_file_id, range }); + branches.push(InactiveBranch { + source, + capability: capability_status(&mapped.model.capabilities().inactive_ranges), + file_id: branch_file_id, + range, + }); } } @@ -524,45 +730,200 @@ fn map_source_range( mapped: &MappedSourcePreprocModel, source_range: SourceRange, ) -> PreprocResult<(FileId, TextRange)> { - let file_id = map_source_id(mapped, source_range.source)?; - Ok((file_id, source_range.range)) + let (source, range) = map_mapped_source_range(mapped, source_range)?; + Ok((source.file_id(), range)) } fn map_source_id( mapped: &MappedSourcePreprocModel, - source: preproc::source::PreprocSourceId, + source: PreprocSourceId, ) -> PreprocResult { mapped - .source_file_ids - .get(&source) - .copied() + .source_map + .file_id(source) .ok_or_else(|| PreprocError::UnmappedSource { buffer_id: source.raw() }) } -fn map_definition_provenance( +fn map_mapped_source_range( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, +) -> PreprocResult<(MappedPreprocSource, TextRange)> { + let source = map_mapped_source_id(mapped, source_range.source)?; + Ok((source, source_range.range)) +} + +fn map_mapped_source_id( + mapped: &MappedSourcePreprocModel, + source: PreprocSourceId, +) -> PreprocResult { + Ok(MappedPreprocSource::RealFile { file_id: map_source_id(mapped, source)? }) +} + +fn map_macro_definition( mapped: &MappedSourcePreprocModel, - provenance: &SourcePreprocProvenance, + definition: &SourceMacroDefinitionFact, +) -> PreprocResult { + let (source, directive_range, name_range) = map_definition_ranges( + mapped, + definition.event_id.raw(), + definition.directive_range, + definition.name_range, + )?; + Ok(MacroDefinition { + id: definition.id.into(), + source, + capability: capability_status(&mapped.model.capabilities().definition_name_ranges), + file_id: source.file_id(), + name: definition.name.clone(), + define_index: define_index_for_definition(mapped, definition)?, + event_id: definition.event_id.raw(), + directive_range, + name_range, + }) +} + +fn map_definition_provenance_from_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, ) -> PreprocResult { - let (file_id, range) = map_source_range(mapped, provenance.range)?; - let Some(source_name_range) = provenance.name_range else { - return Err(PreprocError::MissingDefinitionNameRange { - event_id: provenance.event_id.raw(), + let definition = map_macro_definition(mapped, definition)?; + Ok(MacroDefinitionProvenance { + id: definition.id, + source: definition.source, + capability: definition.capability, + event_id: definition.event_id, + file_id: definition.file_id, + directive_range: definition.directive_range, + name_range: definition.name_range, + }) +} + +fn map_macro_reference( + mapped: &MappedSourcePreprocModel, + reference: &SourceMacroReferenceFact, +) -> PreprocResult { + let (source, directive_range, name_range) = map_reference_ranges(mapped, reference)?; + Ok(MacroReference { + id: reference.id.into(), + source, + capability: capability_status(&mapped.model.capabilities().macro_reference_resolution), + file_id: source.file_id(), + name: reference.name.clone(), + directive_range, + range: name_range, + resolution: map_macro_resolution(mapped, &reference.resolution)?, + }) +} + +fn map_macro_resolution( + mapped: &MappedSourcePreprocModel, + resolution: &SourceMacroResolutionFact, +) -> PreprocResult { + Ok(match resolution { + SourceMacroResolutionFact::Resolved { definition, reason, include_chain } => { + MacroResolution::Resolved { + definition_id: (*definition).into(), + reason: map_macro_resolution_reason(*reason), + include_chain: map_include_chain(mapped, include_chain)?, + } + } + SourceMacroResolutionFact::Undefined => MacroResolution::Undefined, + SourceMacroResolutionFact::Unavailable(reason) => { + MacroResolution::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +fn map_macro_resolution_reason(reason: SourceMacroResolutionReasonFact) -> MacroResolutionReason { + match reason { + SourceMacroResolutionReasonFact::VisibleDefinition => { + MacroResolutionReason::VisibleDefinition + } + SourceMacroResolutionReasonFact::IncludeGuardIfNDef => { + MacroResolutionReason::IncludeGuardIfNDef + } + } +} + +fn map_reference_ranges( + mapped: &MappedSourcePreprocModel, + reference: &SourceMacroReferenceFact, +) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { + let (directive_source, directive_range) = + map_mapped_source_range(mapped, reference.directive_range)?; + let (name_source, name_range) = map_mapped_source_range(mapped, reference.name_range)?; + if directive_source != name_source { + return Err(PreprocError::MismatchedReferenceRangeFiles { + event_id: reference.event_id.raw(), + directive_file_id: directive_source.file_id(), + name_file_id: name_source.file_id(), }); - }; - let (name_file_id, name_range) = map_source_range(mapped, source_name_range)?; - if name_file_id != file_id { + } + Ok((directive_source, directive_range, name_range)) +} + +fn map_include_status( + mapped: &MappedSourcePreprocModel, + status: &SourceIncludeStatus, +) -> PreprocResult { + Ok(match status { + SourceIncludeStatus::Resolved { source } => { + IncludeDirectiveStatus::Resolved { source: map_mapped_source_id(mapped, *source)? } + } + SourceIncludeStatus::Unresolved => IncludeDirectiveStatus::Unresolved, + SourceIncludeStatus::Unavailable(reason) => { + IncludeDirectiveStatus::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +fn capability_status(status: &CapabilityStatus) -> PreprocAvailability { + match status { + CapabilityStatus::Complete => PreprocAvailability::Complete, + CapabilityStatus::Partial => PreprocAvailability::Partial, + CapabilityStatus::Unavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + +fn unavailable_error(reason: SourcePreprocUnavailable) -> PreprocError { + PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) } +} + +fn define_index_for_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, +) -> PreprocResult { + mapped + .model + .defines() + .iter() + .position(|define| define.event_id == definition.event_id) + .ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, + )) + }) +} + +fn map_definition_ranges( + mapped: &MappedSourcePreprocModel, + event_id: u32, + directive_source_range: SourceRange, + name_source_range: SourceRange, +) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { + let (directive_source, directive_range) = + map_mapped_source_range(mapped, directive_source_range)?; + let (name_source, name_range) = map_mapped_source_range(mapped, name_source_range)?; + if directive_source != name_source { return Err(PreprocError::MismatchedDefinitionRangeFiles { - event_id: provenance.event_id.raw(), - directive_file_id: file_id, - name_file_id, + event_id, + directive_file_id: directive_source.file_id(), + name_file_id: name_source.file_id(), }); } - Ok(MacroDefinitionProvenance { - event_id: provenance.event_id.raw(), - file_id, - directive_range: range, - name_range, - }) + Ok((directive_source, directive_range, name_range)) } fn map_include_chain( @@ -588,44 +949,15 @@ fn map_binding_definition( mapped: &MappedSourcePreprocModel, binding: SourceMacroBinding<'_>, ) -> PreprocResult> { - let Some(name) = binding.define.name.clone() else { - return Ok(None); - }; - let Some(source_name_range) = binding.define.name_range else { + let Some(definition) = mapped + .model + .macro_definitions() + .iter() + .find(|definition| definition.event_id == binding.event_id) + else { return Ok(None); }; - let (file_id, directive_range, name_range) = map_definition_ranges( - mapped, - binding.event_id.raw(), - binding.define.range, - source_name_range, - )?; - Ok(Some(MacroDefinition { - file_id, - name, - define_index: binding.define_index, - event_id: binding.event_id.raw(), - directive_range, - name_range, - })) -} - -fn map_definition_ranges( - mapped: &MappedSourcePreprocModel, - event_id: u32, - directive_source_range: SourceRange, - name_source_range: SourceRange, -) -> PreprocResult<(FileId, TextRange, TextRange)> { - let (directive_file_id, directive_range) = map_source_range(mapped, directive_source_range)?; - let (name_file_id, name_range) = map_source_range(mapped, name_source_range)?; - if directive_file_id != name_file_id { - return Err(PreprocError::MismatchedDefinitionRangeFiles { - event_id, - directive_file_id, - name_file_id, - }); - } - Ok((directive_file_id, directive_range, name_range)) + Ok(Some(map_macro_definition(mapped, definition)?)) } fn push_unique_macro_reference(refs: &mut Vec, reference: MacroReference) { @@ -701,55 +1033,6 @@ fn preproc_reference_model_file_ids( file_ids } -fn resolve_literal_include(db: &dyn SourceRootDb, file_id: FileId, path: &str) -> Option { - let includer_path = db.file_path(file_id)?; - let include_dirs = db.file_preprocess_config(file_id).include_dirs.clone(); - let path_file_ids = path_file_ids(db); - resolve_include_target(path, &includer_path, &include_dirs, &path_file_ids) -} - -fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex { - let mut index = PathIdentityIndex::default(); - for file_id in db.files().iter().copied() { - if db.file_is_project_ignored(file_id) { - continue; - } - if let Some(path) = db.file_path(file_id) { - index.insert_path(&path, file_id); - } - } - index -} - -fn resolve_include_target( - path: &str, - includer_path: &AbsPathBuf, - include_dirs: &[AbsPathBuf], - path_file_ids: &PathIdentityIndex, -) -> Option { - let include_path = Utf8Path::new(path); - if include_path.is_absolute() { - let abs_path = AbsPathBuf::try_from(include_path.to_path_buf()).ok()?.normalize(); - return path_file_ids.get_path(abs_path.as_path()); - } - - if let Some(parent) = includer_path.parent() { - let candidate = parent.absolutize(include_path); - if let Some(file_id) = path_file_ids.get_path(candidate.as_path()) { - return Some(file_id); - } - } - - for include_dir in include_dirs { - let candidate = include_dir.absolutize(include_path); - if let Some(file_id) = path_file_ids.get_path(candidate.as_path()) { - return Some(file_id); - } - } - - None -} - fn range_contains_offset(range: TextRange, offset: TextSize) -> bool { range.start() <= offset && offset <= range.end() } @@ -1053,20 +1336,32 @@ localparam int ENABLED = `HEADER_FLAG; .unwrap() .unwrap(); - let refs = macro_references(&db, HEADER, &definition).unwrap(); + assert_eq!(definition.source.file_id(), HEADER); + assert!(matches!(definition.capability, PreprocAvailability::Complete)); + + let refs = macro_references(&db, HEADER, &definition).unwrap().references; assert!(refs.iter().any(|reference| { reference.file_id == TOP && text_at_range(root_text, reference.range) == "HEADER_FLAG" })); assert!(refs.iter().any(|reference| { - reference.file_id == TOP && text_at_range(root_text, reference.range) == "`HEADER_FLAG" + reference.file_id == TOP + && matches!( + reference.resolution, + MacroResolution::Resolved { + reason: MacroResolutionReason::VisibleDefinition, + .. + } + ) + && text_at_range(root_text, reference.range) == "HEADER_FLAG" })); let definitions = - macro_reference_definitions_at(&db, TOP, offset_after(root_text, "ENABLED = ")) + macro_reference_definitions_at(&db, TOP, offset_after(root_text, "ENABLED = `")) .unwrap() .unwrap(); assert_eq!(text_at_range(root_text, definitions.reference.range), "`HEADER_FLAG"); + assert!(matches!(definitions.capability, PreprocAvailability::Complete)); assert!(definitions.definitions.iter().any(|indexed| { indexed.file_id == HEADER && indexed.name_range == definition.name_range @@ -1074,6 +1369,19 @@ localparam int ENABLED = `HEADER_FLAG; })); } + #[test] + fn preproc_macro_definition_at_only_hits_name_range() { + let root_text = "`define HEADER_FLAG 1\n"; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + assert!(macro_definition_at(&db, TOP, offset(root_text, "`define")).unwrap().is_none()); + + let definition = + macro_definition_at(&db, TOP, offset(root_text, "HEADER_FLAG")).unwrap().unwrap(); + assert_eq!(text_at_range(root_text, definition.name_range), "HEADER_FLAG"); + assert_ne!(definition.directive_range, definition.name_range); + } + #[test] fn preproc_ifndef_guard_reference_resolves_to_following_define() { let root_text = "`include \"defs.vh\"\n"; @@ -1092,7 +1400,7 @@ localparam int ENABLED = `HEADER_FLAG; resolution.definitions.iter().find(|definition| definition.file_id == HEADER).unwrap(); assert_eq!(text_at_range(header_text, definition.name_range), "HEADER_FLAG"); - let refs = macro_references(&db, HEADER, definition).unwrap(); + let refs = macro_references(&db, HEADER, definition).unwrap().references; assert!(refs.iter().any(|reference| { reference.file_id == HEADER && reference.range.start() == offset(header_text, "HEADER_FLAG") diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 7f1e615e..59c25cc4 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -67,6 +67,9 @@ fn handle_preproc_macro( let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; let reference_range = resolution.reference.range; let targets = resolution.definitions.into_iter().map(macro_nav_target).collect_vec(); + if targets.is_empty() { + return None; + } Some(RangeInfo::new(reference_range, targets)) } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 2c469b75..2d569589 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -98,6 +98,9 @@ fn handle_preproc_macro( } else { macro_reference_definitions_at(db, file_id, offset).ok()??.definitions }; + if definitions.is_empty() { + return None; + } definitions .into_iter() @@ -113,6 +116,7 @@ fn macro_references_for_definition( ) -> Option { let refs = macro_references(db, file_id, &definition) .ok()? + .references .into_iter() .filter(|usage| { config.search_scope.as_ref().is_none_or(|scope| { From 39dc63ec3636790a2536a758fb67f81d101a0a71 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 19:21:38 +0800 Subject: [PATCH 06/80] feat(preproc): map virtual preprocessor sources --- crates/hir/src/base_db/source_db.rs | 561 ++++++++++++++++++++++++++-- crates/hir/src/preproc.rs | 45 ++- 2 files changed, 567 insertions(+), 39 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index c5db88f7..c827587c 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,11 +1,23 @@ -use preproc::source::{PreprocSourceId, SourcePreprocError, SourcePreprocModel}; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use preproc::source::{ + PreprocSourceId, SourceMacroExpansionId, SourcePreprocError, SourcePreprocModel, + SourcePreprocUnavailable, SourceRange, +}; use rustc_hash::{FxHashMap, FxHashSet}; +use smol_str::SmolStr; use syntax::{ Compilation, ParserExpectedSyntax, PreprocessorTrace, SourceBufferOrigin, SyntaxDiagnostic, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeBufferIds, + SyntaxTree, SyntaxTreeBuffer, SyntaxTreeBufferIds, SyntaxTreeOptions, }; use triomphe::Arc; -use utils::{line_index::TextSize, path_identity::PathIdentityIndex}; +use utils::{ + line_index::{TextRange, TextSize}, + path_identity::PathIdentityIndex, +}; use vfs::{FileId, VfsPath, anchored_path::AnchoredPath}; use crate::base_db::{ @@ -133,26 +145,138 @@ pub struct MappedSourcePreprocModel { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PreprocSourceMap { entries: FxHashMap, + text_lengths: FxHashMap, + range_offsets: FxHashMap, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocSourceMapping { RealFile(FileId), + VirtualFile { file_id: FileId, path: VfsPath, origin: PreprocVirtualOrigin }, + Unmapped(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocVirtualOrigin { + Predefines { profile: Option }, + Builtin { name: SmolStr }, + ExternalIncludeBuffer { source: PreprocSourceId }, + Expansion { expansion: SourceMacroExpansionId }, + Speculative { universe: PreprocSpeculativeUniverseId }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PreprocSpeculativeUniverseId(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocSourceMapError { + MissingSource { + source: PreprocSourceId, + }, + UnmappedSource { + source: PreprocSourceId, + reason: SourcePreprocUnavailable, + }, + RangeOutOfBounds { + source: PreprocSourceId, + range: TextRange, + mapped_range: TextRange, + text_len: usize, + }, } impl PreprocSourceMap { - pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId) { + pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId, text_len: usize) { self.entries.insert(source, PreprocSourceMapping::RealFile(file_id)); + self.text_lengths.insert(source, text_len); + self.range_offsets.insert(source, 0); + } + + pub fn insert_virtual_file( + &mut self, + source: PreprocSourceId, + file_id: FileId, + path: VfsPath, + origin: PreprocVirtualOrigin, + text_len: usize, + ) { + self.insert_virtual_file_with_offset(source, file_id, path, origin, text_len, 0); + } + + fn insert_virtual_file_with_offset( + &mut self, + source: PreprocSourceId, + file_id: FileId, + path: VfsPath, + origin: PreprocVirtualOrigin, + text_len: usize, + range_offset: usize, + ) { + self.entries.insert(source, PreprocSourceMapping::VirtualFile { file_id, path, origin }); + self.text_lengths.insert(source, text_len); + self.range_offsets.insert(source, range_offset); + } + + pub fn insert_unmapped(&mut self, source: PreprocSourceId, reason: SourcePreprocUnavailable) { + self.entries.insert(source, PreprocSourceMapping::Unmapped(reason)); + self.text_lengths.remove(&source); + self.range_offsets.remove(&source); + } + + pub fn get(&self, source: PreprocSourceId) -> Option<&PreprocSourceMapping> { + self.entries.get(&source) } - pub fn get(&self, source: PreprocSourceId) -> Option { - self.entries.get(&source).copied() + pub fn file_id(&self, source: PreprocSourceId) -> Result { + match self.get(source) { + Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), + Some(PreprocSourceMapping::VirtualFile { file_id, .. }) => Ok(*file_id), + Some(PreprocSourceMapping::Unmapped(reason)) => { + Err(PreprocSourceMapError::UnmappedSource { source, reason: reason.clone() }) + } + None => Err(PreprocSourceMapError::MissingSource { source }), + } } - pub fn file_id(&self, source: PreprocSourceId) -> Option { - match self.get(source)? { - PreprocSourceMapping::RealFile(file_id) => Some(file_id), + pub fn map_range(&self, source_range: SourceRange) -> Result { + match self.get(source_range.source) { + Some(PreprocSourceMapping::RealFile(_)) + | Some(PreprocSourceMapping::VirtualFile { .. }) => {} + Some(PreprocSourceMapping::Unmapped(reason)) => { + return Err(PreprocSourceMapError::UnmappedSource { + source: source_range.source, + reason: reason.clone(), + }); + } + None => { + return Err(PreprocSourceMapError::MissingSource { source: source_range.source }); + } } + + let range_offset = self.range_offsets.get(&source_range.source).copied().unwrap_or(0); + let mapped_range = shift_text_range(source_range.range, range_offset).ok_or( + PreprocSourceMapError::RangeOutOfBounds { + source: source_range.source, + range: source_range.range, + mapped_range: source_range.range, + text_len: usize::MAX, + }, + )?; + let text_len = self + .text_lengths + .get(&source_range.source) + .copied() + .ok_or(PreprocSourceMapError::MissingSource { source: source_range.source })?; + if usize::from(mapped_range.end()) <= text_len { + return Ok(mapped_range); + } + + Err(PreprocSourceMapError::RangeOutOfBounds { + source: source_range.source, + range: source_range.range, + mapped_range, + text_len, + }) } } @@ -200,36 +324,258 @@ fn insert_buffer_file_ids( fn source_preproc_file_ids( db: &dyn SourceRootDb, file_id: FileId, + profile_id: Option, trace: &PreprocessorTrace, + options: &SyntaxTreeOptions, ) -> Result { let mut source_map = PreprocSourceMap::default(); let path_file_ids = path_file_ids(db); - source_map.insert_real_file(PreprocSourceId::from(trace.root_buffer_id), file_id); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + source_map.insert_real_file(root_source, file_id, db.file_text(file_id).len()); + let include_buffer_texts = include_buffer_texts_by_path(options); + let predefine_sources = trace + .source_buffers + .iter() + .filter(|source| source.origin == SourceBufferOrigin::Predefine) + .map(|source| PreprocSourceId::from(source.buffer_id)) + .collect::>(); + let predefine_map = + PredefineVirtualMapping::new(db, profile_id, &options.predefines, predefine_sources); for source in &trace.source_buffers { let source_id = PreprocSourceId::from(source.buffer_id); - if source_id == PreprocSourceId::from(trace.root_buffer_id) { - source_map.insert_real_file(source_id, file_id); + if source_id == root_source { + source_map.insert_real_file(source_id, file_id, db.file_text(file_id).len()); continue; } match source.origin { SourceBufferOrigin::Source => { - let Some(mapped_file_id) = path_file_ids.get(&source.path) else { - return Err(SourcePreprocQueryError::UnmappedSource { - buffer_id: source.buffer_id, - path: source.path.clone(), - }); - }; - source_map.insert_real_file(source_id, mapped_file_id); + if let Some(mapped_file_id) = path_file_ids.get(&source.path) { + source_map.insert_real_file( + source_id, + mapped_file_id, + db.file_text(mapped_file_id).len(), + ); + continue; + } + + if let Some(text) = include_buffer_texts.get(&source.path) { + let path = + preproc_virtual_include_buffer_path(profile_id, source_id, &source.path); + let file_id = preproc_virtual_file_id(db, &path); + source_map.insert_virtual_file( + source_id, + file_id, + path, + PreprocVirtualOrigin::ExternalIncludeBuffer { source: source_id }, + text.len(), + ); + continue; + } + + source_map.insert_unmapped( + source_id, + SourcePreprocUnavailable::DetachedSource { source: source_id }, + ); + } + SourceBufferOrigin::Predefine => { + if let Some(entry) = predefine_map.entry(source_id) { + source_map.insert_virtual_file_with_offset( + source_id, + entry.file_id, + entry.path.clone(), + PreprocVirtualOrigin::Predefines { profile: profile_id }, + entry.text_len, + entry.range_offset, + ); + } else { + source_map.insert_unmapped( + source_id, + SourcePreprocUnavailable::DetachedSource { source: source_id }, + ); + } } - SourceBufferOrigin::Predefine => {} } } Ok(source_map) } +pub fn preproc_virtual_predefines_path(profile_id: Option) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/predefines.sv", + profile_path_segment(profile_id) + )) +} + +pub fn preproc_virtual_builtin_path( + profile_id: Option, + name: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/builtin/{}.sv", + profile_path_segment(profile_id), + sanitize_path_segment(name) + )) +} + +pub fn preproc_virtual_expansion_path( + profile_id: Option, + expansion: SourceMacroExpansionId, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/expansion/{}.sv", + profile_path_segment(profile_id), + expansion.raw() + )) +} + +pub fn preproc_virtual_speculative_path( + profile_id: Option, + universe: PreprocSpeculativeUniverseId, + root: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/speculative/{}/{}.sv", + profile_path_segment(profile_id), + universe.0, + sanitize_path_segment(root) + )) +} + +fn preproc_virtual_include_buffer_path( + profile_id: Option, + source_id: PreprocSourceId, + source_path: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/include-buffer/{}/{}.svh", + profile_path_segment(profile_id), + source_id.raw(), + source_basename(source_path) + )) +} + +fn profile_path_segment(profile_id: Option) -> String { + profile_id + .map(|profile_id| format!("profile-{}", profile_id.0)) + .unwrap_or_else(|| "default".to_owned()) +} + +fn source_basename(path: &str) -> String { + let name = path.rsplit(['/', '\\']).next().unwrap_or("buffer"); + let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem); + sanitize_path_segment(stem) +} + +fn sanitize_path_segment(input: &str) -> String { + let mut out = String::new(); + for ch in input.chars() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => out.push(ch), + _ => out.push('_'), + } + } + if out.is_empty() { "unnamed".to_owned() } else { out } +} + +fn include_buffer_texts_by_path(options: &SyntaxTreeOptions) -> FxHashMap { + options + .include_buffers + .iter() + .map(|buffer| (buffer.path.clone(), buffer.text.clone())) + .collect() +} + +fn materialized_predefine_text(predefine: &str) -> String { + let mut definition = predefine.to_owned(); + if let Some(index) = definition.find('=') { + definition.replace_range(index..index + 1, " "); + } else { + definition.push_str(" 1"); + } + format!("`define {definition}\n") +} + +struct PredefineVirtualMapping { + entries: FxHashMap, +} + +struct PredefineVirtualEntry { + file_id: FileId, + path: VfsPath, + text_len: usize, + range_offset: usize, +} + +impl PredefineVirtualMapping { + fn new( + db: &dyn SourceRootDb, + profile_id: Option, + predefines: &[String], + mut sources: Vec, + ) -> Self { + sources.sort_by_key(|source| source.raw()); + if sources.len() != predefines.len() || sources.is_empty() { + return Self { entries: FxHashMap::default() }; + } + + let texts = predefines + .iter() + .map(|predefine| materialized_predefine_text(predefine)) + .collect::>(); + let text_len = texts.iter().map(String::len).sum(); + let path = preproc_virtual_predefines_path(profile_id); + let file_id = preproc_virtual_file_id(db, &path); + let mut range_offset = 0usize; + let mut entries = FxHashMap::default(); + for (source, text) in sources.into_iter().zip(texts) { + entries.insert( + source, + PredefineVirtualEntry { file_id, path: path.clone(), text_len, range_offset }, + ); + range_offset += text.len(); + } + + Self { entries } + } + + fn entry(&self, source: PreprocSourceId) -> Option<&PredefineVirtualEntry> { + self.entries.get(&source) + } +} + +fn preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> FileId { + file_id_for_vfs_path(db, path).unwrap_or_else(|| synthetic_virtual_file_id(path)) +} + +fn file_id_for_vfs_path(db: &dyn SourceRootDb, path: &VfsPath) -> Option { + for file_id in db.files().iter().copied() { + let source_root_id = db.source_root_id(file_id); + let source_root = db.source_root(source_root_id); + if source_root.path_for_file(&file_id) == Some(path) { + return Some(file_id); + } + } + None +} + +fn synthetic_virtual_file_id(path: &VfsPath) -> FileId { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + FileId(0x8000_0000 | ((hasher.finish() as u32) & 0x3fff_ffff)) +} + +fn shift_text_range(range: TextRange, offset: usize) -> Option { + let start = usize::from(range.start()).checked_add(offset)?; + let end = usize::from(range.end()).checked_add(offset)?; + Some(TextRange::new( + TextSize::from(u32::try_from(start).ok()?), + TextSize::from(u32::try_from(end).ok()?), + )) +} + fn syntax_tree_options_for_file( db: &dyn SourceRootDb, file_id: FileId, @@ -476,6 +822,7 @@ fn source_preproc_model( let text = db.file_text(file_id); let identity = source_file_identity(db, file_id); + let profile_id = db.file_compilation_profile(file_id); let options = syntax_tree_options_for_file(db, file_id); let Some(trace) = SyntaxTree::preprocessor_trace(&text, &identity.name, &identity.path, &options) @@ -483,7 +830,7 @@ fn source_preproc_model( return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; - let source_map = match source_preproc_file_ids(db, file_id, &trace) { + let source_map = match source_preproc_file_ids(db, file_id, profile_id, &trace, &options) { Ok(source_map) => source_map, Err(err) => return Arc::new(Err(err)), }; @@ -716,7 +1063,7 @@ mod tests { use std::fmt; use rustc_hash::FxHashSet; - use syntax::{SourceBufferId, SourceBufferOrigin}; + use syntax::{SourceBufferId, SourceBufferOrigin, SyntaxTreeOptions}; use utils::paths::{AbsPathBuf, Utf8PathBuf}; use vfs::{FileSet, VfsPath}; @@ -760,6 +1107,12 @@ mod tests { db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); db.set_source_root_id_with_durability(TOP, ROOT, Durability::LOW); db.set_file_path_with_durability(TOP, Some(top_path), Durability::LOW); + db.set_file_kind_with_durability(TOP, SourceFileKind::SystemVerilog, Durability::LOW); + db.set_file_text_with_durability( + TOP, + Arc::from("module top; endmodule\n"), + Durability::LOW, + ); db } @@ -813,13 +1166,167 @@ mod tests { events: Vec::new(), include_edges: Vec::new(), }; + let options = SyntaxTreeOptions::default(); + let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options).unwrap(); assert_eq!( - source_preproc_file_ids(&db, TOP, &trace), - Err(SourcePreprocQueryError::UnmappedSource { - buffer_id: 2, - path: abs_path("include/missing.vh").to_string(), + source_map.get(PreprocSourceId::from(2)), + Some(&PreprocSourceMapping::Unmapped(SourcePreprocUnavailable::DetachedSource { + source: PreprocSourceId::from(2), + })) + ); + assert!(matches!( + source_map.file_id(PreprocSourceId::from(2)), + Err(PreprocSourceMapError::UnmappedSource { .. }) + )); + } + + #[test] + fn source_preproc_mapping_materializes_predefines_as_virtual_file() { + let db = db_with_root_file(); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![ + SourceBufferId { + path: abs_path("rtl/top.v").to_string(), + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: "".to_owned(), + buffer_id: 2, + origin: SourceBufferOrigin::Predefine, + }, + SourceBufferId { + path: "".to_owned(), + buffer_id: 3, + origin: SourceBufferOrigin::Predefine, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + }; + let options = SyntaxTreeOptions { + predefines: vec!["FIRST=1".to_owned(), "SECOND".to_owned()], + ..SyntaxTreeOptions::default() + }; + + let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options).unwrap(); + let first = PreprocSourceId::from(2); + let second = PreprocSourceId::from(3); + let expected_path = preproc_virtual_predefines_path(None); + let first_text = materialized_predefine_text("FIRST=1"); + + let Some(PreprocSourceMapping::VirtualFile { file_id, path, origin }) = + source_map.get(first) + else { + panic!("first predefine should map to virtual file"); + }; + assert_eq!(path, &expected_path); + assert_eq!(origin, &PreprocVirtualOrigin::Predefines { profile: None }); + + assert_eq!( + source_map.get(second), + Some(&PreprocSourceMapping::VirtualFile { + file_id: *file_id, + path: expected_path, + origin: PreprocVirtualOrigin::Predefines { profile: None }, }) ); + + let second_range = SourceRange { + source: second, + range: TextRange::new(TextSize::from(0), TextSize::from(7)), + }; + assert_eq!( + source_map.map_range(second_range).unwrap(), + TextRange::new( + TextSize::from(u32::try_from(first_text.len()).unwrap()), + TextSize::from(u32::try_from(first_text.len() + 7).unwrap()), + ) + ); + } + + #[test] + fn source_preproc_mapping_materializes_external_include_buffer_with_text() { + let db = db_with_root_file(); + let external_path = "/external/generated_defs.vh".to_owned(); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![ + SourceBufferId { + path: abs_path("rtl/top.v").to_string(), + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: external_path.clone(), + buffer_id: 4, + origin: SourceBufferOrigin::Source, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + }; + let options = SyntaxTreeOptions { + include_buffers: vec![SyntaxTreeBuffer { + path: external_path, + text: "`define FROM_BUFFER 1\n".to_owned(), + }], + ..SyntaxTreeOptions::default() + }; + + let source_map = + source_preproc_file_ids(&db, TOP, Some(CompilationProfileId(7)), &trace, &options) + .unwrap(); + let source = PreprocSourceId::from(4); + let Some(PreprocSourceMapping::VirtualFile { path, origin, .. }) = source_map.get(source) + else { + panic!("external include buffer should map to virtual file"); + }; + + assert_eq!( + path, + &VfsPath::new_virtual_path( + "/__vide/preproc/profile-7/include-buffer/4/generated_defs.svh".to_owned() + ) + ); + assert_eq!(origin, &PreprocVirtualOrigin::ExternalIncludeBuffer { source }); + assert!(matches!( + source_map.map_range(SourceRange { + source, + range: TextRange::new(TextSize::from(0), TextSize::from(128)), + }), + Err(PreprocSourceMapError::RangeOutOfBounds { .. }) + )); + } + + #[test] + fn preproc_virtual_paths_use_reserved_namespace() { + assert_eq!( + preproc_virtual_predefines_path(None), + VfsPath::new_virtual_path("/__vide/preproc/default/predefines.sv".to_owned()) + ); + assert_eq!( + preproc_virtual_builtin_path(Some(CompilationProfileId(3)), "bad/name"), + VfsPath::new_virtual_path("/__vide/preproc/profile-3/builtin/bad_name.sv".to_owned()) + ); + assert_eq!( + preproc_virtual_expansion_path( + Some(CompilationProfileId(3)), + SourceMacroExpansionId::new(9), + ), + VfsPath::new_virtual_path("/__vide/preproc/profile-3/expansion/9.sv".to_owned()) + ); + assert_eq!( + preproc_virtual_speculative_path( + Some(CompilationProfileId(3)), + PreprocSpeculativeUniverseId(11), + "root/top", + ), + VfsPath::new_virtual_path( + "/__vide/preproc/profile-3/speculative/11/root_top.sv".to_owned() + ) + ); } } diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index c5500756..7762e3d4 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -19,7 +19,10 @@ use vfs::FileId; use crate::base_db::{ project::CompilationProfileId, - source_db::{MappedSourcePreprocModel, SourceFileKind, SourcePreprocQueryError, SourceRootDb}, + source_db::{ + MappedSourcePreprocModel, PreprocSourceMapError, PreprocSourceMapping, + PreprocVirtualOrigin, SourceFileKind, SourcePreprocQueryError, SourceRootDb, + }, }; pub type PreprocResult = Result; @@ -44,6 +47,7 @@ pub enum PreprocError { MissingDefinitionNameRange { event_id: u32, }, + SourceMap(PreprocSourceMapError), Unavailable { reason: PreprocUnavailable, }, @@ -90,15 +94,16 @@ impl MacroDefinitionId { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum MappedPreprocSource { RealFile { file_id: FileId }, + VirtualFile { file_id: FileId, path: vfs::VfsPath, origin: PreprocVirtualOrigin }, } impl MappedPreprocSource { - pub fn file_id(self) -> FileId { + pub fn file_id(&self) -> FileId { match self { - Self::RealFile { file_id } => file_id, + Self::RealFile { file_id } | Self::VirtualFile { file_id, .. } => *file_id, } } } @@ -738,25 +743,41 @@ fn map_source_id( mapped: &MappedSourcePreprocModel, source: PreprocSourceId, ) -> PreprocResult { - mapped - .source_map - .file_id(source) - .ok_or_else(|| PreprocError::UnmappedSource { buffer_id: source.raw() }) + mapped.source_map.file_id(source).map_err(PreprocError::SourceMap) } fn map_mapped_source_range( mapped: &MappedSourcePreprocModel, source_range: SourceRange, ) -> PreprocResult<(MappedPreprocSource, TextRange)> { + let range = mapped.source_map.map_range(source_range).map_err(PreprocError::SourceMap)?; let source = map_mapped_source_id(mapped, source_range.source)?; - Ok((source, source_range.range)) + Ok((source, range)) } fn map_mapped_source_id( mapped: &MappedSourcePreprocModel, source: PreprocSourceId, ) -> PreprocResult { - Ok(MappedPreprocSource::RealFile { file_id: map_source_id(mapped, source)? }) + match mapped.source_map.get(source) { + Some(PreprocSourceMapping::RealFile(file_id)) => { + Ok(MappedPreprocSource::RealFile { file_id: *file_id }) + } + Some(PreprocSourceMapping::VirtualFile { file_id, path, origin }) => { + Ok(MappedPreprocSource::VirtualFile { + file_id: *file_id, + path: path.clone(), + origin: origin.clone(), + }) + } + Some(PreprocSourceMapping::Unmapped(reason)) => { + Err(PreprocError::SourceMap(PreprocSourceMapError::UnmappedSource { + source, + reason: reason.clone(), + })) + } + None => Err(PreprocError::SourceMap(PreprocSourceMapError::MissingSource { source })), + } } fn map_macro_definition( @@ -771,9 +792,9 @@ fn map_macro_definition( )?; Ok(MacroDefinition { id: definition.id.into(), + file_id: source.file_id(), source, capability: capability_status(&mapped.model.capabilities().definition_name_ranges), - file_id: source.file_id(), name: definition.name.clone(), define_index: define_index_for_definition(mapped, definition)?, event_id: definition.event_id.raw(), @@ -805,9 +826,9 @@ fn map_macro_reference( let (source, directive_range, name_range) = map_reference_ranges(mapped, reference)?; Ok(MacroReference { id: reference.id.into(), + file_id: source.file_id(), source, capability: capability_status(&mapped.model.capabilities().macro_reference_resolution), - file_id: source.file_id(), name: reference.name.clone(), directive_range, range: name_range, From 159629d530d381f91d91cc2a2f3a5bf4e15bd470 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 19:34:00 +0800 Subject: [PATCH 07/80] refactor(preproc): remove replay-backed semantic queries --- crates/hir/src/preproc.rs | 23 +- crates/preproc/src/source.rs | 2 - crates/preproc/src/source/model.rs | 272 ++++++++++-------------- crates/preproc/src/source/provenance.rs | 111 +++++----- crates/preproc/src/source/references.rs | 169 --------------- crates/preproc/src/source/types.rs | 50 ----- 6 files changed, 172 insertions(+), 455 deletions(-) delete mode 100644 crates/preproc/src/source/references.rs diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 7762e3d4..84474409 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use preproc::source::{ CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceIncludeChainEntry, - SourceIncludeDirectiveId, SourceIncludeStatus, SourceMacroBinding, + SourceIncludeDirectiveId, SourceIncludeStatus, SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, SourceMacroReference as SourceMacroReferenceFact, SourceMacroReferenceId, SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, @@ -374,7 +374,7 @@ pub fn visible_macros_at( .model .visible_macros_at(position) .into_iter() - .filter_map(|binding| map_binding_definition(mapped, binding).transpose()) + .map(|definition| map_macro_definition(mapped, definition)) .collect() } @@ -388,8 +388,8 @@ pub fn visible_macro_names_at( let position = root_position(mapped, offset)?; let mut names = UniqVec::::default(); - for binding in mapped.model.visible_macros_at(position) { - names.push_unique(binding.name); + for definition in mapped.model.visible_macros_at(position) { + names.push_unique(definition.name.clone()); } for name in configured_predefine_names(db, file_id) { names.push_unique(name); @@ -966,21 +966,6 @@ fn map_include_chain( .collect() } -fn map_binding_definition( - mapped: &MappedSourcePreprocModel, - binding: SourceMacroBinding<'_>, -) -> PreprocResult> { - let Some(definition) = mapped - .model - .macro_definitions() - .iter() - .find(|definition| definition.event_id == binding.event_id) - else { - return Ok(None); - }; - Ok(Some(map_macro_definition(mapped, definition)?)) -} - fn push_unique_macro_reference(refs: &mut Vec, reference: MacroReference) { if refs.iter().any(|existing| { existing.file_id == reference.file_id diff --git a/crates/preproc/src/source.rs b/crates/preproc/src/source.rs index 067ef442..dec845a7 100644 --- a/crates/preproc/src/source.rs +++ b/crates/preproc/src/source.rs @@ -1,9 +1,7 @@ mod model; mod provenance; -mod references; mod trace; mod types; pub use provenance::*; -pub use references::SourceMacroReferenceResolution; pub use types::*; diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 45922072..29687b73 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -1,6 +1,3 @@ -use std::collections::BTreeMap; - -use smol_str::SmolStr; use syntax::PreprocessorTrace; use super::{provenance::*, types::*}; @@ -104,11 +101,11 @@ impl SourcePreprocModel { .filter_map(|(source_order, directive)| self.event_from_record(source_order, directive)) } - pub fn visible_macros_at(&self, position: SourcePosition) -> Vec> { + pub fn visible_macros_at(&self, position: SourcePosition) -> Vec<&SourceMacroDefinition> { self.tables .state_timeline .state_at_position(position) - .map(|state| self.bindings_for_state(state)) + .map(|state| self.definitions_for_state(state)) .unwrap_or_default() } @@ -162,101 +159,14 @@ impl SourcePreprocModel { self.index.conditionals.get(index) } - pub fn include_chain_for_source( - &self, - source: PreprocSourceId, - ) -> Result, SourcePreprocError> { - let mut chain = Vec::new(); - let mut current = source; - let mut visited = BTreeMap::new(); - - loop { - if visited.insert(current, ()).is_some() { - return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); - } - - let Some(source) = self.index.sources.iter().find(|candidate| candidate.id == current) - else { - return Err(SourcePreprocError::MissingIncludedSource { - include_event_id: 0, - source: current.raw(), - }); - }; - - match source.origin { - PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, - PreprocSourceOrigin::Detached => { - return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); - } - PreprocSourceOrigin::Included { include_event_id } => { - let directive = self.event_record_by_event_id(include_event_id).ok_or( - SourcePreprocError::MissingIncludeEvent { - include_event_id: include_event_id.raw(), - }, - )?; - if directive.kind != MacroEventKind::Include { - return Err(SourcePreprocError::IncludeEdgeNotInclude { - include_event_id: include_event_id.raw(), - }); - } - chain.push(SourceIncludeChainEntry { - include_event_id, - include_range: directive.range, - included_source: current, - }); - current = directive.range.source; - } - } - } - - chain.reverse(); - Ok(chain) - } - - fn event_record_by_event_id( - &self, - event_id: SourcePreprocEventId, - ) -> Option<&SourcePreprocEventRecord> { - self.index.event_records.iter().find(|directive| directive.event_id == event_id) - } - - fn bindings_for_state(&self, state: &SourceMacroState) -> Vec> { + fn definitions_for_state(&self, state: &SourceMacroState) -> Vec<&SourceMacroDefinition> { state .definitions - .iter() - .filter_map(|(name, definition_id)| { - let define_index = self.define_index_for_definition_id(*definition_id)?; - self.binding_for_define_index(name.clone(), define_index) - }) + .values() + .filter_map(|definition_id| self.tables.macro_definitions.get(*definition_id)) .collect() } - pub(super) fn binding_for_define_index( - &self, - name: SmolStr, - define_index: usize, - ) -> Option> { - let define = self.index.defines.get(define_index)?; - Some(SourceMacroBinding { name, event_id: define.event_id, define_index, define }) - } - - pub(super) fn binding_for_definition_id( - &self, - definition_id: SourceMacroDefinitionId, - ) -> Option> { - let definition = self.tables.macro_definitions.get(definition_id)?; - let define_index = self.define_index_for_definition_id(definition_id)?; - self.binding_for_define_index(definition.name.clone(), define_index) - } - - fn define_index_for_definition_id( - &self, - definition_id: SourceMacroDefinitionId, - ) -> Option { - let definition = self.tables.macro_definitions.get(definition_id)?; - self.index.defines.iter().position(|define| define.event_id == definition.event_id) - } - fn event_from_record( &self, source_order: usize, @@ -323,6 +233,7 @@ impl SourcePreprocModel { #[cfg(test)] mod tests { + use smol_str::SmolStr; use syntax::{ PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceToken, SourceBufferId, SourceBufferOrigin, SourceBufferRange, SyntaxKind, @@ -402,20 +313,62 @@ mod tests { model .visible_macros_at(SourcePosition { source, offset }) .into_iter() - .map(|binding| binding.name) + .map(|definition| definition.name.clone()) .collect() } - fn visible_macro_binding<'a>( + fn visible_macro_definition<'a>( model: &'a SourcePreprocModel, source: PreprocSourceId, offset: TextSize, name: &str, - ) -> Option> { + ) -> Option<&'a SourceMacroDefinition> { model .visible_macros_at(SourcePosition { source, offset }) .into_iter() - .find(|binding| binding.name == name) + .find(|definition| definition.name == name) + } + + fn reference_for_usage( + model: &SourcePreprocModel, + usage_index: usize, + ) -> &SourceMacroReference { + model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::Usage { + usage_index: site_usage_index, + } if site_usage_index == usage_index + ) + }) + .expect("usage reference should be in resolved reference table") + } + + fn reference_for_conditional_token( + model: &SourcePreprocModel, + conditional_index: usize, + token_index: usize, + ) -> &SourceMacroReference { + model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::ConditionalToken { + conditional_index: site_conditional_index, + token_index: site_token_index, + } | SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: site_conditional_index, + token_index: site_token_index, + } if site_conditional_index == conditional_index + && site_token_index == token_index + ) + }) + .expect("conditional token reference should be in resolved reference table") } #[test] @@ -432,24 +385,24 @@ logic [`HEADER_WIDTH-1:0] data; .any(|name| name == "HEADER_WIDTH") ); - let after_include = visible_macro_binding( + let after_include = visible_macro_definition( &model, root_source, offset_after(root_text, "`include \"defs.vh\"\n"), "HEADER_WIDTH", ) .unwrap(); - assert_eq!(after_include.define_index, 0); + assert_eq!(after_include.id.raw(), 0); - let binding = model + let definition = model .visible_macros_at(SourcePosition { source: root_source, offset: offset_after(root_text, "`include \"defs.vh\"\n"), }) .into_iter() - .find(|binding| binding.name == "HEADER_WIDTH") + .find(|definition| definition.name == "HEADER_WIDTH") .unwrap(); - assert_eq!(binding.define.name_range.unwrap().source, header_source); + assert_eq!(definition.name_range.source, header_source); } #[test] @@ -461,18 +414,18 @@ logic [`HEADER_WIDTH-1:0] data; let header_text = "`define HEADER_WIDTH 8\n"; let (model, root_source, header_source) = source_model(root_text, header_text); - let after_include = visible_macro_binding( + let after_include = visible_macro_definition( &model, root_source, offset_after(root_text, "`include \"defs.vh\"\n"), "HEADER_WIDTH", ) .unwrap(); - assert_eq!(after_include.define_index, 0); + assert_eq!(after_include.id.raw(), 0); assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); assert!( - visible_macro_binding( + visible_macro_definition( &model, root_source, offset_after(root_text, "`undef HEADER_WIDTH\n"), @@ -496,25 +449,25 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); assert_eq!(model.defines()[1].name_range.unwrap().source, root_source); - let after_override = visible_macro_binding( + let after_override = visible_macro_definition( &model, root_source, offset_after(root_text, "`define HEADER_WIDTH 16\n"), "HEADER_WIDTH", ) .unwrap(); - assert_eq!(after_override.define_index, 1); + assert_eq!(after_override.id.raw(), 1); - let binding = model + let definition = model .visible_macros_at(SourcePosition { source: root_source, offset: offset_after(root_text, "`define HEADER_WIDTH 16\n"), }) .into_iter() - .find(|binding| binding.name == "HEADER_WIDTH") + .find(|definition| definition.name == "HEADER_WIDTH") .unwrap(); - assert_eq!(binding.define.body[0].value.as_str(), "16"); - assert_eq!(binding.define.name_range.unwrap().source, root_source); + assert_eq!(definition.body_tokens[0].value.as_str(), "16"); + assert_eq!(definition.name_range.source, root_source); } #[test] @@ -600,14 +553,20 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(usage.range.source, root_source); assert_eq!(usage.name_range.unwrap().source, root_source); - let resolution = model.definition_for_usage(usage_index).unwrap().unwrap(); - assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); - assert_eq!(resolution.definition.define.name_range.unwrap().source, header_source); - assert_eq!(resolution.definition.define.body[0].value.as_str(), "8"); - assert_eq!(resolution.definition_provenance.event_id, resolution.definition.event_id); - assert_eq!(resolution.definition_include_chain.len(), 1); - assert_eq!(resolution.definition_include_chain[0].include_range.source, root_source); - assert_eq!(resolution.definition_include_chain[0].included_source, header_source); + let reference = reference_for_usage(&model, usage_index); + let SourceMacroResolution::Resolved { definition, include_chain, reason } = + &reference.resolution + else { + panic!("usage reference should resolve to included definition"); + }; + assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); + let definition = model.macro_definitions().get(*definition).unwrap(); + assert_eq!(definition.name.as_str(), "HEADER_WIDTH"); + assert_eq!(definition.name_range.source, header_source); + assert_eq!(definition.body_tokens[0].value.as_str(), "8"); + assert_eq!(include_chain.len(), 1); + assert_eq!(include_chain[0].include_range.source, root_source); + assert_eq!(include_chain[0].included_source, header_source); } #[test] @@ -700,23 +659,19 @@ wire active; .iter() .position(|conditional| conditional.kind == MacroConditionalKind::IfDef) .expect("ifdef should be traced"); - let binding = model.definition_for_conditional_token(conditional_index, 0).unwrap(); - - assert_eq!(binding.name.as_str(), "HEADER_FLAG"); - assert_eq!(binding.define.name_range.unwrap().source, header_source); + let reference = reference_for_conditional_token(&model, conditional_index, 0); - let references = model.resolved_macro_references().unwrap(); - assert!(references.iter().any(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::ConditionalToken { - conditional_index: site_conditional_index, - token_index: 0, - } if site_conditional_index == conditional_index - ) && reference.name.as_str() == "HEADER_FLAG" - && reference.range.source == root_source - && reference.definition.define.name_range.unwrap().source == header_source - })); + assert_eq!(reference.name.as_str(), "HEADER_FLAG"); + assert_eq!(reference.name_range.source, root_source); + let SourceMacroResolution::Resolved { definition, reason, .. } = reference.resolution + else { + panic!("conditional token reference should resolve to visible definition"); + }; + assert_eq!(reason, SourceMacroResolutionReason::VisibleDefinition); + assert_eq!( + model.macro_definitions().get(definition).unwrap().name_range.source, + header_source + ); } #[test] @@ -740,23 +695,6 @@ wire active; && conditional.range.source == header_source }) .expect("ifndef guard should be traced"); - let binding = model.definition_for_conditional_token(conditional_index, 0).unwrap(); - - assert_eq!(binding.name.as_str(), "HEADER_FLAG"); - assert_eq!(binding.define.name_range.unwrap().source, header_source); - - let references = model.resolved_macro_references().unwrap(); - assert!(references.iter().any(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::IncludeGuardIfNDef { - conditional_index: site_conditional_index, - token_index: 0, - } if site_conditional_index == conditional_index - ) && reference.name.as_str() == "HEADER_FLAG" - && reference.range.source == header_source - && reference.definition.define.name_range.unwrap().source == header_source - })); let reference = model .macro_references() .iter() @@ -770,6 +708,8 @@ wire active; ) }) .expect("include guard token should be modeled as a resolved reference"); + assert_eq!(reference.name.as_str(), "HEADER_FLAG"); + assert_eq!(reference.name_range.source, header_source); assert!(matches!( reference.resolution, SourceMacroResolution::Resolved { @@ -810,14 +750,22 @@ logic [`LEAF_WIDTH-1:0] data; .iter() .position(|usage| usage.name.as_deref() == Some("LEAF_WIDTH")) .expect("root macro usage should be traced"); - let resolution = model.definition_for_usage(usage_index).unwrap().unwrap(); - - assert_eq!(resolution.definition.define.name_range.unwrap().source, leaf_source); - assert_eq!(resolution.definition_include_chain.len(), 2); - assert_eq!(resolution.definition_include_chain[0].include_range.source, root_source); - assert_eq!(resolution.definition_include_chain[0].included_source, header_source); - assert_eq!(resolution.definition_include_chain[1].include_range.source, header_source); - assert_eq!(resolution.definition_include_chain[1].included_source, leaf_source); + let reference = reference_for_usage(&model, usage_index); + let SourceMacroResolution::Resolved { definition, include_chain, .. } = + &reference.resolution + else { + panic!("usage reference should resolve to nested included definition"); + }; + + assert_eq!( + model.macro_definitions().get(*definition).unwrap().name_range.source, + leaf_source + ); + assert_eq!(include_chain.len(), 2); + assert_eq!(include_chain[0].include_range.source, root_source); + assert_eq!(include_chain[0].included_source, header_source); + assert_eq!(include_chain[1].include_range.source, header_source); + assert_eq!(include_chain[1].included_source, leaf_source); } #[test] diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 853a8d6c..8e48f020 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -942,7 +942,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { .expect("definition id should point at inserted definition") .directive_range .source; - match include_chain_for_source(self.index, definition_source) { + match self.include_chain_for_source(definition_source) { Ok(include_chain) => { SourceMacroResolution::Resolved { definition, reason, include_chain } } @@ -975,6 +975,63 @@ impl<'a> SourcePreprocModelBuilder<'a> { }) } + fn include_chain_for_source( + &self, + source: PreprocSourceId, + ) -> Result, SourcePreprocError> { + let mut chain = Vec::new(); + let mut current = source; + let mut visited = BTreeMap::new(); + + loop { + if visited.insert(current, ()).is_some() { + return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); + } + + let Some(source) = self.index.sources.iter().find(|candidate| candidate.id == current) + else { + return Err(SourcePreprocError::MissingIncludedSource { + include_event_id: 0, + source: current.raw(), + }); + }; + + match source.origin { + PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, + PreprocSourceOrigin::Detached => { + return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); + } + PreprocSourceOrigin::Included { .. } => { + let edge = self + .tables + .include_graph + .edges() + .iter() + .find(|edge| edge.included_source == current) + .ok_or(SourcePreprocError::MissingIncludeEdge { source: current.raw() })?; + let directive = self + .tables + .include_graph + .directives() + .iter() + .find(|directive| directive.event_id == edge.include_event_id) + .ok_or(SourcePreprocError::MissingIncludeEvent { + include_event_id: edge.include_event_id.raw(), + })?; + chain.push(SourceIncludeChainEntry { + include_event_id: edge.include_event_id, + include_range: directive.directive_range, + included_source: current, + }); + current = directive.directive_range.source; + } + } + } + + chain.reverse(); + Ok(chain) + } + fn include_guard_definition_after_ifndef( &self, conditional_index: usize, @@ -1059,55 +1116,3 @@ fn boundary_after(directive_range: SourceRange) -> SourcePosition { fn partial_status(is_partial: bool) -> CapabilityStatus { if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } } - -fn include_chain_for_source( - index: &SourcePreprocIndex, - source: PreprocSourceId, -) -> Result, SourcePreprocError> { - let mut chain = Vec::new(); - let mut current = source; - let mut visited = BTreeMap::new(); - - loop { - if visited.insert(current, ()).is_some() { - return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); - } - - let Some(source) = index.sources.iter().find(|candidate| candidate.id == current) else { - return Err(SourcePreprocError::MissingIncludedSource { - include_event_id: 0, - source: current.raw(), - }); - }; - - match source.origin { - PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, - PreprocSourceOrigin::Detached => { - return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); - } - PreprocSourceOrigin::Included { include_event_id } => { - let directive = index - .event_records - .iter() - .find(|directive| directive.event_id == include_event_id) - .ok_or(SourcePreprocError::MissingIncludeEvent { - include_event_id: include_event_id.raw(), - })?; - if directive.kind != MacroEventKind::Include { - return Err(SourcePreprocError::IncludeEdgeNotInclude { - include_event_id: include_event_id.raw(), - }); - } - chain.push(SourceIncludeChainEntry { - include_event_id, - include_range: directive.range, - included_source: current, - }); - current = directive.range.source; - } - } - } - - chain.reverse(); - Ok(chain) -} diff --git a/crates/preproc/src/source/references.rs b/crates/preproc/src/source/references.rs deleted file mode 100644 index 2418bb56..00000000 --- a/crates/preproc/src/source/references.rs +++ /dev/null @@ -1,169 +0,0 @@ -use smol_str::SmolStr; - -use super::*; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceMacroReferenceResolution<'a> { - pub site: SourceMacroReferenceSite, - pub name: SmolStr, - pub range: SourceRange, - pub definition: SourceMacroBinding<'a>, - pub definition_provenance: SourcePreprocProvenance, - pub definition_include_chain: Vec, -} - -impl SourcePreprocModel { - pub fn definition_for_usage( - &self, - usage_index: usize, - ) -> Result>, SourcePreprocError> { - let Some(usage) = self.index.usages.get(usage_index) else { - return Ok(None); - }; - let Some(reference) = self.reference_for_usage(usage_index) else { - return Ok(None); - }; - let SourceMacroResolution::Resolved { definition, include_chain, .. } = - &reference.resolution - else { - return unavailable_reference_result(&reference.resolution); - }; - let Some(definition) = self.binding_for_definition_id(*definition) else { - return Ok(None); - }; - let definition_provenance = self - .provenance(SourcePreprocEntity::Define(definition.define_index)) - .ok_or(SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() })?; - Ok(Some(SourceMacroUsageResolution { - usage_index, - usage, - definition, - definition_provenance, - definition_include_chain: include_chain.clone(), - })) - } - - pub fn definition_for_conditional_token( - &self, - conditional_index: usize, - token_index: usize, - ) -> Option> { - let reference = self.reference_for_conditional_token(conditional_index, token_index)?; - let SourceMacroResolution::Resolved { definition, .. } = reference.resolution else { - return None; - }; - self.binding_for_definition_id(definition) - } - - pub fn resolved_macro_references( - &self, - ) -> Result>, SourcePreprocError> { - let mut references = Vec::new(); - - for reference in self.tables.macro_references.iter() { - let SourceMacroResolution::Resolved { definition, include_chain, .. } = - &reference.resolution - else { - if let Some(error) = source_error_for_unavailable_resolution(&reference.resolution) - { - return Err(error); - } - continue; - }; - let Some(definition) = self.binding_for_definition_id(*definition) else { - continue; - }; - let definition_provenance = self - .provenance(SourcePreprocEntity::Define(definition.define_index)) - .ok_or(SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() })?; - references.push(SourceMacroReferenceResolution { - site: reference.site, - name: reference.name.clone(), - range: reference.name_range, - definition, - definition_provenance, - definition_include_chain: include_chain.clone(), - }); - } - - Ok(references) - } - - fn reference_for_usage(&self, usage_index: usize) -> Option<&SourceMacroReference> { - self.tables.macro_references.iter().find(|reference| { - matches!(reference.site, SourceMacroReferenceSite::Usage { - usage_index: site_usage_index, - } if site_usage_index == usage_index) - }) - } - - fn reference_for_conditional_token( - &self, - conditional_index: usize, - token_index: usize, - ) -> Option<&SourceMacroReference> { - self.tables.macro_references.iter().find(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::ConditionalToken { - conditional_index: site_conditional_index, - token_index: site_token_index, - } | SourceMacroReferenceSite::IncludeGuardIfNDef { - conditional_index: site_conditional_index, - token_index: site_token_index, - } if site_conditional_index == conditional_index && site_token_index == token_index - ) - }) - } -} - -fn unavailable_reference_result<'a>( - resolution: &SourceMacroResolution, -) -> Result>, SourcePreprocError> { - if let Some(error) = source_error_for_unavailable_resolution(resolution) { - return Err(error); - } - - Ok(None) -} - -fn source_error_for_unavailable_resolution( - resolution: &SourceMacroResolution, -) -> Option { - let SourceMacroResolution::Unavailable(unavailable) = resolution else { - return None; - }; - - match unavailable { - SourcePreprocUnavailable::MissingIncludedSource { include_event_id, source } => { - Some(SourcePreprocError::MissingIncludedSource { - include_event_id: include_event_id.raw(), - source: source.raw(), - }) - } - SourcePreprocUnavailable::MissingIncludeEvent { include_event_id } => { - Some(SourcePreprocError::MissingIncludeEvent { - include_event_id: include_event_id.raw(), - }) - } - SourcePreprocUnavailable::IncludeEdgeNotInclude { include_event_id } => { - Some(SourcePreprocError::IncludeEdgeNotInclude { - include_event_id: include_event_id.raw(), - }) - } - SourcePreprocUnavailable::IncludeChainUnavailable { source } - | SourcePreprocUnavailable::DetachedSource { source } => { - Some(SourcePreprocError::MissingIncludeEdge { source: source.raw() }) - } - SourcePreprocUnavailable::MissingDefinitionName { event_id } - | SourcePreprocUnavailable::MissingDefinitionNameRange { event_id } - | SourcePreprocUnavailable::MissingReferenceName { event_id } - | SourcePreprocUnavailable::MissingReferenceNameRange { event_id } => { - Some(SourcePreprocError::MissingEvent { event_id: event_id.raw() }) - } - SourcePreprocUnavailable::MacroCallAuthorityUnavailable - | SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable - | SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable - | SourcePreprocUnavailable::ExpansionAuthorityUnavailable => None, - } -} diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index c61ab863..7637cedc 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use smol_str::SmolStr; use utils::line_index::{TextRange, TextSize}; @@ -161,28 +159,6 @@ pub struct SourcePreprocModel { pub(super) tables: SourcePreprocTables, } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroEnvironment { - pub(super) definitions: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceMacroBinding<'a> { - pub name: SmolStr, - pub event_id: SourcePreprocEventId, - pub define_index: usize, - pub define: &'a SourceMacroDefine, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceMacroUsageResolution<'a> { - pub usage_index: usize, - pub usage: &'a SourceMacroUsage, - pub definition: SourceMacroBinding<'a>, - pub definition_provenance: SourcePreprocProvenance, - pub definition_include_chain: Vec, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SourcePreprocEntity { Define(usize), @@ -275,32 +251,6 @@ impl From for PreprocSourceId { } } -impl SourceMacroEnvironment { - pub fn define_index(&self, name: &str) -> Option { - self.definitions.get(name).copied() - } - - pub fn contains(&self, name: &str) -> bool { - self.definitions.contains_key(name) - } - - pub fn len(&self) -> usize { - self.definitions.len() - } - - pub fn is_empty(&self) -> bool { - self.definitions.is_empty() - } - - pub fn names(&self) -> impl Iterator { - self.definitions.keys() - } - - pub fn definitions(&self) -> &BTreeMap { - &self.definitions - } -} - impl SourcePreprocEvent<'_> { pub fn event_id(&self) -> SourcePreprocEventId { match self { From 53f018986c5478dfd3ce7fcd869206f167358f0c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 20:15:10 +0800 Subject: [PATCH 08/80] feat(preproc): ingest emitted token provenance --- crates/hir/src/base_db/source_db.rs | 3 + crates/preproc/src/source/model.rs | 159 ++++++++++- crates/preproc/src/source/provenance.rs | 268 ++++++++++++++++-- crates/preproc/src/source/trace.rs | 72 ++++- crates/preproc/src/source/types.rs | 38 +++ crates/slang/bindings/rust/ffi.rs | 15 + crates/slang/bindings/rust/ffi/wrapper.cc | 132 +++++++++ crates/slang/bindings/rust/lib.rs | 110 +++++++ crates/slang/bindings/rust/tests.rs | 133 +++++++++ .../slang/include/slang/text/SourceManager.h | 23 +- .../source/parsing/Preprocessor_macros.cpp | 37 ++- crates/slang/source/text/SourceManager.cpp | 25 +- 12 files changed, 975 insertions(+), 40 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index c827587c..cbf0f7ea 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1165,6 +1165,7 @@ mod tests { ], events: Vec::new(), include_edges: Vec::new(), + emitted_tokens: Vec::new(), }; let options = SyntaxTreeOptions::default(); let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options).unwrap(); @@ -1205,6 +1206,7 @@ mod tests { ], events: Vec::new(), include_edges: Vec::new(), + emitted_tokens: Vec::new(), }; let options = SyntaxTreeOptions { predefines: vec!["FIRST=1".to_owned(), "SECOND".to_owned()], @@ -1267,6 +1269,7 @@ mod tests { ], events: Vec::new(), include_edges: Vec::new(), + emitted_tokens: Vec::new(), }; let options = SyntaxTreeOptions { include_buffers: vec![SyntaxTreeBuffer { diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 29687b73..c36085b8 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -237,7 +237,7 @@ mod tests { use syntax::{ PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceToken, SourceBufferId, SourceBufferOrigin, SourceBufferRange, SyntaxKind, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions, + SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions, TokenKind, }; use utils::line_index::{TextRange, TextSize}; @@ -293,6 +293,17 @@ mod tests { .id } + fn source_model_from_root( + root_text: &str, + options: SyntaxTreeOptions, + ) -> (SourcePreprocModel, PreprocSourceId) { + let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let model = SourcePreprocModel::from_trace(trace).unwrap(); + (model, root_source) + } + fn offset_before(text: &str, needle: &str) -> TextSize { TextSize::from(u32::try_from(text.find(needle).unwrap()).unwrap()) } @@ -618,30 +629,152 @@ logic [`HEADER_WIDTH-1:0] data; SourceIncludeStatus::Resolved { source } if *source == header_source )); assert!(!model.state_timeline().checkpoints().is_empty()); - assert!(model.macro_calls().is_empty()); + + let call = model + .macro_calls() + .iter() + .find(|call| call.reference == reference.id) + .expect("macro usage should create a call fact"); + assert_eq!(call.call_range.source, root_source); + assert!(matches!( + call.status, + SourceMacroCallStatus::ExpansionUnavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable + ) + )); + assert!(model.macro_expansions().is_empty()); - assert!(model.emitted_tokens().is_empty()); - assert!(model.token_provenance().is_empty()); + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "8") + .expect("macro body token should be emitted by adapter authority"); + let provenance = model.token_provenance().get(emitted.provenance).unwrap(); assert!(matches!( - &model.capabilities().macro_calls, - CapabilityStatus::Unavailable(SourcePreprocUnavailable::MacroCallAuthorityUnavailable) + provenance, + SourceTokenProvenance::MacroBody { + definition: body_definition, + body_token_range, + call: body_call, + } if *body_definition == *resolved_definition + && body_token_range.source == header_source + && *body_call == call.id )); + assert_eq!(model.capabilities().macro_calls, CapabilityStatus::Complete); assert!(matches!( &model.capabilities().macro_expansions, CapabilityStatus::Unavailable(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) )); + assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Complete); + } + + #[test] + fn source_model_maps_function_macro_argument_emitted_token_to_argument() { + let root_text = r#"`define ID(x) x +module m; +localparam int W = `ID(7); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "7") + .expect("argument replacement token should be emitted"); + let SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("argument replacement should map to MacroArgument provenance"); + }; + assert_eq!(*argument_index, 0); + assert_eq!(argument_token_range.source, root_source); + assert_eq!(text_at_range(root_text, argument_token_range.range), "7"); + + let call = model.macro_calls().get(*call).expect("call id should resolve"); + assert_eq!(call.call_range.source, root_source); + assert_eq!(text_at_range(root_text, call.call_range.range), "`ID(7)"); + } + + #[test] + fn source_model_marks_unsupported_macro_ops_unavailable_without_dropping_tokens() { + let root_text = r#"`define JOIN(a,b) a``b +`define STR(x) `"x`" +module m; +wire `JOIN(foo,bar); +string s = `STR(foo); +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let pasted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "foobar") + .expect("token paste result should not be dropped"); assert!(matches!( - &model.capabilities().emitted_tokens, - CapabilityStatus::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + model.token_provenance().get(pasted.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance ) )); + + let stringified = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "\"foo\"") + .expect("stringification result should not be dropped"); assert!(matches!( - &model.capabilities().emitted_token_provenance, - CapabilityStatus::Unavailable( - SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable + model.token_provenance().get(stringified.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance ) )); + assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); + } + + #[test] + fn source_model_maps_predefine_and_builtin_emitted_token_provenance() { + let root_text = r#"module m; +localparam int P = `FROM_API; +localparam int L = `__LINE__; +endmodule +"#; + let (model, _root_source) = source_model_from_root( + root_text, + SyntaxTreeOptions { + predefines: vec!["FROM_API=11".to_owned()], + ..SyntaxTreeOptions::default() + }, + ); + + let predefine = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "11") + .expect("predefine expansion token should be emitted"); + let SourceTokenProvenance::Predefine { source } = + model.token_provenance().get(predefine.provenance).unwrap() + else { + panic!("configured predefine token should map to Predefine provenance"); + }; + assert!(model.sources().iter().any(|candidate| { + candidate.id == *source && candidate.origin == PreprocSourceOrigin::Predefine + })); + + let builtin = model + .emitted_tokens() + .iter() + .find(|token| { + matches!( + model.token_provenance().get(token.provenance), + Some(SourceTokenProvenance::Builtin { name }) if name == "__LINE__" + ) + }) + .expect("builtin macro token should be emitted"); + assert!(!builtin.text.is_empty()); } #[test] @@ -785,6 +918,7 @@ logic [`LEAF_WIDTH-1:0] data; name: Some(PreprocessorTraceToken { raw_text: "WIDTH".to_owned(), value_text: "WIDTH".to_owned(), + token_kind: TokenKind::IDENTIFIER, range: Some(SourceBufferRange { buffer_id: 1, range: 8..13 }), }), include_file_name: None, @@ -794,6 +928,7 @@ logic [`LEAF_WIDTH-1:0] data; disabled_ranges: Vec::new(), }], include_edges: Vec::new(), + emitted_tokens: Vec::new(), }; assert_eq!( diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 8e48f020..3b2d86c5 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use smol_str::SmolStr; +use utils::line_index::TextSize; use super::types::*; @@ -135,6 +136,20 @@ struct SourceMacroStatePositionBoundary { boundary: SourcePosition, } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct SourceMacroCallSignature { + source: PreprocSourceId, + start: TextSize, + end: TextSize, + name: SmolStr, +} + +impl SourceMacroCallSignature { + fn new(name: SmolStr, range: SourceRange) -> Self { + Self { source: range.source, start: range.range.start(), end: range.range.end(), name } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroCall { pub id: SourceMacroCallId, @@ -190,11 +205,6 @@ pub struct SourceEmittedToken { pub provenance: SourceTokenProvenanceId, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SourceTokenKind { - Unknown, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourceTokenProvenance { Source { @@ -307,6 +317,11 @@ pub enum SourcePreprocUnavailable { EmittedTokenAuthorityUnavailable, TokenProvenanceAuthorityUnavailable, ExpansionAuthorityUnavailable, + MissingEmittedTokenMacroCall { source: PreprocSourceId }, + MissingEmittedTokenMacroDefinition { call: SourceMacroCallId }, + MissingEmittedTokenMacroBody { call: SourceMacroCallId }, + MissingEmittedTokenMacroArgument { call: SourceMacroCallId }, + UnsupportedEmittedTokenProvenance, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -510,6 +525,10 @@ impl SourceMacroCallTable { pub fn is_empty(&self) -> bool { self.calls.is_empty() } + + fn push(&mut self, call: SourceMacroCall) { + self.calls.push(call); + } } impl SourceMacroExpansionTable { @@ -546,6 +565,10 @@ impl SourceEmittedTokenTable { pub fn is_empty(&self) -> bool { self.tokens.is_empty() } + + fn push(&mut self, token: SourceEmittedToken) { + self.tokens.push(token); + } } impl SourceTokenProvenanceTable { @@ -564,6 +587,10 @@ impl SourceTokenProvenanceTable { pub fn is_empty(&self) -> bool { self.provenance.is_empty() } + + fn push(&mut self, provenance: SourceTokenProvenance) { + self.provenance.push(provenance); + } } impl HasDirectiveRange for SourceMacroDefinition { @@ -600,10 +627,12 @@ pub struct SourcePreprocModelBuilder<'a> { index: &'a SourcePreprocIndex, tables: SourcePreprocTables, definition_ids_by_define_index: BTreeMap, + call_ids_by_signature: BTreeMap, current_state: BTreeMap, definition_ranges_partial: bool, include_edges_partial: bool, references_partial: bool, + token_provenance_partial: bool, } impl<'a> SourcePreprocModelBuilder<'a> { @@ -612,10 +641,12 @@ impl<'a> SourcePreprocModelBuilder<'a> { index, tables: SourcePreprocTables::default(), definition_ids_by_define_index: BTreeMap::new(), + call_ids_by_signature: BTreeMap::new(), current_state: BTreeMap::new(), definition_ranges_partial: false, include_edges_partial: false, references_partial: false, + token_provenance_partial: false, } } @@ -630,24 +661,19 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.record_position_boundaries(); self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); self.scan_references_and_state(); + self.build_emitted_token_tables(); self.tables.capabilities = SourcePreprocCapabilities { source_events: CapabilityStatus::Complete, definition_name_ranges: partial_status(self.definition_ranges_partial), include_edges: partial_status(self.include_edges_partial), inactive_ranges: CapabilityStatus::Complete, macro_reference_resolution: partial_status(self.references_partial), - macro_calls: CapabilityStatus::Unavailable( - SourcePreprocUnavailable::MacroCallAuthorityUnavailable, - ), + macro_calls: partial_status(self.references_partial), macro_expansions: CapabilityStatus::Unavailable( SourcePreprocUnavailable::ExpansionAuthorityUnavailable, ), - emitted_tokens: CapabilityStatus::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, - ), - emitted_token_provenance: CapabilityStatus::Unavailable( - SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, - ), + emitted_tokens: CapabilityStatus::Complete, + emitted_token_provenance: partial_status(self.token_provenance_partial), }; } @@ -842,14 +868,15 @@ impl<'a> SourcePreprocModelBuilder<'a> { let event_id = usage.event_id; let directive_range = usage.range; let resolution = self.resolve_visible_reference(name.as_str()); - self.push_reference( + let reference = self.push_reference( event_id, SourceMacroReferenceSite::Usage { usage_index: directive.index }, - name, + name.clone(), name_range, directive_range, - resolution, + resolution.clone(), ); + self.push_call(reference, name, directive_range, resolution); } fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { @@ -910,7 +937,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { name_range: SourceRange, directive_range: SourceRange, resolution: SourceMacroResolution, - ) { + ) -> SourceMacroReferenceId { let id = SourceMacroReferenceId::new(self.tables.macro_references.len()); self.tables.macro_references.push(SourceMacroReference { id, @@ -921,6 +948,211 @@ impl<'a> SourcePreprocModelBuilder<'a> { directive_range, resolution, }); + id + } + + fn push_call( + &mut self, + reference: SourceMacroReferenceId, + name: SmolStr, + call_range: SourceRange, + callee: SourceMacroResolution, + ) { + let id = SourceMacroCallId::new(self.tables.macro_calls.len()); + self.tables.macro_calls.push(SourceMacroCall { + id, + reference, + call_range, + callee, + arguments: Vec::new(), + expansion: None, + status: SourceMacroCallStatus::ExpansionUnavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + }); + self.call_ids_by_signature.insert(SourceMacroCallSignature::new(name, call_range), id); + } + + fn build_emitted_token_tables(&mut self) { + for index in 0..self.index.emitted_tokens.len() { + let token = self.index.emitted_tokens[index].clone(); + let provenance = self.resolve_emitted_token_provenance(&token); + let provenance_id = SourceTokenProvenanceId::new(self.tables.token_provenance.len()); + self.tables.token_provenance.push(provenance); + + let token_id = SourceEmittedTokenId::new(self.tables.emitted_tokens.len()); + self.tables.emitted_tokens.push(SourceEmittedToken { + id: token_id, + text: token.raw, + kind: token.kind, + emitted_range: SourceEmittedTokenRange { start: token_id, len: 1 }, + provenance: provenance_id, + }); + } + } + + fn resolve_emitted_token_provenance( + &mut self, + token: &SourceEmittedTokenFact, + ) -> SourceTokenProvenance { + match &token.provenance { + SourceTokenProvenanceFact::Source { token_range } => { + SourceTokenProvenance::Source { token_range: *token_range } + } + SourceTokenProvenanceFact::MacroBody { macro_name, call_range, body_token_range } => { + self.resolve_macro_body_token_provenance( + token, + macro_name.clone(), + *call_range, + *body_token_range, + ) + } + SourceTokenProvenanceFact::MacroArgument { + macro_name, + call_range, + body_token_range, + argument_token_range, + } => self.resolve_macro_argument_token_provenance( + macro_name.clone(), + *call_range, + *body_token_range, + *argument_token_range, + ), + SourceTokenProvenanceFact::Builtin { name } if !name.is_empty() => { + SourceTokenProvenance::Builtin { name: name.clone() } + } + SourceTokenProvenanceFact::Builtin { .. } | SourceTokenProvenanceFact::Unavailable => { + self.unavailable_token_provenance( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + ) + } + } + } + + fn resolve_macro_body_token_provenance( + &mut self, + token: &SourceEmittedTokenFact, + macro_name: SmolStr, + call_range: SourceRange, + body_token_range: SourceRange, + ) -> SourceTokenProvenance { + if self.source_is_predefine(body_token_range.source) { + return SourceTokenProvenance::Predefine { source: body_token_range.source }; + } + + let Ok(call) = self.call_for_emitted_token(macro_name, call_range) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCall { + source: call_range.source, + }, + ); + }; + let Ok(definition) = self.definition_for_call(call) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, + ); + }; + + if !self.definition_body_contains_raw_token( + definition, + body_token_range, + token.raw.as_str(), + ) { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + ); + } + + SourceTokenProvenance::MacroBody { definition, body_token_range, call } + } + + fn resolve_macro_argument_token_provenance( + &mut self, + macro_name: SmolStr, + call_range: SourceRange, + body_token_range: SourceRange, + argument_token_range: SourceRange, + ) -> SourceTokenProvenance { + let Ok(call) = self.call_for_emitted_token(macro_name, call_range) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCall { + source: call_range.source, + }, + ); + }; + let Ok(argument_index) = self.argument_index_for_body_token(call, body_token_range) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroArgument { call }, + ); + }; + + SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range } + } + + fn call_for_emitted_token( + &self, + macro_name: SmolStr, + call_range: SourceRange, + ) -> Result { + self.call_ids_by_signature + .get(&SourceMacroCallSignature::new(macro_name, call_range)) + .copied() + .ok_or(()) + } + + fn definition_for_call(&self, call: SourceMacroCallId) -> Result { + let Some(call) = self.tables.macro_calls.get(call) else { + return Err(()); + }; + match &call.callee { + SourceMacroResolution::Resolved { definition, .. } => Ok(*definition), + SourceMacroResolution::Undefined | SourceMacroResolution::Unavailable(_) => Err(()), + } + } + + fn definition_body_contains_raw_token( + &self, + definition: SourceMacroDefinitionId, + body_token_range: SourceRange, + raw: &str, + ) -> bool { + let Some(definition) = self.tables.macro_definitions.get(definition) else { + return false; + }; + definition + .body_tokens + .iter() + .any(|token| token.range == Some(body_token_range) && token.raw.as_str() == raw) + } + + fn argument_index_for_body_token( + &self, + call: SourceMacroCallId, + body_token_range: SourceRange, + ) -> Result { + let definition = self.definition_for_call(call)?; + let definition = self.tables.macro_definitions.get(definition).ok_or(())?; + let body_token = definition + .body_tokens + .iter() + .find(|token| token.range == Some(body_token_range)) + .ok_or(())?; + let params = definition.params.as_ref().ok_or(())?; + params.iter().position(|param| param.name.as_ref() == Some(&body_token.value)).ok_or(()) + } + + fn source_is_predefine(&self, source: PreprocSourceId) -> bool { + self.index.sources.iter().any(|candidate| { + candidate.id == source && candidate.origin == PreprocSourceOrigin::Predefine + }) + } + + fn unavailable_token_provenance( + &mut self, + reason: SourcePreprocUnavailable, + ) -> SourceTokenProvenance { + self.token_provenance_partial = true; + SourceTokenProvenance::Unavailable(reason) } fn resolve_visible_reference(&mut self, name: &str) -> SourceMacroResolution { diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index 2812ea2a..3b1bd30f 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -2,9 +2,9 @@ use std::collections::BTreeMap; use smol_str::{SmolStr, ToSmolStr}; use syntax::{ - PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, - PreprocessorTraceMacroParam, PreprocessorTraceToken, SourceBufferOrigin, SourceBufferRange, - SyntaxKind, + PreprocessorTrace, PreprocessorTraceEmittedToken, PreprocessorTraceEvent, + PreprocessorTraceEventId, PreprocessorTraceMacroParam, PreprocessorTraceToken, + PreprocessorTraceTokenProvenance, SourceBufferOrigin, SourceBufferRange, SyntaxKind, }; use utils::line_index::{TextRange, TextSize}; @@ -31,6 +31,7 @@ impl SourcePreprocIndex { .iter() .map(|edge| (edge.included_source, edge.include_event_id)) .collect::>(); + let emitted_tokens = trace.emitted_tokens; let mut index = Self { root_source: Some(root_source), sources: trace @@ -58,6 +59,7 @@ impl SourcePreprocIndex { for (source_order, directive) in trace.events.into_iter().enumerate() { collect_trace_event(&mut index, source_order, directive)?; } + index.emitted_tokens = emitted_tokens.into_iter().map(emitted_token_from_trace).collect(); validate_include_edges(&index)?; @@ -226,6 +228,70 @@ fn macro_token_from_trace(token: PreprocessorTraceToken) -> SourceMacroToken { } } +fn emitted_token_from_trace(token: PreprocessorTraceEmittedToken) -> SourceEmittedTokenFact { + SourceEmittedTokenFact { + raw: token.raw_text.to_smolstr(), + value: token.value_text.to_smolstr(), + kind: SourceTokenKind::Syntax(token.token_kind), + provenance: emitted_token_provenance_from_trace(token.provenance), + } +} + +fn emitted_token_provenance_from_trace( + provenance: PreprocessorTraceTokenProvenance, +) -> SourceTokenProvenanceFact { + match provenance { + PreprocessorTraceTokenProvenance::Source { token_range } => { + source_range_from_trace(&token_range) + .map(|token_range| SourceTokenProvenanceFact::Source { token_range }) + .unwrap_or(SourceTokenProvenanceFact::Unavailable) + } + PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + call_range, + body_token_range, + } => { + let Some(call_range) = source_range_from_trace(&call_range) else { + return SourceTokenProvenanceFact::Unavailable; + }; + let Some(body_token_range) = source_range_from_trace(&body_token_range) else { + return SourceTokenProvenanceFact::Unavailable; + }; + SourceTokenProvenanceFact::MacroBody { + macro_name: macro_name.to_smolstr(), + call_range, + body_token_range, + } + } + PreprocessorTraceTokenProvenance::MacroArgument { + macro_name, + call_range, + body_token_range, + argument_token_range, + } => { + let Some(call_range) = source_range_from_trace(&call_range) else { + return SourceTokenProvenanceFact::Unavailable; + }; + let Some(body_token_range) = source_range_from_trace(&body_token_range) else { + return SourceTokenProvenanceFact::Unavailable; + }; + let Some(argument_token_range) = source_range_from_trace(&argument_token_range) else { + return SourceTokenProvenanceFact::Unavailable; + }; + SourceTokenProvenanceFact::MacroArgument { + macro_name: macro_name.to_smolstr(), + call_range, + body_token_range, + argument_token_range, + } + } + PreprocessorTraceTokenProvenance::Builtin { name } => { + SourceTokenProvenanceFact::Builtin { name: name.to_smolstr() } + } + PreprocessorTraceTokenProvenance::Unavailable => SourceTokenProvenanceFact::Unavailable, + } +} + fn trace_token_value(token: &PreprocessorTraceToken) -> SmolStr { token.value_text.to_smolstr() } diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index 7637cedc..116d1ac4 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -1,4 +1,5 @@ use smol_str::SmolStr; +use syntax::TokenKind; use utils::line_index::{TextRange, TextSize}; use super::provenance::SourcePreprocTables; @@ -80,6 +81,7 @@ pub struct SourcePreprocIndex { pub sources: Vec, pub include_edges: Vec, pub event_records: Vec, + pub emitted_tokens: Vec, pub defines: Vec, pub undefs: Vec, pub includes: Vec, @@ -153,6 +155,42 @@ pub struct SourceMacroToken { pub range: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourceTokenKind { + Unknown, + Syntax(TokenKind), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceEmittedTokenFact { + pub raw: SmolStr, + pub value: SmolStr, + pub kind: SourceTokenKind, + pub provenance: SourceTokenProvenanceFact, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceTokenProvenanceFact { + Source { + token_range: SourceRange, + }, + MacroBody { + macro_name: SmolStr, + call_range: SourceRange, + body_token_range: SourceRange, + }, + MacroArgument { + macro_name: SmolStr, + call_range: SourceRange, + body_token_range: SourceRange, + argument_token_range: SourceRange, + }, + Builtin { + name: SmolStr, + }, + Unavailable, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocModel { pub(super) index: SourcePreprocIndex, diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index a06b6324..53c6796d 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -86,6 +86,7 @@ mod slang_ffi { struct RawPreprocessorTraceToken { raw_text: String, value_text: String, + token_kind: u16, range: RawSourceBufferRange, has_token: bool, } @@ -112,6 +113,19 @@ mod slang_ffi { disabled_ranges: Vec, } + #[derive(Debug, Clone, PartialEq, Eq)] + struct RawPreprocessorTraceEmittedToken { + raw_text: String, + value_text: String, + token_kind: u16, + provenance_kind: u8, + macro_name: String, + token_range: RawSourceBufferRange, + call_range: RawSourceBufferRange, + body_token_range: RawSourceBufferRange, + argument_token_range: RawSourceBufferRange, + } + #[derive(Debug, Clone, PartialEq, Eq)] struct RawPreprocessorTraceIncludeEdge { include_event_id: u32, @@ -125,6 +139,7 @@ mod slang_ffi { source_buffers: Vec, events: Vec, include_edges: Vec, + emitted_tokens: Vec, } #[namespace = "slang"] diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index 6bc464d6..cfdd1b6a 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -6,6 +6,7 @@ #include #include +#include #include namespace wrapper { @@ -160,6 +161,39 @@ ::RawSourceBufferRange to_rust_source_buffer_range(slang::SourceRange range) { return result; } +constexpr uint8_t TRACE_TOKEN_PROVENANCE_UNAVAILABLE = 0; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_SOURCE = 1; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_BODY = 2; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT = 3; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_BUILTIN = 4; + +::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { + ::RawPreprocessorTraceEmittedToken token; + token.raw_text = rust::String(); + token.value_text = rust::String(); + token.token_kind = static_cast(slang::parsing::TokenKind::Unknown); + token.provenance_kind = TRACE_TOKEN_PROVENANCE_UNAVAILABLE; + token.macro_name = rust::String(); + token.token_range = empty_source_buffer_range(); + token.call_range = empty_source_buffer_range(); + token.body_token_range = empty_source_buffer_range(); + token.argument_token_range = empty_source_buffer_range(); + return token; +} + +::RawSourceBufferRange to_rust_original_macro_range( + const slang::SourceManager& sourceManager, + slang::SourceRange range) { + if (range == slang::SourceRange::NoLocation || !range.start().valid() || + !range.end().valid()) { + return empty_source_buffer_range(); + } + + auto start = sourceManager.getOriginalLoc(range.start()); + auto end = sourceManager.getOriginalLoc(range.end()); + return to_rust_source_buffer_range(slang::SourceRange(start, end)); +} + struct TraceSourceLocationKey { uint32_t buffer_id = 0; size_t offset = 0; @@ -187,10 +221,15 @@ std::optional trace_source_location_key(slang::SourceLoc }; } +bool is_intrinsic_builtin_macro(std::string_view name) { + return name == "__FILE__" || name == "__LINE__"; +} + ::RawPreprocessorTraceToken empty_preprocessor_trace_token() { ::RawPreprocessorTraceToken token; token.raw_text = rust::String(); token.value_text = rust::String(); + token.token_kind = static_cast(slang::parsing::TokenKind::Unknown); token.range = empty_source_buffer_range(); token.has_token = false; return token; @@ -203,6 +242,7 @@ ::RawPreprocessorTraceToken to_rust_preprocessor_trace_token(slang::parsing::Tok result.raw_text = rust::String(std::string(token.rawText())); result.value_text = rust::String(std::string(token.valueText())); + result.token_kind = static_cast(token.kind); result.range = to_rust_source_buffer_range(token.range()); result.has_token = true; return result; @@ -217,6 +257,83 @@ rust::Vec<::RawPreprocessorTraceToken> to_rust_preprocessor_trace_tokens( return result; } +::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( + slang::parsing::Token token, + const slang::SourceManager& sourceManager, + const std::unordered_map& + macroUsageNamesByLocation) { + auto result = empty_preprocessor_trace_emitted_token(); + if (!token) + return result; + + result.raw_text = rust::String(std::string(token.rawText())); + result.value_text = rust::String(std::string(token.valueText())); + result.token_kind = static_cast(token.kind); + + auto location = token.location(); + if (!location.valid()) + return result; + + if (sourceManager.isMacroLoc(location)) { + switch (sourceManager.getMacroExpansionKind(location)) { + case slang::SourceManager::MacroExpansionKind::TokenPaste: + case slang::SourceManager::MacroExpansionKind::Stringification: + return result; + case slang::SourceManager::MacroExpansionKind::Body: + case slang::SourceManager::MacroExpansionKind::Argument: + break; + } + + auto macroName = std::string(sourceManager.getMacroName(location)); + result.macro_name = rust::String(macroName); + + if (sourceManager.isMacroArgLoc(location)) { + auto tokenRange = token.range(); + result.argument_token_range = to_rust_original_macro_range(sourceManager, tokenRange); + + auto formalRange = sourceManager.getExpansionRange(location); + result.body_token_range = to_rust_original_macro_range(sourceManager, formalRange); + + if (formalRange != slang::SourceRange::NoLocation && formalRange.start().valid() && + sourceManager.isMacroLoc(formalRange.start())) { + result.call_range = to_rust_source_buffer_range( + sourceManager.getExpansionRange(formalRange.start())); + } + + if (result.call_range.has_range && result.body_token_range.has_range && + result.argument_token_range.has_range) { + result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT; + } + return result; + } + + result.call_range = to_rust_source_buffer_range(sourceManager.getExpansionRange(location)); + result.body_token_range = to_rust_original_macro_range(sourceManager, token.range()); + if (result.call_range.has_range && result.body_token_range.has_range) { + result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_BODY; + } + else if (!macroName.empty()) { + // Slang built-in object-like macros have no source body location. + result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; + } + return result; + } + + if (auto key = trace_source_location_key(location)) { + auto it = macroUsageNamesByLocation.find(*key); + if (it != macroUsageNamesByLocation.end() && is_intrinsic_builtin_macro(it->second)) { + result.macro_name = rust::String(it->second); + result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; + return result; + } + } + + result.token_range = to_rust_source_buffer_range(token.range()); + if (result.token_range.has_range) + result.provenance_kind = TRACE_TOKEN_PROVENANCE_SOURCE; + return result; +} + void collect_leaf_trace_tokens(const slang::syntax::SyntaxNode& node, rust::Vec<::RawPreprocessorTraceToken>& tokens) { for (size_t i = 0; i < node.getChildCount(); i++) { @@ -842,6 +959,7 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( result.source_buffers = rust::Vec<::RawSourceBufferId>(); result.events = rust::Vec<::RawPreprocessorTraceEvent>(); result.include_edges = rust::Vec<::RawPreprocessorTraceIncludeEdge>(); + result.emitted_tokens = rust::Vec<::RawPreprocessorTraceEmittedToken>(); slang::SourceManager sourceManager; std::unordered_map assignedBuffers; @@ -897,6 +1015,8 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( preprocessor.pushSource(rootBuffer); std::unordered_map includeEventIdsByLocation; + std::unordered_map + macroUsageNamesByLocation; while (true) { auto token = preprocessor.next(); @@ -911,12 +1031,24 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( if (auto key = trace_source_location_key(include.directive.location())) includeEventIdsByLocation.emplace(*key, eventId); } + else if (syntax->kind == slang::syntax::SyntaxKind::MacroUsage) { + const auto& usage = syntax->as(); + if (auto key = trace_source_location_key(usage.directive.location())) { + auto name = std::string(usage.directive.valueText()); + if (!name.empty() && name[0] == '`') + name.erase(name.begin()); + macroUsageNamesByLocation.emplace(*key, std::move(name)); + } + } result.events.emplace_back(to_rust_preprocessor_trace_event(*syntax, eventId)); } } if (token.kind == slang::parsing::TokenKind::EndOfFile) break; + + result.emitted_tokens.emplace_back( + to_rust_preprocessor_trace_emitted_token(token, sourceManager, macroUsageNamesByLocation)); } for (auto buffer : sourceManager.getAllBuffers()) { diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 9ae28da6..d7997e4a 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -135,6 +135,7 @@ pub struct PreprocessorTrace { pub source_buffers: Vec, pub events: Vec, pub include_edges: Vec, + pub emitted_tokens: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -160,10 +161,41 @@ pub struct PreprocessorTraceEvent { pub disabled_ranges: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceEmittedToken { + pub raw_text: String, + pub value_text: String, + pub token_kind: TokenKind, + pub provenance: PreprocessorTraceTokenProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocessorTraceTokenProvenance { + Source { + token_range: SourceBufferRange, + }, + MacroBody { + macro_name: String, + call_range: SourceBufferRange, + body_token_range: SourceBufferRange, + }, + MacroArgument { + macro_name: String, + call_range: SourceBufferRange, + body_token_range: SourceBufferRange, + argument_token_range: SourceBufferRange, + }, + Builtin { + name: String, + }, + Unavailable, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocessorTraceToken { pub raw_text: String, pub value_text: String, + pub token_kind: TokenKind, pub range: Option, } @@ -315,6 +347,11 @@ impl PreprocessorTrace { included_buffer_id: edge.included_buffer_id, }) .collect(), + emitted_tokens: raw + .emitted_tokens + .into_iter() + .map(PreprocessorTraceEmittedToken::from_raw) + .collect(), }) } } @@ -349,12 +386,85 @@ impl PreprocessorTraceEvent { } } +impl PreprocessorTraceEmittedToken { + #[inline] + fn from_raw(raw: ffi::RawPreprocessorTraceEmittedToken) -> Self { + Self { + raw_text: raw.raw_text, + value_text: raw.value_text, + token_kind: TokenKind::from_id(raw.token_kind), + provenance: PreprocessorTraceTokenProvenance::from_raw( + raw.provenance_kind, + raw.macro_name, + raw.token_range, + raw.call_range, + raw.body_token_range, + raw.argument_token_range, + ), + } + } +} + +impl PreprocessorTraceTokenProvenance { + const BUILTIN: u8 = 4; + const MACRO_ARGUMENT: u8 = 3; + const MACRO_BODY: u8 = 2; + const SOURCE: u8 = 1; + const UNAVAILABLE: u8 = 0; + + #[inline] + fn from_raw( + kind: u8, + macro_name: String, + token_range: ffi::RawSourceBufferRange, + call_range: ffi::RawSourceBufferRange, + body_token_range: ffi::RawSourceBufferRange, + argument_token_range: ffi::RawSourceBufferRange, + ) -> Self { + match kind { + Self::SOURCE => SourceBufferRange::from_raw(token_range) + .map(|token_range| Self::Source { token_range }) + .unwrap_or(Self::Unavailable), + Self::MACRO_BODY => { + let Some(call_range) = SourceBufferRange::from_raw(call_range) else { + return Self::Unavailable; + }; + let Some(body_token_range) = SourceBufferRange::from_raw(body_token_range) else { + return Self::Unavailable; + }; + Self::MacroBody { macro_name, call_range, body_token_range } + } + Self::MACRO_ARGUMENT => { + let Some(call_range) = SourceBufferRange::from_raw(call_range) else { + return Self::Unavailable; + }; + let Some(body_token_range) = SourceBufferRange::from_raw(body_token_range) else { + return Self::Unavailable; + }; + let Some(argument_token_range) = SourceBufferRange::from_raw(argument_token_range) + else { + return Self::Unavailable; + }; + Self::MacroArgument { + macro_name, + call_range, + body_token_range, + argument_token_range, + } + } + Self::BUILTIN => Self::Builtin { name: macro_name }, + Self::UNAVAILABLE | _ => Self::Unavailable, + } + } +} + impl PreprocessorTraceToken { #[inline] fn from_raw(raw: ffi::RawPreprocessorTraceToken) -> Option { raw.has_token.then(|| Self { raw_text: raw.raw_text, value_text: raw.value_text, + token_kind: TokenKind::from_id(raw.token_kind), range: SourceBufferRange::from_raw(raw.range), }) } diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index a3e59fb6..f5c46cc9 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1061,6 +1061,139 @@ wire disabled_by_header; })); } +#[test] +fn preprocessor_trace_reports_emitted_macro_body_and_argument_provenance() { + let source = r#"`define OBJ 8 +`define ID(x) x +module m; +localparam int A = `OBJ; +localparam int B = `ID(7); +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include emitted tokens"); + + assert!( + trace.emitted_tokens.iter().any(|token| { + token.raw_text == "module" + && matches!(token.provenance, PreprocessorTraceTokenProvenance::Source { .. }) + }), + "source tokens should be retained in emitted stream: {:?}", + trace.emitted_tokens + ); + + let obj = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "8") + .expect("object-like macro body token should be emitted"); + let PreprocessorTraceTokenProvenance::MacroBody { macro_name, call_range, body_token_range } = + &obj.provenance + else { + panic!("expected macro body provenance for `OBJ expansion: {obj:?}"); + }; + assert_eq!(macro_name, "OBJ"); + assert_eq!(&source[call_range.range.clone()], "`OBJ"); + assert_eq!(&source[body_token_range.range.clone()], "8"); + + let arg = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "7") + .expect("function-like argument token should be emitted"); + let PreprocessorTraceTokenProvenance::MacroArgument { + macro_name, + call_range, + body_token_range, + argument_token_range, + } = &arg.provenance + else { + panic!("expected macro argument provenance for `ID expansion: {arg:?}"); + }; + assert_eq!(macro_name, "ID"); + assert_eq!(&source[call_range.range.clone()], "`ID(7)"); + assert_eq!(&source[body_token_range.range.clone()], "x"); + assert_eq!(&source[argument_token_range.range.clone()], "7"); +} + +#[test] +fn preprocessor_trace_keeps_unsupported_macro_ops_as_unavailable_tokens() { + let source = r#"`define JOIN(a,b) a``b +`define STR(x) `"x`" +module m; +wire `JOIN(foo,bar); +string s = `STR(foo); +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include emitted tokens"); + + let pasted = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "foobar") + .expect("token paste result should stay in emitted stream"); + assert!(matches!(pasted.provenance, PreprocessorTraceTokenProvenance::Unavailable)); + + let stringified = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "\"foo\"") + .expect("stringification result should stay in emitted stream"); + assert!(matches!(stringified.provenance, PreprocessorTraceTokenProvenance::Unavailable)); +} + +#[test] +fn preprocessor_trace_reports_predefine_and_builtin_emitted_token_facts() { + let source = r#"module m; +localparam int P = `FROM_API; +localparam int L = `__LINE__; +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions { + predefines: vec!["FROM_API=11".to_owned()], + ..SyntaxTreeOptions::default() + }, + ) + .expect("trace should include emitted tokens"); + + let from_api = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "11") + .expect("predefined macro body token should be emitted"); + assert!(matches!(from_api.provenance, PreprocessorTraceTokenProvenance::MacroBody { .. })); + let PreprocessorTraceTokenProvenance::MacroBody { body_token_range, .. } = &from_api.provenance + else { + unreachable!(); + }; + assert_ne!(body_token_range.buffer_id, trace.root_buffer_id); + + let builtin = trace + .emitted_tokens + .iter() + .find(|token| matches!(token.provenance, PreprocessorTraceTokenProvenance::Builtin { .. })) + .expect("builtin macro token should be emitted with builtin provenance"); + assert!(matches!( + &builtin.provenance, + PreprocessorTraceTokenProvenance::Builtin { name } if name == "__LINE__" + )); +} + #[test] fn preprocessor_trace_records_nested_include_edges() { let dir = TestDir::new("slang-preprocessor-trace-nested"); diff --git a/crates/slang/include/slang/text/SourceManager.h b/crates/slang/include/slang/text/SourceManager.h index 2593ac5f..e179d213 100644 --- a/crates/slang/include/slang/text/SourceManager.h +++ b/crates/slang/include/slang/text/SourceManager.h @@ -42,6 +42,13 @@ class SLANG_EXPORT SourceManager { public: using BufferOrError = nonstd::expected; + enum class MacroExpansionKind : uint8_t { + Body, + Argument, + TokenPaste, + Stringification, + }; + /// Default constructor. SourceManager(); SourceManager(const SourceManager&) = delete; @@ -101,6 +108,9 @@ class SLANG_EXPORT SourceManager { /// Determines whether the given location points to a macro argument expansion. bool isMacroArgLoc(SourceLocation location) const; + /// Gets the kind of macro expansion for the given macro location. + MacroExpansionKind getMacroExpansionKind(SourceLocation location) const; + /// Determines whether the given location is inside an include file. bool isIncludedFileLoc(SourceLocation location) const; @@ -148,6 +158,10 @@ class SLANG_EXPORT SourceManager { SourceLocation createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, std::string_view macroName); + /// Creates a macro expansion location; used by the preprocessor. + SourceLocation createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, + MacroExpansionKind kind); + /// Instead of loading source from a file, copy it from text already in memory. SourceBuffer assignText(std::string_view text, SourceLocation includedFrom = SourceLocation(), const SourceLibrary* library = nullptr); @@ -282,17 +296,22 @@ class SLANG_EXPORT SourceManager { struct ExpansionInfo { SourceLocation originalLoc; SourceRange expansionRange; - bool isMacroArg = false; + MacroExpansionKind kind = MacroExpansionKind::Body; std::string_view macroName; ExpansionInfo() {} ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, bool isMacroArg) : - originalLoc(originalLoc), expansionRange(expansionRange), isMacroArg(isMacroArg) {} + originalLoc(originalLoc), expansionRange(expansionRange), + kind(isMacroArg ? MacroExpansionKind::Argument : MacroExpansionKind::Body) {} ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, std::string_view macroName) : originalLoc(originalLoc), expansionRange(expansionRange), macroName(macroName) {} + + ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, + MacroExpansionKind kind) : + originalLoc(originalLoc), expansionRange(expansionRange), kind(kind) {} }; // This mutex protects pretty much everything in this class. diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index 8d83e526..d63d1966 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -142,6 +142,22 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< bool anyNewMacros = false; bool didConcat = false; + auto markMacroOpToken = [&](Token opToken, SourceManager::MacroExpansionKind kind) { + if (!opToken) + return opToken; + + auto loc = opToken.location(); + auto originalLoc = loc; + auto expansionRange = opToken.range(); + if (sourceManager.isMacroLoc(loc)) { + originalLoc = sourceManager.getOriginalLoc(loc); + expansionRange = sourceManager.getExpansionRange(loc); + } + + auto opLoc = sourceManager.createExpansionLoc(originalLoc, expansionRange, kind); + return opToken.withLocation(alloc, opLoc); + }; + for (size_t i = 0; i < tokens.size(); i++) { Token newToken; bool nextDidConcat = false; @@ -167,6 +183,8 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< // all done stringifying; convert saved tokens to string newToken = Lexer::stringify(*lexerStack.back(), stringify, stringifyBuffer, token); + newToken = markMacroOpToken(newToken, + SourceManager::MacroExpansionKind::Stringification); stringify = Token(); } else if (stringify.kind == TokenKind::MacroTripleQuote) { @@ -183,9 +201,13 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< // next to each other isn't ever valid. newToken = Lexer::stringify(*lexerStack.back(), stringify, stringifyBuffer, token); + newToken = markMacroOpToken(newToken, + SourceManager::MacroExpansionKind::Stringification); stringify = Token(); - extraToAppend = Token(alloc, TokenKind::StringLiteral, {}, "\"\"", - token.location() + 2, ""sv); + extraToAppend = markMacroOpToken( + Token(alloc, TokenKind::StringLiteral, {}, "\"\"", token.location() + 2, + ""sv), + SourceManager::MacroExpansionKind::Stringification); } break; case TokenKind::MacroPaste: @@ -212,6 +234,8 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< newToken = Lexer::concatenateTokens(alloc, stringifyBuffer.back(), tokens[i + 1]); if (newToken) { + newToken = markMacroOpToken( + newToken, SourceManager::MacroExpansionKind::TokenPaste); stringifyBuffer.pop_back(); ++i; } @@ -250,6 +274,8 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< else { newToken = Lexer::concatenateTokens(alloc, dest.back(), tokens[i + 1]); if (newToken) { + newToken = markMacroOpToken( + newToken, SourceManager::MacroExpansionKind::TokenPaste); dest.pop_back(); ++i; @@ -267,6 +293,8 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< if (didConcat && token.trivia().empty() && emptyArgTrivia.empty()) { newToken = Lexer::concatenateTokens(alloc, dest.back(), token); if (newToken) { + newToken = markMacroOpToken( + newToken, SourceManager::MacroExpansionKind::TokenPaste); dest.pop_back(); nextDidConcat = true; break; @@ -328,8 +356,9 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< // Note: endToken parameter here doesn't matter, // we know there is no trivia to take. - dest.push_back( - Lexer::stringify(*lexerStack.back(), stringify, stringifyBuffer, Token())); + dest.push_back(markMacroOpToken( + Lexer::stringify(*lexerStack.back(), stringify, stringifyBuffer, Token()), + SourceManager::MacroExpansionKind::Stringification)); stringify = Token(); // Now we have the unfortunate task of re-lexing the remaining stuff after the diff --git a/crates/slang/source/text/SourceManager.cpp b/crates/slang/source/text/SourceManager.cpp index cd99c0f2..db8e6aad 100644 --- a/crates/slang/source/text/SourceManager.cpp +++ b/crates/slang/source/text/SourceManager.cpp @@ -169,6 +169,20 @@ bool SourceManager::isMacroArgLoc(SourceLocation location) const { return isMacroArgLocImpl(location, lock); } +SourceManager::MacroExpansionKind SourceManager::getMacroExpansionKind( + SourceLocation location) const { + std::shared_lock lock(mutex); + auto buffer = location.buffer(); + if (!buffer || buffer.getId() >= bufferEntries.size()) + return MacroExpansionKind::Body; + + auto info = std::get_if(&bufferEntries[buffer.getId()]); + if (!info) + return MacroExpansionKind::Body; + + return info->kind; +} + bool SourceManager::isIncludedFileLoc(SourceLocation location) const { return getIncludedFrom(location.buffer()).valid(); } @@ -287,6 +301,15 @@ SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, return SourceLocation(BufferID((uint32_t)(bufferEntries.size() - 1), macroName), 0); } +SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, + SourceRange expansionRange, + MacroExpansionKind kind) { + std::unique_lock lock(mutex); + + bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, kind)); + return SourceLocation(BufferID((uint32_t)(bufferEntries.size() - 1), ""sv), 0); +} + SourceBuffer SourceManager::assignText(std::string_view text, SourceLocation includedFrom, const SourceLibrary* library) { return assignText("", text, includedFrom, library); @@ -654,7 +677,7 @@ bool SourceManager::isMacroArgLocImpl(SourceLocation location, TLock&) const { SLANG_ASSERT(buffer.getId() < bufferEntries.size()); auto info = std::get_if(&bufferEntries[buffer.getId()]); - return info && info->isMacroArg; + return info && info->kind == MacroExpansionKind::Argument; } template From 6fde833c1b227b75b91e0900f6149c83fa3c6292 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 21:01:10 +0800 Subject: [PATCH 09/80] fix(slang): map nested macro call ranges to written source --- crates/slang/bindings/rust/ffi/wrapper.cc | 84 +++++++++++++++++++---- crates/slang/bindings/rust/tests.rs | 31 +++++++++ 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index cfdd1b6a..684588b2 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -181,19 +181,80 @@ ::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { return token; } -::RawSourceBufferRange to_rust_original_macro_range( +bool is_single_buffer_range(slang::SourceRange range) { + return range != slang::SourceRange::NoLocation && range.start().valid() && + range.end().valid() && range.start().buffer() == range.end().buffer(); +} + +::RawSourceBufferRange to_rust_original_macro_loc_range( const slang::SourceManager& sourceManager, slang::SourceRange range) { - if (range == slang::SourceRange::NoLocation || !range.start().valid() || - !range.end().valid()) { + if (!is_single_buffer_range(range) || !sourceManager.isMacroLoc(range.start()) || + !sourceManager.isMacroLoc(range.end())) { return empty_source_buffer_range(); } - auto start = sourceManager.getOriginalLoc(range.start()); auto end = sourceManager.getOriginalLoc(range.end()); return to_rust_source_buffer_range(slang::SourceRange(start, end)); } +enum class TraceExpansionRangeSpace { + FileBackedSource, + MacroExpansion, + InvalidOrMixed, +}; + +TraceExpansionRangeSpace classify_expansion_range( + const slang::SourceManager& sourceManager, + slang::SourceRange range) { + if (!is_single_buffer_range(range)) + return TraceExpansionRangeSpace::InvalidOrMixed; + + const bool startIsFile = sourceManager.isFileLoc(range.start()); + const bool endIsFile = sourceManager.isFileLoc(range.end()); + const bool startIsMacro = sourceManager.isMacroLoc(range.start()); + const bool endIsMacro = sourceManager.isMacroLoc(range.end()); + if (startIsFile && endIsFile) + return TraceExpansionRangeSpace::FileBackedSource; + if (startIsMacro && endIsMacro) + return TraceExpansionRangeSpace::MacroExpansion; + return TraceExpansionRangeSpace::InvalidOrMixed; +} + +::RawSourceBufferRange to_rust_written_source_range( + const slang::SourceManager& sourceManager, + slang::SourceRange range) { + switch (classify_expansion_range(sourceManager, range)) { + case TraceExpansionRangeSpace::FileBackedSource: + return to_rust_source_buffer_range(range); + case TraceExpansionRangeSpace::MacroExpansion: + return to_rust_original_macro_loc_range(sourceManager, range); + case TraceExpansionRangeSpace::InvalidOrMixed: + return empty_source_buffer_range(); + } + SLANG_UNREACHABLE; +} + +::RawSourceBufferRange to_rust_macro_callsite_range_from_macro_loc( + const slang::SourceManager& sourceManager, + slang::SourceLocation macroLocation) { + if (!macroLocation.valid() || !sourceManager.isMacroLoc(macroLocation)) + return empty_source_buffer_range(); + + return to_rust_written_source_range(sourceManager, sourceManager.getExpansionRange(macroLocation)); +} + +::RawSourceBufferRange to_rust_macro_argument_callsite_range( + const slang::SourceManager& sourceManager, + slang::SourceRange formalRange) { + if (classify_expansion_range(sourceManager, formalRange) != + TraceExpansionRangeSpace::MacroExpansion) { + return empty_source_buffer_range(); + } + + return to_rust_macro_callsite_range_from_macro_loc(sourceManager, formalRange.start()); +} + struct TraceSourceLocationKey { uint32_t buffer_id = 0; size_t offset = 0; @@ -289,16 +350,11 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( if (sourceManager.isMacroArgLoc(location)) { auto tokenRange = token.range(); - result.argument_token_range = to_rust_original_macro_range(sourceManager, tokenRange); + result.argument_token_range = to_rust_original_macro_loc_range(sourceManager, tokenRange); auto formalRange = sourceManager.getExpansionRange(location); - result.body_token_range = to_rust_original_macro_range(sourceManager, formalRange); - - if (formalRange != slang::SourceRange::NoLocation && formalRange.start().valid() && - sourceManager.isMacroLoc(formalRange.start())) { - result.call_range = to_rust_source_buffer_range( - sourceManager.getExpansionRange(formalRange.start())); - } + result.body_token_range = to_rust_original_macro_loc_range(sourceManager, formalRange); + result.call_range = to_rust_macro_argument_callsite_range(sourceManager, formalRange); if (result.call_range.has_range && result.body_token_range.has_range && result.argument_token_range.has_range) { @@ -307,8 +363,8 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( return result; } - result.call_range = to_rust_source_buffer_range(sourceManager.getExpansionRange(location)); - result.body_token_range = to_rust_original_macro_range(sourceManager, token.range()); + result.call_range = to_rust_macro_callsite_range_from_macro_loc(sourceManager, location); + result.body_token_range = to_rust_original_macro_loc_range(sourceManager, token.range()); if (result.call_range.has_range && result.body_token_range.has_range) { result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_BODY; } diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index f5c46cc9..9fe71e1b 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1121,6 +1121,37 @@ endmodule assert_eq!(&source[argument_token_range.range.clone()], "7"); } +#[test] +fn preprocessor_trace_reports_nested_macro_call_range_in_macro_body() { + let source = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int W = `WRAP; +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include emitted tokens"); + + let leaf = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "3") + .expect("nested macro body token should be emitted"); + let PreprocessorTraceTokenProvenance::MacroBody { macro_name, call_range, body_token_range } = + &leaf.provenance + else { + panic!("expected macro body provenance for nested `LEAF expansion: {leaf:?}"); + }; + assert_eq!(macro_name, "LEAF"); + assert_eq!(&source[call_range.range.clone()], "`LEAF"); + assert_eq!(&source[body_token_range.range.clone()], "3"); +} + #[test] fn preprocessor_trace_keeps_unsupported_macro_ops_as_unavailable_tokens() { let source = r#"`define JOIN(a,b) a``b From 746e04f78c996012226ce1ee6e138cc846e7f8ab Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 21:01:20 +0800 Subject: [PATCH 10/80] feat(preproc): build macro expansion graph --- crates/preproc/src/source/model.rs | 287 +++++++++++++++++- crates/preproc/src/source/provenance.rs | 371 +++++++++++++++++++++++- 2 files changed, 634 insertions(+), 24 deletions(-) diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index c36085b8..2ab1c575 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -109,6 +109,54 @@ impl SourcePreprocModel { .unwrap_or_default() } + pub fn immediate_macro_expansion(&self, call: SourceMacroCallId) -> SourceMacroExpansionQuery { + let Some(call_fact) = self.tables.macro_calls.get(call) else { + return SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::MissingMacroCall { call }, + ); + }; + match (call_fact.expansion, &call_fact.status) { + (Some(expansion), SourceMacroCallStatus::ExpansionAvailable) + if self.tables.macro_expansions.get(expansion).is_some() => + { + SourceMacroExpansionQuery::Available(expansion) + } + (Some(expansion), SourceMacroCallStatus::ExpansionAvailable) => { + SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::MissingMacroExpansion { + call: self + .tables + .macro_expansions + .get(expansion) + .map(|expansion| expansion.call) + .unwrap_or(call), + }, + ) + } + (_, SourceMacroCallStatus::ExpansionUnavailable(reason)) => { + SourceMacroExpansionQuery::Unavailable(reason.clone()) + } + (None, SourceMacroCallStatus::ExpansionAvailable) => { + SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::MissingMacroExpansion { call }, + ) + } + } + } + + pub fn recursive_macro_expansion( + &self, + call: SourceMacroCallId, + ) -> SourceRecursiveMacroExpansion { + let mut result = SourceRecursiveMacroExpansion { + root_call: call, + expansions: Vec::new(), + unavailable: Vec::new(), + }; + self.collect_recursive_macro_expansion(call, &mut result, &mut Vec::new()); + result + } + pub fn provenance(&self, entity: SourcePreprocEntity) -> Option { let (event_id, name, range, name_range) = match entity { SourcePreprocEntity::Define(index) => { @@ -167,6 +215,45 @@ impl SourcePreprocModel { .collect() } + fn collect_recursive_macro_expansion( + &self, + call: SourceMacroCallId, + result: &mut SourceRecursiveMacroExpansion, + visiting: &mut Vec, + ) { + if visiting.contains(&call) { + result.unavailable.push(SourceMacroExpansionUnavailable { + call, + reason: SourcePreprocUnavailable::MissingMacroExpansion { call }, + }); + return; + } + + match self.immediate_macro_expansion(call) { + SourceMacroExpansionQuery::Available(expansion_id) => { + if result.expansions.contains(&expansion_id) { + return; + } + result.expansions.push(expansion_id); + let Some(expansion) = self.tables.macro_expansions.get(expansion_id) else { + result.unavailable.push(SourceMacroExpansionUnavailable { + call, + reason: SourcePreprocUnavailable::MissingMacroExpansion { call }, + }); + return; + }; + visiting.push(call); + for child in &expansion.child_calls { + self.collect_recursive_macro_expansion(*child, result, visiting); + } + visiting.pop(); + } + SourceMacroExpansionQuery::Unavailable(reason) => { + result.unavailable.push(SourceMacroExpansionUnavailable { call, reason }); + } + } + } + fn event_from_record( &self, source_order: usize, @@ -636,19 +723,26 @@ logic [`HEADER_WIDTH-1:0] data; .find(|call| call.reference == reference.id) .expect("macro usage should create a call fact"); assert_eq!(call.call_range.source, root_source); - assert!(matches!( - call.status, - SourceMacroCallStatus::ExpansionUnavailable( - SourcePreprocUnavailable::ExpansionAuthorityUnavailable - ) - )); + assert_eq!(call.status, SourceMacroCallStatus::ExpansionAvailable); + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("object-like macro call should have an immediate expansion"); + }; + assert_eq!(call.expansion, Some(expansion_id)); + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!(expansion.call, call.id); + assert_eq!(*resolved_definition, expansion.definition); + assert!(expansion.child_calls.is_empty()); + assert_eq!(expansion.status, SourceMacroExpansionStatus::Complete); - assert!(model.macro_expansions().is_empty()); let emitted = model .emitted_tokens() .iter() .find(|token| token.text.as_str() == "8") .expect("macro body token should be emitted by adapter authority"); + assert_eq!(expansion.emitted_token_range.start, emitted.id); + assert_eq!(expansion.emitted_token_range.len, 1); let provenance = model.token_provenance().get(emitted.provenance).unwrap(); assert!(matches!( provenance, @@ -660,11 +754,11 @@ logic [`HEADER_WIDTH-1:0] data; && body_token_range.source == header_source && *body_call == call.id )); + let recursive = model.recursive_macro_expansion(call.id); + assert_eq!(recursive.expansions, vec![expansion_id]); + assert!(recursive.unavailable.is_empty()); assert_eq!(model.capabilities().macro_calls, CapabilityStatus::Complete); - assert!(matches!( - &model.capabilities().macro_expansions, - CapabilityStatus::Unavailable(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) - )); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Complete); assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Complete); } @@ -695,6 +789,86 @@ endmodule let call = model.macro_calls().get(*call).expect("call id should resolve"); assert_eq!(call.call_range.source, root_source); assert_eq!(text_at_range(root_text, call.call_range.range), "`ID(7)"); + assert_eq!(call.arguments.len(), 1); + assert_eq!(call.arguments[0].argument_index, 0); + assert_eq!(call.arguments[0].argument_range, Some(*argument_token_range)); + + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("function-like macro call should have an immediate expansion"); + }; + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!(expansion.emitted_token_range.start, emitted.id); + assert_eq!(expansion.emitted_token_range.len, 1); + } + + #[test] + fn source_model_builds_nested_macro_expansion_provenance_chain() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int W = `WRAP; +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let wrap_reference = model + .macro_references() + .iter() + .find(|reference| reference.name.as_str() == "WRAP") + .expect("outer macro usage should create a reference"); + let wrap_call = model + .macro_calls() + .iter() + .find(|call| call.reference == wrap_reference.id) + .expect("outer macro usage should create a call"); + assert_eq!(wrap_call.call_range.source, root_source); + + let leaf_call = model + .macro_calls() + .iter() + .find(|call| { + let reference = model.macro_references().get(call.reference).unwrap(); + reference.name.as_str() == "LEAF" + && matches!( + reference.site, + SourceMacroReferenceSite::ExpansionToken { emitted_token: _ } + ) + }) + .expect("nested macro invocation should create an expansion-token call"); + let leaf_reference = model.macro_references().get(leaf_call.reference).unwrap(); + assert_eq!(text_at_range(root_text, leaf_reference.name_range.range), "`LEAF"); + + let SourceMacroExpansionQuery::Available(wrap_expansion_id) = + model.immediate_macro_expansion(wrap_call.id) + else { + panic!("outer macro should have expansion range from nested emitted tokens"); + }; + let wrap_expansion = model.macro_expansions().get(wrap_expansion_id).unwrap(); + assert_eq!(wrap_expansion.child_calls, vec![leaf_call.id]); + + let SourceMacroExpansionQuery::Available(leaf_expansion_id) = + model.immediate_macro_expansion(leaf_call.id) + else { + panic!("nested macro should have its own immediate expansion"); + }; + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "3") + .expect("nested macro body token should be emitted"); + let SourceTokenProvenance::MacroBody { call, .. } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("nested emitted token should keep macro body provenance"); + }; + assert_eq!(*call, leaf_call.id); + assert_eq!(wrap_expansion.emitted_token_range.start, emitted.id); + + let recursive = model.recursive_macro_expansion(wrap_call.id); + assert_eq!(recursive.expansions, vec![wrap_expansion_id, leaf_expansion_id]); + assert!(recursive.unavailable.is_empty()); } #[test] @@ -733,6 +907,97 @@ endmodule )); assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); + for call in model.macro_calls().iter() { + assert!(matches!( + model.immediate_macro_expansion(call.id), + SourceMacroExpansionQuery::Unavailable(_) + )); + } + } + + #[test] + fn source_model_does_not_create_expansion_without_emitted_token_authority() { + let root_text = "`define A 1\nmodule m; localparam int W = `A; endmodule\n"; + let define_start = root_text.find("`define").unwrap(); + let define_end = root_text.find('\n').unwrap(); + let usage_start = root_text.find("`A").unwrap(); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![SourceBufferId { + path: ROOT_PATH.to_owned(), + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }], + events: vec![ + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(0), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { + buffer_id: 1, + range: define_start..define_end, + }), + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "A".to_owned(), + value_text: "A".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }), + include_file_name: None, + params: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "1".to_owned(), + value_text: "1".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 10..11 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(1), + kind: SyntaxKind::MACRO_USAGE, + range: Some(SourceBufferRange { + buffer_id: 1, + range: usage_start..usage_start + 2, + }), + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "`A".to_owned(), + value_text: "`A".to_owned(), + token_kind: TokenKind::DIRECTIVE, + range: Some(SourceBufferRange { + buffer_id: 1, + range: usage_start..usage_start + 2, + }), + }), + include_file_name: None, + params: Vec::new(), + body_tokens: Vec::new(), + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + ], + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let model = SourcePreprocModel::from_trace(trace).unwrap(); + let call = model.macro_calls().iter().next().expect("usage should create a call"); + + assert!(model.macro_expansions().is_empty()); + assert!(matches!( + model.immediate_macro_expansion(call.id), + SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + ) + )); + assert!(matches!( + &model.capabilities().macro_expansions, + CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + ) + )); } #[test] diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 3b2d86c5..8cc33946 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -44,6 +44,7 @@ pub enum SourceMacroReferenceSite { Usage { usage_index: usize }, ConditionalToken { conditional_index: usize, token_index: usize }, IncludeGuardIfNDef { conditional_index: usize, token_index: usize }, + ExpansionToken { emitted_token: SourceEmittedTokenId }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -190,6 +191,25 @@ pub enum SourceMacroExpansionStatus { Unavailable(SourcePreprocUnavailable), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceMacroExpansionQuery { + Available(SourceMacroExpansionId), + Unavailable(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceRecursiveMacroExpansion { + pub root_call: SourceMacroCallId, + pub expansions: Vec, + pub unavailable: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroExpansionUnavailable { + pub call: SourceMacroCallId, + pub reason: SourcePreprocUnavailable, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SourceEmittedTokenRange { pub start: SourceEmittedTokenId, @@ -317,10 +337,13 @@ pub enum SourcePreprocUnavailable { EmittedTokenAuthorityUnavailable, TokenProvenanceAuthorityUnavailable, ExpansionAuthorityUnavailable, + MissingMacroCall { call: SourceMacroCallId }, + MissingMacroExpansion { call: SourceMacroCallId }, MissingEmittedTokenMacroCall { source: PreprocSourceId }, MissingEmittedTokenMacroDefinition { call: SourceMacroCallId }, MissingEmittedTokenMacroBody { call: SourceMacroCallId }, MissingEmittedTokenMacroArgument { call: SourceMacroCallId }, + NonContiguousEmittedTokenRange { call: SourceMacroCallId }, UnsupportedEmittedTokenProvenance, } @@ -529,6 +552,10 @@ impl SourceMacroCallTable { fn push(&mut self, call: SourceMacroCall) { self.calls.push(call); } + + fn get_mut(&mut self, id: SourceMacroCallId) -> Option<&mut SourceMacroCall> { + self.calls.get_mut(id.raw()) + } } impl SourceMacroExpansionTable { @@ -547,6 +574,10 @@ impl SourceMacroExpansionTable { pub fn is_empty(&self) -> bool { self.expansions.is_empty() } + + fn push(&mut self, expansion: SourceMacroExpansion) { + self.expansions.push(expansion); + } } impl SourceEmittedTokenTable { @@ -633,6 +664,7 @@ pub struct SourcePreprocModelBuilder<'a> { include_edges_partial: bool, references_partial: bool, token_provenance_partial: bool, + expansions_partial: bool, } impl<'a> SourcePreprocModelBuilder<'a> { @@ -647,6 +679,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { include_edges_partial: false, references_partial: false, token_provenance_partial: false, + expansions_partial: false, } } @@ -662,6 +695,16 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); self.scan_references_and_state(); self.build_emitted_token_tables(); + self.build_macro_expansion_graph(); + let macro_expansions = if self.tables.macro_calls.is_empty() { + CapabilityStatus::Complete + } else if self.index.emitted_tokens.is_empty() { + CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ) + } else { + partial_status(self.expansions_partial) + }; self.tables.capabilities = SourcePreprocCapabilities { source_events: CapabilityStatus::Complete, definition_name_ranges: partial_status(self.definition_ranges_partial), @@ -669,9 +712,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { inactive_ranges: CapabilityStatus::Complete, macro_reference_resolution: partial_status(self.references_partial), macro_calls: partial_status(self.references_partial), - macro_expansions: CapabilityStatus::Unavailable( - SourcePreprocUnavailable::ExpansionAuthorityUnavailable, - ), + macro_expansions, emitted_tokens: CapabilityStatus::Complete, emitted_token_provenance: partial_status(self.token_provenance_partial), }; @@ -957,7 +998,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { name: SmolStr, call_range: SourceRange, callee: SourceMacroResolution, - ) { + ) -> SourceMacroCallId { let id = SourceMacroCallId::new(self.tables.macro_calls.len()); self.tables.macro_calls.push(SourceMacroCall { id, @@ -971,16 +1012,17 @@ impl<'a> SourcePreprocModelBuilder<'a> { ), }); self.call_ids_by_signature.insert(SourceMacroCallSignature::new(name, call_range), id); + id } fn build_emitted_token_tables(&mut self) { for index in 0..self.index.emitted_tokens.len() { let token = self.index.emitted_tokens[index].clone(); - let provenance = self.resolve_emitted_token_provenance(&token); + let token_id = SourceEmittedTokenId::new(self.tables.emitted_tokens.len()); + let provenance = self.resolve_emitted_token_provenance(token_id, &token); let provenance_id = SourceTokenProvenanceId::new(self.tables.token_provenance.len()); self.tables.token_provenance.push(provenance); - let token_id = SourceEmittedTokenId::new(self.tables.emitted_tokens.len()); self.tables.emitted_tokens.push(SourceEmittedToken { id: token_id, text: token.raw, @@ -993,6 +1035,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn resolve_emitted_token_provenance( &mut self, + token_id: SourceEmittedTokenId, token: &SourceEmittedTokenFact, ) -> SourceTokenProvenance { match &token.provenance { @@ -1001,6 +1044,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { } SourceTokenProvenanceFact::MacroBody { macro_name, call_range, body_token_range } => { self.resolve_macro_body_token_provenance( + token_id, token, macro_name.clone(), *call_range, @@ -1013,6 +1057,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { body_token_range, argument_token_range, } => self.resolve_macro_argument_token_provenance( + token_id, macro_name.clone(), *call_range, *body_token_range, @@ -1031,6 +1076,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn resolve_macro_body_token_provenance( &mut self, + token_id: SourceEmittedTokenId, token: &SourceEmittedTokenFact, macro_name: SmolStr, call_range: SourceRange, @@ -1040,7 +1086,9 @@ impl<'a> SourcePreprocModelBuilder<'a> { return SourceTokenProvenance::Predefine { source: body_token_range.source }; } - let Ok(call) = self.call_for_emitted_token(macro_name, call_range) else { + let Ok(call) = + self.call_for_emitted_token(token_id, macro_name, call_range, body_token_range) + else { return self.unavailable_token_provenance( SourcePreprocUnavailable::MissingEmittedTokenMacroCall { source: call_range.source, @@ -1068,12 +1116,15 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn resolve_macro_argument_token_provenance( &mut self, + token_id: SourceEmittedTokenId, macro_name: SmolStr, call_range: SourceRange, body_token_range: SourceRange, argument_token_range: SourceRange, ) -> SourceTokenProvenance { - let Ok(call) = self.call_for_emitted_token(macro_name, call_range) else { + let Ok(call) = + self.call_for_emitted_token(token_id, macro_name, call_range, body_token_range) + else { return self.unavailable_token_provenance( SourcePreprocUnavailable::MissingEmittedTokenMacroCall { source: call_range.source, @@ -1085,19 +1136,42 @@ impl<'a> SourcePreprocModelBuilder<'a> { SourcePreprocUnavailable::MissingEmittedTokenMacroArgument { call }, ); }; + self.record_macro_argument(call, argument_index, argument_token_range); SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range } } fn call_for_emitted_token( - &self, + &mut self, + token_id: SourceEmittedTokenId, macro_name: SmolStr, call_range: SourceRange, + body_token_range: SourceRange, ) -> Result { - self.call_ids_by_signature - .get(&SourceMacroCallSignature::new(macro_name, call_range)) - .copied() - .ok_or(()) + let signature = SourceMacroCallSignature::new(macro_name.clone(), call_range); + if let Some(call) = self.call_ids_by_signature.get(&signature).copied() { + return Ok(call); + } + + let definition = self.definition_for_body_token_range(body_token_range)?; + let event_id = self.event_id_for_call_site(call_range).unwrap_or_else(|| { + self.tables + .macro_definitions + .get(definition) + .expect("definition id should point at inserted definition") + .event_id + }); + let resolution = + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition); + let reference = self.push_reference( + event_id, + SourceMacroReferenceSite::ExpansionToken { emitted_token: token_id }, + macro_name.clone(), + call_range, + call_range, + resolution.clone(), + ); + Ok(self.push_call(reference, macro_name, call_range, resolution)) } fn definition_for_call(&self, call: SourceMacroCallId) -> Result { @@ -1110,6 +1184,37 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } + fn definition_for_body_token_range( + &self, + body_token_range: SourceRange, + ) -> Result { + self.tables + .macro_definitions + .iter() + .find(|definition| { + definition.body_tokens.iter().any(|token| token.range == Some(body_token_range)) + }) + .map(|definition| definition.id) + .ok_or(()) + } + + fn event_id_for_call_site(&self, call_range: SourceRange) -> Option { + self.index + .usages + .iter() + .find(|usage| usage.range == call_range) + .map(|usage| usage.event_id) + .or_else(|| { + self.tables + .macro_definitions + .iter() + .find(|definition| { + definition.body_tokens.iter().any(|token| token.range == Some(call_range)) + }) + .map(|definition| definition.event_id) + }) + } + fn definition_body_contains_raw_token( &self, definition: SourceMacroDefinitionId, @@ -1141,6 +1246,215 @@ impl<'a> SourcePreprocModelBuilder<'a> { params.iter().position(|param| param.name.as_ref() == Some(&body_token.value)).ok_or(()) } + fn record_macro_argument( + &mut self, + call: SourceMacroCallId, + argument_index: usize, + argument_token_range: SourceRange, + ) { + let Some(call) = self.tables.macro_calls.get_mut(call) else { + return; + }; + if let Some(argument) = + call.arguments.iter_mut().find(|argument| argument.argument_index == argument_index) + { + argument.argument_range = + merge_source_ranges(argument.argument_range, argument_token_range); + return; + } + call.arguments.push(SourceMacroArgument { + argument_index, + argument_range: Some(argument_token_range), + tokens: Vec::new(), + }); + call.arguments.sort_by_key(|argument| argument.argument_index); + } + + fn build_macro_expansion_graph(&mut self) { + if self.tables.macro_calls.is_empty() { + return; + } + + if self.index.emitted_tokens.is_empty() { + self.mark_all_calls_unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ); + return; + } + + let direct_tokens_by_call = self.direct_emitted_tokens_by_call(); + let child_calls_by_parent = self.child_calls_by_parent(); + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + let mut expansion_tokens_by_call = BTreeMap::new(); + for call in &call_ids { + let mut visiting = Vec::new(); + let tokens = self.recursive_emitted_tokens_for_call( + *call, + &direct_tokens_by_call, + &child_calls_by_parent, + &mut visiting, + ); + expansion_tokens_by_call.insert(*call, tokens); + } + + for call in call_ids { + let tokens = expansion_tokens_by_call.remove(&call).unwrap_or_default(); + let Some(emitted_token_range) = emitted_token_range_from_ids(&tokens) else { + self.mark_call_unavailable( + call, + if tokens.is_empty() { + SourcePreprocUnavailable::ExpansionAuthorityUnavailable + } else { + SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call } + }, + ); + continue; + }; + let Ok(definition) = self.definition_for_call(call) else { + self.mark_call_unavailable( + call, + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, + ); + continue; + }; + + let expansion = SourceMacroExpansionId::new(self.tables.macro_expansions.len()); + self.tables.macro_expansions.push(SourceMacroExpansion { + id: expansion, + call, + definition, + emitted_token_range, + child_calls: child_calls_by_parent.get(&call).cloned().unwrap_or_default(), + status: SourceMacroExpansionStatus::Complete, + }); + if let Some(call) = self.tables.macro_calls.get_mut(call) { + call.expansion = Some(expansion); + call.status = SourceMacroCallStatus::ExpansionAvailable; + } + } + } + + fn direct_emitted_tokens_by_call( + &self, + ) -> BTreeMap> { + let mut tokens_by_call = BTreeMap::>::new(); + for token in self.tables.emitted_tokens.iter() { + let Some(provenance) = self.tables.token_provenance.get(token.provenance) else { + continue; + }; + let call = match provenance { + SourceTokenProvenance::MacroBody { call, .. } + | SourceTokenProvenance::MacroArgument { call, .. } + | SourceTokenProvenance::TokenPaste { call, .. } + | SourceTokenProvenance::Stringification { call, .. } => *call, + SourceTokenProvenance::Source { .. } + | SourceTokenProvenance::Predefine { .. } + | SourceTokenProvenance::Builtin { .. } + | SourceTokenProvenance::Unavailable(_) => continue, + }; + tokens_by_call.entry(call).or_default().push(token.id); + } + tokens_by_call + } + + fn child_calls_by_parent(&mut self) -> BTreeMap> { + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + let mut children = BTreeMap::>::new(); + for child in &call_ids { + let parents = call_ids + .iter() + .copied() + .filter(|parent| parent != child) + .filter(|parent| self.call_site_belongs_to_parent(*child, *parent)) + .collect::>(); + match parents.as_slice() { + [parent] => children.entry(*parent).or_default().push(*child), + [] => {} + _ => self.expansions_partial = true, + } + } + children + } + + fn call_site_belongs_to_parent( + &self, + child: SourceMacroCallId, + parent: SourceMacroCallId, + ) -> bool { + let Some(child) = self.tables.macro_calls.get(child) else { + return false; + }; + if let Ok(parent_definition) = self.definition_for_call(parent) { + if self.definition_body_contains_range(parent_definition, child.call_range) { + return true; + } + } + let Some(parent) = self.tables.macro_calls.get(parent) else { + return false; + }; + parent.arguments.iter().any(|argument| { + argument + .argument_range + .is_some_and(|range| source_range_contains(range, child.call_range)) + }) + } + + fn definition_body_contains_range( + &self, + definition: SourceMacroDefinitionId, + token_range: SourceRange, + ) -> bool { + let Some(definition) = self.tables.macro_definitions.get(definition) else { + return false; + }; + definition.body_tokens.iter().any(|token| token.range == Some(token_range)) + } + + fn recursive_emitted_tokens_for_call( + &mut self, + call: SourceMacroCallId, + direct_tokens_by_call: &BTreeMap>, + child_calls_by_parent: &BTreeMap>, + visiting: &mut Vec, + ) -> Vec { + if visiting.contains(&call) { + self.expansions_partial = true; + return Vec::new(); + } + + visiting.push(call); + let mut tokens = direct_tokens_by_call.get(&call).cloned().unwrap_or_default(); + if let Some(children) = child_calls_by_parent.get(&call) { + for child in children { + tokens.extend(self.recursive_emitted_tokens_for_call( + *child, + direct_tokens_by_call, + child_calls_by_parent, + visiting, + )); + } + } + visiting.pop(); + tokens.sort_by_key(|token| token.raw()); + tokens.dedup(); + tokens + } + + fn mark_all_calls_unavailable(&mut self, reason: SourcePreprocUnavailable) { + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + for call in call_ids { + self.mark_call_unavailable(call, reason.clone()); + } + } + + fn mark_call_unavailable(&mut self, call: SourceMacroCallId, reason: SourcePreprocUnavailable) { + self.expansions_partial = true; + if let Some(call) = self.tables.macro_calls.get_mut(call) { + call.expansion = None; + call.status = SourceMacroCallStatus::ExpansionUnavailable(reason); + } + } + fn source_is_predefine(&self, source: PreprocSourceId) -> bool { self.index.sources.iter().any(|candidate| { candidate.id == source && candidate.origin == PreprocSourceOrigin::Predefine @@ -1348,3 +1662,34 @@ fn boundary_after(directive_range: SourceRange) -> SourcePosition { fn partial_status(is_partial: bool) -> CapabilityStatus { if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } } + +fn emitted_token_range_from_ids( + tokens: &[SourceEmittedTokenId], +) -> Option { + let first = *tokens.first()?; + let last = *tokens.last()?; + let len = last.raw().checked_sub(first.raw())? + 1; + (len == tokens.len()).then_some(SourceEmittedTokenRange { start: first, len }) +} + +fn merge_source_ranges(existing: Option, next: SourceRange) -> Option { + let Some(existing) = existing else { + return Some(next); + }; + if existing.source != next.source { + return Some(existing); + } + Some(SourceRange { + source: existing.source, + range: utils::line_index::TextRange::new( + existing.range.start().min(next.range.start()), + existing.range.end().max(next.range.end()), + ), + }) +} + +fn source_range_contains(outer: SourceRange, inner: SourceRange) -> bool { + outer.source == inner.source + && outer.range.start() <= inner.range.start() + && inner.range.end() <= outer.range.end() +} From f879a658b3bde49bdfe89c376bd7b118a768c62a Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 21:01:30 +0800 Subject: [PATCH 11/80] feat(hir): expose macro expansion queries --- crates/hir/src/preproc.rs | 226 +++++++++++++++++++++++++++++++++++++- 1 file changed, 224 insertions(+), 2 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 84474409..d525beae 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,9 +1,14 @@ use std::collections::BTreeMap; use preproc::source::{ - CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceIncludeChainEntry, - SourceIncludeDirectiveId, SourceIncludeStatus, + CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenRange, + SourceIncludeChainEntry, SourceIncludeDirectiveId, SourceIncludeStatus, + SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, + SourceMacroCallStatus as SourceMacroCallStatusFact, SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, + SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, + SourceMacroExpansionQuery as SourceMacroExpansionQueryFact, + SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, SourceMacroReference as SourceMacroReferenceFact, SourceMacroReferenceId, SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePosition, @@ -87,6 +92,8 @@ macro_rules! mapped_preproc_id { mapped_preproc_id!(MacroDefinitionId, SourceMacroDefinitionId); mapped_preproc_id!(MacroReferenceId, SourceMacroReferenceId); mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); +mapped_preproc_id!(MacroCallId, SourceMacroCallId); +mapped_preproc_id!(MacroExpansionId, SourceMacroExpansionId); impl MacroDefinitionId { fn core_id(self) -> SourceMacroDefinitionId { @@ -204,6 +211,48 @@ pub struct MacroReferenceDefinitions { pub capability: PreprocAvailability, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroCall { + pub id: MacroCallId, + pub reference_id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub directive_range: TextRange, + pub range: TextRange, + pub callee: MacroResolution, + pub expansion: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansion { + pub id: MacroExpansionId, + pub call: MacroCall, + pub definition_id: MacroDefinitionId, + pub emitted_token_range: SourceEmittedTokenRange, + pub child_calls: Vec, + pub capability: PreprocAvailability, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroExpansionQuery { + Available(MacroExpansion), + Unavailable(MacroExpansionUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansionUnavailable { + pub call: MacroCall, + pub reason: PreprocUnavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecursiveMacroExpansion { + pub root_call: MacroCall, + pub expansions: Vec, + pub unavailable: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct MacroDefinitionKey { file_id: FileId, @@ -567,6 +616,78 @@ pub fn macro_reference_definitions_at( })) } +pub fn immediate_macro_expansion_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + return Ok(None); + }; + let call = map_macro_call(mapped, call_fact)?; + + Ok(Some(match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { + return Ok(Some(MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + ), + }))); + }; + MacroExpansionQuery::Available(map_macro_expansion(mapped, expansion)?) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source(reason), + }) + } + })) +} + +pub fn recursive_macro_expansion_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + return Ok(None); + }; + let root_call = map_macro_call(mapped, call_fact)?; + let recursive = mapped.model.recursive_macro_expansion(call_fact.id); + let expansions = recursive + .expansions + .into_iter() + .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) + .map(|expansion| map_macro_expansion(mapped, expansion)) + .collect::>>()?; + let unavailable = recursive + .unavailable + .into_iter() + .map(|unavailable| { + let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, + ), + }); + }; + Ok(MacroExpansionUnavailable { + call: map_macro_call(mapped, call)?, + reason: PreprocUnavailable::Source(unavailable.reason), + }) + }) + .collect::>>()?; + + Ok(Some(RecursiveMacroExpansion { root_call, expansions, unavailable })) +} + pub fn macro_references( db: &dyn SourceRootDb, file_id: FileId, @@ -836,6 +957,58 @@ fn map_macro_reference( }) } +fn map_macro_call( + mapped: &MappedSourcePreprocModel, + call: &SourceMacroCallFact, +) -> PreprocResult { + let (source, range) = map_mapped_source_range(mapped, call.call_range)?; + Ok(MacroCall { + id: call.id.into(), + reference_id: call.reference.into(), + file_id: source.file_id(), + source, + capability: macro_call_availability(&call.status), + directive_range: range, + range, + callee: map_macro_resolution(mapped, &call.callee)?, + expansion: call.expansion.map(Into::into), + }) +} + +fn map_macro_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let Some(call) = mapped.model.macro_calls().get(expansion.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroCall { + call: expansion.call, + }), + }); + }; + Ok(MacroExpansion { + id: expansion.id.into(), + call: map_macro_call(mapped, call)?, + definition_id: expansion.definition.into(), + emitted_token_range: expansion.emitted_token_range, + child_calls: expansion.child_calls.iter().copied().map(Into::into).collect(), + capability: macro_expansion_availability(&expansion.status), + }) +} + +fn source_macro_call_at( + mapped: &MappedSourcePreprocModel, + file_id: FileId, + offset: TextSize, +) -> Option<&SourceMacroCallFact> { + mapped.model.macro_calls().iter().find(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == file_id && range_contains_offset(range, offset) + }) +} + fn map_macro_resolution( mapped: &MappedSourcePreprocModel, resolution: &SourceMacroResolutionFact, @@ -908,6 +1081,24 @@ fn capability_status(status: &CapabilityStatus) -> PreprocAvailability { } } +fn macro_call_availability(status: &SourceMacroCallStatusFact) -> PreprocAvailability { + match status { + SourceMacroCallStatusFact::ExpansionAvailable => PreprocAvailability::Complete, + SourceMacroCallStatusFact::ExpansionUnavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + +fn macro_expansion_availability(status: &SourceMacroExpansionStatusFact) -> PreprocAvailability { + match status { + SourceMacroExpansionStatusFact::Complete => PreprocAvailability::Complete, + SourceMacroExpansionStatusFact::Unavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + fn unavailable_error(reason: SourcePreprocUnavailable) -> PreprocError { PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) } } @@ -1210,6 +1401,37 @@ endmodule assert_eq!(resolved_file, Some(HEADER)); } + #[test] + fn preproc_macro_expansion_queries_map_call_ranges() { + let root_text = r#"`define OBJ 8 +`define LEAF 3 +`define WRAP `LEAF +module top; +localparam int A = `OBJ; +localparam int B = `WRAP; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let immediate = + immediate_macro_expansion_at(&db, TOP, offset(root_text, "`OBJ")).unwrap().unwrap(); + let MacroExpansionQuery::Available(immediate) = immediate else { + panic!("object-like macro expansion should be available"); + }; + assert_eq!(immediate.call.file_id, TOP); + assert_eq!(text_at_range(root_text, immediate.call.range), "`OBJ"); + assert_eq!(immediate.emitted_token_range.len, 1); + assert!(matches!(immediate.capability, PreprocAvailability::Complete)); + + let recursive = + recursive_macro_expansion_at(&db, TOP, offset(root_text, "`WRAP")).unwrap().unwrap(); + assert_eq!(recursive.root_call.file_id, TOP); + assert_eq!(text_at_range(root_text, recursive.root_call.range), "`WRAP"); + assert_eq!(recursive.expansions.len(), 2); + assert!(recursive.expansions.iter().any(|expansion| !expansion.child_calls.is_empty())); + assert!(recursive.unavailable.is_empty()); + } + #[test] fn preproc_nested_include_chain_maps_to_file_ids() { let root_text = r#"`include "defs.vh" From d0d8c32a94df0482a7e9b97b280b21545467b555 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 21:20:31 +0800 Subject: [PATCH 12/80] feat(hir): bridge expansion provenance to source maps --- crates/hir/src/base_db/source_db.rs | 164 ++++++++++- crates/hir/src/preproc.rs | 431 +++++++++++++++++++++++++++- crates/ide/src/diagnostics.rs | 64 ++++- 3 files changed, 638 insertions(+), 21 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index cbf0f7ea..2e6a9602 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -4,8 +4,8 @@ use std::{ }; use preproc::source::{ - PreprocSourceId, SourceMacroExpansionId, SourcePreprocError, SourcePreprocModel, - SourcePreprocUnavailable, SourceRange, + PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, + SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, }; use rustc_hash::{FxHashMap, FxHashSet}; use smol_str::SmolStr; @@ -145,6 +145,7 @@ pub struct MappedSourcePreprocModel { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PreprocSourceMap { entries: FxHashMap, + expansion_entries: FxHashMap, text_lengths: FxHashMap, range_offsets: FxHashMap, } @@ -156,6 +157,16 @@ pub enum PreprocSourceMapping { Unmapped(SourcePreprocUnavailable), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocExpansionMapping { + pub file_id: FileId, + pub path: VfsPath, + pub origin: PreprocVirtualOrigin, + pub text: String, + pub emitted_range: SourceEmittedTokenRange, + token_ranges: FxHashMap, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocVirtualOrigin { Predefines { profile: Option }, @@ -183,6 +194,15 @@ pub enum PreprocSourceMapError { mapped_range: TextRange, text_len: usize, }, + MissingExpansionVirtualFile { + expansion: SourceMacroExpansionId, + }, + MissingEmittedToken { + token: SourceEmittedTokenId, + }, + MissingEmittedTokenRange { + range: SourceEmittedTokenRange, + }, } impl PreprocSourceMap { @@ -227,6 +247,73 @@ impl PreprocSourceMap { self.entries.get(&source) } + pub fn insert_expansion_virtual_file( + &mut self, + expansion: SourceMacroExpansionId, + file_id: FileId, + path: VfsPath, + text: String, + emitted_range: SourceEmittedTokenRange, + token_ranges: FxHashMap, + ) { + self.expansion_entries.insert( + expansion, + PreprocExpansionMapping { + file_id, + path, + origin: PreprocVirtualOrigin::Expansion { expansion }, + text, + emitted_range, + token_ranges, + }, + ); + } + + pub fn expansion(&self, expansion: SourceMacroExpansionId) -> Option<&PreprocExpansionMapping> { + self.expansion_entries.get(&expansion) + } + + pub fn expansion_source( + &self, + expansion: SourceMacroExpansionId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + Ok(PreprocSourceMapping::VirtualFile { + file_id: entry.file_id, + path: entry.path.clone(), + origin: entry.origin.clone(), + }) + } + + pub fn emitted_token_range( + &self, + expansion: SourceMacroExpansionId, + emitted_range: SourceEmittedTokenRange, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + expansion_text_range(entry, emitted_range) + .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) + } + + pub fn emitted_token_text_range( + &self, + expansion: SourceMacroExpansionId, + token: SourceEmittedTokenId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + entry + .token_ranges + .get(&token) + .copied() + .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) + } + pub fn file_id(&self, source: PreprocSourceId) -> Result { match self.get(source) { Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), @@ -576,6 +663,76 @@ fn shift_text_range(range: TextRange, offset: usize) -> Option { )) } +fn expansion_text_range( + entry: &PreprocExpansionMapping, + emitted_range: SourceEmittedTokenRange, +) -> Option { + if emitted_range.len == 0 { + return Some(TextRange::empty(TextSize::from(0))); + } + + let start = emitted_range.start; + let end = SourceEmittedTokenId::new(start.raw().checked_add(emitted_range.len - 1)?); + let start_range = entry.token_ranges.get(&start)?; + let end_range = entry.token_ranges.get(&end)?; + Some(TextRange::new(start_range.start(), end_range.end())) +} + +fn materialize_expansion_virtual_files( + db: &dyn SourceRootDb, + profile_id: Option, + model: &SourcePreprocModel, + source_map: &mut PreprocSourceMap, +) { + for expansion in model.macro_expansions().iter() { + let Some((text, token_ranges)) = + materialized_expansion_text_and_ranges(model, expansion.emitted_token_range) + else { + continue; + }; + let path = preproc_virtual_expansion_path(profile_id, expansion.id); + let file_id = preproc_virtual_file_id(db, &path); + source_map.insert_expansion_virtual_file( + expansion.id, + file_id, + path, + text, + expansion.emitted_token_range, + token_ranges, + ); + } +} + +fn materialized_expansion_text_and_ranges( + model: &SourcePreprocModel, + emitted_range: SourceEmittedTokenRange, +) -> Option<(String, FxHashMap)> { + let mut text = String::new(); + let mut token_ranges = FxHashMap::default(); + + for raw in + emitted_range.start.raw()..emitted_range.start.raw().checked_add(emitted_range.len)? + { + let token_id = SourceEmittedTokenId::new(raw); + let token = model.emitted_tokens().get(token_id)?; + if !text.is_empty() { + text.push(' '); + } + let start = text.len(); + text.push_str(token.text.as_str()); + let end = text.len(); + token_ranges.insert( + token_id, + TextRange::new( + TextSize::from(u32::try_from(start).ok()?), + TextSize::from(u32::try_from(end).ok()?), + ), + ); + } + + Some((text, token_ranges)) +} + fn syntax_tree_options_for_file( db: &dyn SourceRootDb, file_id: FileId, @@ -830,7 +987,7 @@ fn source_preproc_model( return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; - let source_map = match source_preproc_file_ids(db, file_id, profile_id, &trace, &options) { + let mut source_map = match source_preproc_file_ids(db, file_id, profile_id, &trace, &options) { Ok(source_map) => source_map, Err(err) => return Arc::new(Err(err)), }; @@ -838,6 +995,7 @@ fn source_preproc_model( Ok(model) => model, Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), }; + materialize_expansion_virtual_files(db, profile_id, &model, &mut source_map); Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) } diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index d525beae..7c5024a3 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; use preproc::source::{ - CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenRange, - SourceIncludeChainEntry, SourceIncludeDirectiveId, SourceIncludeStatus, - SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, + CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenId, + SourceEmittedTokenRange, SourceIncludeChainEntry, SourceIncludeDirectiveId, + SourceIncludeStatus, SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, SourceMacroCallStatus as SourceMacroCallStatusFact, SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, @@ -13,6 +13,7 @@ use preproc::source::{ SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePosition, SourcePreprocError, SourcePreprocUnavailable, SourceRange, + SourceTokenProvenance as SourceTokenProvenanceFact, }; use rustc_hash::FxHashSet; use smol_str::SmolStr; @@ -230,10 +231,78 @@ pub struct MacroExpansion { pub call: MacroCall, pub definition_id: MacroDefinitionId, pub emitted_token_range: SourceEmittedTokenRange, + pub virtual_source: MappedPreprocSource, + pub virtual_range: TextRange, pub child_calls: Vec, pub capability: PreprocAvailability, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansionProvenance { + pub expansion: MacroExpansion, + pub tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmittedTokenProvenance { + pub token: SourceEmittedTokenId, + pub text: SmolStr, + pub virtual_range: TextRange, + pub provenance: TokenProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenProvenance { + SourceToken { + source: MappedPreprocSource, + range: TextRange, + }, + MacroBody { + call: MacroCall, + definition_id: MacroDefinitionId, + source: MappedPreprocSource, + range: TextRange, + }, + MacroArgument { + call: MacroCall, + argument_index: usize, + source: MappedPreprocSource, + range: TextRange, + }, + Predefine { + source: MappedPreprocSource, + }, + Builtin { + name: SmolStr, + }, + Unavailable(PreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiagnosticProvenance { + SourceToken { + source: MappedPreprocSource, + range: TextRange, + }, + MacroBody { + call: MacroCall, + definition_id: MacroDefinitionId, + source: MappedPreprocSource, + range: TextRange, + }, + MacroArgument { + call: MacroCall, + argument_index: usize, + source: MappedPreprocSource, + range: TextRange, + }, + VirtualExpansion { + source: MappedPreprocSource, + range: TextRange, + }, + Unavailable(PreprocUnavailable), +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MacroExpansionQuery { Available(MacroExpansion), @@ -688,6 +757,58 @@ pub fn recursive_macro_expansion_at( Ok(Some(RecursiveMacroExpansion { root_call, expansions, unavailable })) } +pub fn macro_expansion_provenance_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + return Ok(None); + }; + macro_expansion_provenance_for_call(mapped, call_fact) +} + +pub fn macro_expansion_provenance_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { + return Ok(None); + }; + macro_expansion_provenance_for_call(mapped, call_fact) +} + +pub fn diagnostic_provenance_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mapped = db.source_preproc_model(file_id); + let mapped = mapped_result(mapped.as_ref())?; + let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { + return Ok(None); + }; + + match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(_) => { + let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? else { + return Ok(Some(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + )))); + }; + Ok(Some(diagnostic_target_for_expansion(&provenance))) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + Ok(Some(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason)))) + } + } +} + pub fn macro_references( db: &dyn SourceRootDb, file_id: FileId, @@ -991,11 +1112,31 @@ fn map_macro_expansion( call: map_macro_call(mapped, call)?, definition_id: expansion.definition.into(), emitted_token_range: expansion.emitted_token_range, + virtual_source: map_expansion_virtual_source(mapped, expansion.id)?, + virtual_range: mapped + .source_map + .emitted_token_range(expansion.id, expansion.emitted_token_range) + .map_err(PreprocError::SourceMap)?, child_calls: expansion.child_calls.iter().copied().map(Into::into).collect(), capability: macro_expansion_availability(&expansion.status), }) } +fn map_expansion_virtual_source( + mapped: &MappedSourcePreprocModel, + expansion: SourceMacroExpansionId, +) -> PreprocResult { + match mapped.source_map.expansion_source(expansion).map_err(PreprocError::SourceMap)? { + PreprocSourceMapping::VirtualFile { file_id, path, origin } => { + Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) + } + PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), + PreprocSourceMapping::Unmapped(reason) => { + Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) + } + } +} + fn source_macro_call_at( mapped: &MappedSourcePreprocModel, file_id: FileId, @@ -1009,6 +1150,148 @@ fn source_macro_call_at( }) } +fn source_macro_call_intersecting_range( + mapped: &MappedSourcePreprocModel, + file_id: FileId, + source_range: TextRange, +) -> Option<&SourceMacroCallFact> { + mapped.model.macro_calls().iter().find(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == file_id && range.intersect(source_range).is_some() + }) +} + +fn macro_expansion_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult> { + let SourceMacroExpansionQueryFact::Available(expansion_id) = + mapped.model.immediate_macro_expansion(call_fact.id) + else { + return Ok(None); + }; + let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { + return Ok(None); + }; + let expansion = map_macro_expansion(mapped, expansion)?; + let mut tokens = Vec::new(); + for token_id in emitted_token_ids(expansion.emitted_token_range) { + let Some(token) = mapped.model.emitted_tokens().get(token_id) else { + return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { + token: token_id, + })); + }; + let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { + return Err(unavailable_error( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + )); + }; + tokens.push(EmittedTokenProvenance { + token: token_id, + text: token.text.clone(), + virtual_range: mapped + .source_map + .emitted_token_text_range(expansion_id, token_id) + .map_err(PreprocError::SourceMap)?, + provenance: map_token_provenance(mapped, provenance)?, + }); + } + + Ok(Some(MacroExpansionProvenance { expansion, tokens })) +} + +fn emitted_token_ids(range: SourceEmittedTokenRange) -> impl Iterator { + let start = range.start.raw(); + let end = start.saturating_add(range.len); + (start..end).map(SourceEmittedTokenId::new) +} + +fn map_token_provenance( + mapped: &MappedSourcePreprocModel, + provenance: &SourceTokenProvenanceFact, +) -> PreprocResult { + Ok(match provenance { + SourceTokenProvenanceFact::Source { token_range } => { + let (source, range) = map_mapped_source_range(mapped, *token_range)?; + TokenProvenance::SourceToken { source, range } + } + SourceTokenProvenanceFact::MacroBody { definition, body_token_range, call } => { + let call = mapped_macro_call(mapped, *call)?; + let (source, range) = map_mapped_source_range(mapped, *body_token_range)?; + TokenProvenance::MacroBody { call, definition_id: (*definition).into(), source, range } + } + SourceTokenProvenanceFact::MacroArgument { call, argument_index, argument_token_range } => { + let call = mapped_macro_call(mapped, *call)?; + let (source, range) = map_mapped_source_range(mapped, *argument_token_range)?; + TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } + } + SourceTokenProvenanceFact::TokenPaste { .. } + | SourceTokenProvenanceFact::Stringification { .. } => TokenProvenance::Unavailable( + PreprocUnavailable::Source(SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance), + ), + SourceTokenProvenanceFact::Predefine { source } => { + TokenProvenance::Predefine { source: map_mapped_source_id(mapped, *source)? } + } + SourceTokenProvenanceFact::Builtin { name } => { + TokenProvenance::Builtin { name: name.clone() } + } + SourceTokenProvenanceFact::Unavailable(reason) => { + TokenProvenance::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +fn mapped_macro_call( + mapped: &MappedSourcePreprocModel, + call: SourceMacroCallId, +) -> PreprocResult { + let Some(call) = mapped.model.macro_calls().get(call) else { + return Err(unavailable_error(SourcePreprocUnavailable::MissingMacroCall { call })); + }; + map_macro_call(mapped, call) +} + +fn diagnostic_target_for_expansion(provenance: &MacroExpansionProvenance) -> DiagnosticProvenance { + let mut saw_unavailable = None; + for token in &provenance.tokens { + match &token.provenance { + TokenProvenance::SourceToken { source, range } => { + return DiagnosticProvenance::SourceToken { source: source.clone(), range: *range }; + } + TokenProvenance::MacroBody { call, definition_id, source, range } => { + return DiagnosticProvenance::MacroBody { + call: call.clone(), + definition_id: *definition_id, + source: source.clone(), + range: *range, + }; + } + TokenProvenance::MacroArgument { call, argument_index, source, range } => { + return DiagnosticProvenance::MacroArgument { + call: call.clone(), + argument_index: *argument_index, + source: source.clone(), + range: *range, + }; + } + TokenProvenance::Unavailable(reason) => { + saw_unavailable = Some(reason.clone()); + } + TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } => {} + } + } + + saw_unavailable.map_or_else( + || DiagnosticProvenance::VirtualExpansion { + source: provenance.expansion.virtual_source.clone(), + range: provenance.expansion.virtual_range, + }, + DiagnosticProvenance::Unavailable, + ) +} + fn map_macro_resolution( mapped: &MappedSourcePreprocModel, resolution: &SourceMacroResolutionFact, @@ -1241,21 +1524,28 @@ mod tests { use rustc_hash::FxHashSet; use triomphe::Arc; use utils::{ - line_index::TextSize, + get::Get, + line_index::{TextRange, TextSize}, paths::{AbsPathBuf, Utf8PathBuf}, }; use vfs::{FileId, FileSet, VfsPath, anchored_path::AnchoredPath}; use super::*; - use crate::base_db::{ - diagnostics_config::DiagnosticsConfig, - project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, - salsa::{self, Durability}, - source_db::{ - FileLoader, SourceDb, SourceDbStorage, SourceFileKind, SourceRootDb, - SourceRootDbStorage, + use crate::{ + base_db::{ + diagnostics_config::DiagnosticsConfig, + project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + salsa::{self, Durability}, + source_db::{ + FileLoader, PreprocVirtualOrigin, SourceDb, SourceDbStorage, SourceFileKind, + SourceRootDb, SourceRootDbStorage, + }, + source_root::{SourceRoot, SourceRootId}, }, - source_root::{SourceRoot, SourceRootId}, + container::InFile, + db::{HirDb, HirDbStorage, InternDbStorage}, + hir_def::module::ModuleId, + source_map::IsSrc, }; const TOP: FileId = FileId(0); @@ -1264,7 +1554,7 @@ mod tests { const ROOT: SourceRootId = SourceRootId(0); const PROFILE: CompilationProfileId = CompilationProfileId(0); - #[salsa::database(SourceDbStorage, SourceRootDbStorage)] + #[salsa::database(SourceDbStorage, SourceRootDbStorage, InternDbStorage, HirDbStorage)] #[derive(Default)] struct TestDb { storage: salsa::Storage, @@ -1432,6 +1722,121 @@ endmodule assert!(recursive.unavailable.is_empty()); } + #[test] + fn preproc_macro_expansion_materializes_virtual_source_and_token_provenance() { + let root_text = r#"`define MAKE_DECL(name) logic name; +module top; +`MAKE_DECL(generated) +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = macro_expansion_provenance_at(&db, TOP, offset(root_text, "`MAKE_DECL")) + .unwrap() + .unwrap(); + let MappedPreprocSource::VirtualFile { path, origin, .. } = + &provenance.expansion.virtual_source + else { + panic!("macro expansion should expose a virtual expansion source"); + }; + assert_eq!( + path, + &VfsPath::new_virtual_path("/__vide/preproc/profile-0/expansion/0.sv".to_owned()) + ); + assert_eq!( + origin, + &PreprocVirtualOrigin::Expansion { expansion: SourceMacroExpansionId::new(0) } + ); + + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + let virtual_file = mapped.source_map.expansion(SourceMacroExpansionId::new(0)).unwrap(); + assert_eq!(virtual_file.text, "logic generated ;"); + assert_eq!(provenance.expansion.virtual_range, TextRange::new(0.into(), 17.into())); + + let logic = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "logic") + .expect("macro body token should be present"); + let TokenProvenance::MacroBody { source, range, .. } = &logic.provenance else { + panic!("logic should come from the macro body: {logic:?}"); + }; + assert_eq!(source.file_id(), TOP); + assert_eq!(text_at_range(root_text, *range), "logic"); + assert_eq!(logic.virtual_range, TextRange::new(0.into(), 5.into())); + + let generated = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "generated") + .expect("macro argument token should be present"); + let TokenProvenance::MacroArgument { source, range, argument_index, .. } = + &generated.provenance + else { + panic!("generated should come from the macro argument: {generated:?}"); + }; + assert_eq!(*argument_index, 0); + assert_eq!(source.file_id(), TOP); + assert_eq!(text_at_range(root_text, *range), "generated"); + assert_eq!(generated.virtual_range, TextRange::new(6.into(), 15.into())); + } + + #[test] + fn macro_generated_declaration_hir_range_resolves_to_expanded_token_provenance() { + let root_text = r#"`define MAKE_DECL(name) logic name; +module top; +`MAKE_DECL(generated) +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); + let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); + let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); + let (declaration_id, _) = + module.declarations.iter().next().expect("generated declaration should lower to HIR"); + let declaration_src = module_src_map + .get(declaration_id) + .expect("generated declaration should keep a source-map range"); + + let provenance = macro_expansion_provenance_for_range(&db, TOP, declaration_src.range()) + .unwrap() + .unwrap(); + + assert_eq!(provenance.expansion.emitted_token_range.len, 3); + assert!( + provenance + .tokens + .iter() + .any(|token| matches!(token.provenance, TokenProvenance::MacroBody { .. })) + ); + assert!( + provenance + .tokens + .iter() + .any(|token| matches!(token.provenance, TokenProvenance::MacroArgument { .. })) + ); + } + + #[test] + fn diagnostic_provenance_returns_unavailable_for_unsupported_expansion_mapping() { + let root_text = r#"`define JOIN(a,b) a``b +module top; +wire `JOIN(foo,bar); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let call_range = + TextRange::new(offset(root_text, "`JOIN"), offset_after(root_text, "`JOIN(foo,bar)")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, call_range).unwrap().unwrap(); + assert!(matches!( + provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(_)) + )); + } + #[test] fn preproc_nested_include_chain_maps_to_file_ids() { let root_text = r#"`include "defs.vh" diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 7193e2a0..4d74f6f6 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -334,10 +334,22 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> let Some(module_name) = instantiation.module_name.as_ref() else { continue; }; - let range = src_map - .get(instantiation_id) - .map(|src| src.range()) - .unwrap_or_else(|| TextRange::empty(TextSize::new(0))); + let Some(src) = src_map.get(instantiation_id) else { + continue; + }; + let mut diag_file_id = file_id; + let mut range = src.range(); + if let Ok(Some(provenance)) = + hir::preproc::diagnostic_provenance_for_range(db, file_id, range) + { + let Some((target_file_id, target_range)) = + diagnostic_preproc_target_file_range(&provenance) + else { + continue; + }; + diag_file_id = target_file_id; + range = target_range; + } match resolve_module_name(db, file_id, module_name) { ModuleResolution::Ambiguous { candidates, kind } => { @@ -348,7 +360,7 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> kind, ); diagnostics.push(AMBIGUOUS_MODULE_INSTANTIATION.diagnostic( - file_id, + diag_file_id, range, severity, message, @@ -366,6 +378,20 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> diagnostics } +fn diagnostic_preproc_target_file_range( + provenance: &hir::preproc::DiagnosticProvenance, +) -> Option<(FileId, TextRange)> { + match provenance { + hir::preproc::DiagnosticProvenance::SourceToken { source, range } + | hir::preproc::DiagnosticProvenance::MacroBody { source, range, .. } + | hir::preproc::DiagnosticProvenance::MacroArgument { source, range, .. } + | hir::preproc::DiagnosticProvenance::VirtualExpansion { source, range } => { + Some((source.file_id(), *range)) + } + hir::preproc::DiagnosticProvenance::Unavailable(_) => None, + } +} + fn inactive_preprocessor_branch_diagnostics(db: &RootDb, file_id: FileId) -> Vec { if !vide_diagnostics_enabled(db) { return Vec::new(); @@ -597,6 +623,34 @@ mod tests { ); } + #[test] + fn preproc_macro_generated_instantiation_diagnostic_uses_macro_body_provenance() { + let top = "`define MAKE child u();\nmodule top;\n `MAKE\nendmodule\n"; + let db = db_with_files( + &[ + ("/project/a/child.sv", "module child; endmodule\n"), + ("/project/b/child.sv", "module child; endmodule\n"), + ("/project/top.sv", top), + ], + false, + ); + + let diagnostics = diagnostics(&db, FileId(2)); + let diagnostic = diagnostics + .iter() + .find(|diag| { + diag.source == DiagnosticSource::Vide + && diag.name == AMBIGUOUS_MODULE_INSTANTIATION.name + }) + .unwrap_or_else(|| { + panic!("expected generated instantiation diagnostic: {diagnostics:?}") + }); + + assert_eq!(diagnostic.file_id, FileId(2)); + assert_eq!(diagnostic.range, range_of(top, "child")); + assert_ne!(diagnostic.range, range_of(top, "`MAKE")); + } + #[test] fn semantic_diagnostics_suppress_vide_ambiguous_module_warning() { let db = db_with_files( From bde6ad98354e67f1a6a8b72605e54c2a15698a7e Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 22:28:51 +0800 Subject: [PATCH 13/80] feat(hir): bridge preproc provenance through source maps --- crates/hir/src/base_db/source_db.rs | 38 +- crates/hir/src/preproc.rs | 1161 ++++++++++++++++++----- crates/preproc/src/source/provenance.rs | 77 ++ 3 files changed, 1041 insertions(+), 235 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 2e6a9602..93c30689 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -5,7 +5,7 @@ use std::{ use preproc::source::{ PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, - SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, + SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, }; use rustc_hash::{FxHashMap, FxHashSet}; use smol_str::SmolStr; @@ -325,6 +325,37 @@ impl PreprocSourceMap { } } + pub fn source_positions_for_file_offset( + &self, + file_id: FileId, + offset: TextSize, + ) -> Vec { + let mut positions = self + .entries + .iter() + .filter_map(|(source, mapping)| { + let mapped_file_id = match mapping { + PreprocSourceMapping::RealFile(mapped_file_id) + | PreprocSourceMapping::VirtualFile { file_id: mapped_file_id, .. } => { + *mapped_file_id + } + PreprocSourceMapping::Unmapped(_) => return None, + }; + if mapped_file_id != file_id { + return None; + } + + let range_offset = self.range_offsets.get(source).copied().unwrap_or(0); + let source_offset = unshift_text_size(offset, range_offset)?; + let text_len = self.text_lengths.get(source).copied()?; + (usize::from(source_offset) <= text_len) + .then_some(SourcePosition { source: *source, offset: source_offset }) + }) + .collect::>(); + positions.sort_by_key(|position| position.source.raw()); + positions + } + pub fn map_range(&self, source_range: SourceRange) -> Result { match self.get(source_range.source) { Some(PreprocSourceMapping::RealFile(_)) @@ -663,6 +694,11 @@ fn shift_text_range(range: TextRange, offset: usize) -> Option { )) } +fn unshift_text_size(offset: TextSize, range_offset: usize) -> Option { + let offset = usize::from(offset).checked_sub(range_offset)?; + Some(TextSize::from(u32::try_from(offset).ok()?)) +} + fn expansion_text_range( entry: &PreprocExpansionMapping, emitted_range: SourceEmittedTokenRange, diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 7c5024a3..c2b9f25d 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -11,9 +11,8 @@ use preproc::source::{ SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, SourceMacroReference as SourceMacroReferenceFact, SourceMacroReferenceId, SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, - SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePosition, - SourcePreprocError, SourcePreprocUnavailable, SourceRange, - SourceTokenProvenance as SourceTokenProvenanceFact, + SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePreprocError, + SourcePreprocUnavailable, SourceRange, SourceTokenProvenance as SourceTokenProvenanceFact, }; use rustc_hash::FxHashSet; use smol_str::SmolStr; @@ -62,6 +61,10 @@ pub enum PreprocError { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocUnavailable { Source(SourcePreprocUnavailable), + AmbiguousMacroReferenceContexts { contexts: usize }, + AmbiguousMacroExpansionContexts { contexts: usize }, + AmbiguousDiagnosticProvenance { targets: usize }, + AmbiguousIncludeTargets { targets: usize }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -96,12 +99,6 @@ mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); mapped_preproc_id!(MacroCallId, SourceMacroCallId); mapped_preproc_id!(MacroExpansionId, SourceMacroExpansionId); -impl MacroDefinitionId { - fn core_id(self) -> SourceMacroDefinitionId { - self.0 - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum MappedPreprocSource { RealFile { file_id: FileId }, @@ -206,9 +203,9 @@ pub struct MacroReferenceResolution { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroReferenceDefinitions { - pub reference: MacroReference, + pub references: Vec, + pub range: TextRange, pub definitions: Vec, - pub resolution: MacroResolution, pub capability: PreprocAvailability, } @@ -306,6 +303,7 @@ pub enum DiagnosticProvenance { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MacroExpansionQuery { Available(MacroExpansion), + Ambiguous(Vec), Unavailable(MacroExpansionUnavailable), } @@ -484,16 +482,35 @@ pub fn visible_macros_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let position = root_position(mapped, offset)?; + let mut definitions = Vec::new(); + let mut first_error = None; + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; - mapped - .model - .visible_macros_at(position) - .into_iter() - .map(|definition| map_macro_definition(mapped, definition)) - .collect() + for position in mapped.source_map.source_positions_for_file_offset(file_id, offset) { + for definition in mapped.model.visible_macros_at(position) { + match map_macro_definition(mapped, definition) { + Ok(definition) => push_unique_macro_definition(&mut definitions, definition), + Err(error) => record_first_error(&mut first_error, error), + } + } + } + } + + if definitions.is_empty() + && let Some(error) = first_error + { + return Err(error); + } + + Ok(definitions) } pub fn visible_macro_names_at( @@ -501,12 +518,8 @@ pub fn visible_macro_names_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let position = root_position(mapped, offset)?; - let mut names = UniqVec::::default(); - for definition in mapped.model.visible_macros_at(position) { + for definition in visible_macros_at(db, file_id, offset)? { names.push_unique(definition.name.clone()); } for name in configured_predefine_names(db, file_id) { @@ -566,61 +579,107 @@ pub fn macro_usage_resolution_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; + let mut resolutions = macro_usage_resolutions_at(db, file_id, offset)?; + match resolutions.len() { + 0 => Ok(None), + 1 => Ok(resolutions.pop()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts }, + }), + } +} - for reference in mapped.model.macro_references().iter() { - let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { - continue; +pub fn macro_usage_resolutions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut resolutions = Vec::new(); + let mut first_error = None; + let mut unavailable_contexts = 0; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } }; - let mapped_reference = map_macro_reference(mapped, reference)?; - if mapped_reference.file_id != file_id - || !range_contains_offset(mapped_reference.range, offset) - { - continue; - } - let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = - &reference.resolution - else { - return match &reference.resolution { - SourceMacroResolutionFact::Undefined => Ok(None), - SourceMacroResolutionFact::Unavailable(reason) => { - Err(unavailable_error(reason.clone())) + for reference in mapped.model.macro_references().iter() { + let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { + continue; + }; + match mapped_source_range_contains_offset(mapped, reference.name_range, file_id, offset) + { + Ok(true) => {} + Ok(false) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; } - SourceMacroResolutionFact::Resolved { .. } => unreachable!(), + } + + let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = + &reference.resolution + else { + if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { + unavailable_contexts += 1; + record_first_error(&mut first_error, unavailable_error(reason.clone())); + } + continue; }; - }; - let definition_fact = - mapped.model.macro_definitions().get(*definition).ok_or_else(|| { - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, - )) - })?; - let definition = map_macro_definition(mapped, definition_fact)?; - let definition_provenance = - map_definition_provenance_from_definition(mapped, definition_fact)?; - let include_chain = map_include_chain(mapped, include_chain)?; - - return Ok(Some(MacroUsageResolution { - usage: MacroUsage { - reference_id: mapped_reference.id, - source: mapped_reference.source, - capability: mapped_reference.capability.clone(), - file_id: mapped_reference.file_id, - name: mapped_reference.name, - usage_index, - directive_range: mapped_reference.directive_range, - range: mapped_reference.range, - resolution: mapped_reference.resolution, + let mapped_reference = map_macro_reference(mapped, reference)?; + let definition_fact = + mapped.model.macro_definitions().get(*definition).ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )) + })?; + let definition = map_macro_definition(mapped, definition_fact)?; + let definition_provenance = + map_definition_provenance_from_definition(mapped, definition_fact)?; + let include_chain = map_include_chain(mapped, include_chain)?; + + push_unique_macro_usage_resolution( + &mut resolutions, + MacroUsageResolution { + usage: MacroUsage { + reference_id: mapped_reference.id, + source: mapped_reference.source, + capability: mapped_reference.capability.clone(), + file_id: mapped_reference.file_id, + name: mapped_reference.name, + usage_index, + directive_range: mapped_reference.directive_range, + range: mapped_reference.range, + resolution: mapped_reference.resolution, + }, + definition, + definition_provenance, + include_chain, + }, + ); + } + } + + if !resolutions.is_empty() { + return Ok(resolutions); + } + if unavailable_contexts > 1 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { + contexts: unavailable_contexts, }, - definition, - definition_provenance, - include_chain, - })); + }); + } + if let Some(error) = first_error { + return Err(error); } - Ok(None) + Ok(Vec::new()) } pub fn macro_reference_at( @@ -628,19 +687,17 @@ pub fn macro_reference_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - - for reference in mapped.model.macro_references().iter() { - let mapped_reference = map_macro_reference(mapped, reference)?; - if mapped_reference.file_id == file_id - && range_contains_offset(mapped_reference.range, offset) - { - return Ok(Some(mapped_reference)); - } + let Some(mut contexts) = macro_reference_definitions_at(db, file_id, offset)? else { + return Ok(None); + }; + if contexts.references.len() == 1 { + return Ok(contexts.references.pop()); } - - Ok(None) + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { + contexts: contexts.references.len(), + }, + }) } pub fn macro_reference_resolution_at( @@ -648,13 +705,21 @@ pub fn macro_reference_resolution_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let Some(resolution) = macro_reference_definitions_at(db, file_id, offset)? else { + let Some(mut resolution) = macro_reference_definitions_at(db, file_id, offset)? else { return Ok(None); }; + if resolution.references.len() != 1 || resolution.definitions.len() != 1 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { + contexts: resolution.references.len().max(resolution.definitions.len()), + }, + }); + } + let reference = resolution.references.pop().unwrap(); let Some(definition) = resolution.definitions.into_iter().next() else { return Ok(None); }; - Ok(Some(MacroReferenceResolution { reference: resolution.reference, definition })) + Ok(Some(MacroReferenceResolution { reference, definition })) } pub fn macro_reference_definitions_at( @@ -662,25 +727,82 @@ pub fn macro_reference_definitions_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let Some(reference) = macro_reference_at(db, file_id, offset)? else { - return Ok(None); - }; - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let definitions = match &reference.resolution { - MacroResolution::Resolved { definition_id, .. } => { - let Some(definition) = mapped.model.macro_definitions().get(definition_id.core_id()) + let mut definitions = Vec::new(); + let mut references = Vec::new(); + let mut query_range = None; + let mut first_error = None; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for reference in mapped.model.macro_references().iter() { + let (_, range) = match mapped_source_range_at_offset( + mapped, + reference.name_range, + file_id, + offset, + ) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + query_range.get_or_insert(range); + + let mapped_reference = match map_macro_reference(mapped, reference) { + Ok(reference) => reference, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + push_unique_macro_reference_context(&mut references, mapped_reference); + + let SourceMacroResolutionFact::Resolved { definition, .. } = &reference.resolution else { - return Ok(None); + continue; + }; + let Some(definition) = mapped.model.macro_definitions().get(*definition) else { + record_first_error( + &mut first_error, + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )), + ); + continue; }; - vec![map_macro_definition(mapped, definition)?] + let definition = match map_macro_definition(mapped, definition) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + push_unique_macro_definition(&mut definitions, definition); + } + } + + let Some(range) = query_range else { + if let Some(error) = first_error { + return Err(error); } - MacroResolution::Undefined | MacroResolution::Unavailable(_) => Vec::new(), + return Ok(None); }; + Ok(Some(MacroReferenceDefinitions { - resolution: reference.resolution.clone(), - capability: reference.capability.clone(), - reference, + capability: macro_reference_context_capability(&references), + references, + range, definitions, })) } @@ -690,32 +812,62 @@ pub fn immediate_macro_expansion_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - return Ok(None); - }; - let call = map_macro_call(mapped, call_fact)?; + let mut queries = macro_expansion_queries_at(db, file_id, offset)?; + let available = queries + .iter() + .filter_map(|query| match query { + MacroExpansionQuery::Available(expansion) => Some(expansion.clone()), + MacroExpansionQuery::Ambiguous(expansions) => Some(expansions.first()?.clone()), + MacroExpansionQuery::Unavailable(_) => None, + }) + .collect::>(); + if available.len() > 1 { + return Ok(Some(MacroExpansionQuery::Ambiguous(available))); + } + if available.len() == 1 { + return Ok(Some(MacroExpansionQuery::Available(available.into_iter().next().unwrap()))); + } + match queries.len() { + 0 => Ok(None), + 1 => Ok(queries.pop()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }), + } +} - Ok(Some(match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(expansion) => { - let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { - return Ok(Some(MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { - call, - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, - ), - }))); - }; - MacroExpansionQuery::Available(map_macro_expansion(mapped, expansion)?) - } - SourceMacroExpansionQueryFact::Unavailable(reason) => { - MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { - call, - reason: PreprocUnavailable::Source(reason), - }) - } - })) +pub fn macro_expansion_queries_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut queries = Vec::new(); + let mut first_error = None; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + continue; + }; + let query = immediate_macro_expansion_for_call(mapped, call_fact)?; + push_unique_macro_expansion_query(&mut queries, query); + } + + if !queries.is_empty() { + return Ok(queries); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) } pub fn recursive_macro_expansion_at( @@ -723,38 +875,48 @@ pub fn recursive_macro_expansion_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - return Ok(None); - }; - let root_call = map_macro_call(mapped, call_fact)?; - let recursive = mapped.model.recursive_macro_expansion(call_fact.id); - let expansions = recursive - .expansions - .into_iter() - .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) - .map(|expansion| map_macro_expansion(mapped, expansion)) - .collect::>>()?; - let unavailable = recursive - .unavailable - .into_iter() - .map(|unavailable| { - let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, - ), - }); - }; - Ok(MacroExpansionUnavailable { - call: map_macro_call(mapped, call)?, - reason: PreprocUnavailable::Source(unavailable.reason), - }) - }) - .collect::>>()?; + let expansions = recursive_macro_expansions_at(db, file_id, offset)?; + match expansions.len() { + 0 => Ok(None), + 1 => Ok(expansions.into_iter().next()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }), + } +} + +pub fn recursive_macro_expansions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut expansions = Vec::new(); + let mut first_error = None; - Ok(Some(RecursiveMacroExpansion { root_call, expansions, unavailable })) + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + continue; + }; + let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; + push_unique_recursive_macro_expansion(&mut expansions, recursive); + } + + if !expansions.is_empty() { + return Ok(expansions); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) } pub fn macro_expansion_provenance_at( @@ -762,12 +924,48 @@ pub fn macro_expansion_provenance_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - return Ok(None); - }; - macro_expansion_provenance_for_call(mapped, call_fact) + let provenances = macro_expansion_provenances_at(db, file_id, offset)?; + match provenances.len() { + 0 => Ok(None), + 1 => Ok(provenances.into_iter().next()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }), + } +} + +pub fn macro_expansion_provenances_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut provenances = Vec::new(); + let mut first_error = None; + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + continue; + }; + if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { + push_unique_macro_expansion_provenance(&mut provenances, provenance); + } + } + + if !provenances.is_empty() { + return Ok(provenances); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) } pub fn macro_expansion_provenance_for_range( @@ -775,12 +973,48 @@ pub fn macro_expansion_provenance_for_range( file_id: FileId, range: TextRange, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { - return Ok(None); - }; - macro_expansion_provenance_for_call(mapped, call_fact) + let provenances = macro_expansion_provenances_for_range(db, file_id, range)?; + match provenances.len() { + 0 => Ok(None), + 1 => Ok(provenances.into_iter().next()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }), + } +} + +pub fn macro_expansion_provenances_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mut provenances = Vec::new(); + let mut first_error = None; + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { + continue; + }; + if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { + push_unique_macro_expansion_provenance(&mut provenances, provenance); + } + } + + if !provenances.is_empty() { + return Ok(provenances); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) } pub fn diagnostic_provenance_for_range( @@ -788,25 +1022,51 @@ pub fn diagnostic_provenance_for_range( file_id: FileId, range: TextRange, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { - return Ok(None); - }; + let mut provenances = Vec::new(); + let mut first_error = None; - match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(_) => { - let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? else { - return Ok(Some(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, - )))); - }; - Ok(Some(diagnostic_target_for_expansion(&provenance))) - } - SourceMacroExpansionQueryFact::Unavailable(reason) => { - Ok(Some(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason)))) - } + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { + continue; + }; + let provenance = diagnostic_provenance_for_call(mapped, call_fact)?; + push_unique_diagnostic_provenance(&mut provenances, provenance); + } + + let precise = provenances + .iter() + .filter(|provenance| !matches!(provenance, DiagnosticProvenance::Unavailable(_))) + .cloned() + .collect::>(); + if precise.len() == 1 { + return Ok(Some(precise.into_iter().next().unwrap())); + } + if precise.len() > 1 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: precise.len() }, + ))); + } + if provenances.len() == 1 { + return Ok(provenances.into_iter().next()); } + if provenances.len() > 1 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: provenances.len() }, + ))); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(None) } pub fn macro_references( @@ -901,61 +1161,126 @@ pub fn include_directive_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - for include in mapped.model.include_graph().directives() { - let (source, range) = map_mapped_source_range(mapped, include.directive_range)?; - let include_file_id = source.file_id(); - if include_file_id != file_id || !range_contains_offset(range, offset) { - continue; - } - let status = map_include_status(mapped, &include.status)?; - let resolved_file = match &status { - IncludeDirectiveStatus::Resolved { source } => Some(source.file_id()), - IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, - }; - let target = match &include.target { - MacroIncludeTarget::Literal { path, .. } => { - IncludeTarget::Literal { path: path.clone(), resolved_file } + let mut directives = include_directives_at(db, file_id, offset)?; + match directives.len() { + 0 => Ok(None), + 1 => Ok(directives.pop()), + targets => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousIncludeTargets { targets }, + }), + } +} + +pub fn include_directives_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut directives = Vec::new(); + let mut first_error = None; + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; } - MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, }; - return Ok(Some(IncludeDirective { - id: include.id.into(), - source, - capability: capability_status(&mapped.model.capabilities().include_edges), - file_id: include_file_id, - include_index: include.id.raw(), - range, - target, - status, - })); + for include in mapped.model.include_graph().directives() { + let Some(target_range) = include.target_range else { + continue; + }; + let (source, range) = + match mapped_source_range_at_offset(mapped, target_range, file_id, offset) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let status = map_include_status(mapped, &include.status)?; + let resolved_file = match &status { + IncludeDirectiveStatus::Resolved { source } => Some(source.file_id()), + IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, + }; + let target = match &include.target { + MacroIncludeTarget::Literal { path, .. } => { + IncludeTarget::Literal { path: path.clone(), resolved_file } + } + MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, + }; + let directive = IncludeDirective { + id: include.id.into(), + source, + capability: capability_status(&mapped.model.capabilities().include_edges), + file_id, + include_index: include.id.raw(), + range, + target, + status, + }; + push_unique_include_directive(&mut directives, directive); + } } - Ok(None) + if !directives.is_empty() { + return Ok(directives); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) } pub fn inactive_branches( db: &dyn SourceRootDb, file_id: FileId, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; let mut branches = Vec::new(); + let mut first_error = None; - for source_range in mapped.model.inactive_ranges() { - let (source, range) = map_mapped_source_range(mapped, *source_range)?; - let branch_file_id = source.file_id(); - if branch_file_id == file_id { - branches.push(InactiveBranch { - source, - capability: capability_status(&mapped.model.capabilities().inactive_ranges), - file_id: branch_file_id, - range, - }); + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for source_range in mapped.model.inactive_ranges() { + let (source, range) = match map_mapped_source_range(mapped, *source_range) { + Ok(mapped_range) => mapped_range, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let branch_file_id = source.file_id(); + if branch_file_id == file_id { + push_unique_inactive_branch( + &mut branches, + InactiveBranch { + source, + capability: capability_status(&mapped.model.capabilities().inactive_ranges), + file_id: branch_file_id, + range, + }, + ); + } } } + if branches.is_empty() + && let Some(error) = first_error + { + return Err(error); + } + Ok(branches) } @@ -965,12 +1290,27 @@ fn mapped_result( result.as_ref().map_err(|err| err.clone().into()) } -fn root_position( - mapped: &MappedSourcePreprocModel, - offset: TextSize, -) -> PreprocResult { - let source = mapped.model.root_source().ok_or(PreprocError::MissingRootSource)?; - Ok(SourcePosition { source, offset }) +fn source_preproc_query_model_file_ids(db: &dyn SourceRootDb, file_id: FileId) -> Vec { + let profile_id = db.file_compilation_profile(file_id); + let mut file_ids = Vec::new(); + let mut seen = FxHashSet::default(); + push_unique_file_id(&mut file_ids, &mut seen, file_id); + for model_file_id in preproc_reference_model_file_ids(db, profile_id) { + push_unique_file_id(&mut file_ids, &mut seen, model_file_id); + } + file_ids +} + +fn push_unique_file_id(file_ids: &mut Vec, seen: &mut FxHashSet, file_id: FileId) { + if seen.insert(file_id) { + file_ids.push(file_id); + } +} + +fn record_first_error(first_error: &mut Option, error: PreprocError) { + if first_error.is_none() { + *first_error = Some(error); + } } fn map_source_range( @@ -997,6 +1337,26 @@ fn map_mapped_source_range( Ok((source, range)) } +fn mapped_source_range_at_offset( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let (source, range) = map_mapped_source_range(mapped, source_range)?; + Ok((source.file_id() == file_id && range_contains_offset(range, offset)) + .then_some((source, range))) +} + +fn mapped_source_range_contains_offset( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, + file_id: FileId, + offset: TextSize, +) -> PreprocResult { + Ok(mapped_source_range_at_offset(mapped, source_range, file_id, offset)?.is_some()) +} + fn map_mapped_source_id( mapped: &MappedSourcePreprocModel, source: PreprocSourceId, @@ -1163,6 +1523,84 @@ fn source_macro_call_intersecting_range( }) } +fn immediate_macro_expansion_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let call = map_macro_call(mapped, call_fact)?; + Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { + return Ok(MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + ), + })); + }; + MacroExpansionQuery::Available(map_macro_expansion(mapped, expansion)?) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source(reason), + }) + } + }) +} + +fn recursive_macro_expansion_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let root_call = map_macro_call(mapped, call_fact)?; + let recursive = mapped.model.recursive_macro_expansion(call_fact.id); + let expansions = recursive + .expansions + .into_iter() + .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) + .map(|expansion| map_macro_expansion(mapped, expansion)) + .collect::>>()?; + let unavailable = recursive + .unavailable + .into_iter() + .map(|unavailable| { + let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, + ), + }); + }; + Ok(MacroExpansionUnavailable { + call: map_macro_call(mapped, call)?, + reason: PreprocUnavailable::Source(unavailable.reason), + }) + }) + .collect::>>()?; + + Ok(RecursiveMacroExpansion { root_call, expansions, unavailable }) +} + +fn diagnostic_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(_) => { + let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? else { + return Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + ))); + }; + Ok(diagnostic_target_for_expansion(&provenance)) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason))) + } + } +} + fn macro_expansion_provenance_for_call( mapped: &MappedSourcePreprocModel, call_fact: &SourceMacroCallFact, @@ -1451,6 +1889,112 @@ fn push_unique_macro_reference(refs: &mut Vec, reference: MacroR refs.push(reference); } +fn push_unique_macro_reference_context(refs: &mut Vec, reference: MacroReference) { + if refs.iter().any(|existing| existing == &reference) { + return; + } + refs.push(reference); +} + +fn push_unique_macro_usage_resolution( + resolutions: &mut Vec, + resolution: MacroUsageResolution, +) { + if resolutions.iter().any(|existing| existing == &resolution) { + return; + } + resolutions.push(resolution); +} + +fn push_unique_macro_expansion_query( + queries: &mut Vec, + query: MacroExpansionQuery, +) { + if queries.iter().any(|existing| existing == &query) { + return; + } + queries.push(query); +} + +fn push_unique_recursive_macro_expansion( + expansions: &mut Vec, + expansion: RecursiveMacroExpansion, +) { + if expansions.iter().any(|existing| existing == &expansion) { + return; + } + expansions.push(expansion); +} + +fn push_unique_macro_expansion_provenance( + provenances: &mut Vec, + provenance: MacroExpansionProvenance, +) { + if provenances.iter().any(|existing| existing == &provenance) { + return; + } + provenances.push(provenance); +} + +fn push_unique_diagnostic_provenance( + provenances: &mut Vec, + provenance: DiagnosticProvenance, +) { + if provenances.iter().any(|existing| existing == &provenance) { + return; + } + provenances.push(provenance); +} + +fn push_unique_include_directive( + directives: &mut Vec, + directive: IncludeDirective, +) { + if directives.iter().any(|existing| { + existing.file_id == directive.file_id + && existing.range == directive.range + && existing.target == directive.target + && existing.status == directive.status + }) { + return; + } + directives.push(directive); +} + +fn push_unique_inactive_branch(branches: &mut Vec, branch: InactiveBranch) { + if branches + .iter() + .any(|existing| existing.file_id == branch.file_id && existing.range == branch.range) + { + return; + } + branches.push(branch); +} + +fn macro_reference_context_capability(references: &[MacroReference]) -> PreprocAvailability { + if references + .iter() + .all(|reference| matches!(reference.capability, PreprocAvailability::Complete)) + { + return PreprocAvailability::Complete; + } + if references + .iter() + .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) + { + return PreprocAvailability::Partial; + } + references + .iter() + .find_map(|reference| match &reference.capability { + PreprocAvailability::Unavailable(reason) => { + Some(PreprocAvailability::Unavailable(reason.clone())) + } + PreprocAvailability::Complete | PreprocAvailability::Partial => None, + }) + .unwrap_or(PreprocAvailability::Complete) +} + fn push_unique_macro_definition( definitions: &mut Vec, definition: MacroDefinition, @@ -1661,6 +2205,21 @@ mod tests { TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) } + fn offset_after_n(text: &str, needle: &str, occurrence: usize) -> TextSize { + let mut cursor = 0; + for index in 0..=occurrence { + let relative = text[cursor..].find(needle).unwrap_or_else(|| { + panic!("missing occurrence {occurrence} of {needle:?} in fixture") + }); + let absolute = cursor + relative; + if index == occurrence { + return TextSize::from(u32::try_from(absolute + needle.len()).unwrap()); + } + cursor = absolute + needle.len(); + } + unreachable!() + } + fn text_at_range(text: &str, range: TextRange) -> &str { &text[usize::from(range.start())..usize::from(range.end())] } @@ -1685,6 +2244,8 @@ endmodule let include = include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); + assert_eq!(text_at_range(root_text, include.range), "\"defs.vh\""); + assert!(include_directive_at(&db, TOP, offset(root_text, "`include")).unwrap().is_none()); let IncludeTarget::Literal { resolved_file, .. } = include.target else { panic!("literal include expected"); }; @@ -1993,7 +2554,7 @@ localparam int ENABLED = `HEADER_FLAG; macro_reference_definitions_at(&db, TOP, offset_after(root_text, "ENABLED = `")) .unwrap() .unwrap(); - assert_eq!(text_at_range(root_text, definitions.reference.range), "`HEADER_FLAG"); + assert_eq!(text_at_range(root_text, definitions.range), "`HEADER_FLAG"); assert!(matches!(definitions.capability, PreprocAvailability::Complete)); assert!(definitions.definitions.iter().any(|indexed| { indexed.file_id == HEADER @@ -2002,6 +2563,138 @@ localparam int ENABLED = `HEADER_FLAG; })); } + #[test] + fn preproc_header_ifdef_reference_uses_including_root_context() { + let root_text = r#"`include "defs.vh" +`include "leaf.vh" +"#; + let header_text = "`define FEATURE_B 1\n"; + let leaf_text = r#"`ifdef FEATURE_B +wire enabled; +`endif +"#; + let db = db_with_nested_files(root_text, header_text, leaf_text); + + let definitions = macro_reference_definitions_at(&db, LEAF, offset(leaf_text, "FEATURE_B")) + .unwrap() + .unwrap(); + + assert_eq!(text_at_range(leaf_text, definitions.range), "FEATURE_B"); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == HEADER + && text_at_range(header_text, definition.name_range) == "FEATURE_B" + })); + } + + #[test] + fn preproc_header_macro_body_references_use_expansion_context() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `DEMO_WIDTH; +localparam int N = `DEMO_NEXT(1); +localparam int R = `DEMO_RESET; +endmodule +"#; + let header_text = r#"`ifndef SHARED_DEFS_SVH +`define SHARED_DEFS_SVH +`include "leaf.vh" +`define DEMO_WIDTH `MATH_WIDTH +`define DEMO_RESET {`DEMO_WIDTH{1'b0}} +`define DEMO_NEXT(value) ((value) + `MATH_ONE) +`endif +"#; + let leaf_text = r#"`define MATH_WIDTH 12 +`define MATH_ONE 12'd1 +"#; + let db = db_with_nested_files(root_text, header_text, leaf_text); + + let math_width = + macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_WIDTH")) + .unwrap() + .unwrap(); + assert!(math_width.definitions.iter().any(|definition| { + definition.file_id == LEAF + && text_at_range(leaf_text, definition.name_range) == "MATH_WIDTH" + })); + + let math_one = macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_ONE")) + .unwrap() + .unwrap(); + assert!(math_one.definitions.iter().any(|definition| { + definition.file_id == LEAF + && text_at_range(leaf_text, definition.name_range) == "MATH_ONE" + })); + + let demo_width = macro_reference_definitions_at( + &db, + HEADER, + offset_after(header_text, "`define DEMO_RESET {`"), + ) + .unwrap() + .unwrap(); + assert!(demo_width.definitions.iter().any(|definition| { + definition.file_id == HEADER + && text_at_range(header_text, definition.name_range) == "DEMO_WIDTH" + })); + } + + #[test] + fn preproc_header_reference_reports_all_including_context_definitions() { + let root_text = r#"`define WIDTH 8 +`include "defs.vh" +`undef WIDTH +`define WIDTH 16 +`include "defs.vh" +"#; + let header_text = "localparam int W = `WIDTH;\n"; + let db = db_with_files(root_text, header_text); + + let definitions = macro_reference_definitions_at(&db, HEADER, offset(header_text, "WIDTH")) + .unwrap() + .unwrap(); + + assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); + assert_eq!(definitions.definitions.len(), 2); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) + })); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) + })); + } + + #[test] + fn preproc_header_macro_body_reference_reports_all_expansion_context_definitions() { + let root_text = r#"`define WIDTH 8 +`include "defs.vh" +localparam int A = `USE_WIDTH; +`undef WIDTH +`define WIDTH 16 +`include "defs.vh" +localparam int B = `USE_WIDTH; +"#; + let header_text = "`define USE_WIDTH `WIDTH\n"; + let db = db_with_files(root_text, header_text); + + let definitions = + macro_reference_definitions_at(&db, HEADER, offset_after(header_text, "USE_WIDTH `")) + .unwrap() + .unwrap(); + + assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); + assert_eq!(definitions.definitions.len(), 2); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) + })); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) + })); + } + #[test] fn preproc_macro_definition_at_only_hits_name_range() { let root_text = "`define HEADER_FLAG 1\n"; @@ -2028,7 +2721,7 @@ localparam int ENABLED = `HEADER_FLAG; .unwrap() .unwrap(); - assert_eq!(resolution.reference.file_id, HEADER); + assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); let definition = resolution.definitions.iter().find(|definition| definition.file_id == HEADER).unwrap(); assert_eq!(text_at_range(header_text, definition.name_range), "HEADER_FLAG"); @@ -2054,7 +2747,7 @@ localparam int ENABLED = `HEADER_FLAG; .unwrap() .unwrap(); - assert_eq!(resolution.reference.file_id, HEADER); + assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); assert!(resolution.definitions.iter().any(|definition| { definition.file_id == HEADER && text_at_range(header_text, definition.name_range) == "HEADER_FLAG" diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 8cc33946..21c30baa 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -44,6 +44,7 @@ pub enum SourceMacroReferenceSite { Usage { usage_index: usize }, ConditionalToken { conditional_index: usize, token_index: usize }, IncludeGuardIfNDef { conditional_index: usize, token_index: usize }, + MacroBodyToken { call: SourceMacroCallId, token_index: usize }, ExpansionToken { emitted_token: SourceEmittedTokenId }, } @@ -696,6 +697,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.scan_references_and_state(); self.build_emitted_token_tables(); self.build_macro_expansion_graph(); + self.record_macro_body_references_for_calls(); let macro_expansions = if self.tables.macro_calls.is_empty() { CapabilityStatus::Complete } else if self.index.emitted_tokens.is_empty() { @@ -1334,6 +1336,57 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } + fn record_macro_body_references_for_calls(&mut self) { + let calls = self.tables.macro_calls.iter().cloned().collect::>(); + for call in calls { + let SourceMacroResolution::Resolved { definition, .. } = call.callee else { + continue; + }; + let Some(definition) = self.tables.macro_definitions.get(definition).cloned() else { + continue; + }; + let call_position = SourcePosition { + source: call.call_range.source, + offset: call.call_range.range.start(), + }; + for (token_index, token) in definition.body_tokens.iter().enumerate() { + let Some(name) = macro_reference_name_from_body_token(token) else { + continue; + }; + let Some(name_range) = token.range else { + self.record_missing_reference_name_range(definition.event_id); + continue; + }; + let resolution = + self.resolve_visible_reference_at_position(name.as_str(), call_position); + if self.macro_reference_exists(name.as_str(), name_range, &resolution) { + continue; + } + self.push_reference( + definition.event_id, + SourceMacroReferenceSite::MacroBodyToken { call: call.id, token_index }, + name, + name_range, + definition.directive_range, + resolution, + ); + } + } + } + + fn macro_reference_exists( + &self, + name: &str, + name_range: SourceRange, + resolution: &SourceMacroResolution, + ) -> bool { + self.tables.macro_references.iter().any(|reference| { + reference.name.as_str() == name + && reference.name_range == name_range + && &reference.resolution == resolution + }) + } + fn direct_emitted_tokens_by_call( &self, ) -> BTreeMap> { @@ -1476,6 +1529,22 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) } + fn resolve_visible_reference_at_position( + &mut self, + name: &str, + position: SourcePosition, + ) -> SourceMacroResolution { + let Some(definition) = self + .tables + .state_timeline + .state_at_position(position) + .and_then(|state| state.definitions.get(name).copied()) + else { + return SourceMacroResolution::Undefined; + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + fn resolve_definition( &mut self, definition: SourceMacroDefinitionId, @@ -1663,6 +1732,14 @@ fn partial_status(is_partial: bool) -> CapabilityStatus { if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } } +fn macro_reference_name_from_body_token(token: &SourceMacroToken) -> Option { + if !token.raw.starts_with('`') { + return None; + } + let name = token.value.strip_prefix('`').unwrap_or(token.value.as_str()); + (!name.is_empty()).then(|| SmolStr::new(name)) +} + fn emitted_token_range_from_ids( tokens: &[SourceEmittedTokenId], ) -> Option { From afccca4bef6f9110dd9ea44b68102777fb4d8fb4 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 22:29:33 +0800 Subject: [PATCH 14/80] fix(ide): surface contextual preproc navigation results --- crates/ide/src/goto_definition.rs | 47 ++++++++++++-------- crates/ide/src/hover.rs | 73 ++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 59c25cc4..4eddec93 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -3,7 +3,7 @@ use hir::{ container::InFile, file::HirFileId, preproc::{ - IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, + IncludeTarget, MacroDefinition, include_directives_at, macro_definition_at, macro_reference_definitions_at, }, semantics::Semantics, @@ -65,7 +65,7 @@ fn handle_preproc_macro( } let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; - let reference_range = resolution.reference.range; + let reference_range = resolution.range; let targets = resolution.definitions.into_iter().map(macro_nav_target).collect_vec(); if targets.is_empty() { return None; @@ -90,24 +90,33 @@ fn handle_preproc_include( file_id: FileId, offset: TextSize, ) -> Option>> { - let include = include_directive_at(db, file_id, offset).ok()??; - let IncludeTarget::Literal { path, resolved_file: Some(target_file_id) } = include.target - else { + let includes = include_directives_at(db, file_id, offset).ok()?; + let range = includes.first()?.range; + let targets = includes + .into_iter() + .filter_map(|include| { + let IncludeTarget::Literal { path, resolved_file: Some(target_file_id) } = + include.target + else { + return None; + }; + let target_range = TextRange::empty(TextSize::new(0)); + Some(NavTarget { + file_id: target_file_id, + full_range: target_range, + focus_range: Some(target_range), + name: Some(path), + kind: None, + container_name: None, + description: db.file_path(target_file_id).map(|path| path.to_string()), + }) + }) + .unique() + .collect_vec(); + if targets.is_empty() { return None; - }; - let target_range = TextRange::empty(TextSize::new(0)); - Some(RangeInfo::new( - include.range, - vec![NavTarget { - file_id: target_file_id, - full_range: target_range, - focus_range: Some(target_range), - name: Some(path), - kind: None, - container_name: None, - description: db.file_path(target_file_id).map(|path| path.to_string()), - }], - )) + } + Some(RangeInfo::new(range, targets)) } fn handle_ctrl_flow_kw( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index d22c8d8e..67b57ab5 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -4,7 +4,7 @@ use hir::{ file::HirFileId, hir_def::expr::Expr, preproc::{ - IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, + IncludeTarget, MacroDefinition, include_directives_at, macro_definition_at, macro_reference_definitions_at, }, semantics::Semantics, @@ -91,19 +91,41 @@ fn handle_preproc_macro( offset: TextSize, ) -> Option> { if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { - return Some(RangeInfo::new(definition.name_range, macro_definition_markup(&definition))); + return Some(RangeInfo::new( + definition.name_range, + macro_definition_markup(db, &definition), + )); } let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; - let definition = resolution.definitions.into_iter().next()?; - Some(RangeInfo::new(resolution.reference.range, macro_definition_markup(&definition))) + if resolution.definitions.is_empty() { + return None; + } + Some(RangeInfo::new(resolution.range, macro_definitions_markup(db, &resolution.definitions))) } -fn macro_definition_markup(definition: &MacroDefinition) -> Markup { +fn macro_definition_markup(db: &RootDb, definition: &MacroDefinition) -> Markup { + macro_definitions_markup(db, std::slice::from_ref(definition)) +} + +fn macro_definitions_markup(db: &RootDb, definitions: &[MacroDefinition]) -> Markup { let mut markup = Markup::new(); - markup.print("Macro"); - markup.newline(); - markup.push_with_backticks(definition.name.as_str()); + if definitions.len() == 1 { + markup.print("Macro"); + markup.newline(); + markup.push_with_backticks(definitions[0].name.as_str()); + return markup; + } + + markup.print("Macro definitions"); + for definition in definitions { + markup.newline(); + markup.push_with_backticks(definition.name.as_str()); + if let Some(path) = db.file_path(definition.file_id) { + markup.print(" "); + markup.print(&path.to_string()); + } + } markup } @@ -112,27 +134,28 @@ fn handle_preproc_include( file_id: FileId, offset: TextSize, ) -> Option> { - let include = include_directive_at(db, file_id, offset).ok()??; + let includes = include_directives_at(db, file_id, offset).ok()?; + let range = includes.first()?.range; let mut markup = Markup::new(); - match include.target { - IncludeTarget::Literal { path, resolved_file } => { - markup.print("Include"); - markup.newline(); - markup.push_with_backticks(path.as_str()); - if let Some(target_file_id) = resolved_file - && let Some(path) = db.file_path(target_file_id) - { - markup.newline(); - markup.print(&path.to_string()); + markup.print("Include"); + for include in includes { + markup.newline(); + match include.target { + IncludeTarget::Literal { path, resolved_file } => { + markup.push_with_backticks(path.as_str()); + if let Some(target_file_id) = resolved_file + && let Some(path) = db.file_path(target_file_id) + { + markup.newline(); + markup.print(&path.to_string()); + } + } + IncludeTarget::Token { raw } => { + markup.push_with_backticks(raw.as_str()); } - } - IncludeTarget::Token { raw } => { - markup.print("Include"); - markup.newline(); - markup.push_with_backticks(raw.as_str()); } } - Some(RangeInfo::new(include.range, markup)) + Some(RangeInfo::new(range, markup)) } fn handle_definition( From af923e9e2f8def38decc0a2b48db23ee27d5b01c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 22:40:55 +0800 Subject: [PATCH 15/80] feat(hir): link macro parameters through preproc facts --- crates/hir/src/preproc.rs | 414 +++++++++++++++++++++++++++++- crates/ide/src/goto_definition.rs | 28 +- crates/ide/src/hover.rs | 43 +++- crates/ide/src/references.rs | 74 +++++- crates/ide/src/verilog_2005.rs | 78 ++++++ 5 files changed, 632 insertions(+), 5 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index c2b9f25d..318a276f 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -9,8 +9,9 @@ use preproc::source::{ SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, SourceMacroExpansionQuery as SourceMacroExpansionQueryFact, SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, - SourceMacroReference as SourceMacroReferenceFact, SourceMacroReferenceId, - SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, + SourceMacroParam as SourceMacroParamFact, SourceMacroReference as SourceMacroReferenceFact, + SourceMacroReferenceId, SourceMacroReferenceSite, + SourceMacroResolution as SourceMacroResolutionFact, SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePreprocError, SourcePreprocUnavailable, SourceRange, SourceTokenProvenance as SourceTokenProvenanceFact, }; @@ -63,6 +64,7 @@ pub enum PreprocUnavailable { Source(SourcePreprocUnavailable), AmbiguousMacroReferenceContexts { contexts: usize }, AmbiguousMacroExpansionContexts { contexts: usize }, + AmbiguousMacroParamContexts { contexts: usize }, AmbiguousDiagnosticProvenance { targets: usize }, AmbiguousIncludeTargets { targets: usize }, } @@ -143,6 +145,40 @@ pub struct MacroDefinition { pub name_range: TextRange, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamDefinition { + pub macro_definition: MacroDefinition, + pub param_index: usize, + pub name: SmolStr, + pub range: TextRange, + pub param_range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReference { + pub macro_definition: MacroDefinition, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub param_index: usize, + pub token_index: usize, + pub name: SmolStr, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReferenceDefinitions { + pub references: Vec, + pub range: TextRange, + pub definitions: Vec, + pub capability: PreprocAvailability, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReferences { + pub references: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroUsage { pub reference_id: MacroReferenceId, @@ -574,6 +610,144 @@ pub fn macro_definition_at( Ok(None) } +pub fn macro_param_definition_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = macro_param_definitions_at(db, file_id, offset)?; + match definitions.len() { + 0 => Ok(None), + 1 => Ok(definitions.pop()), + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroParamContexts { contexts }, + }), + } +} + +pub fn macro_param_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = Vec::new(); + let mut first_error = None; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for definition in mapped.model.macro_definitions().iter() { + let Some(params) = &definition.params else { + continue; + }; + for (param_index, param) in params.iter().enumerate() { + let Some(param_definition) = + map_macro_param_definition(mapped, definition, param_index, param)? + else { + continue; + }; + if param_definition.macro_definition.file_id == file_id + && range_contains_offset(param_definition.range, offset) + { + push_unique_macro_param_definition(&mut definitions, param_definition); + } + } + } + } + + if definitions.is_empty() + && let Some(error) = first_error + { + return Err(error); + } + + Ok(definitions) +} + +pub fn macro_param_reference_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = Vec::new(); + let mut references = Vec::new(); + let mut query_range = None; + let mut first_error = None; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for definition in mapped.model.macro_definitions().iter() { + let Some(params) = &definition.params else { + continue; + }; + for (token_index, token) in definition.body_tokens.iter().enumerate() { + let Some(token_range) = token.range else { + continue; + }; + let (_, range) = + match mapped_source_range_at_offset(mapped, token_range, file_id, offset) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for (param_index, param) in params.iter().enumerate() { + if param.name.as_ref() != Some(&token.value) { + continue; + } + let Some(param_definition) = + map_macro_param_definition(mapped, definition, param_index, param)? + else { + continue; + }; + let reference = map_macro_param_reference( + mapped, + definition, + param_index, + token_index, + token_range, + )?; + query_range.get_or_insert(range); + push_unique_macro_param_definition(&mut definitions, param_definition); + push_unique_macro_param_reference(&mut references, reference); + } + } + } + } + + let Some(range) = query_range else { + if let Some(error) = first_error { + return Err(error); + } + return Ok(None); + }; + + Ok(Some(MacroParamReferenceDefinitions { + capability: macro_param_reference_context_capability(&references), + references, + range, + definitions, + })) +} + pub fn macro_usage_resolution_at( db: &dyn SourceRootDb, file_id: FileId, @@ -1081,6 +1255,78 @@ pub fn macro_references( Ok(MacroReferences { references: index.references_for(definition), status: index.status() }) } +pub fn macro_param_references( + db: &dyn SourceRootDb, + file_id: FileId, + definition: &MacroParamDefinition, +) -> PreprocResult { + let profile_id = db + .file_compilation_profile(file_id) + .or_else(|| db.file_compilation_profile(definition.macro_definition.file_id)); + let mut references = Vec::new(); + let mut first_error = None; + + for model_file_id in preproc_reference_model_file_ids(db, profile_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for source_definition in mapped.model.macro_definitions().iter() { + let mapped_definition = match map_macro_definition(mapped, source_definition) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + if !same_macro_definition(&mapped_definition, &definition.macro_definition) { + continue; + } + let Some(params) = &source_definition.params else { + continue; + }; + let Some(param) = params.get(definition.param_index) else { + continue; + }; + if param.name.as_ref() != Some(&definition.name) { + continue; + } + + for (token_index, token) in source_definition.body_tokens.iter().enumerate() { + if param.name.as_ref() != Some(&token.value) { + continue; + } + let Some(token_range) = token.range else { + continue; + }; + match map_macro_param_reference( + mapped, + source_definition, + definition.param_index, + token_index, + token_range, + ) { + Ok(reference) => push_unique_macro_param_reference(&mut references, reference), + Err(error) => record_first_error(&mut first_error, error), + } + } + } + } + + if references.is_empty() + && let Some(error) = first_error + { + return Err(error); + } + + Ok(MacroParamReferences { references }) +} + pub(crate) fn build_macro_reference_index( db: &dyn SourceRootDb, profile_id: Option, @@ -1405,6 +1651,74 @@ fn map_macro_definition( }) } +fn map_macro_param_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, + param_index: usize, + param: &SourceMacroParamFact, +) -> PreprocResult> { + let Some(name) = ¶m.name else { + return Ok(None); + }; + let Some(name_source_range) = param.name_range else { + return Ok(None); + }; + let macro_definition = map_macro_definition(mapped, definition)?; + let (source, range) = map_mapped_source_range(mapped, name_source_range)?; + if source.file_id() != macro_definition.file_id { + return Err(PreprocError::MismatchedDefinitionRangeFiles { + event_id: definition.event_id.raw(), + directive_file_id: macro_definition.file_id, + name_file_id: source.file_id(), + }); + } + let param_range = param + .range + .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) + .transpose()?; + + Ok(Some(MacroParamDefinition { + macro_definition, + param_index, + name: name.clone(), + range, + param_range, + })) +} + +fn map_macro_param_reference( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, + param_index: usize, + token_index: usize, + token_range: SourceRange, +) -> PreprocResult { + let macro_definition = map_macro_definition(mapped, definition)?; + let (source, range) = map_mapped_source_range(mapped, token_range)?; + let file_id = source.file_id(); + let name = definition + .params + .as_ref() + .and_then(|params| params.get(param_index)) + .and_then(|param| param.name.clone()) + .ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, + )) + })?; + + Ok(MacroParamReference { + macro_definition, + source, + capability: PreprocAvailability::Complete, + file_id, + param_index, + token_index, + name, + range, + }) +} + fn map_definition_provenance_from_definition( mapped: &MappedSourcePreprocModel, definition: &SourceMacroDefinitionFact, @@ -2009,6 +2323,63 @@ fn push_unique_macro_definition( definitions.push(definition); } +fn same_macro_definition(left: &MacroDefinition, right: &MacroDefinition) -> bool { + left.file_id == right.file_id && left.name_range == right.name_range && left.name == right.name +} + +fn same_macro_param_definition(left: &MacroParamDefinition, right: &MacroParamDefinition) -> bool { + same_macro_definition(&left.macro_definition, &right.macro_definition) + && left.param_index == right.param_index + && left.range == right.range + && left.name == right.name +} + +fn push_unique_macro_param_definition( + definitions: &mut Vec, + definition: MacroParamDefinition, +) { + if definitions.iter().any(|existing| same_macro_param_definition(existing, &definition)) { + return; + } + definitions.push(definition); +} + +fn push_unique_macro_param_reference( + refs: &mut Vec, + reference: MacroParamReference, +) { + if refs.iter().any(|existing| { + same_macro_definition(&existing.macro_definition, &reference.macro_definition) + && existing.param_index == reference.param_index + && existing.file_id == reference.file_id + && existing.range == reference.range + && existing.name == reference.name + }) { + return; + } + refs.push(reference); +} + +fn macro_param_reference_context_capability( + references: &[MacroParamReference], +) -> PreprocAvailability { + if references + .iter() + .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) + { + return PreprocAvailability::Partial; + } + references + .iter() + .find_map(|reference| match &reference.capability { + PreprocAvailability::Unavailable(reason) => { + Some(PreprocAvailability::Unavailable(reason.clone())) + } + PreprocAvailability::Complete | PreprocAvailability::Partial => None, + }) + .unwrap_or(PreprocAvailability::Complete) +} + fn preproc_reference_model_file_ids( db: &dyn SourceRootDb, profile_id: Option, @@ -2638,6 +3009,45 @@ endmodule })); } + #[test] + fn preproc_macro_param_references_resolve_to_formals() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `SHIFT(4, 1); +endmodule +"#; + let header_text = "`define SHIFT(value, amount) ((value) << amount)\n"; + let db = db_with_files(root_text, header_text); + + let value_definition = + macro_param_definition_at(&db, HEADER, offset_after(header_text, "SHIFT(")) + .unwrap() + .unwrap(); + assert_eq!(value_definition.name.as_str(), "value"); + assert_eq!(text_at_range(header_text, value_definition.range), "value"); + + let value_reference = macro_param_reference_definitions_at( + &db, + HEADER, + offset_after(header_text, "SHIFT(value, amount) (("), + ) + .unwrap() + .unwrap(); + assert_eq!(text_at_range(header_text, value_reference.range), "value"); + assert!(value_reference.definitions.iter().any(|definition| { + definition.param_index == value_definition.param_index + && text_at_range(header_text, definition.range) == "value" + })); + + let refs = macro_param_references(&db, HEADER, &value_definition).unwrap().references; + assert!(refs.iter().any(|reference| { + reference.file_id == HEADER && text_at_range(header_text, reference.range) == "value" + })); + assert!( + !refs.iter().any(|reference| text_at_range(header_text, reference.range) == "amount") + ); + } + #[test] fn preproc_header_reference_reports_all_including_context_definitions() { let root_text = r#"`define WIDTH 8 diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 4eddec93..9e3db490 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -3,7 +3,8 @@ use hir::{ container::InFile, file::HirFileId, preproc::{ - IncludeTarget, MacroDefinition, include_directives_at, macro_definition_at, + IncludeTarget, MacroDefinition, MacroParamDefinition, include_directives_at, + macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, macro_reference_definitions_at, }, semantics::Semantics, @@ -60,6 +61,19 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option>> { + if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { + return Some(RangeInfo::new(definition.range, vec![macro_param_nav_target(definition)])); + } + + if let Some(resolution) = macro_param_reference_definitions_at(db, file_id, offset).ok()? { + let reference_range = resolution.range; + let targets = resolution.definitions.into_iter().map(macro_param_nav_target).collect_vec(); + if targets.is_empty() { + return None; + } + return Some(RangeInfo::new(reference_range, targets)); + } + if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { return Some(RangeInfo::new(definition.name_range, vec![macro_nav_target(definition)])); } @@ -73,6 +87,18 @@ fn handle_preproc_macro( Some(RangeInfo::new(reference_range, targets)) } +fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { + NavTarget { + file_id: definition.macro_definition.file_id, + full_range: definition.range, + focus_range: Some(definition.range), + name: Some(definition.name), + kind: None, + container_name: Some(definition.macro_definition.name), + description: Some("macro parameter".to_owned()), + } +} + fn macro_nav_target(definition: MacroDefinition) -> NavTarget { NavTarget { file_id: definition.file_id, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 67b57ab5..11ee95f9 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -4,7 +4,8 @@ use hir::{ file::HirFileId, hir_def::expr::Expr, preproc::{ - IncludeTarget, MacroDefinition, include_directives_at, macro_definition_at, + IncludeTarget, MacroDefinition, MacroParamDefinition, include_directives_at, + macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, macro_reference_definitions_at, }, semantics::Semantics, @@ -90,6 +91,21 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option> { + if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { + return Some(RangeInfo::new(definition.range, macro_param_definition_markup(&definition))); + } + + let param_resolution = macro_param_reference_definitions_at(db, file_id, offset).ok()?; + if let Some(param_resolution) = param_resolution { + if param_resolution.definitions.is_empty() { + return None; + } + return Some(RangeInfo::new( + param_resolution.range, + macro_param_definitions_markup(¶m_resolution.definitions), + )); + } + if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { return Some(RangeInfo::new( definition.name_range, @@ -104,6 +120,31 @@ fn handle_preproc_macro( Some(RangeInfo::new(resolution.range, macro_definitions_markup(db, &resolution.definitions))) } +fn macro_param_definition_markup(definition: &MacroParamDefinition) -> Markup { + macro_param_definitions_markup(std::slice::from_ref(definition)) +} + +fn macro_param_definitions_markup(definitions: &[MacroParamDefinition]) -> Markup { + let mut markup = Markup::new(); + if definitions.len() == 1 { + markup.print("Macro parameter"); + markup.newline(); + markup.push_with_backticks(definitions[0].name.as_str()); + markup.print(" of "); + markup.push_with_backticks(definitions[0].macro_definition.name.as_str()); + return markup; + } + + markup.print("Macro parameters"); + for definition in definitions { + markup.newline(); + markup.push_with_backticks(definition.name.as_str()); + markup.print(" of "); + markup.push_with_backticks(definition.macro_definition.name.as_str()); + } + markup +} + fn macro_definition_markup(db: &RootDb, definition: &MacroDefinition) -> Markup { macro_definitions_markup(db, std::slice::from_ref(definition)) } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 2d569589..29882a3f 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1,7 +1,9 @@ use hir::{ file::HirFileId, preproc::{ - MacroDefinition, macro_definition_at, macro_reference_definitions_at, macro_references, + MacroDefinition, MacroParamDefinition, macro_definition_at, macro_param_definition_at, + macro_param_reference_definitions_at, macro_param_references, + macro_reference_definitions_at, macro_references, }, semantics::Semantics, }; @@ -93,6 +95,10 @@ fn handle_preproc_macro( offset: TextSize, config: &ReferencesConfig, ) -> Option> { + if let Some(param_refs) = handle_preproc_macro_param(db, file_id, offset, config) { + return Some(param_refs); + } + let definitions = if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { vec![definition] } else { @@ -108,6 +114,60 @@ fn handle_preproc_macro( .collect() } +fn handle_preproc_macro_param( + db: &RootDb, + file_id: FileId, + offset: TextSize, + config: &ReferencesConfig, +) -> Option> { + let definitions = + if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { + vec![definition] + } else { + macro_param_reference_definitions_at(db, file_id, offset).ok()??.definitions + }; + if definitions.is_empty() { + return None; + } + + definitions + .into_iter() + .map(|definition| macro_param_references_for_definition(db, file_id, definition, config)) + .collect() +} + +fn macro_param_references_for_definition( + db: &RootDb, + file_id: FileId, + definition: MacroParamDefinition, + config: &ReferencesConfig, +) -> Option { + let refs = macro_param_references(db, file_id, &definition) + .ok()? + .references + .into_iter() + .filter(|usage| { + config.search_scope.as_ref().is_none_or(|scope| { + scope.range_for_file(usage.file_id).is_some_and(|range| { + range.is_none_or(|range| range.intersect(usage.range).is_some()) + }) + }) + }) + .into_group_map_by(|usage| usage.file_id) + .into_iter() + .map(|(file_id, usages)| { + ( + file_id, + usages + .into_iter() + .map(|usage| (usage.range, ReferenceCategory::empty())) + .collect_vec(), + ) + }) + .collect(); + Some(References { def: Some(vec![macro_param_nav_target(definition)]), refs }) +} + fn macro_references_for_definition( db: &RootDb, file_id: FileId, @@ -140,6 +200,18 @@ fn macro_references_for_definition( Some(References { def: Some(vec![macro_nav_target(definition)]), refs }) } +fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { + NavTarget { + file_id: definition.macro_definition.file_id, + full_range: definition.range, + focus_range: Some(definition.range), + name: Some(definition.name), + kind: None, + container_name: Some(definition.macro_definition.name), + description: Some("macro parameter".to_owned()), + } +} + fn macro_nav_target(definition: MacroDefinition) -> NavTarget { NavTarget { file_id: definition.file_id, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 909d1133..d2dbaf40 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1238,6 +1238,84 @@ endmodule assert!(hover.info.as_str().contains("WIDTH"), "hover should mention macro name"); } +#[test] +fn preproc_macro_param_supports_navigation_hover_and_references() { + let text = r#" +`define SHIFT(/*marker:param_def*/value, amount) ((/*marker:param_ref*/value) << amount) +module top; + localparam int W = `SHIFT(4, 1); +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + let param_def = position(file_id, &markers, "param_def"); + let param_ref = position(file_id, &markers, "param_ref"); + let param_def_range = marked_range(&markers, "param_def", TextSize::of("value")); + let param_ref_range = marked_range(&markers, "param_ref", TextSize::of("value")); + + let nav = analysis + .goto_definition(param_ref) + .unwrap() + .expect("macro parameter reference navigation expected"); + assert!( + nav.info.iter().any(|target| target.focus_range == Some(param_def_range)), + "macro parameter body reference should navigate to formal: {nav:?}" + ); + + let definition_nav = analysis + .goto_definition(param_def) + .unwrap() + .expect("macro parameter definition navigation expected"); + assert!( + definition_nav.info.iter().any(|target| target.focus_range == Some(param_def_range)), + "macro parameter definition should be linkable: {definition_nav:?}" + ); + + let hover = analysis + .hover(param_ref, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro parameter hover expected"); + assert!( + hover.info.as_str().contains("Macro parameter") + && hover.info.as_str().contains("value") + && hover.info.as_str().contains("SHIFT"), + "hover should identify macro parameter: {}", + hover.info.as_str() + ); + + let refs = analysis + .references(param_def, ReferencesConfig::new(ScopeVisibility::Public, None)) + .unwrap() + .expect("macro parameter references expected"); + assert!( + refs.iter().any(|refs| { + refs.def.as_ref().is_some_and(|defs| { + defs.iter().any(|target| target.focus_range == Some(param_def_range)) + }) && refs + .refs + .get(&file_id) + .is_some_and(|ranges| ranges.iter().any(|(range, _)| *range == param_ref_range)) + }), + "references should connect formal and body parameter use: {refs:?}" + ); + + let refs_from_ref = analysis + .references(param_ref, ReferencesConfig::new(ScopeVisibility::Public, None)) + .unwrap() + .expect("macro parameter references from body use expected"); + assert!( + refs_from_ref.iter().any(|refs| { + refs.def.as_ref().is_some_and(|defs| { + defs.iter().any(|target| target.focus_range == Some(param_def_range)) + }) && refs + .refs + .get(&file_id) + .is_some_and(|ranges| ranges.iter().any(|(range, _)| *range == param_ref_range)) + }), + "references from body use should include the formal and use: {refs_from_ref:?}" + ); +} + #[test] fn preproc_macro_definition_supports_navigation_and_hover() { let text = r#" From 33fcc25cdff090ca2cc8ed034035d61c66bc0462 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 23:09:02 +0800 Subject: [PATCH 16/80] chore: clippy --- crates/preproc/src/source/provenance.rs | 29 +++++-------------------- crates/slang/bindings/rust/lib.rs | 3 ++- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 21c30baa..1073b755 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -87,7 +87,7 @@ pub enum SourceMacroResolutionReason { IncludeGuardIfNDef, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SourceIncludeGraph { directives: Vec, edges: Vec, @@ -111,7 +111,7 @@ pub enum SourceIncludeStatus { Unavailable(SourcePreprocUnavailable), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SourceMacroStateTimeline { states: Vec, checkpoints: Vec, @@ -389,12 +389,6 @@ impl Default for SourcePreprocTables { } } -impl Default for SourceIncludeGraph { - fn default() -> Self { - Self { directives: Vec::new(), edges: Vec::new() } - } -} - impl SourceIncludeGraph { pub fn directives(&self) -> &[SourceIncludeDirective] { &self.directives @@ -415,17 +409,6 @@ impl SourceMacroStateTimeline { } } -impl Default for SourceMacroStateTimeline { - fn default() -> Self { - Self { - states: Vec::new(), - checkpoints: Vec::new(), - source_order_boundaries: BTreeMap::new(), - final_source_order: 0, - } - } -} - impl SourceMacroStateTimeline { pub fn state_at_position(&self, position: SourcePosition) -> Option<&SourceMacroState> { let source_order = self.source_order_at_position(position); @@ -1437,10 +1420,10 @@ impl<'a> SourcePreprocModelBuilder<'a> { let Some(child) = self.tables.macro_calls.get(child) else { return false; }; - if let Ok(parent_definition) = self.definition_for_call(parent) { - if self.definition_body_contains_range(parent_definition, child.call_range) { - return true; - } + if let Ok(parent_definition) = self.definition_for_call(parent) + && self.definition_body_contains_range(parent_definition, child.call_range) + { + return true; } let Some(parent) = self.tables.macro_calls.get(parent) else { return false; diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index d7997e4a..57523944 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -453,7 +453,8 @@ impl PreprocessorTraceTokenProvenance { } } Self::BUILTIN => Self::Builtin { name: macro_name }, - Self::UNAVAILABLE | _ => Self::Unavailable, + Self::UNAVAILABLE => Self::Unavailable, + _ => Self::Unavailable, } } } From dbc87229ea4e410e5d79a186742c57e1c9619797 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 23:11:03 +0800 Subject: [PATCH 17/80] fix(ide): resolve macro argument source tokens --- crates/ide/src/document_highlight.rs | 43 +++++++--- crates/ide/src/goto_definition.rs | 38 +++++++-- crates/ide/src/hover.rs | 40 ++++++++-- crates/ide/src/lib.rs | 1 + crates/ide/src/references.rs | 30 +++++-- crates/ide/src/references/search.rs | 51 ++++++++---- crates/ide/src/source_tokens.rs | 115 +++++++++++++++++++++++++++ crates/ide/src/verilog_2005.rs | 68 ++++++++++++++++ 8 files changed, 338 insertions(+), 48 deletions(-) create mode 100644 crates/ide/src/source_tokens.rs diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 0f0e05f3..bd5af005 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -1,5 +1,5 @@ use hir::{container::InFile, file::HirFileId, semantics::Semantics}; -use syntax::{SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, token::TokenKindExt}; +use syntax::{SyntaxTokenWithParent, TokenKind, token::TokenKindExt}; use utils::line_index::TextRange; use vfs::FileId; @@ -33,16 +33,20 @@ pub(crate) fn document_highlight( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let token = root.token_at_offset(offset).pick_bext_token(token_precedence)?; - - handle_ctrl_flow_kw(&sema, hir_file_id, token).or_else(|| { - let def = match DefinitionClass::resolve(&sema, hir_file_id, token)? { - DefinitionClass::Definition(def) => def, - DefinitionClass::PortConnShorthand { local, .. } => local, - DefinitionClass::Ambiguous(_) => return None, - }; - highlight_refs(&sema, file_id, def, config) - }) + let selection = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )?; + let highlights = selection + .tokens + .into_iter() + .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) + .flatten() + .collect::>(); + (!highlights.is_empty()).then_some(highlights) } fn token_precedence(kind: TokenKind) -> usize { @@ -68,6 +72,23 @@ fn handle_ctrl_flow_kw( Some(highlights) } +fn highlight_for_token( + sema: &Semantics<'_, RootDb>, + file_id: FileId, + hir_file_id: HirFileId, + token: SyntaxTokenWithParent, + config: DocumentHighlightConfig, +) -> Option> { + handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { + let def = match DefinitionClass::resolve(sema, hir_file_id, token)? { + DefinitionClass::Definition(def) => def, + DefinitionClass::PortConnShorthand { local, .. } => local, + DefinitionClass::Ambiguous(_) => return None, + }; + highlight_refs(sema, file_id, def, config) + }) +} + fn highlight_refs<'a>( sema: &'a Semantics<'a, RootDb>, file_id: FileId, diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 9e3db490..5dc4ec29 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -11,8 +11,7 @@ use hir::{ }; use itertools::Itertools; use syntax::{ - SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, - has_text_range::HasTextRange, + SyntaxTokenWithParent, TokenKind, token::{TokenKindExt, pair_token}, }; use utils::line_index::{TextRange, TextSize}; @@ -41,19 +40,42 @@ pub(crate) fn goto_definition( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let token = root.token_at_offset(offset).pick_bext_token(token_precedence)?; + let selection = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )?; + let navs = selection + .tokens + .into_iter() + .filter_map(|token| nav_targets_for_token(db, &sema, hir_file_id, token)) + .flatten() + .unique() + .collect_vec(); + if navs.is_empty() { + return None; + } - let navs = handle_ctrl_flow_kw(&sema, hir_file_id, token).or_else(|| { - DefinitionClass::resolve(&sema, hir_file_id, token)? + Some(RangeInfo::new(selection.range, navs)) +} + +fn nav_targets_for_token( + db: &RootDb, + sema: &Semantics, + hir_file_id: HirFileId, + token: SyntaxTokenWithParent, +) -> Option> { + handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { + DefinitionClass::resolve(sema, hir_file_id, token)? .origins() .into_iter() .unique() .filter_map(|def| def.to_nav(db)) .collect_vec() .into() - })?; - - Some(RangeInfo::new(token.text_range()?, navs)) + }) } fn handle_preproc_macro( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 11ee95f9..ddf81780 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -11,9 +11,8 @@ use hir::{ semantics::Semantics, }; use syntax::{ - SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, ast::{self, AstNode}, - has_text_range::HasTextRange, token::TokenKindExt, }; use utils::{get::GetRef, line_index::TextSize}; @@ -52,11 +51,20 @@ pub(crate) fn hover( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let token = root.token_at_offset(offset).pick_bext_token(token_precedence)?; - - let res = handle_literal(&sema, hir_file_id, token) - .or_else(|| handle_definition(&sema, hir_file_id, token))?; - Some(RangeInfo::new(token.text_range()?, res)) + let selection = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )?; + let markups = selection + .tokens + .into_iter() + .filter_map(|token| hover_for_token(&sema, hir_file_id, token)) + .collect::>(); + let res = merge_hover_results(markups)?; + Some(RangeInfo::new(selection.range, res)) } pub(crate) fn token_precedence(kind: TokenKind) -> usize { @@ -86,6 +94,24 @@ fn handle_literal( render::render_literal(literal) } +fn hover_for_token( + sema: &Semantics, + file_id: HirFileId, + token: SyntaxTokenWithParent, +) -> Option { + handle_literal(sema, file_id, token).or_else(|| handle_definition(sema, file_id, token)) +} + +fn merge_hover_results(markups: Vec) -> Option { + let mut iter = markups.into_iter(); + let mut res = iter.next()?; + for markup in iter { + res.horizontal_line(); + res.merge(markup); + } + Some(res) +} + fn handle_preproc_macro( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index cdac0865..36e6ec26 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -40,6 +40,7 @@ pub mod rename; pub mod selection_ranges; pub mod semantic_tokens; pub mod signature_help; +pub(crate) mod source_tokens; #[cfg(test)] mod test_utils; #[cfg(test)] diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 29882a3f..5968bc4a 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -11,7 +11,7 @@ use itertools::Itertools; use nohash_hasher::IntMap; use search::{ReferencesCtx, SearchScope}; use syntax::{ - SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, has_text_range::HasTextRange, token::{TokenKindExt, pair_token}, }; @@ -77,15 +77,35 @@ pub(crate) fn references( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let token = root.token_at_offset(offset).pick_bext_token(token_precedence)?; + let selection = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )?; + let references = selection + .tokens + .into_iter() + .filter_map(|token| references_for_token(&sema, hir_file_id, token, config.clone())) + .flatten() + .collect_vec(); + (!references.is_empty()).then_some(references) +} - handle_ctrl_flow_kw(&sema, hir_file_id, token).or_else(|| { - let def = match DefinitionClass::resolve(&sema, hir_file_id, token)? { +fn references_for_token( + sema: &Semantics, + hir_file_id: HirFileId, + token: SyntaxTokenWithParent, + config: ReferencesConfig, +) -> Option> { + handle_ctrl_flow_kw(sema, hir_file_id, token).or_else(|| { + let def = match DefinitionClass::resolve(sema, hir_file_id, token)? { DefinitionClass::Definition(def) => def, DefinitionClass::PortConnShorthand { local, .. } => local, DefinitionClass::Ambiguous(_) => return None, }; - Some(vec![search_refs(&sema, def, config)]) + Some(vec![search_refs(sema, def, config)]) }) } diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index 911c8d17..bdae5907 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -12,8 +12,8 @@ use nohash_hasher::IntMap; use rustc_hash::FxHashMap; use smallvec::SmallVec; use syntax::{ - SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, has_text_range::HasTextRange, - ptr::SyntaxTokenPtr, token::TokenKindExt, + SyntaxNode, SyntaxTokenWithParent, has_text_range::HasTextRange, ptr::SyntaxTokenPtr, + token::TokenKindExt, }; use triomphe::Arc; use utils::{ @@ -209,8 +209,11 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { let parsed_file = LazyCell::new(|| sema.parse_file(file_id)); Self::match_text(&text, finder, range) - .filter_map(|offset| { - Self::filter_token((*parsed_file).root()?, file_id, &def_ranges, offset) + .flat_map(|offset| { + let Some(root) = (*parsed_file).root() else { + return Vec::new(); + }; + Self::filter_tokens(sema.db, root, file_id, &def_ranges, offset) }) .filter(|tp| self.classify_and_filter(sema, file_id.into(), tp)) .for_each(|token| { @@ -256,23 +259,37 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { }) } - fn filter_token<'tree>( + fn filter_tokens<'tree>( + db: &RootDb, node: SyntaxNode<'tree>, file_id: FileId, names: &[InFile], offset: TextSize, - ) -> Option> { - let tok = node.token_at_offset(offset).find(|tok| tok.kind().name_like())?; - let tok_range = tok.text_range()?; - - // filter out definitions - if names.iter().any(|InFile { value: range, file_id: name_file_id }| { - &tok_range == range && *name_file_id == file_id.into() - }) { - None - } else { - Some(tok) - } + ) -> Vec> { + let Some(selection) = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + node, + offset, + super::token_precedence, + ) else { + return Vec::new(); + }; + + selection + .tokens + .into_iter() + .filter(|tok| tok.kind().name_like()) + .filter(|tok| { + let Some(tok_range) = tok.text_range() else { + return false; + }; + + !names.iter().any(|InFile { value: range, file_id: name_file_id }| { + tok_range == *range && *name_file_id == file_id.into() + }) + }) + .collect() } fn classify_and_filter<'tree>( diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs new file mode 100644 index 00000000..678dba5d --- /dev/null +++ b/crates/ide/src/source_tokens.rs @@ -0,0 +1,115 @@ +use hir::preproc::{TokenProvenance, macro_expansion_provenances_at}; +use syntax::{ + SyntaxElement, SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, WalkEvent, + has_text_range::HasTextRange, +}; +use utils::line_index::{TextRange, TextSize}; +use vfs::FileId; + +use crate::db::root_db::RootDb; + +#[derive(Debug, Clone)] +pub(crate) struct SourceTokenSelection<'tree> { + pub range: TextRange, + pub tokens: Vec>, +} + +pub(crate) fn token_candidates_at_offset<'tree>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: impl Fn(TokenKind) -> usize, +) -> Option> { + match provenance_token_candidates_at_offset(db, file_id, root, offset) { + ProvenanceTokenLookup::Available(selection) => return Some(selection), + ProvenanceTokenLookup::Unavailable => return None, + ProvenanceTokenLookup::NotApplicable => {} + } + + let token = root.token_at_offset(offset).pick_bext_token(precedence)?; + Some(SourceTokenSelection { range: token.text_range()?, tokens: vec![token] }) +} + +enum ProvenanceTokenLookup<'tree> { + Available(SourceTokenSelection<'tree>), + Unavailable, + NotApplicable, +} + +fn provenance_token_candidates_at_offset<'tree>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, +) -> ProvenanceTokenLookup<'tree> { + let Ok(provenances) = macro_expansion_provenances_at(db, file_id, offset) else { + return ProvenanceTokenLookup::NotApplicable; + }; + + let mut source_ranges = Vec::new(); + for provenance in provenances { + for token in provenance.tokens { + let Some(range) = source_token_range_for_offset(&token.provenance, file_id, offset) + else { + continue; + }; + if !source_ranges.contains(&range) { + source_ranges.push(range); + } + } + } + + if source_ranges.is_empty() { + return ProvenanceTokenLookup::NotApplicable; + } + + let tokens = tokens_with_exact_ranges(root, &source_ranges); + if tokens.is_empty() { + return ProvenanceTokenLookup::Unavailable; + } + + let range = covering_range(&source_ranges); + ProvenanceTokenLookup::Available(SourceTokenSelection { range, tokens }) +} + +fn source_token_range_for_offset( + provenance: &TokenProvenance, + file_id: FileId, + offset: TextSize, +) -> Option { + let (source, range) = match provenance { + TokenProvenance::SourceToken { source, range } + | TokenProvenance::MacroArgument { source, range, .. } => (source, *range), + TokenProvenance::MacroBody { .. } + | TokenProvenance::Predefine { .. } + | TokenProvenance::Builtin { .. } + | TokenProvenance::Unavailable(_) => return None, + }; + (source.file_id() == file_id && range.contains_inclusive(offset)).then_some(range) +} + +fn tokens_with_exact_ranges<'tree>( + root: SyntaxNode<'tree>, + ranges: &[TextRange], +) -> Vec> { + let mut tokens = Vec::new(); + for event in root.elem_preorder() { + let WalkEvent::Enter(SyntaxElement::Token(token)) = event else { + continue; + }; + let Some(range) = token.text_range() else { + continue; + }; + if ranges.contains(&range) && !tokens.contains(&token) { + tokens.push(token); + } + } + tokens +} + +fn covering_range(ranges: &[TextRange]) -> TextRange { + let start = ranges.iter().map(|range| range.start()).min().unwrap_or_default(); + let end = ranges.iter().map(|range| range.end()).max().unwrap_or_default(); + TextRange::new(start, end) +} diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index d2dbaf40..17f5fdbd 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1316,6 +1316,74 @@ endmodule ); } +#[test] +fn preproc_macro_argument_source_token_resolves_to_hir_definition() { + let text = r#" +`define NEXT(value) (value + 1) +module top(input logic /*marker:def*/payload_i); + logic active_data; + assign active_data = `NEXT(/*marker:arg*/payload_i); +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + let definition_range = marked_range(&markers, "def", TextSize::of("payload_i")); + let arg_range = marked_range(&markers, "arg", TextSize::of("payload_i")); + let def = position(file_id, &markers, "def"); + let arg = position(file_id, &markers, "arg"); + + let nav = analysis + .goto_definition(arg) + .unwrap() + .expect("macro argument source token navigation expected"); + assert!( + nav.info.iter().any(|target| target.focus_range == Some(definition_range)), + "macro argument should navigate through expanded HIR to payload_i definition: {nav:?}" + ); + + let hover = analysis + .hover(arg, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro argument source token hover expected"); + assert!( + hover.info.as_str().contains("payload_i"), + "macro argument hover should render the HIR definition: {}", + hover.info.as_str() + ); + + let refs = analysis + .references(arg, ReferencesConfig::new(ScopeVisibility::Public, None)) + .unwrap() + .expect("macro argument source token references expected"); + assert!( + refs.iter().any(|refs| { + refs.def.as_ref().is_some_and(|defs| { + defs.iter().any(|target| target.focus_range == Some(definition_range)) + }) && refs + .refs + .get(&file_id) + .is_some_and(|ranges| ranges.iter().any(|(range, _)| *range == arg_range)) + }), + "macro argument references should include the source argument token: {refs:?}" + ); + + let refs_from_def = analysis + .references(def, ReferencesConfig::new(ScopeVisibility::Public, None)) + .unwrap() + .expect("payload_i definition references expected"); + assert!( + refs_from_def.iter().any(|refs| { + refs.def.as_ref().is_some_and(|defs| { + defs.iter().any(|target| target.focus_range == Some(definition_range)) + }) && refs + .refs + .get(&file_id) + .is_some_and(|ranges| ranges.iter().any(|(range, _)| *range == arg_range)) + }), + "references from payload_i definition should include macro argument source token: {refs_from_def:?}" + ); +} + #[test] fn preproc_macro_definition_supports_navigation_and_hover() { let text = r#" From 07da17e361e636e9b7f25b0b90cb08194c72d8f2 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 00:08:02 +0800 Subject: [PATCH 18/80] feat(hir): link manifest predefines to source ranges --- crates/hir/src/base_db/compilation_plan.rs | 10 +- crates/hir/src/base_db/project.rs | 56 +++- crates/hir/src/base_db/source_db.rs | 97 +++++-- crates/hir/src/preproc.rs | 318 ++++++++++++++++++--- crates/ide/src/diagnostics.rs | 9 +- crates/ide/src/goto_definition.rs | 21 +- crates/ide/src/hover.rs | 21 +- crates/ide/src/verilog_2005.rs | 90 +++++- crates/project-model/src/lib.rs | 126 ++++++-- crates/project-model/src/macro_def.rs | 39 ++- crates/project-model/src/toml_workspace.rs | 130 ++++++--- src/tests.rs | 74 +++++ 12 files changed, 837 insertions(+), 154 deletions(-) diff --git a/crates/hir/src/base_db/compilation_plan.rs b/crates/hir/src/base_db/compilation_plan.rs index 9a0b62e5..29094cdb 100644 --- a/crates/hir/src/base_db/compilation_plan.rs +++ b/crates/hir/src/base_db/compilation_plan.rs @@ -140,17 +140,13 @@ fn profile_inputs( profile.source_roots.clone(), profile.top_modules.clone(), profile.preprocess.include_dirs.clone(), - profile.preprocess.predefines.clone(), + profile.preprocess.predefine_strings(), ); } let preprocess = project_config.preprocess_for_profile(profile_id); - ( - root_scoped_source_root.into_iter().collect(), - Vec::new(), - preprocess.include_dirs, - preprocess.predefines, - ) + let predefines = preprocess.predefine_strings(); + (root_scoped_source_root.into_iter().collect(), Vec::new(), preprocess.include_dirs, predefines) } fn all_non_ignored_roots(db: &dyn SourceRootDb) -> Vec { diff --git a/crates/hir/src/base_db/project.rs b/crates/hir/src/base_db/project.rs index a42623d1..76669e48 100644 --- a/crates/hir/src/base_db/project.rs +++ b/crates/hir/src/base_db/project.rs @@ -1,5 +1,5 @@ use triomphe::Arc; -use utils::paths::AbsPathBuf; +use utils::{line_index::TextRange, paths::AbsPathBuf}; use crate::base_db::source_root::SourceRootId; @@ -8,14 +8,66 @@ pub struct CompilationProfileId(pub u32); #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PreprocessConfig { - pub predefines: Vec, + pub predefines: Vec, pub include_dirs: Vec, } impl PreprocessConfig { + pub fn with_predefine_strings( + predefines: impl IntoIterator>, + include_dirs: Vec, + ) -> Self { + Self { + predefines: predefines.into_iter().map(|predefine| Predefine::new(predefine)).collect(), + include_dirs, + } + } + pub fn include_dir_strings(&self) -> Vec { self.include_dirs.iter().map(ToString::to_string).collect() } + + pub fn predefine_strings(&self) -> Vec { + self.predefines.iter().map(|predefine| predefine.definition.clone()).collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Predefine { + pub definition: String, + pub source: Option, +} + +impl Predefine { + pub fn new(definition: impl Into) -> Self { + Self { definition: definition.into(), source: None } + } + + pub fn with_source(definition: impl Into, source: PredefineSource) -> Self { + Self { definition: definition.into(), source: Some(source) } + } + + pub fn as_str(&self) -> &str { + self.definition.as_str() + } +} + +impl From for Predefine { + fn from(value: String) -> Self { + Predefine::new(value) + } +} + +impl From<&str> for Predefine { + fn from(value: &str) -> Self { + Predefine::new(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PredefineSource { + pub path: AbsPathBuf, + pub range: TextRange, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 93c30689..cb1b5024 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -23,7 +23,7 @@ use vfs::{FileId, VfsPath, anchored_path::AnchoredPath}; use crate::base_db::{ compilation_plan::{self, CompilationPlan}, diagnostics_config::{DiagnosticSource, DiagnosticsConfig}, - project::{CompilationProfileId, PreprocessConfig, ProjectConfig}, + project::{CompilationProfileId, Predefine, PreprocessConfig, ProjectConfig}, source_root::{SourceRoot, SourceRootId}, }; @@ -104,7 +104,7 @@ fn parse_src(db: &dyn SourceDb, file_id: FileId) -> SyntaxTree { let preprocess = db.file_preprocess_config(file_id); let include_paths = preprocess.include_dir_strings(); let options = syntax::SyntaxTreeOptions { - predefines: preprocess.predefines.clone(), + predefines: preprocess.predefine_strings(), include_paths, ..syntax::SyntaxTreeOptions::without_include_expansion() }; @@ -146,6 +146,7 @@ pub struct MappedSourcePreprocModel { pub struct PreprocSourceMap { entries: FxHashMap, expansion_entries: FxHashMap, + predefine_sources: FxHashMap, text_lengths: FxHashMap, range_offsets: FxHashMap, } @@ -167,6 +168,12 @@ pub struct PreprocExpansionMapping { token_ranges: FxHashMap, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PreprocManifestSource { + pub file_id: FileId, + pub range: TextRange, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocVirtualOrigin { Predefines { profile: Option }, @@ -208,6 +215,7 @@ pub enum PreprocSourceMapError { impl PreprocSourceMap { pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId, text_len: usize) { self.entries.insert(source, PreprocSourceMapping::RealFile(file_id)); + self.predefine_sources.remove(&source); self.text_lengths.insert(source, text_len); self.range_offsets.insert(source, 0); } @@ -233,20 +241,37 @@ impl PreprocSourceMap { range_offset: usize, ) { self.entries.insert(source, PreprocSourceMapping::VirtualFile { file_id, path, origin }); + self.predefine_sources.remove(&source); self.text_lengths.insert(source, text_len); self.range_offsets.insert(source, range_offset); } pub fn insert_unmapped(&mut self, source: PreprocSourceId, reason: SourcePreprocUnavailable) { self.entries.insert(source, PreprocSourceMapping::Unmapped(reason)); + self.predefine_sources.remove(&source); self.text_lengths.remove(&source); self.range_offsets.remove(&source); } + fn insert_predefine_manifest_source( + &mut self, + source: PreprocSourceId, + manifest_source: PreprocManifestSource, + ) { + self.predefine_sources.insert(source, manifest_source); + } + pub fn get(&self, source: PreprocSourceId) -> Option<&PreprocSourceMapping> { self.entries.get(&source) } + pub fn predefine_manifest_source( + &self, + source: PreprocSourceId, + ) -> Option { + self.predefine_sources.get(&source).copied() + } + pub fn insert_expansion_virtual_file( &mut self, expansion: SourceMacroExpansionId, @@ -445,6 +470,7 @@ fn source_preproc_file_ids( profile_id: Option, trace: &PreprocessorTrace, options: &SyntaxTreeOptions, + preprocess: &PreprocessConfig, ) -> Result { let mut source_map = PreprocSourceMap::default(); let path_file_ids = path_file_ids(db); @@ -458,7 +484,7 @@ fn source_preproc_file_ids( .map(|source| PreprocSourceId::from(source.buffer_id)) .collect::>(); let predefine_map = - PredefineVirtualMapping::new(db, profile_id, &options.predefines, predefine_sources); + PredefineVirtualMapping::new(db, profile_id, &preprocess.predefines, predefine_sources); for source in &trace.source_buffers { let source_id = PreprocSourceId::from(source.buffer_id); @@ -507,6 +533,9 @@ fn source_preproc_file_ids( entry.text_len, entry.range_offset, ); + if let Some(manifest_source) = entry.manifest_source(&path_file_ids) { + source_map.insert_predefine_manifest_source(source_id, manifest_source); + } } else { source_map.insert_unmapped( source_id, @@ -625,13 +654,14 @@ struct PredefineVirtualEntry { path: VfsPath, text_len: usize, range_offset: usize, + predefine: Predefine, } impl PredefineVirtualMapping { fn new( db: &dyn SourceRootDb, profile_id: Option, - predefines: &[String], + predefines: &[Predefine], mut sources: Vec, ) -> Self { sources.sort_by_key(|source| source.raw()); @@ -641,17 +671,23 @@ impl PredefineVirtualMapping { let texts = predefines .iter() - .map(|predefine| materialized_predefine_text(predefine)) + .map(|predefine| materialized_predefine_text(predefine.as_str())) .collect::>(); let text_len = texts.iter().map(String::len).sum(); let path = preproc_virtual_predefines_path(profile_id); let file_id = preproc_virtual_file_id(db, &path); let mut range_offset = 0usize; let mut entries = FxHashMap::default(); - for (source, text) in sources.into_iter().zip(texts) { + for (index, (source, text)) in sources.into_iter().zip(texts).enumerate() { entries.insert( source, - PredefineVirtualEntry { file_id, path: path.clone(), text_len, range_offset }, + PredefineVirtualEntry { + file_id, + path: path.clone(), + text_len, + range_offset, + predefine: predefines[index].clone(), + }, ); range_offset += text.len(); } @@ -664,6 +700,17 @@ impl PredefineVirtualMapping { } } +impl PredefineVirtualEntry { + fn manifest_source( + &self, + path_file_ids: &PathIdentityIndex, + ) -> Option { + let source = self.predefine.source.as_ref()?; + let file_id = path_file_ids.get_path(source.path.as_path())?; + Some(PreprocManifestSource { file_id, range: source.range }) + } +} + fn preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> FileId { file_id_for_vfs_path(db, path).unwrap_or_else(|| synthetic_virtual_file_id(path)) } @@ -778,7 +825,7 @@ fn syntax_tree_options_for_file( let profile_id = db.file_compilation_profile(file_id); let include_buffers = db.include_buffers_for_profile(profile_id).as_ref().clone(); syntax::SyntaxTreeOptions { - predefines: preprocess.predefines.clone(), + predefines: preprocess.predefine_strings(), include_paths: preprocess.include_dir_strings(), include_buffers, ..syntax::SyntaxTreeOptions::default() @@ -793,7 +840,7 @@ fn syntax_tree_options_for_profile( let preprocess = project_config.preprocess_for_profile(profile_id); let include_paths = preprocess.include_dir_strings(); syntax::SyntaxTreeOptions { - predefines: preprocess.predefines, + predefines: preprocess.predefine_strings(), include_paths, include_buffers, ..syntax::SyntaxTreeOptions::default() @@ -1016,6 +1063,7 @@ fn source_preproc_model( let text = db.file_text(file_id); let identity = source_file_identity(db, file_id); let profile_id = db.file_compilation_profile(file_id); + let preprocess = db.file_preprocess_config(file_id); let options = syntax_tree_options_for_file(db, file_id); let Some(trace) = SyntaxTree::preprocessor_trace(&text, &identity.name, &identity.path, &options) @@ -1023,10 +1071,11 @@ fn source_preproc_model( return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; - let mut source_map = match source_preproc_file_ids(db, file_id, profile_id, &trace, &options) { - Ok(source_map) => source_map, - Err(err) => return Arc::new(Err(err)), - }; + let mut source_map = + match source_preproc_file_ids(db, file_id, profile_id, &trace, &options, &preprocess) { + Ok(source_map) => source_map, + Err(err) => return Arc::new(Err(err)), + }; let model = match SourcePreprocModel::from_trace(trace) { Ok(model) => model, Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), @@ -1362,7 +1411,9 @@ mod tests { emitted_tokens: Vec::new(), }; let options = SyntaxTreeOptions::default(); - let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options).unwrap(); + let preprocess = PreprocessConfig::default(); + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); assert_eq!( source_map.get(PreprocSourceId::from(2)), @@ -1406,8 +1457,11 @@ mod tests { predefines: vec!["FIRST=1".to_owned(), "SECOND".to_owned()], ..SyntaxTreeOptions::default() }; + let preprocess = + PreprocessConfig::with_predefine_strings(["FIRST=1", "SECOND"], Vec::new()); - let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options).unwrap(); + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); let first = PreprocSourceId::from(2); let second = PreprocSourceId::from(3); let expected_path = preproc_virtual_predefines_path(None); @@ -1473,9 +1527,16 @@ mod tests { ..SyntaxTreeOptions::default() }; - let source_map = - source_preproc_file_ids(&db, TOP, Some(CompilationProfileId(7)), &trace, &options) - .unwrap(); + let preprocess = PreprocessConfig::default(); + let source_map = source_preproc_file_ids( + &db, + TOP, + Some(CompilationProfileId(7)), + &trace, + &options, + &preprocess, + ) + .unwrap(); let source = PreprocSourceId::from(4); let Some(PreprocSourceMapping::VirtualFile { path, origin, .. }) = source_map.get(source) else { diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 318a276f..b90186c4 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -24,7 +24,7 @@ use utils::{ use vfs::FileId; use crate::base_db::{ - project::CompilationProfileId, + project::{CompilationProfileId, Predefine}, source_db::{ MappedSourcePreprocModel, PreprocSourceMapError, PreprocSourceMapping, PreprocVirtualOrigin, SourceFileKind, SourcePreprocQueryError, SourceRootDb, @@ -65,6 +65,7 @@ pub enum PreprocUnavailable { AmbiguousMacroReferenceContexts { contexts: usize }, AmbiguousMacroExpansionContexts { contexts: usize }, AmbiguousMacroParamContexts { contexts: usize }, + AmbiguousMacroDefinitionContexts { contexts: usize }, AmbiguousDiagnosticProvenance { targets: usize }, AmbiguousIncludeTargets { targets: usize }, } @@ -95,12 +96,26 @@ macro_rules! mapped_preproc_id { }; } -mapped_preproc_id!(MacroDefinitionId, SourceMacroDefinitionId); mapped_preproc_id!(MacroReferenceId, SourceMacroReferenceId); mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); mapped_preproc_id!(MacroCallId, SourceMacroCallId); mapped_preproc_id!(MacroExpansionId, SourceMacroExpansionId); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MacroDefinitionId { + Source(SourceMacroDefinitionId), + ConfiguredPredefine { file_id: FileId, range: TextRange }, +} + +impl From for MacroDefinitionId { + fn from(value: SourceMacroDefinitionId) -> Self { + Self::Source(value) + } +} + +const CONFIGURED_PREDEFINE_DEFINE_INDEX: usize = usize::MAX; +const CONFIGURED_PREDEFINE_EVENT_ID: u32 = u32::MAX; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MappedPreprocSource { RealFile { file_id: FileId }, @@ -570,13 +585,13 @@ fn configured_predefine_names(db: &dyn SourceRootDb, file_id: FileId) -> Vec Option { if name.is_empty() { None } else { Some(SmolStr::new(name)) } } +fn configured_predefine_definitions_for_name( + db: &dyn SourceRootDb, + context_file_id: FileId, + name: &SmolStr, +) -> Vec { + let mut definitions = Vec::new(); + let profile_id = db.file_compilation_profile(context_file_id); + let project_preprocess = db.project_config().preprocess_for_profile(profile_id); + for predefine in &project_preprocess.predefines { + if let Some(definition) = configured_predefine_definition(db, predefine, name) { + push_unique_macro_definition(&mut definitions, definition); + } + } + for predefine in &db.file_preprocess_config(context_file_id).predefines { + if let Some(definition) = configured_predefine_definition(db, predefine, name) { + push_unique_macro_definition(&mut definitions, definition); + } + } + definitions +} + +fn configured_predefine_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> Vec { + let mut definitions = Vec::new(); + for context_file_id in source_preproc_query_model_file_ids(db, file_id) { + let profile_id = db.file_compilation_profile(context_file_id); + let project_preprocess = db.project_config().preprocess_for_profile(profile_id); + for predefine in &project_preprocess.predefines { + if let Some(definition) = + configured_predefine_definition_at(db, predefine, file_id, offset) + { + push_unique_macro_definition(&mut definitions, definition); + } + } + for predefine in &db.file_preprocess_config(context_file_id).predefines { + if let Some(definition) = + configured_predefine_definition_at(db, predefine, file_id, offset) + { + push_unique_macro_definition(&mut definitions, definition); + } + } + } + definitions +} + +fn configured_predefine_definition_at( + db: &dyn SourceRootDb, + predefine: &Predefine, + file_id: FileId, + offset: TextSize, +) -> Option { + let definition = + configured_predefine_definition(db, predefine, &predefine_macro_name(predefine.as_str())?)?; + (definition.file_id == file_id && range_contains_offset(definition.name_range, offset)) + .then_some(definition) +} + +fn configured_predefine_definition( + db: &dyn SourceRootDb, + predefine: &Predefine, + name: &SmolStr, +) -> Option { + let predefine_name = predefine_macro_name(predefine.as_str())?; + if &predefine_name != name { + return None; + } + let source = predefine.source.as_ref()?; + let file_id = file_id_for_predefine_source_path(db, &source.path)?; + Some(MacroDefinition { + id: MacroDefinitionId::ConfiguredPredefine { file_id, range: source.range }, + source: MappedPreprocSource::RealFile { file_id }, + capability: PreprocAvailability::Complete, + file_id, + name: predefine_name, + define_index: CONFIGURED_PREDEFINE_DEFINE_INDEX, + event_id: CONFIGURED_PREDEFINE_EVENT_ID, + directive_range: source.range, + name_range: source.range, + }) +} + +fn file_id_for_predefine_source_path( + db: &dyn SourceRootDb, + path: &utils::paths::AbsPathBuf, +) -> Option { + db.files().iter().copied().find(|file_id| db.file_path(*file_id).as_ref() == Some(path)) +} + pub fn macro_definition_at( db: &dyn SourceRootDb, file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; + let mut first_error = None; + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; - for definition in mapped.model.macro_definitions().iter() { - let mapped_definition = map_macro_definition(mapped, definition)?; - if mapped_definition.file_id == file_id - && range_contains_offset(mapped_definition.name_range, offset) - { - return Ok(Some(mapped_definition)); + for definition in mapped.model.macro_definitions().iter() { + let mapped_definition = map_macro_definition(mapped, definition)?; + if mapped_definition.file_id == file_id + && range_contains_offset(mapped_definition.name_range, offset) + { + return Ok(Some(mapped_definition)); + } } } + let mut configured_definitions = configured_predefine_definitions_at(db, file_id, offset); + match configured_definitions.len() { + 0 => {} + 1 => return Ok(configured_definitions.pop()), + contexts => { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroDefinitionContexts { contexts }, + }); + } + } + + if let Some(error) = first_error { + return Err(error); + } + Ok(None) } @@ -939,30 +1069,42 @@ pub fn macro_reference_definitions_at( continue; } }; - push_unique_macro_reference_context(&mut references, mapped_reference); + push_unique_macro_reference_context(&mut references, mapped_reference.clone()); + + match &reference.resolution { + SourceMacroResolutionFact::Resolved { definition, .. } => { + let Some(definition) = mapped.model.macro_definitions().get(*definition) else { + record_first_error( + &mut first_error, + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { + event_id: reference.event_id.raw(), + }, + )), + ); + continue; + }; + let definition = match map_macro_definition(mapped, definition) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; - let SourceMacroResolutionFact::Resolved { definition, .. } = &reference.resolution - else { - continue; - }; - let Some(definition) = mapped.model.macro_definitions().get(*definition) else { - record_first_error( - &mut first_error, - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, - )), - ); - continue; - }; - let definition = match map_macro_definition(mapped, definition) { - Ok(definition) => definition, - Err(error) => { - record_first_error(&mut first_error, error); - continue; + push_unique_macro_definition(&mut definitions, definition); } - }; - - push_unique_macro_definition(&mut definitions, definition); + SourceMacroResolutionFact::Undefined => { + for definition in configured_predefine_definitions_for_name( + db, + model_file_id, + &mapped_reference.name, + ) { + push_unique_macro_definition(&mut definitions, definition); + } + } + SourceMacroResolutionFact::Unavailable(_) => {} + } } } @@ -1345,19 +1487,24 @@ pub(crate) fn build_macro_reference_index( continue; } }; - collect_macro_references_in_model(mapped, model_file_id, &mut index); + collect_macro_references_in_model(db, mapped, model_file_id, &mut index); } index } fn collect_macro_references_in_model( + db: &dyn SourceRootDb, mapped: &MappedSourcePreprocModel, model_file_id: FileId, index: &mut MacroReferenceIndex, ) { for reference in mapped.model.macro_references().iter() { let SourceMacroResolutionFact::Resolved { definition, .. } = reference.resolution else { + if reference.resolution == SourceMacroResolutionFact::Undefined { + collect_configured_predefine_reference(db, mapped, model_file_id, reference, index); + continue; + } if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { index.push_issue(MacroReferenceIndexIssue::UnavailableReference { file_id: model_file_id, @@ -1402,6 +1549,29 @@ fn collect_macro_references_in_model( } } +fn collect_configured_predefine_reference( + db: &dyn SourceRootDb, + mapped: &MappedSourcePreprocModel, + model_file_id: FileId, + source_reference: &SourceMacroReferenceFact, + index: &mut MacroReferenceIndex, +) { + let reference = match map_macro_reference(mapped, source_reference) { + Ok(reference) => reference, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + return; + } + }; + for definition in configured_predefine_definitions_for_name(db, model_file_id, &reference.name) + { + index.push(definition, reference.clone()); + } +} + pub fn include_directive_at( db: &dyn SourceRootDb, file_id: FileId, @@ -1632,12 +1802,19 @@ fn map_macro_definition( mapped: &MappedSourcePreprocModel, definition: &SourceMacroDefinitionFact, ) -> PreprocResult { - let (source, directive_range, name_range) = map_definition_ranges( + let (mut source, mut directive_range, mut name_range) = map_definition_ranges( mapped, definition.event_id.raw(), definition.directive_range, definition.name_range, )?; + if let Some(manifest_source) = + mapped.source_map.predefine_manifest_source(definition.name_range.source) + { + source = MappedPreprocSource::RealFile { file_id: manifest_source.file_id }; + directive_range = manifest_source.range; + name_range = manifest_source.range; + } Ok(MacroDefinition { id: definition.id.into(), file_id: source.file_id(), @@ -2449,7 +2626,10 @@ mod tests { use crate::{ base_db::{ diagnostics_config::DiagnosticsConfig, - project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + project::{ + CompilationProfile, CompilationProfileId, Predefine, PredefineSource, + PreprocessConfig, ProjectConfig, + }, salsa::{self, Durability}, source_db::{ FileLoader, PreprocVirtualOrigin, SourceDb, SourceDbStorage, SourceFileKind, @@ -2466,6 +2646,7 @@ mod tests { const TOP: FileId = FileId(0); const HEADER: FileId = FileId(1); const LEAF: FileId = FileId(2); + const MANIFEST: FileId = FileId(3); const ROOT: SourceRootId = SourceRootId(0); const PROFILE: CompilationProfileId = CompilationProfileId(0); @@ -2509,6 +2690,16 @@ mod tests { fn db_with_entries_and_predefines( entries: &[(FileId, &str, &str)], predefines: Vec, + ) -> TestDb { + db_with_entries_and_predefine_entries( + entries, + predefines.into_iter().map(Predefine::new).collect(), + ) + } + + fn db_with_entries_and_predefine_entries( + entries: &[(FileId, &str, &str)], + predefines: Vec, ) -> TestDb { let include_dir = abs_path("include"); @@ -2845,6 +3036,59 @@ endmodule assert!(names.iter().any(|name| name == "A005_MAGIC"), "{names:?}"); } + #[test] + fn preproc_manifest_predefine_definition_uses_manifest_provenance() { + let root_text = r#"`ifdef FROM_MANIFEST +module top; +localparam int W = `FROM_MANIFEST; +endmodule +`endif +"#; + let manifest_text = "defines = [\"FROM_MANIFEST=1\"]\n"; + let manifest_range = TextRange::new( + offset(manifest_text, "\"FROM_MANIFEST=1\""), + offset_after(manifest_text, "\"FROM_MANIFEST=1\""), + ); + let predefine = Predefine::with_source( + "FROM_MANIFEST=1", + PredefineSource { path: abs_path("vide.toml"), range: manifest_range }, + ); + let db = db_with_entries_and_predefine_entries( + &[(TOP, "rtl/top.v", root_text), (MANIFEST, "vide.toml", manifest_text)], + vec![predefine], + ); + + let resolution = macro_reference_definitions_at( + &db, + TOP, + offset_after_n(root_text, "`FROM_MANIFEST", 0), + ) + .unwrap() + .unwrap(); + assert!( + resolution.definitions.iter().any(|definition| { + definition.file_id == MANIFEST && definition.name_range == manifest_range + }), + "predefine reference should target the manifest source range: {resolution:?}" + ); + + let definition = + macro_definition_at(&db, MANIFEST, manifest_range.start()).unwrap().unwrap(); + assert_eq!(definition.file_id, MANIFEST); + assert_eq!(definition.name.as_str(), "FROM_MANIFEST"); + assert_eq!(definition.name_range, manifest_range); + assert_eq!(text_at_range(manifest_text, definition.name_range), "\"FROM_MANIFEST=1\""); + + let references = macro_references(&db, MANIFEST, &definition).unwrap(); + assert!( + references.references.iter().any(|reference| { + reference.file_id == TOP + && text_at_range(root_text, reference.range) == "FROM_MANIFEST" + }), + "manifest predefine definition should find source references: {references:?}" + ); + } + #[test] fn preproc_visible_macro_names_follow_define_undef_boundaries() { let root_text = r#"`define A005_LOCAL 1 diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 4d74f6f6..97b3f076 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -495,7 +495,7 @@ mod tests { files, SourceRootRole::Local, true, - PreprocessConfig { predefines, ..PreprocessConfig::default() }, + PreprocessConfig::with_predefine_strings(predefines, Vec::new()), ) } @@ -833,7 +833,10 @@ mod tests { vec![CompilationProfile { source_roots: vec![SourceRootId(0)], top_modules: Vec::new(), - preprocess: PreprocessConfig { predefines: Vec::new(), include_dirs: vec![root] }, + preprocess: PreprocessConfig { + include_dirs: vec![root], + ..PreprocessConfig::default() + }, }], ))); db.apply_change(change); @@ -950,8 +953,8 @@ mod tests { source_roots: vec![SourceRootId(0)], top_modules: Vec::new(), preprocess: PreprocessConfig { - predefines: Vec::new(), include_dirs: vec![include_root], + ..PreprocessConfig::default() }, }], ))); diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 5dc4ec29..77377252 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -83,11 +83,11 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option>> { - if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { + if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { return Some(RangeInfo::new(definition.range, vec![macro_param_nav_target(definition)])); } - if let Some(resolution) = macro_param_reference_definitions_at(db, file_id, offset).ok()? { + if let Ok(Some(resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { let reference_range = resolution.range; let targets = resolution.definitions.into_iter().map(macro_param_nav_target).collect_vec(); if targets.is_empty() { @@ -96,17 +96,20 @@ fn handle_preproc_macro( return Some(RangeInfo::new(reference_range, targets)); } - if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { + if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { return Some(RangeInfo::new(definition.name_range, vec![macro_nav_target(definition)])); } - let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; - let reference_range = resolution.range; - let targets = resolution.definitions.into_iter().map(macro_nav_target).collect_vec(); - if targets.is_empty() { - return None; + if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { + let reference_range = resolution.range; + let targets = resolution.definitions.into_iter().map(macro_nav_target).collect_vec(); + if targets.is_empty() { + return None; + } + return Some(RangeInfo::new(reference_range, targets)); } - Some(RangeInfo::new(reference_range, targets)) + + None } fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index ddf81780..120d7ee9 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -117,12 +117,11 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option> { - if let Some(definition) = macro_param_definition_at(db, file_id, offset).ok()? { + if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { return Some(RangeInfo::new(definition.range, macro_param_definition_markup(&definition))); } - let param_resolution = macro_param_reference_definitions_at(db, file_id, offset).ok()?; - if let Some(param_resolution) = param_resolution { + if let Ok(Some(param_resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { if param_resolution.definitions.is_empty() { return None; } @@ -132,18 +131,24 @@ fn handle_preproc_macro( )); } - if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { + if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { return Some(RangeInfo::new( definition.name_range, macro_definition_markup(db, &definition), )); } - let resolution = macro_reference_definitions_at(db, file_id, offset).ok()??; - if resolution.definitions.is_empty() { - return None; + if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { + if resolution.definitions.is_empty() { + return None; + } + return Some(RangeInfo::new( + resolution.range, + macro_definitions_markup(db, &resolution.definitions), + )); } - Some(RangeInfo::new(resolution.range, macro_definitions_markup(db, &resolution.definitions))) + + None } fn macro_param_definition_markup(definition: &MacroParamDefinition) -> Markup { diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 17f5fdbd..fdedb0bd 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -7,7 +7,10 @@ use std::{ use hir::{ base_db::{ change::Change, - project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, + project::{ + CompilationProfile, CompilationProfileId, Predefine, PredefineSource, PreprocessConfig, + ProjectConfig, + }, salsa::Durability, source_db::SourceDb, source_root::{SourceRoot, SourceRootId}, @@ -202,7 +205,7 @@ fn setup_marked_with_predefines( vec![CompilationProfile { source_roots: vec![SourceRootId(0)], top_modules: Vec::new(), - preprocess: PreprocessConfig { predefines, include_dirs: Vec::new() }, + preprocess: PreprocessConfig::with_predefine_strings(predefines, Vec::new()), }], ))); change.add_changed_file(ChangedFile { @@ -259,8 +262,8 @@ fn setup_include_macro_project( source_roots: vec![SourceRootId(0)], top_modules: Vec::new(), preprocess: PreprocessConfig { - predefines: Vec::new(), include_dirs: vec![include_dir], + ..PreprocessConfig::default() }, }], ))); @@ -917,6 +920,85 @@ endmodule ); } +#[test] +fn manifest_predefine_usage_navigates_to_vide_toml_define() { + let dir = TestDir::new("manifest-predefine-navigation"); + let top_path = dir.path().join("top.sv"); + let manifest_path = dir.path().join("vide.toml"); + let marked_top_text = normalize_fixture_text( + r#" +`ifdef FROM_MANIFEST +module top; +localparam int W = `/*marker:usage*/FROM_MANIFEST; +endmodule +`endif +"#, + ); + let marked_manifest_text = + normalize_fixture_text(r#"defines = [/*marker:def*/"FROM_MANIFEST=1"]"#); + let (top_text, top_markers) = strip_markers(marked_top_text); + let (manifest_text, manifest_markers) = strip_markers(marked_manifest_text); + let manifest_range = + marked_range(&manifest_markers, "def", TextSize::of("\"FROM_MANIFEST=1\"")); + + let top_file_id = FileId(0); + let manifest_file_id = FileId(1); + let mut file_set = FileSet::default(); + file_set.insert(top_file_id, VfsPath::from(top_path)); + file_set.insert(manifest_file_id, VfsPath::from(manifest_path.clone())); + + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local_with_source_files(file_set, vec![top_file_id])]); + 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 { + predefines: vec![Predefine::with_source( + "FROM_MANIFEST=1", + PredefineSource { path: manifest_path, range: manifest_range }, + )], + include_dirs: Vec::new(), + }, + }], + ))); + change.add_changed_file(ChangedFile { + file_id: top_file_id, + change_kind: ChangeKind::Create(Arc::from(top_text.as_str()), LineEnding::Unix), + }); + change.add_changed_file(ChangedFile { + file_id: manifest_file_id, + change_kind: ChangeKind::Create(Arc::from(manifest_text.as_str()), LineEnding::Unix), + }); + + let mut host = AnalysisHost::default(); + host.apply_change(change); + let analysis = host.make_analysis(); + + let nav = analysis + .goto_definition(position(top_file_id, &top_markers, "usage")) + .unwrap() + .expect("manifest predefine navigation expected"); + assert!( + nav.info.iter().any(|target| { + target.file_id == manifest_file_id && target.focus_range == Some(manifest_range) + }), + "predefine usage should navigate to vide.toml define: {nav:?}" + ); + + let manifest_nav = analysis + .goto_definition(position(manifest_file_id, &manifest_markers, "def")) + .unwrap() + .expect("manifest predefine definition should be linkable"); + assert!( + manifest_nav.info.iter().any(|target| { + target.file_id == manifest_file_id && target.focus_range == Some(manifest_range) + }), + "manifest define should resolve to its own authoritative range: {manifest_nav:?}" + ); +} + #[test] fn file_preprocess_config_selects_same_ifdef_branch_for_diagnostics_and_navigation() { let text = r#" @@ -1029,8 +1111,8 @@ endmodule source_roots: vec![SourceRootId(0)], top_modules: Vec::new(), preprocess: PreprocessConfig { - predefines: Vec::new(), include_dirs: vec![src_dir.clone()], + ..PreprocessConfig::default() }, }], ))); diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index ac90d4c1..450a1c7a 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -58,6 +58,8 @@ pub struct WorkspaceRoot { pub source_directories: PathMatcher, /// Literal source files from the manifest. pub source_files: Vec, + /// Non-source files that still need a VFS identity for IDE features. + pub extra_files: Vec, /// Include/search roots loaded as headers and passed to preprocessing. pub include_dirs: Vec, pub exclude_globs: Option, @@ -71,6 +73,8 @@ impl WorkspaceRoot { pub fn file_set_paths(&self) -> Vec { let mut paths = self.include_dirs.clone(); paths.extend(self.source.scan_roots().cloned()); + paths.extend(self.source_files.iter().cloned()); + paths.extend(self.extra_files.iter().cloned()); sort_and_remove_subfolders(&mut paths); paths } @@ -89,6 +93,7 @@ impl WorkspaceRoot { fn has_load_paths(&self) -> bool { !self.source_files.is_empty() + || !self.extra_files.is_empty() || !self.include_dirs.is_empty() || !self.source_directories.is_empty() } @@ -166,6 +171,7 @@ impl Workspace { fn from_toml(toml: TomlWorkspace, is_lib: bool) -> anyhow::Result { let TomlWorkspace { + manifest_path, top_modules, workspace_root, macro_defs, @@ -209,6 +215,7 @@ impl Workspace { source, source_directories, source_files: source_locations.source_files, + extra_files: vec![manifest_path.clone()], include_dirs: include_dirs.clone(), exclude_globs, }; @@ -216,7 +223,7 @@ impl Workspace { let semantic_profile = roots .iter() .any(|root| root.role.participates_in_semantic_profile()) - .then(|| semantic_profile(top_modules, macro_defs, include_dirs)); + .then(|| semantic_profile(top_modules, macro_defs, include_dirs, Some(manifest_path))); Ok(Self { workspace_root, library_paths, kind, roots, semantic_profile }) } @@ -230,6 +237,7 @@ impl Workspace { source: source.clone(), source_directories: source, source_files: Vec::new(), + extra_files: Vec::new(), include_dirs: include_dirs.clone(), exclude_globs: None, }; @@ -237,7 +245,7 @@ impl Workspace { let semantic_profile = roots .iter() .any(|root| root.role.participates_in_semantic_profile()) - .then(|| semantic_profile(Vec::new(), MacroDef::default(), include_dirs)); + .then(|| semantic_profile(Vec::new(), MacroDef::default(), include_dirs, None)); Self { workspace_root: path.clone(), @@ -273,11 +281,12 @@ fn semantic_profile( top_modules: Vec, macro_defs: MacroDef, include_dirs: Vec, + manifest_path: Option, ) -> WorkspaceSemanticProfile { WorkspaceSemanticProfile { top_modules, preprocess: PreprocessConfig { - predefines: macro_defs.to_predefine_strings(), + predefines: macro_defs.to_predefines(manifest_path.as_ref()), include_dirs, }, } @@ -290,6 +299,7 @@ struct WorkspaceRootParts { source: PathMatcher, source_directories: PathMatcher, source_files: Vec, + extra_files: Vec, include_dirs: Vec, exclude_globs: Option, } @@ -300,6 +310,7 @@ impl WorkspaceRootParts { source: PathMatcher::all_under_roots(Vec::new()), source_directories: PathMatcher::all_under_roots(Vec::new()), source_files: Vec::new(), + extra_files: self.extra_files.clone(), include_dirs: self.include_dirs.clone(), exclude_globs: self.exclude_globs.clone(), } @@ -310,6 +321,7 @@ impl WorkspaceRootParts { source: self.source.clone(), source_directories: self.source_directories.clone(), source_files: self.source_files.clone(), + extra_files: Vec::new(), include_dirs: Vec::new(), exclude_globs: self.exclude_globs.clone(), } @@ -358,6 +370,7 @@ fn push_workspace_root( source: parts.source, source_directories: parts.source_directories, source_files: parts.source_files, + extra_files: parts.extra_files, include_dirs: parts.include_dirs, exclude_globs: parts.exclude_globs, }; @@ -610,8 +623,17 @@ pub fn get_workspace_folder( if !root.source.is_empty() { include.push(root.source.clone()); } - let source = + if !root.source_files.is_empty() { + include.push(PathMatcher::all_under_roots(root.source_files.clone())); + } + if !root.extra_files.is_empty() { + include.push(PathMatcher::all_under_roots(root.extra_files.clone())); + } + let mut source = if root.source.is_empty() { Vec::new() } else { vec![root.source.clone()] }; + if !root.source_files.is_empty() { + source.push(PathMatcher::all_under_roots(root.source_files.clone())); + } let mut load_entries = Vec::new(); let source_files = root @@ -643,6 +665,18 @@ pub fn get_workspace_folder( load_entries.push(vfs::loader::Entry::Directories(dirs)); } + let extra_files = root + .extra_files + .iter() + .filter(|path| { + !is_excluded_load_file(path.as_path(), &exclude_paths, &root.exclude_globs) + }) + .cloned() + .collect_vec(); + if !extra_files.is_empty() { + load_entries.push(vfs::loader::Entry::Files(extra_files)); + } + let root_idx = fsc.len(); fileset_roles.push(root.role); @@ -901,33 +935,36 @@ libraries = ["../pkg/rtl"] } #[test] - fn empty_manifest_has_no_compilation_profile() { + fn empty_manifest_loads_manifest_without_systemverilog_source() { let root = TestDir::new("project-model-empty-manifest"); - fs::write(root.join(project_manifest::MANIFEST_FILE_NAME), "").unwrap(); + let manifest_path = root.join(project_manifest::MANIFEST_FILE_NAME); + fs::write(&manifest_path, "").unwrap(); let manifest = ProjectManifest::from_path(&root.path().to_path_buf()).unwrap(); let (model, errors) = ProjectModel::load(vec![manifest]); - let (_, _, source_root_config, project_config) = + let (load, _, source_root_config, project_config) = get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); assert_eq!(model.workspaces.len(), 1); - assert_eq!(model.workspaces[0].roots()[0].role, SourceRootRole::BestEffortIndex); + assert_eq!(model.workspaces[0].roots()[0].role, SourceRootRole::Local); assert_eq!( source_root_config.fileset_roles, - vec![SourceRootRole::BestEffortIndex, SourceRootRole::Ignored] + vec![SourceRootRole::Local, SourceRootRole::BestEffortIndex, SourceRootRole::Ignored] ); - assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); + assert_eq!(load.len(), 2); + assert!( + matches!(&load[0], vfs::loader::Entry::Files(files) if files == std::slice::from_ref(&manifest_path)) + ); + assert!(matches!(&load[1], vfs::loader::Entry::Directories(_))); + assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); } #[test] - fn syntax_only_default_manifest_has_no_compilation_profile() { + fn syntax_only_default_manifest_loads_manifest_without_systemverilog_source() { let root = TestDir::new("project-model-syntax-only-manifest"); - fs::write( - root.join(project_manifest::MANIFEST_FILE_NAME), - "sources = []\ninclude_dirs = []\n", - ) - .unwrap(); + let manifest_path = root.join(project_manifest::MANIFEST_FILE_NAME); + fs::write(&manifest_path, "sources = []\ninclude_dirs = []\n").unwrap(); let manifest = ProjectManifest::from_path(&root.path().to_path_buf()).unwrap(); let (model, errors) = ProjectModel::load(vec![manifest]); @@ -936,10 +973,16 @@ libraries = ["../pkg/rtl"] assert!(errors.is_empty(), "{errors:#?}"); assert_eq!(model.workspaces.len(), 1); - assert!(model.workspaces[0].roots().is_empty()); - assert!(load.is_empty()); - assert_eq!(source_root_config.fileset_roles, vec![SourceRootRole::Ignored]); - assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); + assert_eq!(model.workspaces[0].roots()[0].role, SourceRootRole::Local); + assert_eq!(load.len(), 1); + assert!( + matches!(&load[0], vfs::loader::Entry::Files(files) if files == std::slice::from_ref(&manifest_path)) + ); + assert_eq!( + source_root_config.fileset_roles, + vec![SourceRootRole::Local, SourceRootRole::Ignored] + ); + assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); } #[test] @@ -961,7 +1004,7 @@ include_dirs = ["include"] let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); - assert_eq!(load.len(), 1); + assert_eq!(load.len(), 2); assert_eq!( source_root_config.fileset_roles, vec![SourceRootRole::Local, SourceRootRole::Ignored] @@ -991,7 +1034,7 @@ include_dirs = ["include"] get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); - assert_eq!(load.len(), 2); + assert_eq!(load.len(), 3); assert_eq!( source_root_config.fileset_roles, vec![SourceRootRole::Local, SourceRootRole::BestEffortIndex, SourceRootRole::Ignored] @@ -1047,6 +1090,39 @@ include_dirs = ["include"] assert_eq!(profile.preprocess.include_dirs, [rtl]); } + #[test] + fn manifest_file_is_loaded_for_navigation_without_becoming_systemverilog_source() { + let root = TestDir::new("project-model-manifest-vfs-root"); + let rtl = root.create_dir_all("rtl"); + let top = rtl.join("top.sv"); + fs::write(&top, "module top; endmodule\n").unwrap(); + let manifest = root.join(project_manifest::MANIFEST_FILE_NAME); + fs::write(&manifest, "sources = [\"rtl/**\"]\ndefines = [\"FROM_MANIFEST=1\"]\n").unwrap(); + + let project_manifest = ProjectManifest::from_path(&root.path().to_path_buf()).unwrap(); + let (model, errors) = ProjectModel::load(vec![project_manifest]); + let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); + + assert!(errors.is_empty(), "{errors:#?}"); + assert!(load.iter().any(|entry| { + matches!(entry, vfs::loader::Entry::Files(files) if files.contains(&manifest)) + })); + + let mut vfs = Vfs::default(); + for file in [&top, &manifest] { + vfs.set_file_contents( + &VfsPath::from(file.clone()), + LoadResult::Loaded(String::new(), LineEnding::Unix), + ); + } + + let roots = source_root_config.partition(&vfs); + let manifest_id = roots[0].file_for_path(&VfsPath::from(manifest)).unwrap(); + let top_id = roots[0].file_for_path(&VfsPath::from(top)).unwrap(); + assert_eq!(roots[0].file_kind(&manifest_id), SourceFileKind::ProjectManifest); + assert_eq!(roots[0].file_kind(&top_id), SourceFileKind::SystemVerilog); + } + #[test] fn exclude_globs_filter_loaded_source_files() { let base = TestDir::new("project-model-excluded-source-root"); @@ -1066,7 +1142,7 @@ exclude = ["rtl/**"] let (load, _, _, project_config) = get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); - assert_eq!(load.len(), 1); + assert_eq!(load.len(), 2); let dirs = match &load[0] { vfs::loader::Entry::Directories(dirs) => dirs, other => panic!("expected directory loader entry, got {other:?}"), @@ -1099,7 +1175,7 @@ exclude = ["rtl/excluded/**"] let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); - assert_eq!(load.len(), 1); + assert_eq!(load.len(), 2); let dirs = match &load[0] { vfs::loader::Entry::Directories(dirs) => dirs, other => panic!("expected directory loader entry, got {other:?}"), @@ -1224,7 +1300,7 @@ include_dirs = [] let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); - assert_eq!(load.len(), 1); + assert_eq!(load.len(), 2); assert!( matches!(&load[0], vfs::loader::Entry::Files(files) if files == std::slice::from_ref(&top)) ); diff --git a/crates/project-model/src/macro_def.rs b/crates/project-model/src/macro_def.rs index 1347ec00..0a8d6e14 100644 --- a/crates/project-model/src/macro_def.rs +++ b/crates/project-model/src/macro_def.rs @@ -1,5 +1,7 @@ +use hir::base_db::project::{Predefine, PredefineSource}; use rustc_hash::FxHashSet; use smol_str::SmolStr; +use utils::{line_index::TextRange, paths::AbsPathBuf}; #[derive(Debug, Hash, PartialEq, Eq, Clone)] pub enum MacroAtom { @@ -10,19 +12,48 @@ pub enum MacroAtom { #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct MacroDef { pub macros: FxHashSet, + pub sources: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MacroDefSource { + pub atom: MacroAtom, + pub range: TextRange, } impl MacroDef { pub fn to_predefine_strings(&self) -> Vec { + self.to_predefines(None).into_iter().map(|predefine| predefine.definition).collect() + } + + pub fn to_predefines(&self, manifest_path: Option<&AbsPathBuf>) -> Vec { let mut predefines = self .macros .iter() - .map(|macro_atom| match macro_atom { - MacroAtom::Flag(name) => name.to_string(), - MacroAtom::KeyValue { key, value } => format!("{key}={value}"), + .map(|macro_atom| { + let definition = macro_atom.predefine_string(); + let source = manifest_path.and_then(|path| { + let mut matches = self + .sources + .iter() + .filter(|source| source.atom == *macro_atom) + .map(|source| source.range); + let range = matches.next()?; + matches.next().is_none().then(|| PredefineSource { path: path.clone(), range }) + }); + Predefine { definition, source } }) .collect::>(); - predefines.sort(); + predefines.sort_by(|left, right| left.definition.cmp(&right.definition)); predefines } } + +impl MacroAtom { + fn predefine_string(&self) -> String { + match self { + MacroAtom::Flag(name) => name.to_string(), + MacroAtom::KeyValue { key, value } => format!("{key}={value}"), + } + } +} diff --git a/crates/project-model/src/toml_workspace.rs b/crates/project-model/src/toml_workspace.rs index 7761089e..17609367 100644 --- a/crates/project-model/src/toml_workspace.rs +++ b/crates/project-model/src/toml_workspace.rs @@ -6,9 +6,13 @@ use regex::Regex; use rustc_hash::FxHashSet; use serde::Deserialize; use smol_str::SmolStr; -use utils::paths::{AbsPathBuf, Utf8PathBuf}; +use toml::Spanned; +use utils::{ + line_index::{TextRange, TextSize}, + paths::{AbsPathBuf, Utf8PathBuf}, +}; -use crate::macro_def::{MacroAtom, MacroDef}; +use crate::macro_def::{MacroAtom, MacroDef, MacroDefSource}; #[cfg(feature = "manifest-schema")] use crate::project_manifest::MANIFEST_FILE_NAME; @@ -144,54 +148,59 @@ fn de_macros<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - let res = Vec::::deserialize(deserializer)?; + let res = Vec::>::deserialize(deserializer)?; let ident_re = IDENT_RE.as_ref().map_err(|err| { serde::de::Error::custom(format!("invalid macro identifier regex: {err}")) })?; let kv_re = KV_RE .as_ref() .map_err(|err| serde::de::Error::custom(format!("invalid macro key-value regex: {err}")))?; - let macros = res - .into_iter() - .map(|macr: SmolStr| { - if ident_re.is_match(¯) { - Ok(MacroAtom::Flag(macr)) - } else if let Some(caps) = kv_re.captures(¯) { - let Some(key_match) = caps.get(1) else { + let mut macros = FxHashSet::default(); + let mut sources = Vec::new(); + for macr in res { + let range = spanned_text_range(macr.span()).map_err(serde::de::Error::custom)?; + let macr = macr.into_inner(); + let atom = if ident_re.is_match(¯) { + Ok(MacroAtom::Flag(macr)) + } else if let Some(caps) = kv_re.captures(¯) { + let Some(key_match) = caps.get(1) else { + return Err(serde::de::Error::custom(format!("Invalid macro definition: {macr}"))); + }; + let Some(value_match) = caps.get(2) else { + return Err(serde::de::Error::custom(format!("Invalid macro definition: {macr}"))); + }; + let mut key: SmolStr = key_match.as_str().into(); + let value = value_match.as_str().into(); + if key.starts_with('\\') { + let Some(stripped) = key.strip_prefix('\\').and_then(|key| key.strip_suffix(' ')) + else { return Err(serde::de::Error::custom(format!( - "Invalid macro definition: {macr}" + "Invalid escaped macro name: {macr}" ))); }; - let Some(value_match) = caps.get(2) else { - return Err(serde::de::Error::custom(format!( - "Invalid macro definition: {macr}" - ))); - }; - let mut key: SmolStr = key_match.as_str().into(); - let value = value_match.as_str().into(); - if key.starts_with('\\') { - let Some(stripped) = - key.strip_prefix('\\').and_then(|key| key.strip_suffix(' ')) - else { - return Err(serde::de::Error::custom(format!( - "Invalid escaped macro name: {macr}" - ))); - }; - key = stripped.into(); - } - Ok(MacroAtom::KeyValue { key, value }) - } else { - Err(serde::de::Error::custom(format!("Invalid macro definition: {macr}"))) + key = stripped.into(); } - }) - .collect::, _>>()? - .into_iter() - .collect::>(); - Ok(MacroDef { macros }) + Ok(MacroAtom::KeyValue { key, value }) + } else { + Err(serde::de::Error::custom(format!("Invalid macro definition: {macr}"))) + }?; + macros.insert(atom.clone()); + sources.push(MacroDefSource { atom, range }); + } + Ok(MacroDef { macros, sources }) +} + +fn spanned_text_range(span: std::ops::Range) -> Result { + let start = u32::try_from(span.start) + .map_err(|_| format!("manifest range start is too large: {}", span.start))?; + let end = u32::try_from(span.end) + .map_err(|_| format!("manifest range end is too large: {}", span.end))?; + Ok(TextRange::new(TextSize::from(start), TextSize::from(end))) } #[derive(Debug, PartialEq, Eq)] pub struct TomlWorkspace { + pub manifest_path: AbsPathBuf, pub top_modules: Vec, pub workspace_root: AbsPathBuf, pub macro_defs: MacroDef, @@ -228,6 +237,7 @@ impl TomlWorkspace { let exclude_patterns = toml_schema.exclude; Ok(TomlWorkspace { + manifest_path: toml.clone(), top_modules, workspace_root, macro_defs, @@ -267,7 +277,22 @@ defines = [ macros.insert(MacroAtom::KeyValue { key: "BAR".into(), value: "foo".into() }); macros.insert(MacroAtom::KeyValue { key: "BAZ".into(), value: "foo bar".into() }); macros.insert(MacroAtom::KeyValue { key: "eqwe".into(), value: "123".into() }); - assert_eq!(toml_schema.defines, MacroDef { macros }); + assert_eq!(toml_schema.defines.macros, macros); + assert_eq!(toml_schema.defines.sources.len(), 6); + assert_eq!( + toml_schema.defines.sources[0], + MacroDefSource { + atom: MacroAtom::Flag("foo".into()), + range: range_of(toml, "\"foo\"") + } + ); + assert_eq!( + toml_schema.defines.sources[2], + MacroDefSource { + atom: MacroAtom::KeyValue { key: "FOO".into(), value: "bar".into() }, + range: range_of(toml, "\"FOO=bar\"") + } + ); } #[test] @@ -282,6 +307,29 @@ defines = [ assert_eq!(toml_schema.defines.to_predefine_strings(), ["BAR=foo", "FOO"]); } + #[test] + fn macro_predefines_keep_manifest_source_ranges() { + let root = TestDir::new("manifest-predefine-ranges"); + let manifest_text = r#" +defines = [ + "BAR=foo", + "FOO", +] +"#; + let manifest = root.write("vide.toml", manifest_text); + + let workspace = TomlWorkspace::load_from_file(&manifest).unwrap(); + let predefines = workspace.macro_defs.to_predefines(Some(&workspace.manifest_path)); + + let foo = predefines + .iter() + .find(|predefine| predefine.definition == "FOO") + .expect("FOO predefine expected"); + let source = foo.source.as_ref().expect("FOO should carry manifest source"); + assert_eq!(source.path, manifest); + assert_eq!(source.range, range_of(manifest_text, "\"FOO\"")); + } + #[test] fn empty_manifest_omits_source_patterns() { let root = TestDir::new("empty-manifest"); @@ -346,4 +394,12 @@ libraries = ["../pkg"] assert_eq!(workspace.include_dirs, Some(vec![root.join("include")])); assert_eq!(workspace.libraries, [root.join("../pkg")]); } + + fn range_of(text: &str, needle: &str) -> TextRange { + let start = text.find(needle).expect("needle should exist in fixture"); + TextRange::new( + TextSize::from(u32::try_from(start).unwrap()), + TextSize::from(u32::try_from(start + needle.len()).unwrap()), + ) + } } diff --git a/src/tests.rs b/src/tests.rs index 0037b78a..1aa51d65 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4155,6 +4155,80 @@ endmodule shutdown_test_server(&client, server_thread); } +#[test] +fn manifest_defined_macro_powers_lsp_ide_features() { + let temp_dir = TempDir::new("manifest-macro-lsp-features"); + let rtl_dir = temp_dir.path().join("rtl"); + fs::create_dir_all(&rtl_dir).unwrap(); + + let top_text = r#"`ifdef FROM_MANIFEST +module top; + localparam int W = `FROM_MANIFEST; +endmodule +`endif +"#; + let manifest_text = + "top_modules = [\"top\"]\nsources = [\"rtl/*.sv\"]\ndefines = [\"FROM_MANIFEST=1\"]\n"; + + let top_path = rtl_dir.join("top.sv"); + let manifest_path = temp_dir.path().join("vide.toml"); + fs::write(&top_path, top_text).unwrap(); + fs::write(&manifest_path, manifest_text).unwrap(); + + let (client, server_thread) = spawn_test_workspace( + temp_dir.path().to_path_buf(), + ClientCapabilities::default(), + UserConfig::default(), + ); + let top_uri = to_proto::url_from_abs_path(top_path.as_path()).unwrap(); + let manifest_uri = to_proto::url_from_abs_path(manifest_path.as_path()).unwrap(); + open_test_document(&client, top_uri.clone(), top_text); + + let (_result_id, diagnostics) = request_document_diagnostics(&client, top_uri.clone(), 1); + assert!( + diagnostics.iter().all(|diag| !diag.message.contains("unknown macro")), + "manifest define should feed preprocessor diagnostics: {diagnostics:?}" + ); + + let definition_uris = + request_goto_definition_uris(&client, top_uri.clone(), top_text, "FROM_MANIFEST;", 2); + assert!( + definition_uris.contains(&manifest_uri), + "manifest macro goto should reach vide.toml define: {definition_uris:?}" + ); + + let hover = request_hover(&client, top_uri.clone(), top_text, "FROM_MANIFEST;", 3) + .expect("manifest macro hover expected from source use"); + let hover_text = format!("{:?}", hover.contents); + assert!( + hover_text.contains("FROM_MANIFEST"), + "manifest macro hover should mention macro name: {hover_text}" + ); + + let manifest_hover = + request_hover(&client, manifest_uri.clone(), manifest_text, "FROM_MANIFEST=1", 4) + .expect("manifest macro hover expected from manifest define"); + let manifest_hover_text = format!("{:?}", manifest_hover.contents); + assert!( + manifest_hover_text.contains("FROM_MANIFEST"), + "manifest define hover should mention macro name: {manifest_hover_text}" + ); + + let manifest_definition_uris = request_goto_definition_uris( + &client, + manifest_uri.clone(), + manifest_text, + "FROM_MANIFEST=1", + 5, + ); + assert!( + manifest_definition_uris.contains(&manifest_uri), + "manifest define should be linkable to itself: {manifest_definition_uris:?}" + ); + + shutdown_test_server(&client, server_thread); +} + #[test] fn references_request_respects_include_declaration() { let temp_dir = TempDir::new("references-include-declaration"); From f78c18c2fe543709948a579d03ff89c69becd25a Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 00:48:57 +0800 Subject: [PATCH 19/80] feat(ide): explain macro expansion steps in hover --- crates/hir/src/preproc.rs | 167 ++++++++++++++++++++++- crates/ide/src/hover.rs | 237 ++++++++++++++++++++++++++++++++- crates/ide/src/markup.rs | 26 +++- crates/ide/src/verilog_2005.rs | 106 +++++++++++++++ 4 files changed, 525 insertions(+), 11 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index b90186c4..24db9297 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -3,7 +3,8 @@ use std::collections::BTreeMap; use preproc::source::{ CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceIncludeChainEntry, SourceIncludeDirectiveId, - SourceIncludeStatus, SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, + SourceIncludeStatus, SourceMacroArgument as SourceMacroArgumentFact, + SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, SourceMacroCallStatus as SourceMacroCallStatusFact, SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, @@ -154,12 +155,20 @@ pub struct MacroDefinition { pub capability: PreprocAvailability, pub file_id: FileId, pub name: SmolStr, + pub params: Option>, pub define_index: usize, pub event_id: u32, pub directive_range: TextRange, pub name_range: TextRange, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefinitionParam { + pub param_index: usize, + pub name: Option, + pub range: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroParamDefinition { pub macro_definition: MacroDefinition, @@ -267,17 +276,27 @@ pub struct MacroCall { pub source: MappedPreprocSource, pub capability: PreprocAvailability, pub file_id: FileId, + pub arguments: Vec, pub directive_range: TextRange, pub range: TextRange, pub callee: MacroResolution, pub expansion: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroArgument { + pub argument_index: usize, + pub source: Option, + pub range: Option, + pub tokens: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroExpansion { pub id: MacroExpansionId, pub call: MacroCall, pub definition_id: MacroDefinitionId, + pub definition: MacroDefinition, pub emitted_token_range: SourceEmittedTokenRange, pub virtual_source: MappedPreprocSource, pub virtual_range: TextRange, @@ -371,6 +390,13 @@ pub struct RecursiveMacroExpansion { pub unavailable: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecursiveMacroExpansionProvenance { + pub root_call: MacroCall, + pub expansions: Vec, + pub unavailable: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct MacroDefinitionKey { file_id: FileId, @@ -682,6 +708,7 @@ fn configured_predefine_definition( capability: PreprocAvailability::Complete, file_id, name: predefine_name, + params: None, define_index: CONFIGURED_PREDEFINE_DEFINE_INDEX, event_id: CONFIGURED_PREDEFINE_EVENT_ID, directive_range: source.range, @@ -1235,6 +1262,40 @@ pub fn recursive_macro_expansions_at( Ok(Vec::new()) } +pub fn recursive_macro_expansion_provenances_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut expansions = Vec::new(); + let mut first_error = None; + + for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { + continue; + }; + let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; + push_unique_recursive_macro_expansion_provenance(&mut expansions, recursive); + } + + if !expansions.is_empty() { + return Ok(expansions); + } + if let Some(error) = first_error { + return Err(error); + } + + Ok(Vec::new()) +} + pub fn macro_expansion_provenance_at( db: &dyn SourceRootDb, file_id: FileId, @@ -1815,12 +1876,30 @@ fn map_macro_definition( directive_range = manifest_source.range; name_range = manifest_source.range; } + let params = definition + .params + .as_ref() + .map(|params| { + params + .iter() + .enumerate() + .map(|(param_index, param)| { + let range = param + .name_range + .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) + .transpose()?; + Ok(MacroDefinitionParam { param_index, name: param.name.clone(), range }) + }) + .collect::>>() + }) + .transpose()?; Ok(MacroDefinition { id: definition.id.into(), file_id: source.file_id(), source, capability: capability_status(&mapped.model.capabilities().definition_name_ranges), name: definition.name.clone(), + params, define_index: define_index_for_definition(mapped, definition)?, event_id: definition.event_id.raw(), directive_range, @@ -1934,12 +2013,18 @@ fn map_macro_call( call: &SourceMacroCallFact, ) -> PreprocResult { let (source, range) = map_mapped_source_range(mapped, call.call_range)?; + let arguments = call + .arguments + .iter() + .map(|argument| map_macro_argument(mapped, argument)) + .collect::>>()?; Ok(MacroCall { id: call.id.into(), reference_id: call.reference.into(), file_id: source.file_id(), source, capability: macro_call_availability(&call.status), + arguments, directive_range: range, range, callee: map_macro_resolution(mapped, &call.callee)?, @@ -1947,6 +2032,23 @@ fn map_macro_call( }) } +fn map_macro_argument( + mapped: &MappedSourcePreprocModel, + argument: &SourceMacroArgumentFact, +) -> PreprocResult { + let (source, range) = argument + .argument_range + .map(|range| map_mapped_source_range(mapped, range)) + .transpose()? + .map_or((None, None), |(source, range)| (Some(source), Some(range))); + Ok(MacroArgument { + argument_index: argument.argument_index, + source, + range, + tokens: argument.tokens.iter().map(|token| token.raw.clone()).collect(), + }) +} + fn map_macro_expansion( mapped: &MappedSourcePreprocModel, expansion: &SourceMacroExpansionFact, @@ -1958,10 +2060,20 @@ fn map_macro_expansion( }), }); }; + let Some(definition) = mapped.model.macro_definitions().get(expansion.definition) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { + call: expansion.call, + }, + ), + }); + }; Ok(MacroExpansion { id: expansion.id.into(), call: map_macro_call(mapped, call)?, definition_id: expansion.definition.into(), + definition: map_macro_definition(mapped, definition)?, emitted_token_range: expansion.emitted_token_range, virtual_source: map_expansion_virtual_source(mapped, expansion.id)?, virtual_range: mapped @@ -2073,6 +2185,39 @@ fn recursive_macro_expansion_for_call( Ok(RecursiveMacroExpansion { root_call, expansions, unavailable }) } +fn recursive_macro_expansion_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let root_call = map_macro_call(mapped, call_fact)?; + let recursive = mapped.model.recursive_macro_expansion(call_fact.id); + let expansions = recursive + .expansions + .into_iter() + .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) + .map(|expansion| macro_expansion_provenance_for_expansion(mapped, expansion)) + .collect::>>()?; + let unavailable = recursive + .unavailable + .into_iter() + .map(|unavailable| { + let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, + ), + }); + }; + Ok(MacroExpansionUnavailable { + call: map_macro_call(mapped, call)?, + reason: PreprocUnavailable::Source(unavailable.reason), + }) + }) + .collect::>>()?; + + Ok(RecursiveMacroExpansionProvenance { root_call, expansions, unavailable }) +} + fn diagnostic_provenance_for_call( mapped: &MappedSourcePreprocModel, call_fact: &SourceMacroCallFact, @@ -2104,6 +2249,14 @@ fn macro_expansion_provenance_for_call( let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { return Ok(None); }; + Ok(Some(macro_expansion_provenance_for_expansion(mapped, expansion)?)) +} + +fn macro_expansion_provenance_for_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let expansion_id = expansion.id; let expansion = map_macro_expansion(mapped, expansion)?; let mut tokens = Vec::new(); for token_id in emitted_token_ids(expansion.emitted_token_range) { @@ -2128,7 +2281,7 @@ fn macro_expansion_provenance_for_call( }); } - Ok(Some(MacroExpansionProvenance { expansion, tokens })) + Ok(MacroExpansionProvenance { expansion, tokens }) } fn emitted_token_ids(range: SourceEmittedTokenRange) -> impl Iterator { @@ -2417,6 +2570,16 @@ fn push_unique_recursive_macro_expansion( expansions.push(expansion); } +fn push_unique_recursive_macro_expansion_provenance( + expansions: &mut Vec, + expansion: RecursiveMacroExpansionProvenance, +) { + if expansions.iter().any(|existing| existing == &expansion) { + return; + } + expansions.push(expansion); +} + fn push_unique_macro_expansion_provenance( provenances: &mut Vec, provenance: MacroExpansionProvenance, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 120d7ee9..fe43eae8 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -1,12 +1,14 @@ use hir::{ - base_db::source_db::SourceDb, + base_db::source_db::{SourceDb, SourceRootDb}, container::InContainer, file::HirFileId, hir_def::expr::Expr, preproc::{ - IncludeTarget, MacroDefinition, MacroParamDefinition, include_directives_at, - macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, - macro_reference_definitions_at, + EmittedTokenProvenance, IncludeTarget, MacroArgument, MacroDefinition, + MacroExpansionProvenance, MacroExpansionUnavailable, MacroParamDefinition, + RecursiveMacroExpansionProvenance, include_directives_at, macro_definition_at, + macro_param_definition_at, macro_param_reference_definitions_at, + macro_reference_definitions_at, recursive_macro_expansion_provenances_at, }, semantics::Semantics, }; @@ -15,7 +17,10 @@ use syntax::{ ast::{self, AstNode}, token::TokenKindExt, }; -use utils::{get::GetRef, line_index::TextSize}; +use utils::{ + get::GetRef, + line_index::{TextRange, TextSize}, +}; use vfs::FileId; use crate::{ @@ -40,7 +45,7 @@ pub(crate) fn hover( _config: HoverConfig, ) -> Option> { if let Some(macro_hover) = handle_preproc_macro(db, file_id, offset) { - return Some(macro_hover); + return Some(with_expanded_macro_hover(db, file_id, offset, macro_hover)); } if let Some(include) = handle_preproc_include(db, file_id, offset) { @@ -64,7 +69,7 @@ pub(crate) fn hover( .filter_map(|token| hover_for_token(&sema, hir_file_id, token)) .collect::>(); let res = merge_hover_results(markups)?; - Some(RangeInfo::new(selection.range, res)) + Some(with_expanded_macro_hover(db, file_id, offset, RangeInfo::new(selection.range, res))) } pub(crate) fn token_precedence(kind: TokenKind) -> usize { @@ -112,6 +117,224 @@ fn merge_hover_results(markups: Vec) -> Option { Some(res) } +fn with_expanded_macro_hover( + db: &RootDb, + file_id: FileId, + offset: TextSize, + mut hover: RangeInfo, +) -> RangeInfo { + let Some(expanded) = expanded_macro_hover(db, file_id, offset) else { + return hover; + }; + if let Some(range) = covering_range(&[hover.range, expanded.range]) { + hover.range = range; + } + hover.info.horizontal_line(); + hover.info.merge(expanded.info); + hover +} + +fn expanded_macro_hover( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option> { + let expansions = + recursive_macro_expansion_provenances_at(db, file_id, offset).ok().unwrap_or_default(); + if expansions.is_empty() { + return None; + } + + let ranges = expansions.iter().map(|expansion| expansion.root_call.range).collect::>(); + let range = covering_range(&ranges).unwrap_or_else(|| TextRange::empty(offset)); + let markup = expanded_macro_markup(db, &expansions); + Some(RangeInfo::new(range, markup)) +} + +fn expanded_macro_markup(db: &RootDb, expansions: &[RecursiveMacroExpansionProvenance]) -> Markup { + let mut markup = Markup::new(); + markup.print("Macro expansion"); + + for (index, expansion) in expansions.iter().enumerate() { + if expansions.len() > 1 { + markup.newline(); + markup.print("Context "); + markup.print(&(index + 1).to_string()); + } + render_recursive_expansion(db, &mut markup, expansion); + } + + markup +} + +fn render_recursive_expansion( + db: &RootDb, + markup: &mut Markup, + expansion: &RecursiveMacroExpansionProvenance, +) { + let Some(root) = expansion.expansions.first() else { + render_unavailable_expansion(db, markup, &expansion.unavailable); + return; + }; + + markup.newline(); + markup.print("Signature"); + markup.newline(); + render_signature_line(db, markup, &root.expansion.definition); + + if !root.expansion.call.arguments.is_empty() { + markup.newline(); + markup.print("Arguments"); + render_arguments(db, markup, &root.expansion.definition, &root.expansion.call.arguments); + } + + markup.newline(); + markup.print("Expanded result"); + markup.newline(); + markup.push_with_code_fence(&expanded_text_from_tokens(&root.tokens)); + + markup.print("Expansion steps"); + for (index, step) in expansion.expansions.iter().enumerate() { + render_expansion_step(db, markup, index + 1, step); + } + render_unavailable_expansion(db, markup, &expansion.unavailable); +} + +fn render_signature_line(db: &RootDb, markup: &mut Markup, definition: &MacroDefinition) { + markup.push_with_backticks(¯o_signature(definition)); + if let Some(source) = macro_definition_source_label(db, definition) { + markup.print(" from "); + markup.push_with_backticks(&source); + } +} + +fn render_arguments( + db: &RootDb, + markup: &mut Markup, + definition: &MacroDefinition, + arguments: &[MacroArgument], +) { + for argument in arguments { + markup.print("\n- "); + let name = definition + .params + .as_ref() + .and_then(|params| params.get(argument.argument_index)) + .and_then(|param| param.name.as_ref()) + .map_or_else(|| format!("${}", argument.argument_index), ToString::to_string); + markup.push_with_backticks(&name); + markup.print(" = "); + markup.push_with_backticks(&argument_text(db, argument)); + } +} + +fn render_expansion_step( + db: &RootDb, + markup: &mut Markup, + index: usize, + provenance: &MacroExpansionProvenance, +) { + markup.newline(); + if let Some(call_text) = + text_at_file_range(db, provenance.expansion.call.file_id, provenance.expansion.call.range) + { + markup.print(&index.to_string()); + markup.print(". "); + markup.push_with_backticks(call_text.trim()); + markup.print(" from "); + markup.push_with_backticks(¯o_signature(&provenance.expansion.definition)); + if let Some(source) = macro_definition_source_label(db, &provenance.expansion.definition) { + markup.print(" in "); + markup.push_with_backticks(&source); + } + } else { + markup.print(&index.to_string()); + markup.print(". Expansion from "); + markup.push_with_backticks(¯o_signature(&provenance.expansion.definition)); + } + markup.newline(); + markup.push_with_code_fence(&expanded_text_from_tokens(&provenance.tokens)); +} + +fn render_unavailable_expansion( + db: &RootDb, + markup: &mut Markup, + unavailable: &[MacroExpansionUnavailable], +) { + for unavailable in unavailable { + markup.newline(); + if let Some(call_text) = + text_at_file_range(db, unavailable.call.file_id, unavailable.call.range) + { + markup.push_with_backticks(call_text.trim()); + markup.print(" expansion unavailable."); + } else { + markup.print("Expansion unavailable."); + } + } +} + +fn macro_signature(definition: &MacroDefinition) -> String { + let mut signature = format!("`{}", definition.name); + if let Some(params) = &definition.params { + signature.push('('); + for (index, param) in params.iter().enumerate() { + if index > 0 { + signature.push_str(", "); + } + signature.push_str(param.name.as_deref().unwrap_or("")); + } + signature.push(')'); + } + signature +} + +fn macro_definition_source_label(db: &RootDb, definition: &MacroDefinition) -> Option { + match &definition.source { + hir::preproc::MappedPreprocSource::RealFile { file_id } => { + db.file_path(*file_id).map(|path| path.to_string()).or_else(|| { + db.source_root(db.source_root_id(*file_id)) + .path_for_file(file_id) + .map(|path| path.to_string()) + }) + } + hir::preproc::MappedPreprocSource::VirtualFile { .. } => None, + } +} + +fn argument_text(db: &RootDb, argument: &MacroArgument) -> String { + if let (Some(source), Some(range)) = (&argument.source, argument.range) + && let Some(text) = text_at_file_range(db, source.file_id(), range) + { + return text.trim().to_owned(); + } + argument.tokens.iter().map(|token| token.as_str()).collect::>().join(" ") +} + +fn expanded_text_from_tokens(tokens: &[EmittedTokenProvenance]) -> String { + let mut text = String::new(); + for (index, token) in tokens.iter().enumerate() { + if index > 0 { + text.push(' '); + } + text.push_str(token.text.as_str()); + } + text +} + +fn text_at_file_range(db: &RootDb, file_id: FileId, range: TextRange) -> Option { + let text = db.file_text(file_id); + let start = usize::from(range.start()); + let end = usize::from(range.end()); + text.get(start..end).map(ToOwned::to_owned) +} + +fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) +} + fn handle_preproc_macro( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/markup.rs b/crates/ide/src/markup.rs index 3e46c345..6072db97 100644 --- a/crates/ide/src/markup.rs +++ b/crates/ide/src/markup.rs @@ -77,12 +77,34 @@ impl Markup { } pub fn push_with_backticks(&mut self, contents: &str) { - self.text.push('`'); + let delimiter_len = max_backtick_run(contents).saturating_add(1); + let delimiter = "`".repeat(delimiter_len); + self.text.push_str(&delimiter); + if contents.contains('`') { + self.text.push(' '); + } self.text.push_str(contents); - self.text.push('`'); + if contents.contains('`') { + self.text.push(' '); + } + self.text.push_str(&delimiter); } pub fn is_empty(&self) -> bool { self.text.is_empty() } } + +fn max_backtick_run(contents: &str) -> usize { + let mut max_run = 0usize; + let mut current = 0usize; + for ch in contents.chars() { + if ch == '`' { + current += 1; + max_run = max_run.max(current); + } else { + current = 0; + } + } + max_run +} diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index fdedb0bd..563f4781 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1432,6 +1432,14 @@ endmodule "macro argument hover should render the HIR definition: {}", hover.info.as_str() ); + assert!( + hover.info.as_str().contains("Macro expansion") + && hover.info.as_str().contains("payload_i + 1") + && hover.info.as_str().contains("Expanded result") + && hover.info.as_str().contains("Expansion steps"), + "macro argument hover should show macro expansion: {}", + hover.info.as_str() + ); let refs = analysis .references(arg, ReferencesConfig::new(ScopeVisibility::Public, None)) @@ -1466,6 +1474,104 @@ endmodule ); } +#[test] +fn preproc_macro_call_hover_shows_expanded_text() { + let text = r#" +`define MAKE_DECL(name) logic name; +module top; + `/*marker:call*/MAKE_DECL(/*marker:arg*/generated) +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let hover = analysis + .hover(position(file_id, &markers, "call"), HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro call hover expected"); + let info = hover.info.as_str(); + assert!( + info.contains("Macro") + && info.contains("MAKE_DECL") + && info.contains("Macro expansion") + && info.contains("Signature") + && info.contains("`` `MAKE_DECL(name) ``") + && info.contains("Arguments") + && info.contains("`name` = `generated`") + && info.contains("Expanded result") + && info.contains("Expansion steps") + && info.contains("1. `` `MAKE_DECL(generated) `` from `` `MAKE_DECL(name) ``") + && info.contains("logic generated ;") + && !info.contains("Virtual expansion source") + && !info.contains("Token provenance"), + "macro call hover should include the expanded macro text: {info}" + ); + + let arg_hover = analysis + .hover(position(file_id, &markers, "arg"), HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro argument hover expected"); + let arg_info = arg_hover.info.as_str(); + assert!( + arg_info.contains("Macro expansion") && arg_info.contains("logic generated ;"), + "macro argument hover should include expanded macro text: {arg_info}" + ); +} + +#[test] +fn preproc_macro_hover_shows_nested_expansion_steps() { + let text = r#" +`define MATH_ONE 12'd1 +`define DEMO_NEXT(value) ((value) + `MATH_ONE) +module top(input logic payload_i); + assign active_data = `/*marker:call*/DEMO_NEXT(payload_i); +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let hover = analysis + .hover(position(file_id, &markers, "call"), HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("nested macro call hover expected"); + let info = hover.info.as_str(); + assert!( + info.contains("`` `DEMO_NEXT(value) ``") + && info.contains("`value` = `payload_i`") + && info.contains("Expanded result") + && info.contains("payload_i") + && info.contains("12") + && info.contains("'d") + && info.contains("Expansion steps") + && info.contains("1. `` `DEMO_NEXT(payload_i) `` from `` `DEMO_NEXT(value) ``") + && info.contains("2. `` `MATH_ONE `` from `` `MATH_ONE ``"), + "nested macro hover should show signature, arguments, result, and steps: {info}" + ); +} + +#[test] +fn preproc_macro_hover_reports_unavailable_expansion() { + let text = r#" +`define JOIN(a,b) a``b +module top; + wire `/*marker:call*/JOIN(foo,bar); +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let hover = analysis + .hover(position(file_id, &markers, "call"), HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro call hover expected"); + let info = hover.info.as_str(); + assert!( + info.contains("Macro expansion") + && info.contains("`` `JOIN(foo,bar) `` expansion unavailable."), + "unsupported expansion should be explicit in hover: {info}" + ); +} + #[test] fn preproc_macro_definition_supports_navigation_and_hover() { let text = r#" From 8ac0b600f3e612cfa7349fcda27d159ec5f353cc Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 01:15:09 +0800 Subject: [PATCH 20/80] fix(ci): restore syntax-only diagnostics --- crates/hir/src/preproc.rs | 10 ++++--- crates/project-model/src/lib.rs | 6 ++-- src/global_state/snapshot.rs | 50 ++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 24db9297..d8b24a30 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -372,7 +372,7 @@ pub enum DiagnosticProvenance { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MacroExpansionQuery { - Available(MacroExpansion), + Available(Box), Ambiguous(Vec), Unavailable(MacroExpansionUnavailable), } @@ -1159,7 +1159,7 @@ pub fn immediate_macro_expansion_at( let available = queries .iter() .filter_map(|query| match query { - MacroExpansionQuery::Available(expansion) => Some(expansion.clone()), + MacroExpansionQuery::Available(expansion) => Some(expansion.as_ref().clone()), MacroExpansionQuery::Ambiguous(expansions) => Some(expansions.first()?.clone()), MacroExpansionQuery::Unavailable(_) => None, }) @@ -1168,7 +1168,9 @@ pub fn immediate_macro_expansion_at( return Ok(Some(MacroExpansionQuery::Ambiguous(available))); } if available.len() == 1 { - return Ok(Some(MacroExpansionQuery::Available(available.into_iter().next().unwrap()))); + return Ok(Some(MacroExpansionQuery::Available(Box::new( + available.into_iter().next().unwrap(), + )))); } match queries.len() { 0 => Ok(None), @@ -2141,7 +2143,7 @@ fn immediate_macro_expansion_for_call( ), })); }; - MacroExpansionQuery::Available(map_macro_expansion(mapped, expansion)?) + MacroExpansionQuery::Available(Box::new(map_macro_expansion(mapped, expansion)?)) } SourceMacroExpansionQueryFact::Unavailable(reason) => { MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 450a1c7a..3d568839 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -264,7 +264,7 @@ impl Workspace { self.semantic_profile.as_ref() } - fn root(&self) -> &AbsPathBuf { + pub fn root(&self) -> &AbsPathBuf { &self.workspace_root } @@ -1119,8 +1119,8 @@ include_dirs = ["include"] let roots = source_root_config.partition(&vfs); let manifest_id = roots[0].file_for_path(&VfsPath::from(manifest)).unwrap(); let top_id = roots[0].file_for_path(&VfsPath::from(top)).unwrap(); - assert_eq!(roots[0].file_kind(&manifest_id), SourceFileKind::ProjectManifest); - assert_eq!(roots[0].file_kind(&top_id), SourceFileKind::SystemVerilog); + assert_eq!(roots[0].file_kind(manifest_id), SourceFileKind::ProjectManifest); + assert_eq!(roots[0].file_kind(top_id), SourceFileKind::SystemVerilog); } #[test] diff --git a/src/global_state/snapshot.rs b/src/global_state/snapshot.rs index 668627f6..d4474a1e 100644 --- a/src/global_state/snapshot.rs +++ b/src/global_state/snapshot.rs @@ -140,6 +140,10 @@ impl GlobalStateSnapshot { return Ok(Vec::new()); } + if self.open_file_syntax_diagnostics_for_disabled_root(file_id) { + return self.analysis.parse_diagnostics(file_id); + } + self.analysis.diagnostics(file_id) } @@ -251,7 +255,8 @@ impl GlobalStateSnapshot { // the diagnostic model. A workspace with no compilation // profiles still allows open-file syntax diagnostics. if matches!(scope, DiagnosticRequestScope::Document) - && !self.analysis.has_compilation_profiles().ok()? + && (!self.analysis.has_compilation_profiles().ok()? + || self.open_file_syntax_diagnostics_for_disabled_root(file_id)) { return Some(DiagnosticOwner::File(file_id)); } @@ -281,6 +286,49 @@ impl GlobalStateSnapshot { self.diagnostic_owner(file_id, DiagnosticRequestScope::Document).is_some() } + fn open_file_syntax_diagnostics_for_disabled_root(&self, file_id: FileId) -> bool { + self.mem_docs.contains_file_id(file_id) + && self.file_is_in_syntax_only_workspace(file_id) + && !self.file_is_manifest_excluded(file_id) + } + + fn file_is_in_syntax_only_workspace(&self, file_id: FileId) -> bool { + let Some(path) = self.file_path(file_id) else { + return false; + }; + + self.workspaces.iter().any(|workspace| { + if workspace.is_lib() || !path.starts_with(workspace.root()) { + return false; + } + + let roots = workspace.roots(); + !roots.is_empty() + && roots.iter().any(|root| matches!(root.role, SourceRootRole::Local)) + && roots.iter().all(|root| { + root.source.is_empty() + && root.source_directories.is_empty() + && root.source_files.is_empty() + && root.include_dirs.is_empty() + }) + }) + } + + fn file_is_manifest_excluded(&self, file_id: FileId) -> bool { + let Some(path) = self.file_path(file_id) else { + return false; + }; + + self.workspaces.iter().any(|workspace| { + path.starts_with(workspace.root()) + && workspace.roots().iter().any(|root| { + root.exclude_globs + .as_ref() + .is_some_and(|exclude| exclude.is_match(path.as_path())) + }) + }) + } + fn diagnostic_owner_file_ids( &self, owner: DiagnosticOwner, From 1b607fda4e678a2d4ec68c44531958e9205114ab Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 13:09:15 +0800 Subject: [PATCH 21/80] fix(preproc): use half-open provenance hit testing --- crates/hir/src/preproc.rs | 52 +++++++++++++++++++-------------- crates/ide/src/source_tokens.rs | 23 ++++++++++++++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index d8b24a30..3b6b082d 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -687,8 +687,7 @@ fn configured_predefine_definition_at( ) -> Option { let definition = configured_predefine_definition(db, predefine, &predefine_macro_name(predefine.as_str())?)?; - (definition.file_id == file_id && range_contains_offset(definition.name_range, offset)) - .then_some(definition) + (definition.file_id == file_id && definition.name_range.contains(offset)).then_some(definition) } fn configured_predefine_definition( @@ -741,8 +740,7 @@ pub fn macro_definition_at( for definition in mapped.model.macro_definitions().iter() { let mapped_definition = map_macro_definition(mapped, definition)?; - if mapped_definition.file_id == file_id - && range_contains_offset(mapped_definition.name_range, offset) + if mapped_definition.file_id == file_id && mapped_definition.name_range.contains(offset) { return Ok(Some(mapped_definition)); } @@ -811,7 +809,7 @@ pub fn macro_param_definitions_at( continue; }; if param_definition.macro_definition.file_id == file_id - && range_contains_offset(param_definition.range, offset) + && param_definition.range.contains(offset) { push_unique_macro_param_definition(&mut definitions, param_definition); } @@ -943,8 +941,12 @@ pub fn macro_usage_resolutions_at( let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { continue; }; - match mapped_source_range_contains_offset(mapped, reference.name_range, file_id, offset) - { + match mapped_source_range_contains_provenance_offset( + mapped, + reference.name_range, + file_id, + offset, + ) { Ok(true) => {} Ok(false) => continue, Err(error) => { @@ -1823,11 +1825,10 @@ fn mapped_source_range_at_offset( offset: TextSize, ) -> PreprocResult> { let (source, range) = map_mapped_source_range(mapped, source_range)?; - Ok((source.file_id() == file_id && range_contains_offset(range, offset)) - .then_some((source, range))) + Ok((source.file_id() == file_id && range.contains(offset)).then_some((source, range))) } -fn mapped_source_range_contains_offset( +fn mapped_source_range_contains_provenance_offset( mapped: &MappedSourcePreprocModel, source_range: SourceRange, file_id: FileId, @@ -2111,7 +2112,7 @@ fn source_macro_call_at( let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { return false; }; - source.file_id() == file_id && range_contains_offset(range, offset) + source.file_id() == file_id && range.contains(offset) }) } @@ -2770,10 +2771,6 @@ fn preproc_reference_model_file_ids( file_ids } -fn range_contains_offset(range: TextRange, offset: TextSize) -> bool { - range.start() <= offset && offset <= range.end() -} - #[cfg(test)] mod tests { use std::fmt; @@ -2973,6 +2970,7 @@ endmodule include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); assert_eq!(text_at_range(root_text, include.range), "\"defs.vh\""); assert!(include_directive_at(&db, TOP, offset(root_text, "`include")).unwrap().is_none()); + assert!(include_directive_at(&db, TOP, include.range.end()).unwrap().is_none()); let IncludeTarget::Literal { resolved_file, .. } = include.target else { panic!("literal include expected"); }; @@ -3223,13 +3221,10 @@ endmodule vec![predefine], ); - let resolution = macro_reference_definitions_at( - &db, - TOP, - offset_after_n(root_text, "`FROM_MANIFEST", 0), - ) - .unwrap() - .unwrap(); + let resolution = + macro_reference_definitions_at(&db, TOP, offset(root_text, "FROM_MANIFEST;")) + .unwrap() + .unwrap(); assert!( resolution.definitions.iter().any(|definition| { definition.file_id == MANIFEST && definition.name_range == manifest_range @@ -3335,6 +3330,10 @@ localparam int ENABLED = `HEADER_FLAG; .unwrap() .unwrap(); assert_eq!(text_at_range(root_text, definitions.range), "`HEADER_FLAG"); + assert!( + macro_reference_definitions_at(&db, TOP, definitions.range.end()).unwrap().is_none() + ); + assert!(macro_usage_resolution_at(&db, TOP, definitions.range.end()).unwrap().is_none()); assert!(matches!(definitions.capability, PreprocAvailability::Complete)); assert!(definitions.definitions.iter().any(|indexed| { indexed.file_id == HEADER @@ -3434,6 +3433,9 @@ endmodule .unwrap(); assert_eq!(value_definition.name.as_str(), "value"); assert_eq!(text_at_range(header_text, value_definition.range), "value"); + assert!( + macro_param_definition_at(&db, HEADER, value_definition.range.end()).unwrap().is_none() + ); let value_reference = macro_param_reference_definitions_at( &db, @@ -3443,6 +3445,11 @@ endmodule .unwrap() .unwrap(); assert_eq!(text_at_range(header_text, value_reference.range), "value"); + assert!( + macro_param_reference_definitions_at(&db, HEADER, value_reference.range.end()) + .unwrap() + .is_none() + ); assert!(value_reference.definitions.iter().any(|definition| { definition.param_index == value_definition.param_index && text_at_range(header_text, definition.range) == "value" @@ -3524,6 +3531,7 @@ localparam int B = `USE_WIDTH; let definition = macro_definition_at(&db, TOP, offset(root_text, "HEADER_FLAG")).unwrap().unwrap(); assert_eq!(text_at_range(root_text, definition.name_range), "HEADER_FLAG"); + assert!(macro_definition_at(&db, TOP, definition.name_range.end()).unwrap().is_none()); assert_ne!(definition.directive_range, definition.name_range); } diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index 678dba5d..c50fd041 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -86,7 +86,7 @@ fn source_token_range_for_offset( | TokenProvenance::Builtin { .. } | TokenProvenance::Unavailable(_) => return None, }; - (source.file_id() == file_id && range.contains_inclusive(offset)).then_some(range) + (source.file_id() == file_id && range.contains(offset)).then_some(range) } fn tokens_with_exact_ranges<'tree>( @@ -113,3 +113,24 @@ fn covering_range(ranges: &[TextRange]) -> TextRange { let end = ranges.iter().map(|range| range.end()).max().unwrap_or_default(); TextRange::new(start, end) } + +#[cfg(test)] +mod tests { + use hir::preproc::{MappedPreprocSource, TokenProvenance}; + + use super::*; + + #[test] + fn source_tokens_provenance_source_range_hit_test_is_half_open() { + let file_id = FileId(0); + let range = TextRange::new(5.into(), 10.into()); + let provenance = TokenProvenance::SourceToken { + source: MappedPreprocSource::RealFile { file_id }, + range, + }; + + assert_eq!(source_token_range_for_offset(&provenance, file_id, 5.into()), Some(range)); + assert_eq!(source_token_range_for_offset(&provenance, file_id, 9.into()), Some(range)); + assert_eq!(source_token_range_for_offset(&provenance, file_id, 10.into()), None); + } +} From 90870b5cf8c1e72bbe4b9f8321e1be8a21641b00 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 13:29:43 +0800 Subject: [PATCH 22/80] fix(ide): avoid publishing diagnostics to synthetic virtual files --- crates/hir/src/base_db/source_db.rs | 86 ++++++++------ crates/hir/src/preproc.rs | 174 ++++++++++++++++++++-------- crates/ide/src/diagnostics.rs | 96 ++++++++++++--- crates/ide/src/hover.rs | 6 +- crates/ide/src/source_tokens.rs | 2 +- 5 files changed, 262 insertions(+), 102 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index cb1b5024..e3911576 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,8 +1,3 @@ -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; - use preproc::source::{ PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, @@ -155,12 +150,13 @@ pub struct PreprocSourceMap { pub enum PreprocSourceMapping { RealFile(FileId), VirtualFile { file_id: FileId, path: VfsPath, origin: PreprocVirtualOrigin }, + VirtualDisplay { path: VfsPath, origin: PreprocVirtualOrigin }, Unmapped(SourcePreprocUnavailable), } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocExpansionMapping { - pub file_id: FileId, + pub file_id: Option, pub path: VfsPath, pub origin: PreprocVirtualOrigin, pub text: String, @@ -210,6 +206,10 @@ pub enum PreprocSourceMapError { MissingEmittedTokenRange { range: SourceEmittedTokenRange, }, + DisplayOnlyVirtualSource { + path: VfsPath, + origin: PreprocVirtualOrigin, + }, } impl PreprocSourceMap { @@ -223,7 +223,7 @@ impl PreprocSourceMap { pub fn insert_virtual_file( &mut self, source: PreprocSourceId, - file_id: FileId, + file_id: Option, path: VfsPath, origin: PreprocVirtualOrigin, text_len: usize, @@ -234,13 +234,17 @@ impl PreprocSourceMap { fn insert_virtual_file_with_offset( &mut self, source: PreprocSourceId, - file_id: FileId, + file_id: Option, path: VfsPath, origin: PreprocVirtualOrigin, text_len: usize, range_offset: usize, ) { - self.entries.insert(source, PreprocSourceMapping::VirtualFile { file_id, path, origin }); + let mapping = match file_id { + Some(file_id) => PreprocSourceMapping::VirtualFile { file_id, path, origin }, + None => PreprocSourceMapping::VirtualDisplay { path, origin }, + }; + self.entries.insert(source, mapping); self.predefine_sources.remove(&source); self.text_lengths.insert(source, text_len); self.range_offsets.insert(source, range_offset); @@ -275,7 +279,7 @@ impl PreprocSourceMap { pub fn insert_expansion_virtual_file( &mut self, expansion: SourceMacroExpansionId, - file_id: FileId, + file_id: Option, path: VfsPath, text: String, emitted_range: SourceEmittedTokenRange, @@ -305,10 +309,16 @@ impl PreprocSourceMap { let entry = self .expansion(expansion) .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - Ok(PreprocSourceMapping::VirtualFile { - file_id: entry.file_id, - path: entry.path.clone(), - origin: entry.origin.clone(), + Ok(match entry.file_id { + Some(file_id) => PreprocSourceMapping::VirtualFile { + file_id, + path: entry.path.clone(), + origin: entry.origin.clone(), + }, + None => PreprocSourceMapping::VirtualDisplay { + path: entry.path.clone(), + origin: entry.origin.clone(), + }, }) } @@ -343,6 +353,12 @@ impl PreprocSourceMap { match self.get(source) { Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), Some(PreprocSourceMapping::VirtualFile { file_id, .. }) => Ok(*file_id), + Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { + Err(PreprocSourceMapError::DisplayOnlyVirtualSource { + path: path.clone(), + origin: origin.clone(), + }) + } Some(PreprocSourceMapping::Unmapped(reason)) => { Err(PreprocSourceMapError::UnmappedSource { source, reason: reason.clone() }) } @@ -364,6 +380,7 @@ impl PreprocSourceMap { | PreprocSourceMapping::VirtualFile { file_id: mapped_file_id, .. } => { *mapped_file_id } + PreprocSourceMapping::VirtualDisplay { .. } => return None, PreprocSourceMapping::Unmapped(_) => return None, }; if mapped_file_id != file_id { @@ -384,7 +401,8 @@ impl PreprocSourceMap { pub fn map_range(&self, source_range: SourceRange) -> Result { match self.get(source_range.source) { Some(PreprocSourceMapping::RealFile(_)) - | Some(PreprocSourceMapping::VirtualFile { .. }) => {} + | Some(PreprocSourceMapping::VirtualFile { .. }) + | Some(PreprocSourceMapping::VirtualDisplay { .. }) => {} Some(PreprocSourceMapping::Unmapped(reason)) => { return Err(PreprocSourceMapError::UnmappedSource { source: source_range.source, @@ -507,7 +525,7 @@ fn source_preproc_file_ids( if let Some(text) = include_buffer_texts.get(&source.path) { let path = preproc_virtual_include_buffer_path(profile_id, source_id, &source.path); - let file_id = preproc_virtual_file_id(db, &path); + let file_id = materialized_preproc_virtual_file_id(db, &path); source_map.insert_virtual_file( source_id, file_id, @@ -650,7 +668,7 @@ struct PredefineVirtualMapping { } struct PredefineVirtualEntry { - file_id: FileId, + file_id: Option, path: VfsPath, text_len: usize, range_offset: usize, @@ -675,7 +693,7 @@ impl PredefineVirtualMapping { .collect::>(); let text_len = texts.iter().map(String::len).sum(); let path = preproc_virtual_predefines_path(profile_id); - let file_id = preproc_virtual_file_id(db, &path); + let file_id = materialized_preproc_virtual_file_id(db, &path); let mut range_offset = 0usize; let mut entries = FxHashMap::default(); for (index, (source, text)) in sources.into_iter().zip(texts).enumerate() { @@ -711,8 +729,8 @@ impl PredefineVirtualEntry { } } -fn preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> FileId { - file_id_for_vfs_path(db, path).unwrap_or_else(|| synthetic_virtual_file_id(path)) +fn materialized_preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> Option { + file_id_for_vfs_path(db, path) } fn file_id_for_vfs_path(db: &dyn SourceRootDb, path: &VfsPath) -> Option { @@ -726,12 +744,6 @@ fn file_id_for_vfs_path(db: &dyn SourceRootDb, path: &VfsPath) -> Option None } -fn synthetic_virtual_file_id(path: &VfsPath) -> FileId { - let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); - FileId(0x8000_0000 | ((hasher.finish() as u32) & 0x3fff_ffff)) -} - fn shift_text_range(range: TextRange, offset: usize) -> Option { let start = usize::from(range.start()).checked_add(offset)?; let end = usize::from(range.end()).checked_add(offset)?; @@ -774,7 +786,7 @@ fn materialize_expansion_virtual_files( continue; }; let path = preproc_virtual_expansion_path(profile_id, expansion.id); - let file_id = preproc_virtual_file_id(db, &path); + let file_id = materialized_preproc_virtual_file_id(db, &path); source_map.insert_expansion_virtual_file( expansion.id, file_id, @@ -1428,7 +1440,7 @@ mod tests { } #[test] - fn source_preproc_mapping_materializes_predefines_as_virtual_file() { + fn source_preproc_mapping_records_predefines_as_display_virtual_source_without_backing() { let db = db_with_root_file(); let trace = PreprocessorTrace { root_buffer_id: 1, @@ -1467,22 +1479,24 @@ mod tests { let expected_path = preproc_virtual_predefines_path(None); let first_text = materialized_predefine_text("FIRST=1"); - let Some(PreprocSourceMapping::VirtualFile { file_id, path, origin }) = - source_map.get(first) + let Some(PreprocSourceMapping::VirtualDisplay { path, origin }) = source_map.get(first) else { - panic!("first predefine should map to virtual file"); + panic!("first predefine should map to display-only virtual source"); }; assert_eq!(path, &expected_path); assert_eq!(origin, &PreprocVirtualOrigin::Predefines { profile: None }); assert_eq!( source_map.get(second), - Some(&PreprocSourceMapping::VirtualFile { - file_id: *file_id, + Some(&PreprocSourceMapping::VirtualDisplay { path: expected_path, origin: PreprocVirtualOrigin::Predefines { profile: None }, }) ); + assert!(matches!( + source_map.file_id(first), + Err(PreprocSourceMapError::DisplayOnlyVirtualSource { .. }) + )); let second_range = SourceRange { source: second, @@ -1498,7 +1512,7 @@ mod tests { } #[test] - fn source_preproc_mapping_materializes_external_include_buffer_with_text() { + fn source_preproc_mapping_records_external_include_buffer_as_display_virtual_source() { let db = db_with_root_file(); let external_path = "/external/generated_defs.vh".to_owned(); let trace = PreprocessorTrace { @@ -1538,9 +1552,9 @@ mod tests { ) .unwrap(); let source = PreprocSourceId::from(4); - let Some(PreprocSourceMapping::VirtualFile { path, origin, .. }) = source_map.get(source) + let Some(PreprocSourceMapping::VirtualDisplay { path, origin }) = source_map.get(source) else { - panic!("external include buffer should map to virtual file"); + panic!("external include buffer should map to display-only virtual source"); }; assert_eq!( diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 3b6b082d..a95a58bd 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -121,12 +121,14 @@ const CONFIGURED_PREDEFINE_EVENT_ID: u32 = u32::MAX; pub enum MappedPreprocSource { RealFile { file_id: FileId }, VirtualFile { file_id: FileId, path: vfs::VfsPath, origin: PreprocVirtualOrigin }, + VirtualDisplay { path: vfs::VfsPath, origin: PreprocVirtualOrigin }, } impl MappedPreprocSource { - pub fn file_id(&self) -> FileId { + pub fn file_id(&self) -> Option { match self { - Self::RealFile { file_id } | Self::VirtualFile { file_id, .. } => *file_id, + Self::RealFile { file_id } | Self::VirtualFile { file_id, .. } => Some(*file_id), + Self::VirtualDisplay { .. } => None, } } } @@ -1683,7 +1685,7 @@ pub fn include_directives_at( }; let status = map_include_status(mapped, &include.status)?; let resolved_file = match &status { - IncludeDirectiveStatus::Resolved { source } => Some(source.file_id()), + IncludeDirectiveStatus::Resolved { source } => source.file_id(), IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, }; let target = match &include.target { @@ -1741,7 +1743,9 @@ pub fn inactive_branches( continue; } }; - let branch_file_id = source.file_id(); + let Some(branch_file_id) = source.file_id() else { + continue; + }; if branch_file_id == file_id { push_unique_inactive_branch( &mut branches, @@ -1794,12 +1798,24 @@ fn record_first_error(first_error: &mut Option, error: PreprocErro } } +fn require_file_backed_source(source: &MappedPreprocSource) -> PreprocResult { + source.file_id().ok_or_else(|| { + let MappedPreprocSource::VirtualDisplay { path, origin } = source else { + unreachable!("file-backed source should have a FileId"); + }; + PreprocError::SourceMap(PreprocSourceMapError::DisplayOnlyVirtualSource { + path: path.clone(), + origin: origin.clone(), + }) + }) +} + fn map_source_range( mapped: &MappedSourcePreprocModel, source_range: SourceRange, ) -> PreprocResult<(FileId, TextRange)> { let (source, range) = map_mapped_source_range(mapped, source_range)?; - Ok((source.file_id(), range)) + Ok((require_file_backed_source(&source)?, range)) } fn map_source_id( @@ -1825,7 +1841,7 @@ fn mapped_source_range_at_offset( offset: TextSize, ) -> PreprocResult> { let (source, range) = map_mapped_source_range(mapped, source_range)?; - Ok((source.file_id() == file_id && range.contains(offset)).then_some((source, range))) + Ok((source.file_id() == Some(file_id) && range.contains(offset)).then_some((source, range))) } fn mapped_source_range_contains_provenance_offset( @@ -1852,6 +1868,9 @@ fn map_mapped_source_id( origin: origin.clone(), }) } + Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { + Ok(MappedPreprocSource::VirtualDisplay { path: path.clone(), origin: origin.clone() }) + } Some(PreprocSourceMapping::Unmapped(reason)) => { Err(PreprocError::SourceMap(PreprocSourceMapError::UnmappedSource { source, @@ -1896,9 +1915,10 @@ fn map_macro_definition( .collect::>>() }) .transpose()?; + let file_id = require_file_backed_source(&source)?; Ok(MacroDefinition { id: definition.id.into(), - file_id: source.file_id(), + file_id, source, capability: capability_status(&mapped.model.capabilities().definition_name_ranges), name: definition.name.clone(), @@ -1924,11 +1944,12 @@ fn map_macro_param_definition( }; let macro_definition = map_macro_definition(mapped, definition)?; let (source, range) = map_mapped_source_range(mapped, name_source_range)?; - if source.file_id() != macro_definition.file_id { + let name_file_id = require_file_backed_source(&source)?; + if name_file_id != macro_definition.file_id { return Err(PreprocError::MismatchedDefinitionRangeFiles { event_id: definition.event_id.raw(), directive_file_id: macro_definition.file_id, - name_file_id: source.file_id(), + name_file_id, }); } let param_range = param @@ -1954,7 +1975,7 @@ fn map_macro_param_reference( ) -> PreprocResult { let macro_definition = map_macro_definition(mapped, definition)?; let (source, range) = map_mapped_source_range(mapped, token_range)?; - let file_id = source.file_id(); + let file_id = require_file_backed_source(&source)?; let name = definition .params .as_ref() @@ -1999,9 +2020,10 @@ fn map_macro_reference( reference: &SourceMacroReferenceFact, ) -> PreprocResult { let (source, directive_range, name_range) = map_reference_ranges(mapped, reference)?; + let file_id = require_file_backed_source(&source)?; Ok(MacroReference { id: reference.id.into(), - file_id: source.file_id(), + file_id, source, capability: capability_status(&mapped.model.capabilities().macro_reference_resolution), name: reference.name.clone(), @@ -2021,10 +2043,11 @@ fn map_macro_call( .iter() .map(|argument| map_macro_argument(mapped, argument)) .collect::>>()?; + let file_id = require_file_backed_source(&source)?; Ok(MacroCall { id: call.id.into(), reference_id: call.reference.into(), - file_id: source.file_id(), + file_id, source, capability: macro_call_availability(&call.status), arguments, @@ -2096,6 +2119,9 @@ fn map_expansion_virtual_source( PreprocSourceMapping::VirtualFile { file_id, path, origin } => { Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) } + PreprocSourceMapping::VirtualDisplay { path, origin } => { + Ok(MappedPreprocSource::VirtualDisplay { path, origin }) + } PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), PreprocSourceMapping::Unmapped(reason) => { Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) @@ -2112,7 +2138,7 @@ fn source_macro_call_at( let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { return false; }; - source.file_id() == file_id && range.contains(offset) + source.file_id() == Some(file_id) && range.contains(offset) }) } @@ -2125,7 +2151,7 @@ fn source_macro_call_intersecting_range( let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { return false; }; - source.file_id() == file_id && range.intersect(source_range).is_some() + source.file_id() == Some(file_id) && range.intersect(source_range).is_some() }) } @@ -2226,13 +2252,13 @@ fn diagnostic_provenance_for_call( call_fact: &SourceMacroCallFact, ) -> PreprocResult { match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(_) => { - let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? else { + SourceMacroExpansionQueryFact::Available(expansion_id) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { return Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, ))); }; - Ok(diagnostic_target_for_expansion(&provenance)) + diagnostic_target_for_source_expansion(mapped, expansion) } SourceMacroExpansionQueryFact::Unavailable(reason) => { Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason))) @@ -2338,43 +2364,53 @@ fn mapped_macro_call( map_macro_call(mapped, call) } -fn diagnostic_target_for_expansion(provenance: &MacroExpansionProvenance) -> DiagnosticProvenance { +fn diagnostic_target_for_source_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let virtual_source = map_expansion_virtual_source(mapped, expansion.id)?; + let virtual_range = mapped + .source_map + .emitted_token_range(expansion.id, expansion.emitted_token_range) + .map_err(PreprocError::SourceMap)?; let mut saw_unavailable = None; - for token in &provenance.tokens { - match &token.provenance { + for token_id in emitted_token_ids(expansion.emitted_token_range) { + let Some(token) = mapped.model.emitted_tokens().get(token_id) else { + return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { + token: token_id, + })); + }; + let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { + return Err(unavailable_error( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + )); + }; + match map_token_provenance(mapped, provenance)? { TokenProvenance::SourceToken { source, range } => { - return DiagnosticProvenance::SourceToken { source: source.clone(), range: *range }; + return Ok(DiagnosticProvenance::SourceToken { source, range }); } TokenProvenance::MacroBody { call, definition_id, source, range } => { - return DiagnosticProvenance::MacroBody { - call: call.clone(), - definition_id: *definition_id, - source: source.clone(), - range: *range, - }; + return Ok(DiagnosticProvenance::MacroBody { call, definition_id, source, range }); } TokenProvenance::MacroArgument { call, argument_index, source, range } => { - return DiagnosticProvenance::MacroArgument { - call: call.clone(), - argument_index: *argument_index, - source: source.clone(), - range: *range, - }; + return Ok(DiagnosticProvenance::MacroArgument { + call, + argument_index, + source, + range, + }); } TokenProvenance::Unavailable(reason) => { - saw_unavailable = Some(reason.clone()); + saw_unavailable = Some(reason); } TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } => {} } } - saw_unavailable.map_or_else( - || DiagnosticProvenance::VirtualExpansion { - source: provenance.expansion.virtual_source.clone(), - range: provenance.expansion.virtual_range, - }, + Ok(saw_unavailable.map_or_else( + || DiagnosticProvenance::VirtualExpansion { source: virtual_source, range: virtual_range }, DiagnosticProvenance::Unavailable, - ) + )) } fn map_macro_resolution( @@ -2415,10 +2451,12 @@ fn map_reference_ranges( map_mapped_source_range(mapped, reference.directive_range)?; let (name_source, name_range) = map_mapped_source_range(mapped, reference.name_range)?; if directive_source != name_source { + let directive_file_id = require_file_backed_source(&directive_source)?; + let name_file_id = require_file_backed_source(&name_source)?; return Err(PreprocError::MismatchedReferenceRangeFiles { event_id: reference.event_id.raw(), - directive_file_id: directive_source.file_id(), - name_file_id: name_source.file_id(), + directive_file_id, + name_file_id, }); } Ok((directive_source, directive_range, name_range)) @@ -2497,10 +2535,12 @@ fn map_definition_ranges( map_mapped_source_range(mapped, directive_source_range)?; let (name_source, name_range) = map_mapped_source_range(mapped, name_source_range)?; if directive_source != name_source { + let directive_file_id = require_file_backed_source(&directive_source)?; + let name_file_id = require_file_backed_source(&name_source)?; return Err(PreprocError::MismatchedDefinitionRangeFiles { event_id, - directive_file_id: directive_source.file_id(), - name_file_id: name_source.file_id(), + directive_file_id, + name_file_id, }); } Ok((directive_source, directive_range, name_range)) @@ -3009,7 +3049,7 @@ endmodule } #[test] - fn preproc_macro_expansion_materializes_virtual_source_and_token_provenance() { + fn preproc_macro_expansion_exposes_display_virtual_source_and_token_provenance() { let root_text = r#"`define MAKE_DECL(name) logic name; module top; `MAKE_DECL(generated) @@ -3020,10 +3060,10 @@ endmodule let provenance = macro_expansion_provenance_at(&db, TOP, offset(root_text, "`MAKE_DECL")) .unwrap() .unwrap(); - let MappedPreprocSource::VirtualFile { path, origin, .. } = + let MappedPreprocSource::VirtualDisplay { path, origin } = &provenance.expansion.virtual_source else { - panic!("macro expansion should expose a virtual expansion source"); + panic!("macro expansion should expose a display-only virtual expansion source"); }; assert_eq!( path, @@ -3048,7 +3088,7 @@ endmodule let TokenProvenance::MacroBody { source, range, .. } = &logic.provenance else { panic!("logic should come from the macro body: {logic:?}"); }; - assert_eq!(source.file_id(), TOP); + assert_eq!(source.file_id(), Some(TOP)); assert_eq!(text_at_range(root_text, *range), "logic"); assert_eq!(logic.virtual_range, TextRange::new(0.into(), 5.into())); @@ -3063,7 +3103,7 @@ endmodule panic!("generated should come from the macro argument: {generated:?}"); }; assert_eq!(*argument_index, 0); - assert_eq!(source.file_id(), TOP); + assert_eq!(source.file_id(), Some(TOP)); assert_eq!(text_at_range(root_text, *range), "generated"); assert_eq!(generated.virtual_range, TextRange::new(6.into(), 15.into())); } @@ -3123,6 +3163,40 @@ endmodule )); } + #[test] + fn diagnostic_provenance_for_unbacked_predefine_expansion_is_structured_unavailable() { + let root_text = r#"module top; +`MAKE_CHILD +endmodule +"#; + let db = db_with_entries_and_predefines( + &[(TOP, "rtl/top.v", root_text)], + vec!["MAKE_CHILD=child u();".to_owned()], + ); + let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); + let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); + let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); + let (instantiation_id, _) = module + .instantiations + .iter() + .next() + .expect("predefine expansion should lower to a module instantiation"); + let instantiation_src = module_src_map + .get(instantiation_id) + .expect("generated instantiation should keep a source-map range"); + + let provenance = + diagnostic_provenance_for_range(&db, TOP, instantiation_src.range()).unwrap().unwrap(); + + assert!(matches!( + provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable + )) + )); + } + #[test] fn preproc_nested_include_chain_maps_to_file_ids() { let root_text = r#"`include "defs.vh" @@ -3305,7 +3379,7 @@ localparam int ENABLED = `HEADER_FLAG; .unwrap() .unwrap(); - assert_eq!(definition.source.file_id(), HEADER); + assert_eq!(definition.source.file_id(), Some(HEADER)); assert!(matches!(definition.capability, PreprocAvailability::Complete)); let refs = macro_references(&db, HEADER, &definition).unwrap().references; diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 97b3f076..2da49330 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -339,16 +339,18 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) -> }; let mut diag_file_id = file_id; let mut range = src.range(); - if let Ok(Some(provenance)) = - hir::preproc::diagnostic_provenance_for_range(db, file_id, range) - { - let Some((target_file_id, target_range)) = - diagnostic_preproc_target_file_range(&provenance) - else { - continue; - }; - diag_file_id = target_file_id; - range = target_range; + match hir::preproc::diagnostic_provenance_for_range(db, file_id, range) { + Ok(Some(provenance)) => { + let Some((target_file_id, target_range)) = + diagnostic_preproc_target_file_range(&provenance) + else { + continue; + }; + diag_file_id = target_file_id; + range = target_range; + } + Ok(None) => {} + Err(_) => continue, } match resolve_module_name(db, file_id, module_name) { @@ -386,7 +388,7 @@ fn diagnostic_preproc_target_file_range( | hir::preproc::DiagnosticProvenance::MacroBody { source, range, .. } | hir::preproc::DiagnosticProvenance::MacroArgument { source, range, .. } | hir::preproc::DiagnosticProvenance::VirtualExpansion { source, range } => { - Some((source.file_id(), *range)) + Some((source.file_id()?, *range)) } hir::preproc::DiagnosticProvenance::Unavailable(_) => None, } @@ -468,7 +470,7 @@ mod tests { diagnostics_config::DiagnosticsConfig, project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig}, salsa::Durability, - source_db::{SourceDb, SourceRootDb}, + source_db::{PreprocVirtualOrigin, SourceDb, SourceRootDb}, source_root::{SourceRoot, SourceRootId, SourceRootRole}, }; use triomphe::Arc; @@ -482,7 +484,8 @@ mod tests { use super::{ AMBIGUOUS_MODULE_INSTANTIATION, DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH, DiagnosticSource, - DiagnosticTag, INACTIVE_PREPROCESSOR_BRANCH, diagnostics, source_root_diagnostics, + DiagnosticTag, INACTIVE_PREPROCESSOR_BRANCH, diagnostic_preproc_target_file_range, + diagnostics, source_root_diagnostics, }; use crate::db::root_db::RootDb; @@ -506,6 +509,12 @@ mod tests { ); } + fn disable_semantic_diagnostics(db: &mut RootDb) { + let mut config = DiagnosticsConfig::default(); + config.semantic.enabled = false; + db.set_diagnostics_config_with_durability(Arc::new(config), Durability::HIGH); + } + fn db_with_files_in_role( files: &[(&str, &str)], role: SourceRootRole, @@ -651,6 +660,67 @@ mod tests { assert_ne!(diagnostic.range, range_of(top, "`MAKE")); } + #[test] + fn preproc_display_only_virtual_expansion_diagnostic_is_not_published() { + let top = "module top;\n `MAKE\nendmodule\n"; + let mut db = db_with_predefines( + &[ + ("/project/a/child.sv", "module child; endmodule\n"), + ("/project/b/child.sv", "module child; endmodule\n"), + ("/project/top.sv", top), + ], + vec!["MAKE=child u();".to_owned()], + ); + disable_semantic_diagnostics(&mut db); + + let diagnostics = diagnostics(&db, FileId(2)); + + assert!( + diagnostics.iter().all(|diag| { + diag.source != DiagnosticSource::Vide + || diag.name != AMBIGUOUS_MODULE_INSTANTIATION.name + }), + "display-only virtual expansion must not publish ambiguous module diagnostics: {diagnostics:?}" + ); + assert!( + diagnostics.iter().all(|diag| diag.file_id.0 < 3), + "diagnostics must not target synthetic virtual FileIds: {diagnostics:?}" + ); + } + + #[test] + fn diagnostic_target_rejects_display_only_virtual_expansion() { + let provenance = hir::preproc::DiagnosticProvenance::VirtualExpansion { + source: hir::preproc::MappedPreprocSource::VirtualDisplay { + path: VfsPath::new_virtual_path( + "/__vide/preproc/profile-0/expansion/0.sv".to_owned(), + ), + origin: PreprocVirtualOrigin::Builtin { name: "display-only".into() }, + }, + range: TextRange::new(TextSize::from(0), TextSize::from(5)), + }; + + assert_eq!(diagnostic_preproc_target_file_range(&provenance), None); + } + + #[test] + fn diagnostic_target_accepts_materialized_virtual_expansion() { + let file_id = FileId(7); + let range = TextRange::new(TextSize::from(0), TextSize::from(5)); + let provenance = hir::preproc::DiagnosticProvenance::VirtualExpansion { + source: hir::preproc::MappedPreprocSource::VirtualFile { + file_id, + path: VfsPath::new_virtual_path( + "/__vide/preproc/profile-0/expansion/0.sv".to_owned(), + ), + origin: PreprocVirtualOrigin::Builtin { name: "materialized".into() }, + }, + range, + }; + + assert_eq!(diagnostic_preproc_target_file_range(&provenance), Some((file_id, range))); + } + #[test] fn semantic_diagnostics_suppress_vide_ambiguous_module_warning() { let db = db_with_files( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index fe43eae8..0a1f59ed 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -298,13 +298,15 @@ fn macro_definition_source_label(db: &RootDb, definition: &MacroDefinition) -> O .map(|path| path.to_string()) }) } - hir::preproc::MappedPreprocSource::VirtualFile { .. } => None, + hir::preproc::MappedPreprocSource::VirtualFile { .. } + | hir::preproc::MappedPreprocSource::VirtualDisplay { .. } => None, } } fn argument_text(db: &RootDb, argument: &MacroArgument) -> String { if let (Some(source), Some(range)) = (&argument.source, argument.range) - && let Some(text) = text_at_file_range(db, source.file_id(), range) + && let Some(file_id) = source.file_id() + && let Some(text) = text_at_file_range(db, file_id, range) { return text.trim().to_owned(); } diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index c50fd041..ae7c72e2 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -86,7 +86,7 @@ fn source_token_range_for_offset( | TokenProvenance::Builtin { .. } | TokenProvenance::Unavailable(_) => return None, }; - (source.file_id() == file_id && range.contains(offset)).then_some(range) + (source.file_id() == Some(file_id) && range.contains(offset)).then_some(range) } fn tokens_with_exact_ranges<'tree>( From 89d561421bfde82d0837bc9ebe0907bd9a372666 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 14:02:32 +0800 Subject: [PATCH 23/80] feat(slang): expose direct macro provenance identity --- crates/preproc/src/source/model.rs | 6 + crates/preproc/src/source/trace.rs | 2 + crates/slang/bindings/rust/ffi.rs | 18 ++ crates/slang/bindings/rust/ffi/wrapper.cc | 105 ++++++--- crates/slang/bindings/rust/lib.rs | 167 +++++++++++++- crates/slang/bindings/rust/tests.rs | 217 +++++++++++++++++- .../include/slang/parsing/Preprocessor.h | 35 ++- .../slang/include/slang/text/SourceManager.h | 51 ++++ crates/slang/source/parsing/Preprocessor.cpp | 52 ++++- .../source/parsing/Preprocessor_macros.cpp | 128 ++++++++--- crates/slang/source/text/SourceManager.cpp | 60 ++++- 11 files changed, 738 insertions(+), 103 deletions(-) diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 2ab1c575..e5e1d17a 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -937,6 +937,8 @@ endmodule buffer_id: 1, range: define_start..define_end, }), + macro_definition_id: None, + macro_call_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "A".to_owned(), @@ -962,6 +964,8 @@ endmodule buffer_id: 1, range: usage_start..usage_start + 2, }), + macro_definition_id: None, + macro_call_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "`A".to_owned(), @@ -1179,6 +1183,8 @@ logic [`LEAF_WIDTH-1:0] data; event_id: PreprocessorTraceEventId(0), kind: SyntaxKind::DEFINE_DIRECTIVE, range: None, + macro_definition_id: None, + macro_call_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "WIDTH".to_owned(), diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index 3b1bd30f..e5b7270c 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -248,6 +248,7 @@ fn emitted_token_provenance_from_trace( } PreprocessorTraceTokenProvenance::MacroBody { macro_name, + identity: _, call_range, body_token_range, } => { @@ -265,6 +266,7 @@ fn emitted_token_provenance_from_trace( } PreprocessorTraceTokenProvenance::MacroArgument { macro_name, + identity: _, call_range, body_token_range, argument_token_range, diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index 53c6796d..d17a0d95 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -104,6 +104,10 @@ mod slang_ffi { event_id: u32, kind: u16, range: RawSourceBufferRange, + macro_definition_id: u32, + has_macro_definition_id: bool, + macro_call_id: u32, + has_macro_call_id: bool, directive: RawPreprocessorTraceToken, name: RawPreprocessorTraceToken, include_file_name: RawPreprocessorTraceToken, @@ -120,6 +124,20 @@ mod slang_ffi { token_kind: u16, provenance_kind: u8, macro_name: String, + macro_call_id: u32, + has_macro_call_id: bool, + macro_definition_id: u32, + has_macro_definition_id: bool, + macro_expansion_id: u32, + has_macro_expansion_id: bool, + parent_macro_expansion_id: u32, + has_parent_macro_expansion_id: bool, + body_token_index: u32, + has_body_token_index: bool, + argument_index: u32, + has_argument_index: bool, + argument_token_index: u32, + has_argument_token_index: bool, token_range: RawSourceBufferRange, call_range: RawSourceBufferRange, body_token_range: RawSourceBufferRange, diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index 684588b2..ceb2f2c0 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -165,7 +165,6 @@ constexpr uint8_t TRACE_TOKEN_PROVENANCE_UNAVAILABLE = 0; constexpr uint8_t TRACE_TOKEN_PROVENANCE_SOURCE = 1; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_BODY = 2; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT = 3; -constexpr uint8_t TRACE_TOKEN_PROVENANCE_BUILTIN = 4; ::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { ::RawPreprocessorTraceEmittedToken token; @@ -174,6 +173,20 @@ ::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { token.token_kind = static_cast(slang::parsing::TokenKind::Unknown); token.provenance_kind = TRACE_TOKEN_PROVENANCE_UNAVAILABLE; token.macro_name = rust::String(); + token.macro_call_id = 0; + token.has_macro_call_id = false; + token.macro_definition_id = 0; + token.has_macro_definition_id = false; + token.macro_expansion_id = 0; + token.has_macro_expansion_id = false; + token.parent_macro_expansion_id = 0; + token.has_parent_macro_expansion_id = false; + token.body_token_index = 0; + token.has_body_token_index = false; + token.argument_index = 0; + token.has_argument_index = false; + token.argument_token_index = 0; + token.has_argument_token_index = false; token.token_range = empty_source_buffer_range(); token.call_range = empty_source_buffer_range(); token.body_token_range = empty_source_buffer_range(); @@ -282,8 +295,32 @@ std::optional trace_source_location_key(slang::SourceLoc }; } -bool is_intrinsic_builtin_macro(std::string_view name) { - return name == "__FILE__" || name == "__LINE__"; +bool has_direct_macro_token_provenance( + const std::optional& provenance) { + return provenance && provenance->expansionId != 0 && provenance->callId != 0 && + provenance->definitionId != 0; +} + +void apply_direct_macro_token_provenance( + ::RawPreprocessorTraceEmittedToken& token, + const slang::SourceManager::MacroTokenProvenance& provenance) { + token.macro_call_id = provenance.callId; + token.has_macro_call_id = provenance.callId != 0; + token.macro_definition_id = provenance.definitionId; + token.has_macro_definition_id = provenance.definitionId != 0; + token.macro_expansion_id = provenance.expansionId; + token.has_macro_expansion_id = provenance.expansionId != 0; + token.parent_macro_expansion_id = provenance.parentExpansionId; + token.has_parent_macro_expansion_id = provenance.parentExpansionId != 0; + token.body_token_index = provenance.bodyTokenIndex; + token.has_body_token_index = + provenance.bodyTokenIndex != slang::SourceManager::MacroTokenProvenance::InvalidIndex; + token.argument_index = provenance.argumentIndex; + token.has_argument_index = + provenance.argumentIndex != slang::SourceManager::MacroTokenProvenance::InvalidIndex; + token.argument_token_index = provenance.argumentTokenIndex; + token.has_argument_token_index = + provenance.argumentTokenIndex != slang::SourceManager::MacroTokenProvenance::InvalidIndex; } ::RawPreprocessorTraceToken empty_preprocessor_trace_token() { @@ -320,9 +357,7 @@ rust::Vec<::RawPreprocessorTraceToken> to_rust_preprocessor_trace_tokens( ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( slang::parsing::Token token, - const slang::SourceManager& sourceManager, - const std::unordered_map& - macroUsageNamesByLocation) { + const slang::SourceManager& sourceManager) { auto result = empty_preprocessor_trace_emitted_token(); if (!token) return result; @@ -347,6 +382,7 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( auto macroName = std::string(sourceManager.getMacroName(location)); result.macro_name = rust::String(macroName); + auto directProvenance = sourceManager.getMacroTokenProvenance(location); if (sourceManager.isMacroArgLoc(location)) { auto tokenRange = token.range(); @@ -356,8 +392,16 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( result.body_token_range = to_rust_original_macro_loc_range(sourceManager, formalRange); result.call_range = to_rust_macro_argument_callsite_range(sourceManager, formalRange); - if (result.call_range.has_range && result.body_token_range.has_range && + if (has_direct_macro_token_provenance(directProvenance) && + directProvenance->bodyTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + directProvenance->argumentIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + directProvenance->argumentTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + result.call_range.has_range && result.body_token_range.has_range && result.argument_token_range.has_range) { + apply_direct_macro_token_provenance(result, *directProvenance); result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT; } return result; @@ -365,25 +409,16 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( result.call_range = to_rust_macro_callsite_range_from_macro_loc(sourceManager, location); result.body_token_range = to_rust_original_macro_loc_range(sourceManager, token.range()); - if (result.call_range.has_range && result.body_token_range.has_range) { + if (has_direct_macro_token_provenance(directProvenance) && + directProvenance->bodyTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + result.call_range.has_range && result.body_token_range.has_range) { + apply_direct_macro_token_provenance(result, *directProvenance); result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_BODY; } - else if (!macroName.empty()) { - // Slang built-in object-like macros have no source body location. - result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; - } return result; } - if (auto key = trace_source_location_key(location)) { - auto it = macroUsageNamesByLocation.find(*key); - if (it != macroUsageNamesByLocation.end() && is_intrinsic_builtin_macro(it->second)) { - result.macro_name = rust::String(it->second); - result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; - return result; - } - } - result.token_range = to_rust_source_buffer_range(token.range()); if (result.token_range.has_range) result.provenance_kind = TRACE_TOKEN_PROVENANCE_SOURCE; @@ -523,11 +558,16 @@ ::RawPreprocessorTraceMacroParam to_rust_trace_macro_param( ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( const slang::syntax::SyntaxNode& syntax, - uint32_t eventId) { + uint32_t eventId, + const slang::parsing::Preprocessor& preprocessor) { ::RawPreprocessorTraceEvent directive; directive.event_id = eventId; directive.kind = static_cast(syntax.kind); directive.range = to_rust_source_buffer_range(trace_event_source_range(syntax)); + directive.macro_definition_id = 0; + directive.has_macro_definition_id = false; + directive.macro_call_id = 0; + directive.has_macro_call_id = false; directive.directive = empty_preprocessor_trace_token(); directive.name = empty_preprocessor_trace_token(); directive.include_file_name = empty_preprocessor_trace_token(); @@ -542,6 +582,9 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( switch (syntax.kind) { case slang::syntax::SyntaxKind::DefineDirective: { const auto& define = syntax.as(); + auto definitionId = preprocessor.getMacroDefinitionId(define); + directive.macro_definition_id = definitionId; + directive.has_macro_definition_id = definitionId != 0; directive.name = to_rust_preprocessor_trace_token(define.name); if (define.formalArguments) { for (auto* param : define.formalArguments->args) @@ -576,6 +619,9 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( } case slang::syntax::SyntaxKind::MacroUsage: { const auto& usage = syntax.as(); + auto callId = preprocessor.getMacroCallId(usage); + directive.macro_call_id = callId; + directive.has_macro_call_id = callId != 0; directive.name = to_rust_preprocessor_trace_token(usage.directive); break; } @@ -1071,8 +1117,6 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( preprocessor.pushSource(rootBuffer); std::unordered_map includeEventIdsByLocation; - std::unordered_map - macroUsageNamesByLocation; while (true) { auto token = preprocessor.next(); @@ -1087,16 +1131,7 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( if (auto key = trace_source_location_key(include.directive.location())) includeEventIdsByLocation.emplace(*key, eventId); } - else if (syntax->kind == slang::syntax::SyntaxKind::MacroUsage) { - const auto& usage = syntax->as(); - if (auto key = trace_source_location_key(usage.directive.location())) { - auto name = std::string(usage.directive.valueText()); - if (!name.empty() && name[0] == '`') - name.erase(name.begin()); - macroUsageNamesByLocation.emplace(*key, std::move(name)); - } - } - result.events.emplace_back(to_rust_preprocessor_trace_event(*syntax, eventId)); + result.events.emplace_back(to_rust_preprocessor_trace_event(*syntax, eventId, preprocessor)); } } @@ -1104,7 +1139,7 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( break; result.emitted_tokens.emplace_back( - to_rust_preprocessor_trace_emitted_token(token, sourceManager, macroUsageNamesByLocation)); + to_rust_preprocessor_trace_emitted_token(token, sourceManager)); } for (auto buffer : sourceManager.getAllBuffers()) { diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 57523944..76c826d0 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -141,6 +141,15 @@ pub struct PreprocessorTrace { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PreprocessorTraceEventId(pub u32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PreprocessorTraceMacroCallId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PreprocessorTraceMacroDefinitionId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PreprocessorTraceMacroExpansionId(pub u32); + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocessorTraceIncludeEdge { pub include_event_id: PreprocessorTraceEventId, @@ -152,6 +161,8 @@ pub struct PreprocessorTraceEvent { pub event_id: PreprocessorTraceEventId, pub kind: SyntaxKind, pub range: Option, + pub macro_definition_id: Option, + pub macro_call_id: Option, pub directive: Option, pub name: Option, pub include_file_name: Option, @@ -176,21 +187,40 @@ pub enum PreprocessorTraceTokenProvenance { }, MacroBody { macro_name: String, + identity: PreprocessorTraceMacroBodyIdentity, call_range: SourceBufferRange, body_token_range: SourceBufferRange, }, MacroArgument { macro_name: String, + identity: PreprocessorTraceMacroArgumentIdentity, call_range: SourceBufferRange, body_token_range: SourceBufferRange, argument_token_range: SourceBufferRange, }, - Builtin { - name: String, - }, Unavailable, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceMacroBodyIdentity { + pub call_id: PreprocessorTraceMacroCallId, + pub definition_id: PreprocessorTraceMacroDefinitionId, + pub expansion_id: PreprocessorTraceMacroExpansionId, + pub parent_expansion_id: Option, + pub body_token_index: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceMacroArgumentIdentity { + pub call_id: PreprocessorTraceMacroCallId, + pub definition_id: PreprocessorTraceMacroDefinitionId, + pub expansion_id: PreprocessorTraceMacroExpansionId, + pub parent_expansion_id: Option, + pub body_token_index: u32, + pub argument_index: u32, + pub argument_token_index: u32, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocessorTraceToken { pub raw_text: String, @@ -363,6 +393,12 @@ impl PreprocessorTraceEvent { event_id: PreprocessorTraceEventId(raw.event_id), kind: SyntaxKind::from_id(raw.kind), range: SourceBufferRange::from_raw(raw.range), + macro_definition_id: raw + .has_macro_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(raw.macro_definition_id)), + macro_call_id: raw + .has_macro_call_id + .then_some(PreprocessorTraceMacroCallId(raw.macro_call_id)), directive: PreprocessorTraceToken::from_raw(raw.directive), name: PreprocessorTraceToken::from_raw(raw.name), include_file_name: PreprocessorTraceToken::from_raw(raw.include_file_name), @@ -396,6 +432,20 @@ impl PreprocessorTraceEmittedToken { provenance: PreprocessorTraceTokenProvenance::from_raw( raw.provenance_kind, raw.macro_name, + raw.macro_call_id, + raw.has_macro_call_id, + raw.macro_definition_id, + raw.has_macro_definition_id, + raw.macro_expansion_id, + raw.has_macro_expansion_id, + raw.parent_macro_expansion_id, + raw.has_parent_macro_expansion_id, + raw.body_token_index, + raw.has_body_token_index, + raw.argument_index, + raw.has_argument_index, + raw.argument_token_index, + raw.has_argument_token_index, raw.token_range, raw.call_range, raw.body_token_range, @@ -406,7 +456,6 @@ impl PreprocessorTraceEmittedToken { } impl PreprocessorTraceTokenProvenance { - const BUILTIN: u8 = 4; const MACRO_ARGUMENT: u8 = 3; const MACRO_BODY: u8 = 2; const SOURCE: u8 = 1; @@ -416,6 +465,20 @@ impl PreprocessorTraceTokenProvenance { fn from_raw( kind: u8, macro_name: String, + macro_call_id: u32, + has_macro_call_id: bool, + macro_definition_id: u32, + has_macro_definition_id: bool, + macro_expansion_id: u32, + has_macro_expansion_id: bool, + parent_macro_expansion_id: u32, + has_parent_macro_expansion_id: bool, + body_token_index: u32, + has_body_token_index: bool, + argument_index: u32, + has_argument_index: bool, + argument_token_index: u32, + has_argument_token_index: bool, token_range: ffi::RawSourceBufferRange, call_range: ffi::RawSourceBufferRange, body_token_range: ffi::RawSourceBufferRange, @@ -432,7 +495,21 @@ impl PreprocessorTraceTokenProvenance { let Some(body_token_range) = SourceBufferRange::from_raw(body_token_range) else { return Self::Unavailable; }; - Self::MacroBody { macro_name, call_range, body_token_range } + let Some(identity) = PreprocessorTraceMacroBodyIdentity::from_raw( + macro_call_id, + has_macro_call_id, + macro_definition_id, + has_macro_definition_id, + macro_expansion_id, + has_macro_expansion_id, + parent_macro_expansion_id, + has_parent_macro_expansion_id, + body_token_index, + has_body_token_index, + ) else { + return Self::Unavailable; + }; + Self::MacroBody { macro_name, identity, call_range, body_token_range } } Self::MACRO_ARGUMENT => { let Some(call_range) = SourceBufferRange::from_raw(call_range) else { @@ -445,20 +522,98 @@ impl PreprocessorTraceTokenProvenance { else { return Self::Unavailable; }; + let Some(identity) = PreprocessorTraceMacroArgumentIdentity::from_raw( + macro_call_id, + has_macro_call_id, + macro_definition_id, + has_macro_definition_id, + macro_expansion_id, + has_macro_expansion_id, + parent_macro_expansion_id, + has_parent_macro_expansion_id, + body_token_index, + has_body_token_index, + argument_index, + has_argument_index, + argument_token_index, + has_argument_token_index, + ) else { + return Self::Unavailable; + }; Self::MacroArgument { macro_name, + identity, call_range, body_token_range, argument_token_range, } } - Self::BUILTIN => Self::Builtin { name: macro_name }, Self::UNAVAILABLE => Self::Unavailable, _ => Self::Unavailable, } } } +impl PreprocessorTraceMacroBodyIdentity { + #[inline] + fn from_raw( + macro_call_id: u32, + has_macro_call_id: bool, + macro_definition_id: u32, + has_macro_definition_id: bool, + macro_expansion_id: u32, + has_macro_expansion_id: bool, + parent_macro_expansion_id: u32, + has_parent_macro_expansion_id: bool, + body_token_index: u32, + has_body_token_index: bool, + ) -> Option { + Some(Self { + call_id: has_macro_call_id.then_some(PreprocessorTraceMacroCallId(macro_call_id))?, + definition_id: has_macro_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(macro_definition_id))?, + expansion_id: has_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(macro_expansion_id))?, + parent_expansion_id: has_parent_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(parent_macro_expansion_id)), + body_token_index: has_body_token_index.then_some(body_token_index)?, + }) + } +} + +impl PreprocessorTraceMacroArgumentIdentity { + #[inline] + fn from_raw( + macro_call_id: u32, + has_macro_call_id: bool, + macro_definition_id: u32, + has_macro_definition_id: bool, + macro_expansion_id: u32, + has_macro_expansion_id: bool, + parent_macro_expansion_id: u32, + has_parent_macro_expansion_id: bool, + body_token_index: u32, + has_body_token_index: bool, + argument_index: u32, + has_argument_index: bool, + argument_token_index: u32, + has_argument_token_index: bool, + ) -> Option { + Some(Self { + call_id: has_macro_call_id.then_some(PreprocessorTraceMacroCallId(macro_call_id))?, + definition_id: has_macro_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(macro_definition_id))?, + expansion_id: has_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(macro_expansion_id))?, + parent_expansion_id: has_parent_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(parent_macro_expansion_id)), + body_token_index: has_body_token_index.then_some(body_token_index)?, + argument_index: has_argument_index.then_some(argument_index)?, + argument_token_index: has_argument_token_index.then_some(argument_token_index)?, + }) + } +} + impl PreprocessorTraceToken { #[inline] fn from_raw(raw: ffi::RawPreprocessorTraceToken) -> Option { diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index 9fe71e1b..c2b4cd0a 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1092,12 +1092,39 @@ endmodule .iter() .find(|token| token.raw_text == "8") .expect("object-like macro body token should be emitted"); - let PreprocessorTraceTokenProvenance::MacroBody { macro_name, call_range, body_token_range } = - &obj.provenance + let PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } = &obj.provenance else { panic!("expected macro body provenance for `OBJ expansion: {obj:?}"); }; + let obj_define_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::DEFINE_DIRECTIVE + && event.name.as_ref().is_some_and(|name| name.value_text == "OBJ") + }) + .and_then(|event| event.macro_definition_id) + .expect("OBJ define should carry direct definition identity"); + let obj_call_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`OBJ") + }) + .and_then(|event| event.macro_call_id) + .expect("OBJ usage should carry direct call identity"); assert_eq!(macro_name, "OBJ"); + assert_eq!(identity.definition_id, obj_define_id); + assert_eq!(identity.call_id, obj_call_id); + assert!(identity.expansion_id.0 != 0); + assert_eq!(identity.parent_expansion_id, None); + assert_eq!(identity.body_token_index, 0); assert_eq!(&source[call_range.range.clone()], "`OBJ"); assert_eq!(&source[body_token_range.range.clone()], "8"); @@ -1108,6 +1135,7 @@ endmodule .expect("function-like argument token should be emitted"); let PreprocessorTraceTokenProvenance::MacroArgument { macro_name, + identity, call_range, body_token_range, argument_token_range, @@ -1115,7 +1143,32 @@ endmodule else { panic!("expected macro argument provenance for `ID expansion: {arg:?}"); }; + let id_define_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::DEFINE_DIRECTIVE + && event.name.as_ref().is_some_and(|name| name.value_text == "ID") + }) + .and_then(|event| event.macro_definition_id) + .expect("ID define should carry direct definition identity"); + let id_call_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`ID") + }) + .and_then(|event| event.macro_call_id) + .expect("ID usage should carry direct call identity"); assert_eq!(macro_name, "ID"); + assert_eq!(identity.definition_id, id_define_id); + assert_eq!(identity.call_id, id_call_id); + assert!(identity.expansion_id.0 != 0); + assert!(identity.parent_expansion_id.is_some()); + assert_eq!(identity.body_token_index, 0); + assert_eq!(identity.argument_index, 0); + assert_eq!(identity.argument_token_index, 0); assert_eq!(&source[call_range.range.clone()], "`ID(7)"); assert_eq!(&source[body_token_range.range.clone()], "x"); assert_eq!(&source[argument_token_range.range.clone()], "7"); @@ -1142,16 +1195,159 @@ endmodule .iter() .find(|token| token.raw_text == "3") .expect("nested macro body token should be emitted"); - let PreprocessorTraceTokenProvenance::MacroBody { macro_name, call_range, body_token_range } = - &leaf.provenance + let PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } = &leaf.provenance else { panic!("expected macro body provenance for nested `LEAF expansion: {leaf:?}"); }; assert_eq!(macro_name, "LEAF"); + assert!(identity.expansion_id.0 != 0); + assert!( + identity.parent_expansion_id.is_some_and(|parent| parent != identity.expansion_id), + "nested expansion should carry direct parent expansion identity: {leaf:?}" + ); + assert_eq!(identity.body_token_index, 0); assert_eq!(&source[call_range.range.clone()], "`LEAF"); assert_eq!(&source[body_token_range.range.clone()], "3"); } +#[test] +fn preprocessor_trace_reports_next_macro_argument_identity() { + let source = r#"`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(payload_i[3:0]); +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include emitted tokens"); + + let payload = trace + .emitted_tokens + .iter() + .find(|token| { + token.raw_text == "payload_i" + && matches!( + token.provenance, + PreprocessorTraceTokenProvenance::MacroArgument { .. } + ) + }) + .expect("macro argument identifier should be emitted"); + let PreprocessorTraceTokenProvenance::MacroArgument { + macro_name, + identity: payload_identity, + call_range, + body_token_range, + argument_token_range, + } = &payload.provenance + else { + panic!("expected direct argument provenance for NEXT payload token: {payload:?}"); + }; + assert_eq!(macro_name, "NEXT"); + assert_eq!(payload_identity.body_token_index, 2); + assert_eq!(payload_identity.argument_index, 0); + assert_eq!(payload_identity.argument_token_index, 0); + assert_eq!(&source[call_range.range.clone()], "`NEXT(payload_i[3:0])"); + assert_eq!(&source[body_token_range.range.clone()], "x"); + assert_eq!(&source[argument_token_range.range.clone()], "payload_i"); + + let slice_index = trace + .emitted_tokens + .iter() + .find(|token| { + token.raw_text == "3" + && matches!( + token.provenance, + PreprocessorTraceTokenProvenance::MacroArgument { .. } + ) + }) + .expect("macro argument slice index should be emitted"); + let PreprocessorTraceTokenProvenance::MacroArgument { identity: slice_identity, .. } = + &slice_index.provenance + else { + panic!("expected direct argument provenance for NEXT slice token: {slice_index:?}"); + }; + assert_eq!(slice_identity.argument_index, 0); + assert_eq!(slice_identity.argument_token_index, 2); + + for (literal_part, body_token_index) in [("12", 5), ("'d", 6), ("1", 7)] { + let increment = trace + .emitted_tokens + .iter() + .find(|token| { + matches!( + &token.provenance, + PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + body_token_range, + .. + } if macro_name == "NEXT" + && &source[body_token_range.range.clone()] == literal_part + ) + }) + .unwrap_or_else(|| panic!("macro body literal part should be emitted: {literal_part}")); + let PreprocessorTraceTokenProvenance::MacroBody { macro_name, identity, .. } = + &increment.provenance + else { + panic!("expected direct body provenance for NEXT literal part: {increment:?}"); + }; + assert_eq!(macro_name, "NEXT"); + assert_eq!(identity.call_id, payload_identity.call_id); + assert_eq!(identity.definition_id, payload_identity.definition_id); + assert!(identity.expansion_id.0 != 0); + assert_eq!(identity.parent_expansion_id, None); + assert_eq!(identity.body_token_index, body_token_index); + } +} + +#[test] +fn preprocessor_trace_reports_escaped_identifier_macro_body_identity() { + let source = concat!( + "`define ESC \\escaped_payload ", + "\n", + "module m;\n", + "wire `ESC;\n", + "endmodule\n" + ); + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include emitted tokens"); + + let escaped = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text.starts_with("\\escaped_payload")) + .expect("escaped identifier macro body token should be emitted"); + let PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } = &escaped.provenance + else { + panic!("expected direct body provenance for escaped identifier: {escaped:?}"); + }; + assert_eq!(macro_name, "ESC"); + assert!(identity.call_id.0 != 0); + assert!(identity.definition_id.0 != 0); + assert!(identity.expansion_id.0 != 0); + assert_eq!(identity.body_token_index, 0); + assert_eq!(&source[call_range.range.clone()], "`ESC"); + assert!(source[body_token_range.range.clone()].starts_with("\\escaped_payload")); +} + #[test] fn preprocessor_trace_keeps_unsupported_macro_ops_as_unavailable_tokens() { let source = r#"`define JOIN(a,b) a``b @@ -1185,7 +1381,7 @@ endmodule } #[test] -fn preprocessor_trace_reports_predefine_and_builtin_emitted_token_facts() { +fn preprocessor_trace_reports_predefine_and_intrinsic_emitted_token_facts() { let source = r#"module m; localparam int P = `FROM_API; localparam int L = `__LINE__; @@ -1214,15 +1410,12 @@ endmodule }; assert_ne!(body_token_range.buffer_id, trace.root_buffer_id); - let builtin = trace + let intrinsic = trace .emitted_tokens .iter() - .find(|token| matches!(token.provenance, PreprocessorTraceTokenProvenance::Builtin { .. })) - .expect("builtin macro token should be emitted with builtin provenance"); - assert!(matches!( - &builtin.provenance, - PreprocessorTraceTokenProvenance::Builtin { name } if name == "__LINE__" - )); + .find(|token| token.raw_text == "3") + .expect("intrinsic macro token should stay in emitted stream"); + assert!(matches!(intrinsic.provenance, PreprocessorTraceTokenProvenance::Unavailable)); } #[test] diff --git a/crates/slang/include/slang/parsing/Preprocessor.h b/crates/slang/include/slang/parsing/Preprocessor.h index e8c5abd8..c1a39ffc 100644 --- a/crates/slang/include/slang/parsing/Preprocessor.h +++ b/crates/slang/include/slang/parsing/Preprocessor.h @@ -13,6 +13,7 @@ #include "slang/parsing/NumberParser.h" #include "slang/parsing/Token.h" #include "slang/syntax/SyntaxNode.h" +#include "slang/text/SourceManager.h" #include "slang/text/SourceLocation.h" #include "slang/util/Bag.h" #include "slang/util/SmallVector.h" @@ -25,6 +26,7 @@ struct MacroActualArgumentListSyntax; struct MacroFormalArgumentListSyntax; struct MacroActualArgumentSyntax; struct MacroFormalArgumentSyntax; +struct MacroUsageSyntax; struct PragmaDirectiveSyntax; struct PragmaExpressionSyntax; @@ -150,6 +152,12 @@ class SLANG_EXPORT Preprocessor { /// Gets all macros that have been defined thus far in the preprocessor. std::vector getDefinedMacros() const; + /// Gets the frontend identity assigned to a macro definition syntax node. + uint32_t getMacroDefinitionId(const syntax::DefineDirectiveSyntax& syntax) const; + + /// Gets the frontend identity assigned to a macro usage syntax node. + uint32_t getMacroCallId(const syntax::MacroUsageSyntax& syntax) const; + private: Preprocessor(const Preprocessor& other); Preprocessor& operator=(const Preprocessor& other) = delete; @@ -257,6 +265,7 @@ class SLANG_EXPORT Preprocessor { MacroIntrinsic intrinsic = MacroIntrinsic::None; bool builtIn = false; bool commandLine = false; + uint32_t definitionId = 0; MacroDef() = default; MacroDef(const syntax::DefineDirectiveSyntax* syntax) : syntax(syntax) {} @@ -271,18 +280,26 @@ class SLANG_EXPORT Preprocessor { class MacroExpansion { public: MacroExpansion(SourceManager& sourceManager, BumpAllocator& alloc, - SmallVectorBase& dest, Token usageSite, bool isTopLevel) : + SmallVectorBase& dest, Token usageSite, bool isTopLevel, + SourceManager::MacroExpansionMetadata metadata) : sourceManager(sourceManager), alloc(alloc), dest(dest), usageSite(usageSite), - isTopLevel(isTopLevel) {} + isTopLevel(isTopLevel), metadata(metadata) {} SourceRange getRange() const; + const SourceManager::MacroExpansionMetadata& getMetadata() const { return metadata; } + SourceManager::MacroTokenProvenance tokenProvenance( + uint32_t bodyTokenIndex, + uint32_t argumentIndex = SourceManager::MacroTokenProvenance::InvalidIndex, + uint32_t argumentTokenIndex = SourceManager::MacroTokenProvenance::InvalidIndex) const; SourceLocation adjustLoc(Token token, SourceLocation& macroLoc, SourceLocation& firstLoc, SourceRange expansionRange) const; - void append(Token token, SourceLocation location, bool allowLineContinuation = false); + void append(Token token, SourceLocation location, bool allowLineContinuation = false, + SourceManager::MacroTokenProvenance provenance = {}); void append(Token token, SourceLocation& macroLoc, SourceLocation& firstLoc, - SourceRange expansionRange, bool allowLineContinuation = false); + SourceRange expansionRange, bool allowLineContinuation = false, + SourceManager::MacroTokenProvenance provenance = {}); private: SourceManager& sourceManager; @@ -291,11 +308,13 @@ class SLANG_EXPORT Preprocessor { Token usageSite; bool any = false; bool isTopLevel = false; + SourceManager::MacroExpansionMetadata metadata; }; // Macro handling methods MacroDef findMacro(Token directive); - std::pair handleTopLevelMacro(Token directive); + std::pair handleTopLevelMacro( + Token directive, uint32_t* callId = nullptr); bool expandMacro(MacroDef macro, MacroExpansion& expansion, syntax::MacroActualArgumentListSyntax* actualArgs); bool expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& expansion); @@ -308,6 +327,8 @@ class SLANG_EXPORT Preprocessor { static bool isSameMacro(const syntax::DefineDirectiveSyntax& left, const syntax::DefineDirectiveSyntax& right); + uint32_t allocateMacroDefinitionId(const syntax::DefineDirectiveSyntax* syntax); + uint32_t allocateMacroCallId(); // functions to advance the underlying token stream Token peek(); @@ -392,6 +413,10 @@ class SLANG_EXPORT Preprocessor { // map from macro name to macro definition flat_hash_map macros; + flat_hash_map macroDefinitionIds; + flat_hash_map macroCallIds; + uint32_t nextMacroDefinitionId = 1; + uint32_t nextMacroCallId = 1; // list of expanded macro tokens to drain before continuing with active lexer SmallVector expandedTokens; diff --git a/crates/slang/include/slang/text/SourceManager.h b/crates/slang/include/slang/text/SourceManager.h index e179d213..5bbb7dbe 100644 --- a/crates/slang/include/slang/text/SourceManager.h +++ b/crates/slang/include/slang/text/SourceManager.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,26 @@ class SLANG_EXPORT SourceManager { Stringification, }; + struct MacroExpansionMetadata { + uint32_t callId = 0; + uint32_t definitionId = 0; + uint32_t parentExpansionId = 0; + }; + + struct MacroTokenProvenance { + static constexpr uint32_t InvalidIndex = std::numeric_limits::max(); + + uint32_t expansionId = 0; + uint32_t callId = 0; + uint32_t definitionId = 0; + uint32_t parentExpansionId = 0; + uint32_t bodyTokenIndex = InvalidIndex; + uint32_t argumentIndex = InvalidIndex; + uint32_t argumentTokenIndex = InvalidIndex; + + bool valid() const { return expansionId != 0 && callId != 0; } + }; + /// Default constructor. SourceManager(); SourceManager(const SourceManager&) = delete; @@ -132,6 +153,9 @@ class SLANG_EXPORT SourceManager { /// Gets the original source location of a given macro location. SourceLocation getOriginalLoc(SourceLocation location) const; + /// Gets directly recorded macro provenance for a token emitted from a macro expansion. + std::optional getMacroTokenProvenance(SourceLocation location) const; + /// Gets the actual original location where source is written, given a location /// inside a macro. Otherwise just returns the location itself. SourceLocation getFullyOriginalLoc(SourceLocation location) const; @@ -162,6 +186,19 @@ class SLANG_EXPORT SourceManager { SourceLocation createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, MacroExpansionKind kind); + /// Creates a macro expansion location with provenance metadata; used by the preprocessor. + SourceLocation createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, + std::string_view macroName, + MacroExpansionMetadata metadata); + + /// Creates a macro expansion location with provenance metadata; used by the preprocessor. + SourceLocation createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, + MacroExpansionKind kind, + MacroExpansionMetadata metadata); + + /// Records directly observed provenance metadata for an emitted macro token. + void setMacroTokenProvenance(SourceLocation location, MacroTokenProvenance provenance); + /// Instead of loading source from a file, copy it from text already in memory. SourceBuffer assignText(std::string_view text, SourceLocation includedFrom = SourceLocation(), const SourceLibrary* library = nullptr); @@ -299,6 +336,7 @@ class SLANG_EXPORT SourceManager { MacroExpansionKind kind = MacroExpansionKind::Body; std::string_view macroName; + MacroExpansionMetadata metadata; ExpansionInfo() {} ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, bool isMacroArg) : @@ -312,6 +350,16 @@ class SLANG_EXPORT SourceManager { ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, MacroExpansionKind kind) : originalLoc(originalLoc), expansionRange(expansionRange), kind(kind) {} + + ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, + std::string_view macroName, MacroExpansionMetadata metadata) : + originalLoc(originalLoc), expansionRange(expansionRange), macroName(macroName), + metadata(metadata) {} + + ExpansionInfo(SourceLocation originalLoc, SourceRange expansionRange, + MacroExpansionKind kind, MacroExpansionMetadata metadata) : + originalLoc(originalLoc), expansionRange(expansionRange), kind(kind), + metadata(metadata) {} }; // This mutex protects pretty much everything in this class. @@ -337,6 +385,9 @@ class SLANG_EXPORT SourceManager { // map from buffer to diagnostic directive lists flat_hash_map> diagDirectives; + // Direct token provenance recorded by the preprocessor while macro tokens are emitted. + flat_hash_map macroTokenProvenance; + std::atomic unnamedBufferCount = 0; bool disableProximatePaths = false; diff --git a/crates/slang/source/parsing/Preprocessor.cpp b/crates/slang/source/parsing/Preprocessor.cpp index 3cba1524..f7984475 100644 --- a/crates/slang/source/parsing/Preprocessor.cpp +++ b/crates/slang/source/parsing/Preprocessor.cpp @@ -37,8 +37,11 @@ Preprocessor::Preprocessor(SourceManager& sourceManager, BumpAllocator& alloc, // Add in any inherited macros that aren't already set in our map. for (auto define : inheritedMacros) { auto name = define->name.valueText(); - if (!name.empty()) - macros.emplace(name, define); + if (!name.empty()) { + MacroDef def(define); + def.definitionId = allocateMacroDefinitionId(define); + macros.emplace(name, def); + } } // clang-format off @@ -121,8 +124,10 @@ void Preprocessor::predefine(const std::string& definition, std::string_view nam // be copied over to our own map. for (auto& pair : pp.macros) { if (!pair.second.isIntrinsic()) { - pair.second.commandLine = true; - macros.insert(pair); + MacroDef def = pair.second; + def.commandLine = true; + def.definitionId = allocateMacroDefinitionId(def.syntax); + macros.insert({pair.first, def}); } } } @@ -210,6 +215,33 @@ std::vector Preprocessor::getDefinedMacros() const return results; } +uint32_t Preprocessor::getMacroDefinitionId(const DefineDirectiveSyntax& syntax) const { + auto it = macroDefinitionIds.find(&syntax); + return it == macroDefinitionIds.end() ? 0 : it->second; +} + +uint32_t Preprocessor::getMacroCallId(const MacroUsageSyntax& syntax) const { + auto it = macroCallIds.find(&syntax); + return it == macroCallIds.end() ? 0 : it->second; +} + +uint32_t Preprocessor::allocateMacroDefinitionId(const DefineDirectiveSyntax* syntax) { + if (!syntax) + return 0; + + auto it = macroDefinitionIds.find(syntax); + if (it != macroDefinitionIds.end()) + return it->second; + + auto id = nextMacroDefinitionId++; + macroDefinitionIds.emplace(syntax, id); + return id; +} + +uint32_t Preprocessor::allocateMacroCallId() { + return nextMacroCallId++; +} + Token Preprocessor::next() { return consume(); } @@ -677,18 +709,24 @@ Trivia Preprocessor::handleDefineDirective(Token directive) { } } - if (!bad) - macros[name.valueText()] = result; + if (!bad) { + MacroDef def(result); + def.definitionId = allocateMacroDefinitionId(result); + macros[name.valueText()] = def; + } return Trivia(TriviaKind::Directive, result); } std::pair Preprocessor::handleMacroUsage(Token directive) { // delegate to a nested function to simplify the error handling paths inMacroBody = true; - auto [actualArgs, extraTrivia] = handleTopLevelMacro(directive); + uint32_t callId = 0; + auto [actualArgs, extraTrivia] = handleTopLevelMacro(directive, &callId); inMacroBody = false; auto syntax = alloc.emplace(directive, actualArgs); + if (callId != 0) + macroCallIds.emplace(syntax, callId); return std::make_pair(Trivia(TriviaKind::Directive, syntax), extraTrivia); } diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index d63d1966..b08bdf8c 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -51,13 +51,17 @@ void Preprocessor::createBuiltInMacro(std::string_view name, int value, std::str def.syntax = alloc.emplace(directive, nameTok, nullptr, body.copy(alloc)); def.builtIn = true; + def.definitionId = allocateMacroDefinitionId(def.syntax); macros[name] = def; #undef NL } std::pair Preprocessor::handleTopLevelMacro( - Token directive) { + Token directive, uint32_t* callId) { + if (callId) + *callId = 0; + auto macro = findMacro(directive); if (!macro.valid()) { if (options.ignoreDirectives.find(directive.valueText().substr(1)) != @@ -93,7 +97,15 @@ std::pair Preprocessor::handleTopLevelMa // Expand out the macro SmallVector buffer; - MacroExpansion expansion{sourceManager, alloc, buffer, directive, true}; + SourceManager::MacroExpansionMetadata metadata; + metadata.callId = allocateMacroCallId(); + metadata.definitionId = macro.definitionId; + if (sourceManager.isMacroLoc(directive.location())) + metadata.parentExpansionId = directive.location().buffer().getId(); + if (callId) + *callId = metadata.callId; + + MacroExpansion expansion{sourceManager, alloc, buffer, directive, true, metadata}; if (!expandMacro(macro, expansion, actualArgs)) return {actualArgs, Trivia()}; @@ -402,11 +414,16 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // each macro expansion gets its own location entry SourceLocation start = body[0].location(); SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansion.getRange(), - macroName); + macroName, + expansion.getMetadata()); // simple macro; just take body tokens - for (auto token : body) - expansion.append(token, expansionLoc, start, expansion.getRange()); + uint32_t bodyTokenIndex = 0; + for (auto token : body) { + expansion.append(token, expansionLoc, start, expansion.getRange(), false, + expansion.tokenProvenance(bodyTokenIndex)); + bodyTokenIndex++; + } return true; } @@ -423,7 +440,12 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, struct ArgTokens : public std::span { using std::span::span; using std::span::operator=; + + ArgTokens(std::span tokens, uint32_t formalIndex) : + std::span(tokens), formalIndex(formalIndex) {} + bool isExpanded = false; + uint32_t formalIndex = SourceManager::MacroTokenProvenance::InvalidIndex; }; SmallMap argumentMap; @@ -448,7 +470,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, auto name = formal->name.valueText(); if (!name.empty()) - argumentMap.emplace(name, ArgTokens(*tokenList)); + argumentMap.emplace(name, ArgTokens(*tokenList, uint32_t(i))); } Token endOfArgs = actualArgs->getLastToken(); @@ -457,23 +479,25 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, SourceLocation start = body[0].location(); SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, - macroName); + macroName, + expansion.getMetadata()); - auto append = [&](Token token) { - expansion.append(token, expansionLoc, start, expansionRange); + auto append = [&](Token token, uint32_t bodyTokenIndex) { + expansion.append(token, expansionLoc, start, expansionRange, false, + expansion.tokenProvenance(bodyTokenIndex)); return true; }; bool inDefineDirective = false; - auto handleToken = [&](Token token) { + auto handleToken = [&](Token token, uint32_t bodyTokenIndex) { if (inDefineDirective && !token.isOnSameLine()) inDefineDirective = false; if (token.kind != TokenKind::Identifier && !LF::isKeyword(token.kind) && token.kind != TokenKind::Directive) { // Non-identifier, can't be argument substituted. - return append(token); + return append(token, bodyTokenIndex); } std::string_view text = token.valueText(); @@ -483,7 +507,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // during argument expansion we will insert line continuations. if (token.directiveKind() == SyntaxKind::DefineDirective) inDefineDirective = true; - return append(token); + return append(token, bodyTokenIndex); } // Other tools allow arguments to replace matching directive names, e.g.: @@ -496,7 +520,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // check for formal param auto it = argumentMap.find(text); if (it == argumentMap.end()) - return append(token); + return append(token, bodyTokenIndex); // Fully expand out arguments before substitution to make sure we can detect whether // a usage of a macro in a replacement list is valid or an illegal recursion. @@ -517,7 +541,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // here to ensure that the trivia of the formal parameter is passed on. Token empty(alloc, TokenKind::EmptyMacroArgument, token.trivia(), ""sv, token.location()); - return append(empty); + return append(empty, bodyTokenIndex); } // We need to ensure that we get correct spacing for the leading token here; @@ -531,7 +555,10 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // points into the macro body where the formal argument was used. SourceLocation tokenLoc = expansion.adjustLoc(token, expansionLoc, start, expansionRange); SourceRange argRange(tokenLoc, tokenLoc + token.rawText().length()); - SourceLocation argLoc = sourceManager.createExpansionLoc(firstLoc, argRange, true); + auto argumentMetadata = expansion.getMetadata(); + argumentMetadata.parentExpansionId = tokenLoc.buffer().getId(); + SourceLocation argLoc = sourceManager.createExpansionLoc( + firstLoc, argRange, SourceManager::MacroExpansionKind::Argument, argumentMetadata); // See note above about weird macro usage being argument replaced. // In that case we want to fabricate the correct directive token here. @@ -550,16 +577,20 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, if (inDefineDirective) { // Inside a define directive we need to insert line continuations // any time an expanded token will end up on a new line. + uint32_t argumentTokenIndex = 0; auto appendBody = [&](Token token) { + auto provenance = expansion.tokenProvenance( + bodyTokenIndex, it->second.formalIndex, argumentTokenIndex); if (!token.isOnSameLine()) { Token lc(alloc, TokenKind::LineContinuation, token.trivia(), "\\"sv, token.location()); expansion.append(lc, argLoc, firstLoc, argRange, - /* allowLineContinuation */ true); + /* allowLineContinuation */ true, provenance); token = token.withTrivia(alloc, {}); } - expansion.append(token, argLoc, firstLoc, argRange); + expansion.append(token, argLoc, firstLoc, argRange, false, provenance); + argumentTokenIndex++; }; appendBody(first); @@ -567,16 +598,27 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, appendBody(*begin); } else { - expansion.append(first, argLoc, firstLoc, argRange); + uint32_t argumentTokenIndex = 0; + auto appendArgument = [&](Token token) { + expansion.append(token, argLoc, firstLoc, argRange, false, + expansion.tokenProvenance(bodyTokenIndex, + it->second.formalIndex, + argumentTokenIndex)); + argumentTokenIndex++; + }; + + appendArgument(first); for (++begin; begin != end; begin++) - expansion.append(*begin, argLoc, firstLoc, argRange); + appendArgument(*begin); } return true; }; // Now add each body token, substituting arguments as necessary. - for (auto token : body) { + for (size_t index = 0; index < body.size(); index++) { + auto token = body[index]; + auto bodyTokenIndex = uint32_t(index); if (token.kind == TokenKind::Identifier && !token.rawText().empty() && token.rawText()[0] == '\\') { // Escaped identifier, might need to break apart and substitute @@ -584,13 +626,13 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, size_t index = token.rawText().find("``"); if (index != std::string_view::npos) { Token first = token.withRawText(alloc, token.rawText().substr(0, index)); - if (!handleToken(first)) + if (!handleToken(first, bodyTokenIndex)) return false; SmallVector splits; splitTokens(token, index, splits); for (auto t : splits) { - if (!handleToken(t)) + if (!handleToken(t, bodyTokenIndex)) return false; } @@ -604,7 +646,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, Token empty(alloc, TokenKind::EmptyMacroArgument, triviaBuf.copy(alloc), ""sv, loc); - if (!handleToken(empty)) + if (!handleToken(empty, bodyTokenIndex)) return false; } @@ -612,7 +654,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, } } - if (!handleToken(token)) + if (!handleToken(token, bodyTokenIndex)) return false; } @@ -623,6 +665,17 @@ SourceRange Preprocessor::MacroExpansion::getRange() const { return {usageSite.location(), usageSite.location() + usageSite.rawText().length()}; } +SourceManager::MacroTokenProvenance Preprocessor::MacroExpansion::tokenProvenance( + uint32_t bodyTokenIndex, uint32_t argumentIndex, uint32_t argumentTokenIndex) const { + SourceManager::MacroTokenProvenance provenance; + provenance.callId = metadata.callId; + provenance.definitionId = metadata.definitionId; + provenance.bodyTokenIndex = bodyTokenIndex; + provenance.argumentIndex = argumentIndex; + provenance.argumentTokenIndex = argumentTokenIndex; + return provenance; +} + SourceLocation Preprocessor::MacroExpansion::adjustLoc(Token token, SourceLocation& macroLoc, SourceLocation& firstLoc, SourceRange expansionRange) const { @@ -631,7 +684,8 @@ SourceLocation Preprocessor::MacroExpansion::adjustLoc(Token token, SourceLocati // the new buffer as its original location. if (token.location().buffer() != firstLoc.buffer()) { firstLoc = token.location(); - macroLoc = sourceManager.createExpansionLoc(firstLoc, expansionRange, true); + macroLoc = sourceManager.createExpansionLoc( + firstLoc, expansionRange, SourceManager::MacroExpansionKind::Argument, metadata); } return macroLoc + (token.location() - firstLoc); @@ -639,13 +693,15 @@ SourceLocation Preprocessor::MacroExpansion::adjustLoc(Token token, SourceLocati void Preprocessor::MacroExpansion::append(Token token, SourceLocation& macroLoc, SourceLocation& firstLoc, SourceRange expansionRange, - bool allowLineContinuation) { + bool allowLineContinuation, + SourceManager::MacroTokenProvenance provenance) { SourceLocation location = adjustLoc(token, macroLoc, firstLoc, expansionRange); - append(token, location, allowLineContinuation); + append(token, location, allowLineContinuation, provenance); } void Preprocessor::MacroExpansion::append(Token token, SourceLocation location, - bool allowLineContinuation) { + bool allowLineContinuation, + SourceManager::MacroTokenProvenance provenance) { if (!any) { if (!isTopLevel) token = token.withTrivia(alloc, usageSite.trivia()); @@ -664,6 +720,7 @@ void Preprocessor::MacroExpansion::append(Token token, SourceLocation location, Token(alloc, TokenKind::EmptyMacroArgument, newTrivia.copy(alloc), "", location)); } else { + sourceManager.setMacroTokenProvenance(location, provenance); dest.push_back(token.withLocation(alloc, location)); } } @@ -709,7 +766,13 @@ bool Preprocessor::expandReplacementList( } expansionBuffer.clear(); - MacroExpansion expansion{sourceManager, alloc, expansionBuffer, token, false}; + SourceManager::MacroExpansionMetadata metadata; + metadata.callId = allocateMacroCallId(); + metadata.definitionId = macro.definitionId; + if (sourceManager.isMacroLoc(token.location())) + metadata.parentExpansionId = token.location().buffer().getId(); + + MacroExpansion expansion{sourceManager, alloc, expansionBuffer, token, false, metadata}; if (!expandMacro(macro, expansion, actualArgs)) return false; @@ -733,6 +796,9 @@ bool Preprocessor::expandReplacementList( bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& expansion) { auto loc = expansion.getRange().start(); + auto macroLoc = sourceManager.createExpansionLoc( + loc, expansion.getRange(), SourceManager::MacroExpansionKind::Body, + expansion.getMetadata()); SmallVector text; switch (intrinsic) { case MacroIntrinsic::File: { @@ -743,7 +809,7 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp std::string_view rawText = toStringView(text.copy(alloc)); Token token(alloc, TokenKind::StringLiteral, {}, rawText, loc, fileName); - expansion.append(token, loc); + expansion.append(token, macroLoc); break; } case MacroIntrinsic::Line: { @@ -752,7 +818,7 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp std::string_view rawText = toStringView(text.copy(alloc)); Token token(alloc, TokenKind::IntegerLiteral, {}, rawText, loc, lineNum); - expansion.append(token, loc); + expansion.append(token, macroLoc); break; } case MacroIntrinsic::None: diff --git a/crates/slang/source/text/SourceManager.cpp b/crates/slang/source/text/SourceManager.cpp index db8e6aad..fe644bc5 100644 --- a/crates/slang/source/text/SourceManager.cpp +++ b/crates/slang/source/text/SourceManager.cpp @@ -247,6 +247,15 @@ SourceLocation SourceManager::getOriginalLoc(SourceLocation location) const { return getOriginalLocImpl(location, lock); } +std::optional SourceManager::getMacroTokenProvenance( + SourceLocation location) const { + std::shared_lock lock(mutex); + auto it = macroTokenProvenance.find(location); + if (it == macroTokenProvenance.end()) + return std::nullopt; + return it->second; +} + SourceLocation SourceManager::getFullyOriginalLoc(SourceLocation location) const { std::shared_lock lock(mutex); while (isMacroLocImpl(location, lock)) @@ -286,30 +295,67 @@ uint64_t SourceManager::getSortKey(BufferID buffer) const { SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, bool isMacroArg) { - std::unique_lock lock(mutex); - - bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, isMacroArg)); - return SourceLocation(BufferID((uint32_t)(bufferEntries.size() - 1), ""sv), 0); + return createExpansionLoc(originalLoc, expansionRange, + isMacroArg ? MacroExpansionKind::Argument + : MacroExpansionKind::Body, + {}); } SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, std::string_view macroName) { + return createExpansionLoc(originalLoc, expansionRange, macroName, {}); +} + +SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, + SourceRange expansionRange, + MacroExpansionKind kind) { + return createExpansionLoc(originalLoc, expansionRange, kind, {}); +} + +SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, + SourceRange expansionRange, + std::string_view macroName, + MacroExpansionMetadata metadata) { std::unique_lock lock(mutex); - bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, macroName)); + bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, macroName, metadata)); return SourceLocation(BufferID((uint32_t)(bufferEntries.size() - 1), macroName), 0); } SourceLocation SourceManager::createExpansionLoc(SourceLocation originalLoc, SourceRange expansionRange, - MacroExpansionKind kind) { + MacroExpansionKind kind, + MacroExpansionMetadata metadata) { std::unique_lock lock(mutex); - bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, kind)); + bufferEntries.emplace_back(ExpansionInfo(originalLoc, expansionRange, kind, metadata)); return SourceLocation(BufferID((uint32_t)(bufferEntries.size() - 1), ""sv), 0); } +void SourceManager::setMacroTokenProvenance(SourceLocation location, + MacroTokenProvenance provenance) { + if (!location.valid()) + return; + + std::unique_lock lock(mutex); + auto buffer = location.buffer(); + if (!buffer || buffer.getId() >= bufferEntries.size()) + return; + + provenance.expansionId = buffer.getId(); + if (auto info = std::get_if(&bufferEntries[buffer.getId()])) { + if (provenance.callId == 0) + provenance.callId = info->metadata.callId; + if (provenance.definitionId == 0) + provenance.definitionId = info->metadata.definitionId; + provenance.parentExpansionId = info->metadata.parentExpansionId; + } + + if (provenance.valid()) + macroTokenProvenance[location] = provenance; +} + SourceBuffer SourceManager::assignText(std::string_view text, SourceLocation includedFrom, const SourceLibrary* library) { return assignText("", text, includedFrom, library); From 01bc796c8d3d9a857f5f9ec39a0659209a9f549f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 14:32:30 +0800 Subject: [PATCH 24/80] feat(preproc): consume direct macro provenance identity --- crates/hir/src/preproc.rs | 24 +- crates/preproc/src/source/model.rs | 353 ++++++++++++++++++++-- crates/preproc/src/source/provenance.rs | 380 ++++++++++++++---------- crates/preproc/src/source/trace.rs | 62 +++- crates/preproc/src/source/types.rs | 45 +++ 5 files changed, 668 insertions(+), 196 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index a95a58bd..f6673e37 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -2328,12 +2328,17 @@ fn map_token_provenance( let (source, range) = map_mapped_source_range(mapped, *token_range)?; TokenProvenance::SourceToken { source, range } } - SourceTokenProvenanceFact::MacroBody { definition, body_token_range, call } => { + SourceTokenProvenanceFact::MacroBody { definition, body_token_range, call, .. } => { let call = mapped_macro_call(mapped, *call)?; let (source, range) = map_mapped_source_range(mapped, *body_token_range)?; TokenProvenance::MacroBody { call, definition_id: (*definition).into(), source, range } } - SourceTokenProvenanceFact::MacroArgument { call, argument_index, argument_token_range } => { + SourceTokenProvenanceFact::MacroArgument { + call, + argument_index, + argument_token_range, + .. + } => { let call = mapped_macro_call(mapped, *call)?; let (source, range) = map_mapped_source_range(mapped, *argument_token_range)?; TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } @@ -3043,9 +3048,16 @@ endmodule recursive_macro_expansion_at(&db, TOP, offset(root_text, "`WRAP")).unwrap().unwrap(); assert_eq!(recursive.root_call.file_id, TOP); assert_eq!(text_at_range(root_text, recursive.root_call.range), "`WRAP"); - assert_eq!(recursive.expansions.len(), 2); - assert!(recursive.expansions.iter().any(|expansion| !expansion.child_calls.is_empty())); - assert!(recursive.unavailable.is_empty()); + assert!(recursive.expansions.is_empty()); + assert!(matches!( + recursive.unavailable.as_slice(), + [MacroExpansionUnavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + ), + .. + }] + )); } #[test] @@ -3192,7 +3204,7 @@ endmodule assert!(matches!( provenance, DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::ExpansionAuthorityUnavailable + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } )) )); } diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index e5e1d17a..d33fa4b7 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -323,8 +323,11 @@ mod tests { use smol_str::SmolStr; use syntax::{ PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, - PreprocessorTraceToken, SourceBufferId, SourceBufferOrigin, SourceBufferRange, SyntaxKind, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions, TokenKind, + PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, + PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, + PreprocessorTraceToken, PreprocessorTraceTokenProvenance, SourceBufferId, + SourceBufferOrigin, SourceBufferRange, SyntaxKind, SyntaxTree, SyntaxTreeBuffer, + SyntaxTreeOptions, TokenKind, }; use utils::line_index::{TextRange, TextSize}; @@ -403,6 +406,10 @@ mod tests { &text[usize::from(range.start())..usize::from(range.end())] } + fn source_range(source: PreprocSourceId, start: u32, end: u32) -> SourceRange { + SourceRange { source, range: TextRange::new(TextSize::from(start), TextSize::from(end)) } + } + fn visible_macro_names( model: &SourcePreprocModel, source: PreprocSourceId, @@ -750,6 +757,7 @@ logic [`HEADER_WIDTH-1:0] data; definition: body_definition, body_token_range, call: body_call, + .. } if *body_definition == *resolved_definition && body_token_range.source == header_source && *body_call == call.id @@ -777,8 +785,9 @@ endmodule .iter() .find(|token| token.text.as_str() == "7") .expect("argument replacement token should be emitted"); - let SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range } = - model.token_provenance().get(emitted.provenance).unwrap() + let SourceTokenProvenance::MacroArgument { + call, argument_index, argument_token_range, .. + } = model.token_provenance().get(emitted.provenance).unwrap() else { panic!("argument replacement should map to MacroArgument provenance"); }; @@ -804,7 +813,265 @@ endmodule } #[test] - fn source_model_builds_nested_macro_expansion_provenance_chain() { + fn source_model_uses_direct_definition_identity_when_body_ranges_collide() { + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![SourceBufferId { + path: ROOT_PATH.to_owned(), + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }], + events: vec![ + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(0), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 0..12 }), + macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(10)), + macro_call_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "A".to_owned(), + value_text: "A".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }), + include_file_name: None, + params: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "1".to_owned(), + value_text: "1".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(1), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 13..25 }), + macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(20)), + macro_call_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "B".to_owned(), + value_text: "B".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 21..22 }), + }), + include_file_name: None, + params: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "2".to_owned(), + value_text: "2".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(2), + kind: SyntaxKind::MACRO_USAGE, + range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), + macro_definition_id: None, + macro_call_id: Some(PreprocessorTraceMacroCallId(200)), + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "`B".to_owned(), + value_text: "`B".to_owned(), + token_kind: TokenKind::DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), + }), + include_file_name: None, + params: Vec::new(), + body_tokens: Vec::new(), + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + ], + include_edges: Vec::new(), + emitted_tokens: vec![syntax::PreprocessorTraceEmittedToken { + raw_text: "2".to_owned(), + value_text: "2".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + provenance: PreprocessorTraceTokenProvenance::MacroBody { + macro_name: "B".to_owned(), + identity: PreprocessorTraceMacroBodyIdentity { + call_id: PreprocessorTraceMacroCallId(200), + definition_id: PreprocessorTraceMacroDefinitionId(20), + expansion_id: PreprocessorTraceMacroExpansionId(300), + parent_expansion_id: None, + body_token_index: 0, + }, + call_range: SourceBufferRange { buffer_id: 1, range: 40..42 }, + body_token_range: SourceBufferRange { buffer_id: 1, range: 8..9 }, + }, + }], + }; + let model = SourcePreprocModel::from_trace(trace).unwrap(); + let emitted = model.emitted_tokens().iter().find(|token| token.text == "2").unwrap(); + let SourceTokenProvenance::MacroBody { definition, call, identity, .. } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("colliding range token should still resolve through direct body identity"); + }; + + let definition = model.macro_definitions().get(*definition).unwrap(); + assert_eq!(definition.name.as_str(), "B"); + assert_eq!(definition.identity, Some(identity.definition)); + assert_eq!(model.macro_calls().get(*call).unwrap().identity, Some(identity.call)); + } + + #[test] + fn source_model_preserves_multi_token_argument_direct_identity() { + let root_text = r#"`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(payload_i[3:0]); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let payload = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index, + body_token_range, + argument_token_range, + } = model.token_provenance().get(token.provenance)? + else { + return None; + }; + (token.text.as_str() == "payload_i").then_some(( + *identity, + *call, + *argument_index, + *body_token_range, + *argument_token_range, + )) + }) + .expect("payload identifier should be direct macro argument provenance"); + let slice = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index, + body_token_range, + argument_token_range, + } = model.token_provenance().get(token.provenance)? + else { + return None; + }; + (token.text.as_str() == "3").then_some(( + *identity, + *call, + *argument_index, + *body_token_range, + *argument_token_range, + )) + }) + .expect("slice index should be direct macro argument provenance"); + + assert_eq!(payload.0.call, slice.0.call); + assert_eq!(payload.1, slice.1); + assert_eq!(payload.2, 0); + assert_eq!(slice.2, 0); + assert_eq!(payload.0.argument_token_index, 0); + assert_eq!(slice.0.argument_token_index, 2); + assert_eq!(payload.3, slice.3); + assert_eq!(payload.4.source, root_source); + assert_eq!(slice.4.source, root_source); + let call = model.macro_calls().get(payload.1).unwrap(); + assert_eq!(call.arguments.len(), 1); + assert_eq!( + text_at_range(root_text, call.arguments[0].argument_range.unwrap().range), + "payload_i[3:0]" + ); + } + + #[test] + fn source_model_marks_missing_direct_identity_partial_without_range_fallback() { + let root_source = PreprocSourceId::from(1); + let define_range = source_range(root_source, 0, 11); + let name_range = source_range(root_source, 8, 9); + let body_range = source_range(root_source, 10, 11); + let usage_range = source_range(root_source, 24, 26); + let index = SourcePreprocIndex { + root_source: Some(root_source), + sources: vec![PreprocSource { + id: root_source, + path: SmolStr::new(ROOT_PATH), + origin: PreprocSourceOrigin::Root, + }], + event_records: vec![ + SourcePreprocEventRecord { + event_id: SourcePreprocEventId(0), + kind: MacroEventKind::Define, + range: define_range, + index: 0, + }, + SourcePreprocEventRecord { + event_id: SourcePreprocEventId(1), + kind: MacroEventKind::Usage, + range: usage_range, + index: 0, + }, + ], + emitted_tokens: vec![SourceEmittedTokenFact { + raw: SmolStr::new("1"), + value: SmolStr::new("1"), + kind: SourceTokenKind::Syntax(TokenKind::INTEGER_LITERAL), + provenance: SourceTokenProvenanceFact::MacroBody { + macro_name: SmolStr::new("A"), + identity: None, + call_range: usage_range, + body_token_range: body_range, + }, + }], + defines: vec![SourceMacroDefine { + event_id: SourcePreprocEventId(0), + identity: Some(SourceMacroDefinitionKey::new(10)), + name: Some(SmolStr::new("A")), + name_range: Some(name_range), + params: None, + body: vec![SourceMacroToken { + raw: SmolStr::new("1"), + value: SmolStr::new("1"), + range: Some(body_range), + }], + range: define_range, + }], + usages: vec![SourceMacroUsage { + event_id: SourcePreprocEventId(1), + identity: Some(SourceMacroCallKey::new(20)), + name: Some(SmolStr::new("A")), + name_range: Some(usage_range), + range: usage_range, + }], + ..SourcePreprocIndex::default() + }; + + let model = SourcePreprocModel::new(index); + let emitted = model.emitted_tokens().iter().next().unwrap(); + assert!(matches!( + model.token_provenance().get(emitted.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity + ) + )); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); + } + + #[test] + fn source_model_keeps_nested_macro_identity_without_range_recovery() { let root_text = r#"`define LEAF 3 `define WRAP `LEAF module m; @@ -843,10 +1110,46 @@ endmodule let SourceMacroExpansionQuery::Available(wrap_expansion_id) = model.immediate_macro_expansion(wrap_call.id) else { - panic!("outer macro should have expansion range from nested emitted tokens"); + assert!(matches!( + model.immediate_macro_expansion(wrap_call.id), + SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + ) + )); + assert_eq!(wrap_call.expansion_identity, None); + assert!(matches!(model.capabilities().macro_expansions, CapabilityStatus::Partial)); + return; }; - let wrap_expansion = model.macro_expansions().get(wrap_expansion_id).unwrap(); - assert_eq!(wrap_expansion.child_calls, vec![leaf_call.id]); + panic!( + "outer macro should not recover tokenless nested expansion by range: {wrap_expansion_id:?}" + ); + } + + #[test] + fn source_model_builds_nested_leaf_expansion_from_direct_identity() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int W = `WRAP; +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let leaf_call = model + .macro_calls() + .iter() + .find(|call| { + let reference = model.macro_references().get(call.reference).unwrap(); + reference.name.as_str() == "LEAF" + && matches!( + reference.site, + SourceMacroReferenceSite::ExpansionToken { emitted_token: _ } + ) + }) + .expect("nested macro invocation should create an expansion-token call"); + assert!(leaf_call.identity.is_some()); + assert!(leaf_call.expansion_identity.is_some()); + assert!(leaf_call.parent_expansion_identity.is_some()); let SourceMacroExpansionQuery::Available(leaf_expansion_id) = model.immediate_macro_expansion(leaf_call.id) @@ -858,16 +1161,22 @@ endmodule .iter() .find(|token| token.text.as_str() == "3") .expect("nested macro body token should be emitted"); - let SourceTokenProvenance::MacroBody { call, .. } = + let SourceTokenProvenance::MacroBody { identity, definition, call, .. } = model.token_provenance().get(emitted.provenance).unwrap() else { panic!("nested emitted token should keep macro body provenance"); }; assert_eq!(*call, leaf_call.id); - assert_eq!(wrap_expansion.emitted_token_range.start, emitted.id); + assert_eq!(Some(identity.call), leaf_call.identity); + assert_eq!(Some(identity.expansion), leaf_call.expansion_identity); + assert_eq!(identity.parent_expansion, leaf_call.parent_expansion_identity); + assert_eq!( + Some(identity.definition), + model.macro_definitions().get(*definition).unwrap().identity + ); - let recursive = model.recursive_macro_expansion(wrap_call.id); - assert_eq!(recursive.expansions, vec![wrap_expansion_id, leaf_expansion_id]); + let recursive = model.recursive_macro_expansion(leaf_call.id); + assert_eq!(recursive.expansions, vec![leaf_expansion_id]); assert!(recursive.unavailable.is_empty()); } @@ -1005,7 +1314,7 @@ endmodule } #[test] - fn source_model_maps_predefine_and_builtin_emitted_token_provenance() { + fn source_model_maps_predefine_and_marks_intrinsic_unavailable() { let root_text = r#"module m; localparam int P = `FROM_API; localparam int L = `__LINE__; @@ -1033,17 +1342,17 @@ endmodule candidate.id == *source && candidate.origin == PreprocSourceOrigin::Predefine })); - let builtin = model + let intrinsic = model .emitted_tokens() .iter() - .find(|token| { - matches!( - model.token_provenance().get(token.provenance), - Some(SourceTokenProvenance::Builtin { name }) if name == "__LINE__" - ) - }) - .expect("builtin macro token should be emitted"); - assert!(!builtin.text.is_empty()); + .find(|token| token.text.as_str() == "3") + .expect("intrinsic macro token should stay in emitted stream"); + assert!(matches!( + model.token_provenance().get(intrinsic.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + ) + )); } #[test] diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 1073b755..acaacef3 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -1,7 +1,6 @@ use std::collections::BTreeMap; use smol_str::SmolStr; -use utils::line_index::TextSize; use super::types::*; @@ -52,6 +51,7 @@ pub enum SourceMacroReferenceSite { pub struct SourceMacroDefinition { pub id: SourceMacroDefinitionId, pub event_id: SourcePreprocEventId, + pub identity: Option, pub name: SmolStr, pub name_range: SourceRange, pub directive_range: SourceRange, @@ -138,23 +138,12 @@ struct SourceMacroStatePositionBoundary { boundary: SourcePosition, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct SourceMacroCallSignature { - source: PreprocSourceId, - start: TextSize, - end: TextSize, - name: SmolStr, -} - -impl SourceMacroCallSignature { - fn new(name: SmolStr, range: SourceRange) -> Self { - Self { source: range.source, start: range.range.start(), end: range.range.end(), name } - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroCall { pub id: SourceMacroCallId, + pub identity: Option, + pub expansion_identity: Option, + pub parent_expansion_identity: Option, pub reference: SourceMacroReferenceId, pub call_range: SourceRange, pub callee: SourceMacroResolution, @@ -179,6 +168,7 @@ pub enum SourceMacroCallStatus { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroExpansion { pub id: SourceMacroExpansionId, + pub identity: Option, pub call: SourceMacroCallId, pub definition: SourceMacroDefinitionId, pub emitted_token_range: SourceEmittedTokenRange, @@ -232,13 +222,16 @@ pub enum SourceTokenProvenance { token_range: SourceRange, }, MacroBody { + identity: SourceMacroBodyIdentity, definition: SourceMacroDefinitionId, body_token_range: SourceRange, call: SourceMacroCallId, }, MacroArgument { + identity: SourceMacroArgumentIdentity, call: SourceMacroCallId, argument_index: usize, + body_token_range: SourceRange, argument_token_range: SourceRange, }, TokenPaste { @@ -341,6 +334,12 @@ pub enum SourcePreprocUnavailable { MissingMacroCall { call: SourceMacroCallId }, MissingMacroExpansion { call: SourceMacroCallId }, MissingEmittedTokenMacroCall { source: PreprocSourceId }, + MissingEmittedTokenMacroCallIdentity, + UnknownEmittedTokenMacroCallIdentity { identity: SourceMacroCallKey }, + MissingEmittedTokenMacroDefinitionIdentity, + UnknownEmittedTokenMacroDefinitionIdentity { identity: SourceMacroDefinitionKey }, + MissingEmittedTokenMacroExpansionIdentity { call: SourceMacroCallId }, + UnmappedParentMacroExpansionIdentity { identity: SourceMacroExpansionKey }, MissingEmittedTokenMacroDefinition { call: SourceMacroCallId }, MissingEmittedTokenMacroBody { call: SourceMacroCallId }, MissingEmittedTokenMacroArgument { call: SourceMacroCallId }, @@ -642,11 +641,14 @@ pub struct SourcePreprocModelBuilder<'a> { index: &'a SourcePreprocIndex, tables: SourcePreprocTables, definition_ids_by_define_index: BTreeMap, - call_ids_by_signature: BTreeMap, + definition_ids_by_identity: BTreeMap, + call_ids_by_identity: BTreeMap, + call_ids_by_expansion_identity: BTreeMap, current_state: BTreeMap, definition_ranges_partial: bool, include_edges_partial: bool, references_partial: bool, + macro_calls_partial: bool, token_provenance_partial: bool, expansions_partial: bool, } @@ -657,11 +659,14 @@ impl<'a> SourcePreprocModelBuilder<'a> { index, tables: SourcePreprocTables::default(), definition_ids_by_define_index: BTreeMap::new(), - call_ids_by_signature: BTreeMap::new(), + definition_ids_by_identity: BTreeMap::new(), + call_ids_by_identity: BTreeMap::new(), + call_ids_by_expansion_identity: BTreeMap::new(), current_state: BTreeMap::new(), definition_ranges_partial: false, include_edges_partial: false, references_partial: false, + macro_calls_partial: false, token_provenance_partial: false, expansions_partial: false, } @@ -696,7 +701,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { include_edges: partial_status(self.include_edges_partial), inactive_ranges: CapabilityStatus::Complete, macro_reference_resolution: partial_status(self.references_partial), - macro_calls: partial_status(self.references_partial), + macro_calls: partial_status(self.references_partial || self.macro_calls_partial), macro_expansions, emitted_tokens: CapabilityStatus::Complete, emitted_token_provenance: partial_status(self.token_provenance_partial), @@ -724,6 +729,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.tables.macro_definitions.push(SourceMacroDefinition { id, event_id: define.event_id, + identity: define.identity, name, name_range, directive_range: define.range, @@ -731,6 +737,9 @@ impl<'a> SourcePreprocModelBuilder<'a> { body_tokens: define.body.clone(), }); self.definition_ids_by_define_index.insert(define_index, id); + if let Some(identity) = define.identity { + self.definition_ids_by_identity.insert(identity, id); + } } } @@ -902,7 +911,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { directive_range, resolution.clone(), ); - self.push_call(reference, name, directive_range, resolution); + self.push_call(reference, directive_range, resolution, usage.identity, None, None); } fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { @@ -980,13 +989,18 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn push_call( &mut self, reference: SourceMacroReferenceId, - name: SmolStr, call_range: SourceRange, callee: SourceMacroResolution, + identity: Option, + expansion_identity: Option, + parent_expansion_identity: Option, ) -> SourceMacroCallId { let id = SourceMacroCallId::new(self.tables.macro_calls.len()); self.tables.macro_calls.push(SourceMacroCall { id, + identity, + expansion_identity, + parent_expansion_identity, reference, call_range, callee, @@ -996,7 +1010,14 @@ impl<'a> SourcePreprocModelBuilder<'a> { SourcePreprocUnavailable::ExpansionAuthorityUnavailable, ), }); - self.call_ids_by_signature.insert(SourceMacroCallSignature::new(name, call_range), id); + if let Some(identity) = identity { + self.call_ids_by_identity.insert(identity, id); + } else { + self.macro_calls_partial = true; + } + if let Some(expansion_identity) = expansion_identity { + self.call_ids_by_expansion_identity.insert(expansion_identity, id); + } id } @@ -1027,23 +1048,28 @@ impl<'a> SourcePreprocModelBuilder<'a> { SourceTokenProvenanceFact::Source { token_range } => { SourceTokenProvenance::Source { token_range: *token_range } } - SourceTokenProvenanceFact::MacroBody { macro_name, call_range, body_token_range } => { - self.resolve_macro_body_token_provenance( - token_id, - token, - macro_name.clone(), - *call_range, - *body_token_range, - ) - } + SourceTokenProvenanceFact::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } => self.resolve_macro_body_token_provenance( + token_id, + macro_name.clone(), + *identity, + *call_range, + *body_token_range, + ), SourceTokenProvenanceFact::MacroArgument { macro_name, + identity, call_range, body_token_range, argument_token_range, } => self.resolve_macro_argument_token_provenance( token_id, macro_name.clone(), + *identity, *call_range, *body_token_range, *argument_token_range, @@ -1062,8 +1088,8 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn resolve_macro_body_token_provenance( &mut self, token_id: SourceEmittedTokenId, - token: &SourceEmittedTokenFact, macro_name: SmolStr, + identity: Option, call_range: SourceRange, body_token_range: SourceRange, ) -> SourceTokenProvenance { @@ -1071,81 +1097,126 @@ impl<'a> SourcePreprocModelBuilder<'a> { return SourceTokenProvenance::Predefine { source: body_token_range.source }; } - let Ok(call) = - self.call_for_emitted_token(token_id, macro_name, call_range, body_token_range) - else { + let Some(identity) = identity else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + let Ok(definition) = self.definition_for_identity(identity.definition) else { return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroCall { - source: call_range.source, + SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { + identity: identity.definition, }, ); }; - let Ok(definition) = self.definition_for_call(call) else { + let Ok(call) = self.call_for_emitted_token( + token_id, + macro_name, + identity.call, + definition, + call_range, + identity.expansion, + identity.parent_expansion, + ) else { return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, + }, ); }; - if !self.definition_body_contains_raw_token( - definition, - body_token_range, - token.raw.as_str(), - ) { + if !self.definition_body_token_exists(definition, identity.body_token_index) { return self.unavailable_token_provenance( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, ); } - SourceTokenProvenance::MacroBody { definition, body_token_range, call } + SourceTokenProvenance::MacroBody { identity, definition, body_token_range, call } } fn resolve_macro_argument_token_provenance( &mut self, token_id: SourceEmittedTokenId, macro_name: SmolStr, + identity: Option, call_range: SourceRange, body_token_range: SourceRange, argument_token_range: SourceRange, ) -> SourceTokenProvenance { - let Ok(call) = - self.call_for_emitted_token(token_id, macro_name, call_range, body_token_range) - else { + let Some(identity) = identity else { return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroCall { - source: call_range.source, + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + let Ok(definition) = self.definition_for_identity(identity.definition) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { + identity: identity.definition, + }, + ); + }; + let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); + let Ok(call) = self.call_for_emitted_token( + token_id, + macro_name, + identity.call, + definition, + call_range, + call_expansion_identity, + None, + ) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, }, ); }; - let Ok(argument_index) = self.argument_index_for_body_token(call, body_token_range) else { + if !self.definition_body_token_exists(definition, identity.body_token_index) { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, + ); + } + if !self.definition_parameter_exists(definition, identity.argument_index) { return self.unavailable_token_provenance( SourcePreprocUnavailable::MissingEmittedTokenMacroArgument { call }, ); }; - self.record_macro_argument(call, argument_index, argument_token_range); + self.record_macro_argument(call, identity.argument_index, argument_token_range); - SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range } + SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index: identity.argument_index, + body_token_range, + argument_token_range, + } } fn call_for_emitted_token( &mut self, token_id: SourceEmittedTokenId, macro_name: SmolStr, + call_identity: SourceMacroCallKey, + definition: SourceMacroDefinitionId, call_range: SourceRange, - body_token_range: SourceRange, - ) -> Result { - let signature = SourceMacroCallSignature::new(macro_name.clone(), call_range); - if let Some(call) = self.call_ids_by_signature.get(&signature).copied() { + expansion_identity: SourceMacroExpansionKey, + parent_expansion_identity: Option, + ) -> Result { + if let Some(call) = self.call_ids_by_identity.get(&call_identity).copied() { + self.record_call_expansion_identity( + call, + expansion_identity, + parent_expansion_identity, + )?; return Ok(call); } - let definition = self.definition_for_body_token_range(body_token_range)?; - let event_id = self.event_id_for_call_site(call_range).unwrap_or_else(|| { - self.tables - .macro_definitions - .get(definition) - .expect("definition id should point at inserted definition") - .event_id - }); + let event_id = self + .tables + .macro_definitions + .get(definition) + .expect("definition id should point at inserted definition") + .event_id; let resolution = self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition); let reference = self.push_reference( @@ -1156,7 +1227,14 @@ impl<'a> SourcePreprocModelBuilder<'a> { call_range, resolution.clone(), ); - Ok(self.push_call(reference, macro_name, call_range, resolution)) + Ok(self.push_call( + reference, + call_range, + resolution, + Some(call_identity), + Some(expansion_identity), + parent_expansion_identity, + )) } fn definition_for_call(&self, call: SourceMacroCallId) -> Result { @@ -1169,66 +1247,68 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } - fn definition_for_body_token_range( + fn definition_for_identity( &self, - body_token_range: SourceRange, + identity: SourceMacroDefinitionKey, ) -> Result { - self.tables - .macro_definitions - .iter() - .find(|definition| { - definition.body_tokens.iter().any(|token| token.range == Some(body_token_range)) - }) - .map(|definition| definition.id) - .ok_or(()) + self.definition_ids_by_identity.get(&identity).copied().ok_or(()) } - fn event_id_for_call_site(&self, call_range: SourceRange) -> Option { - self.index - .usages - .iter() - .find(|usage| usage.range == call_range) - .map(|usage| usage.event_id) - .or_else(|| { - self.tables - .macro_definitions - .iter() - .find(|definition| { - definition.body_tokens.iter().any(|token| token.range == Some(call_range)) - }) - .map(|definition| definition.event_id) - }) + fn definition_body_token_exists( + &self, + definition: SourceMacroDefinitionId, + body_token_index: usize, + ) -> bool { + let Some(definition) = self.tables.macro_definitions.get(definition) else { + return false; + }; + definition.body_tokens.get(body_token_index).is_some() } - fn definition_body_contains_raw_token( + fn definition_parameter_exists( &self, definition: SourceMacroDefinitionId, - body_token_range: SourceRange, - raw: &str, + argument_index: usize, ) -> bool { let Some(definition) = self.tables.macro_definitions.get(definition) else { return false; }; - definition - .body_tokens - .iter() - .any(|token| token.range == Some(body_token_range) && token.raw.as_str() == raw) + definition.params.as_ref().is_some_and(|params| params.get(argument_index).is_some()) } - fn argument_index_for_body_token( - &self, + fn record_call_expansion_identity( + &mut self, call: SourceMacroCallId, - body_token_range: SourceRange, - ) -> Result { - let definition = self.definition_for_call(call)?; - let definition = self.tables.macro_definitions.get(definition).ok_or(())?; - let body_token = definition - .body_tokens - .iter() - .find(|token| token.range == Some(body_token_range)) - .ok_or(())?; - let params = definition.params.as_ref().ok_or(())?; - params.iter().position(|param| param.name.as_ref() == Some(&body_token.value)).ok_or(()) + expansion_identity: SourceMacroExpansionKey, + parent_expansion_identity: Option, + ) -> Result<(), SourcePreprocUnavailable> { + let Some(call_fact) = self.tables.macro_calls.get_mut(call) else { + return Err(SourcePreprocUnavailable::MissingMacroCall { call }); + }; + if let Some(existing) = call_fact.expansion_identity { + if existing != expansion_identity { + self.expansions_partial = true; + return Err(SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { + call, + }); + } + } else { + call_fact.expansion_identity = Some(expansion_identity); + self.call_ids_by_expansion_identity.insert(expansion_identity, call); + } + if let Some(parent_expansion_identity) = parent_expansion_identity { + match call_fact.parent_expansion_identity { + Some(existing) if existing != parent_expansion_identity => { + self.expansions_partial = true; + return Err(SourcePreprocUnavailable::UnmappedParentMacroExpansionIdentity { + identity: parent_expansion_identity, + }); + } + Some(_) => {} + None => call_fact.parent_expansion_identity = Some(parent_expansion_identity), + } + } + Ok(()) } fn record_macro_argument( @@ -1284,6 +1364,15 @@ impl<'a> SourcePreprocModelBuilder<'a> { for call in call_ids { let tokens = expansion_tokens_by_call.remove(&call).unwrap_or_default(); + let Some(expansion_identity) = + self.tables.macro_calls.get(call).and_then(|call| call.expansion_identity) + else { + self.mark_call_unavailable( + call, + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { call }, + ); + continue; + }; let Some(emitted_token_range) = emitted_token_range_from_ids(&tokens) else { self.mark_call_unavailable( call, @@ -1306,6 +1395,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { let expansion = SourceMacroExpansionId::new(self.tables.macro_expansions.len()); self.tables.macro_expansions.push(SourceMacroExpansion { id: expansion, + identity: Some(expansion_identity), call, definition, emitted_token_range, @@ -1397,53 +1487,27 @@ impl<'a> SourcePreprocModelBuilder<'a> { let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); let mut children = BTreeMap::>::new(); for child in &call_ids { - let parents = call_ids - .iter() - .copied() - .filter(|parent| parent != child) - .filter(|parent| self.call_site_belongs_to_parent(*child, *parent)) - .collect::>(); - match parents.as_slice() { - [parent] => children.entry(*parent).or_default().push(*child), - [] => {} - _ => self.expansions_partial = true, + let Some(child_call) = self.tables.macro_calls.get(*child) else { + self.expansions_partial = true; + continue; + }; + let Some(parent_expansion_identity) = child_call.parent_expansion_identity else { + continue; + }; + match self.call_ids_by_expansion_identity.get(&parent_expansion_identity).copied() { + Some(parent) if parent != *child => { + children.entry(parent).or_default().push(*child); + } + Some(_) | None => { + self.expansions_partial = true; + } } } - children - } - - fn call_site_belongs_to_parent( - &self, - child: SourceMacroCallId, - parent: SourceMacroCallId, - ) -> bool { - let Some(child) = self.tables.macro_calls.get(child) else { - return false; - }; - if let Ok(parent_definition) = self.definition_for_call(parent) - && self.definition_body_contains_range(parent_definition, child.call_range) - { - return true; + for child_calls in children.values_mut() { + child_calls.sort_by_key(|call| call.raw()); + child_calls.dedup(); } - let Some(parent) = self.tables.macro_calls.get(parent) else { - return false; - }; - parent.arguments.iter().any(|argument| { - argument - .argument_range - .is_some_and(|range| source_range_contains(range, child.call_range)) - }) - } - - fn definition_body_contains_range( - &self, - definition: SourceMacroDefinitionId, - token_range: SourceRange, - ) -> bool { - let Some(definition) = self.tables.macro_definitions.get(definition) else { - return false; - }; - definition.body_tokens.iter().any(|token| token.range == Some(token_range)) + children } fn recursive_emitted_tokens_for_call( @@ -1747,9 +1811,3 @@ fn merge_source_ranges(existing: Option, next: SourceRange) -> Opti ), }) } - -fn source_range_contains(outer: SourceRange, inner: SourceRange) -> bool { - outer.source == inner.source - && outer.range.start() <= inner.range.start() - && inner.range.end() <= outer.range.end() -} diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index e5b7270c..c1653fa9 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -3,8 +3,11 @@ use std::collections::BTreeMap; use smol_str::{SmolStr, ToSmolStr}; use syntax::{ PreprocessorTrace, PreprocessorTraceEmittedToken, PreprocessorTraceEvent, - PreprocessorTraceEventId, PreprocessorTraceMacroParam, PreprocessorTraceToken, - PreprocessorTraceTokenProvenance, SourceBufferOrigin, SourceBufferRange, SyntaxKind, + PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, + PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, + PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, + PreprocessorTraceMacroParam, PreprocessorTraceToken, PreprocessorTraceTokenProvenance, + SourceBufferOrigin, SourceBufferRange, SyntaxKind, }; use utils::line_index::{TextRange, TextSize}; @@ -16,6 +19,50 @@ impl From for SourcePreprocEventId { } } +impl From for SourceMacroDefinitionKey { + fn from(value: PreprocessorTraceMacroDefinitionId) -> Self { + Self::new(value.0) + } +} + +impl From for SourceMacroCallKey { + fn from(value: PreprocessorTraceMacroCallId) -> Self { + Self::new(value.0) + } +} + +impl From for SourceMacroExpansionKey { + fn from(value: PreprocessorTraceMacroExpansionId) -> Self { + Self::new(value.0) + } +} + +impl From for SourceMacroBodyIdentity { + fn from(value: PreprocessorTraceMacroBodyIdentity) -> Self { + Self { + call: SourceMacroCallKey::from(value.call_id), + definition: SourceMacroDefinitionKey::from(value.definition_id), + expansion: SourceMacroExpansionKey::from(value.expansion_id), + parent_expansion: value.parent_expansion_id.map(SourceMacroExpansionKey::from), + body_token_index: value.body_token_index as usize, + } + } +} + +impl From for SourceMacroArgumentIdentity { + fn from(value: PreprocessorTraceMacroArgumentIdentity) -> Self { + Self { + call: SourceMacroCallKey::from(value.call_id), + definition: SourceMacroDefinitionKey::from(value.definition_id), + expansion: SourceMacroExpansionKey::from(value.expansion_id), + parent_expansion: value.parent_expansion_id.map(SourceMacroExpansionKey::from), + body_token_index: value.body_token_index as usize, + argument_index: value.argument_index as usize, + argument_token_index: value.argument_token_index as usize, + } + } +} + impl SourcePreprocIndex { pub fn from_trace(trace: PreprocessorTrace) -> Result { let root_source = PreprocSourceId::from(trace.root_buffer_id); @@ -182,6 +229,7 @@ fn collect_trace_event( let event_index = index.usages.len(); index.usages.push(SourceMacroUsage { event_id, + identity: directive.macro_call_id.map(SourceMacroCallKey::from), name: directive.name.as_ref().map(|token| macro_name(token.value_text.as_str())), name_range: directive.name.as_ref().and_then(trace_token_range), range, @@ -200,6 +248,7 @@ fn collect_trace_define( ) -> SourceMacroDefine { SourceMacroDefine { event_id, + identity: directive.macro_definition_id.map(SourceMacroDefinitionKey::from), name: directive.name.as_ref().map(trace_token_value), name_range: directive.name.as_ref().and_then(trace_token_range), params: (!directive.params.is_empty()) @@ -248,7 +297,7 @@ fn emitted_token_provenance_from_trace( } PreprocessorTraceTokenProvenance::MacroBody { macro_name, - identity: _, + identity, call_range, body_token_range, } => { @@ -260,13 +309,14 @@ fn emitted_token_provenance_from_trace( }; SourceTokenProvenanceFact::MacroBody { macro_name: macro_name.to_smolstr(), + identity: Some(SourceMacroBodyIdentity::from(identity)), call_range, body_token_range, } } PreprocessorTraceTokenProvenance::MacroArgument { macro_name, - identity: _, + identity, call_range, body_token_range, argument_token_range, @@ -282,14 +332,12 @@ fn emitted_token_provenance_from_trace( }; SourceTokenProvenanceFact::MacroArgument { macro_name: macro_name.to_smolstr(), + identity: Some(SourceMacroArgumentIdentity::from(identity)), call_range, body_token_range, argument_token_range, } } - PreprocessorTraceTokenProvenance::Builtin { name } => { - SourceTokenProvenanceFact::Builtin { name: name.to_smolstr() } - } PreprocessorTraceTokenProvenance::Unavailable => SourceTokenProvenanceFact::Unavailable, } } diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index 116d1ac4..0194fa26 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -47,6 +47,47 @@ pub struct SourceRange { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourcePreprocEventId(pub(super) u32); +macro_rules! source_identity_key { + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(u32); + + impl $name { + pub fn new(raw: u32) -> Self { + Self(raw) + } + + pub fn raw(self) -> u32 { + self.0 + } + } + }; +} + +source_identity_key!(SourceMacroDefinitionKey); +source_identity_key!(SourceMacroCallKey); +source_identity_key!(SourceMacroExpansionKey); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceMacroBodyIdentity { + pub call: SourceMacroCallKey, + pub definition: SourceMacroDefinitionKey, + pub expansion: SourceMacroExpansionKey, + pub parent_expansion: Option, + pub body_token_index: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceMacroArgumentIdentity { + pub call: SourceMacroCallKey, + pub definition: SourceMacroDefinitionKey, + pub expansion: SourceMacroExpansionKey, + pub parent_expansion: Option, + pub body_token_index: usize, + pub argument_index: usize, + pub argument_token_index: usize, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocSource { pub id: PreprocSourceId, @@ -101,6 +142,7 @@ pub struct SourcePreprocEventRecord { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroDefine { pub event_id: SourcePreprocEventId, + pub identity: Option, pub name: Option, pub name_range: Option, pub params: Option>, @@ -143,6 +185,7 @@ pub struct SourceMacroConditional { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroUsage { pub event_id: SourcePreprocEventId, + pub identity: Option, pub name: Option, pub name_range: Option, pub range: SourceRange, @@ -176,11 +219,13 @@ pub enum SourceTokenProvenanceFact { }, MacroBody { macro_name: SmolStr, + identity: Option, call_range: SourceRange, body_token_range: SourceRange, }, MacroArgument { macro_name: SmolStr, + identity: Option, call_range: SourceRange, body_token_range: SourceRange, argument_token_range: SourceRange, From 6d067242979eb1bfd85150b9df16744b63b9ec34 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 14:53:35 +0800 Subject: [PATCH 25/80] fix(ide): select preproc tokens by provenance identity --- crates/ide/src/document_highlight.rs | 19 +- crates/ide/src/goto_declaration.rs | 53 ++- crates/ide/src/goto_definition.rs | 21 +- crates/ide/src/hover.rs | 47 ++- crates/ide/src/references.rs | 19 +- crates/ide/src/references/search.rs | 20 +- crates/ide/src/source_tokens.rs | 465 ++++++++++++++++++++++++--- 7 files changed, 568 insertions(+), 76 deletions(-) diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index bd5af005..b94dba38 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -11,6 +11,7 @@ use crate::{ self, ReferenceCategory, ReferencesConfig, search::{ReferencesCtx, SearchScope}, }, + source_tokens::SourceTokenSelection, }; #[derive(Debug, Clone)] @@ -40,8 +41,22 @@ pub(crate) fn document_highlight( offset, token_precedence, )?; - let highlights = selection - .tokens + let tokens = match selection { + SourceTokenSelection::NormalSyntax(selection) => selection.tokens, + SourceTokenSelection::Preproc(selection) => { + let _ = selection.hits.len(); + selection.tokens + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + return None; + } + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + return None; + } + }; + let highlights = tokens .into_iter() .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) .flatten() diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 7bdaf711..437f0601 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -1,6 +1,5 @@ use hir::semantics::Semantics; use itertools::Itertools; -use syntax::{SyntaxNodeExt, has_text_range::HasTextRange}; use crate::{ FilePosition, RangeInfo, @@ -8,6 +7,7 @@ use crate::{ definitions::DefinitionClass, goto_definition, navigation_target::{NavTarget, ToNav}, + source_tokens::SourceTokenSelection, }; pub(crate) fn goto_declaration( @@ -18,22 +18,49 @@ pub(crate) fn goto_declaration( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let token = root.token_at_offset(offset).pick_bext_token(goto_definition::token_precedence)?; - - let origins = match DefinitionClass::resolve(&sema, hir_file_id, token)? { - DefinitionClass::Definition(definition) => { - definition.declaration_origins().into_iter().collect_vec() + let selection = crate::source_tokens::token_candidates_at_offset( + db, + file_id, + root, + offset, + goto_definition::token_precedence, + )?; + let (range, tokens) = match selection { + SourceTokenSelection::NormalSyntax(selection) => (selection.range, selection.tokens), + SourceTokenSelection::Preproc(selection) => { + let _ = selection.hits.len(); + (selection.range, selection.tokens) + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + return None; } - DefinitionClass::PortConnShorthand { port, .. } => { - port.declaration_origins().into_iter().collect_vec() + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + return None; } - DefinitionClass::Ambiguous(definitions) => definitions - .into_iter() - .filter_map(|definition| definition.declaration_origins()) - .collect_vec(), }; + let origins = tokens + .into_iter() + .filter_map(|token| match DefinitionClass::resolve(&sema, hir_file_id, token)? { + DefinitionClass::Definition(definition) => { + Some(definition.declaration_origins().into_iter().collect_vec()) + } + DefinitionClass::PortConnShorthand { port, .. } => { + Some(port.declaration_origins().into_iter().collect_vec()) + } + DefinitionClass::Ambiguous(definitions) => Some( + definitions + .into_iter() + .filter_map(|definition| definition.declaration_origins()) + .collect_vec(), + ), + }) + .flatten() + .collect_vec(); + let navs = origins.into_iter().unique().filter_map(|def| def.to_nav(db)).collect_vec(); - Some(RangeInfo::new(token.text_range()?, navs)) + Some(RangeInfo::new(range, navs)) } diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 77377252..426d0e98 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -22,6 +22,7 @@ use crate::{ db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, + source_tokens::SourceTokenSelection, }; pub(crate) fn goto_definition( @@ -47,8 +48,22 @@ pub(crate) fn goto_definition( offset, token_precedence, )?; - let navs = selection - .tokens + let (range, tokens) = match selection { + SourceTokenSelection::NormalSyntax(selection) => (selection.range, selection.tokens), + SourceTokenSelection::Preproc(selection) => { + let _ = selection.hits.len(); + (selection.range, selection.tokens) + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + return None; + } + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + return None; + } + }; + let navs = tokens .into_iter() .filter_map(|token| nav_targets_for_token(db, &sema, hir_file_id, token)) .flatten() @@ -58,7 +73,7 @@ pub(crate) fn goto_definition( return None; } - Some(RangeInfo::new(selection.range, navs)) + Some(RangeInfo::new(range, navs)) } fn nav_targets_for_token( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 0a1f59ed..4613eedb 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -24,8 +24,12 @@ use utils::{ use vfs::FileId; use crate::{ - FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, markup::Markup, + FilePosition, RangeInfo, + db::root_db::RootDb, + definitions::DefinitionClass, + markup::Markup, render, + source_tokens::{PreprocTokenSelection, SourceTokenSelection}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -63,13 +67,46 @@ pub(crate) fn hover( offset, token_precedence, )?; - let markups = selection - .tokens + let hover = match selection { + SourceTokenSelection::NormalSyntax(selection) => { + hover_for_token_selection(&sema, hir_file_id, selection.range, selection.tokens) + } + SourceTokenSelection::Preproc(selection) => { + hover_for_preproc_selection(&sema, hir_file_id, selection) + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + None + } + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + None + } + }?; + Some(with_expanded_macro_hover(db, file_id, offset, hover)) +} + +fn hover_for_preproc_selection( + sema: &Semantics, + hir_file_id: HirFileId, + selection: PreprocTokenSelection<'_>, +) -> Option> { + let _ = selection.hits.len(); + hover_for_token_selection(sema, hir_file_id, selection.range, selection.tokens) +} + +fn hover_for_token_selection( + sema: &Semantics, + hir_file_id: HirFileId, + range: TextRange, + tokens: Vec>, +) -> Option> { + let markups = tokens .into_iter() - .filter_map(|token| hover_for_token(&sema, hir_file_id, token)) + .filter_map(|token| hover_for_token(sema, hir_file_id, token)) .collect::>(); let res = merge_hover_results(markups)?; - Some(with_expanded_macro_hover(db, file_id, offset, RangeInfo::new(selection.range, res))) + Some(RangeInfo::new(range, res)) } pub(crate) fn token_precedence(kind: TokenKind) -> usize { diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 5968bc4a..0a7eec42 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -23,6 +23,7 @@ use crate::{ db::root_db::RootDb, definitions::{Definition, DefinitionClass}, navigation_target::{NavTarget, ToNav}, + source_tokens::SourceTokenSelection, }; pub(crate) mod search; @@ -84,8 +85,22 @@ pub(crate) fn references( offset, token_precedence, )?; - let references = selection - .tokens + let tokens = match selection { + SourceTokenSelection::NormalSyntax(selection) => selection.tokens, + SourceTokenSelection::Preproc(selection) => { + let _ = selection.hits.len(); + selection.tokens + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + return None; + } + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + return None; + } + }; + let references = tokens .into_iter() .filter_map(|token| references_for_token(&sema, hir_file_id, token, config.clone())) .flatten() diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index bdae5907..b8e975d2 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -27,6 +27,7 @@ use crate::{ ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, + source_tokens::SourceTokenSelection, }; /// A search scope is a set of files and ranges within those files that should @@ -276,8 +277,23 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { return Vec::new(); }; - selection - .tokens + let tokens = match selection { + SourceTokenSelection::NormalSyntax(selection) => selection.tokens, + SourceTokenSelection::Preproc(selection) => { + let _ = selection.hits.len(); + selection.tokens + } + SourceTokenSelection::Unavailable(unavailable) => { + let _ = unavailable.range; + return Vec::new(); + } + SourceTokenSelection::Ambiguous(ambiguous) => { + let _ = (ambiguous.range, ambiguous.hits.len()); + return Vec::new(); + } + }; + + tokens .into_iter() .filter(|tok| tok.kind().name_like()) .filter(|tok| { diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index ae7c72e2..2b87b40c 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -1,4 +1,7 @@ -use hir::preproc::{TokenProvenance, macro_expansion_provenances_at}; +use hir::preproc::{ + EmittedTokenProvenance, MacroDefinitionId, MacroExpansionProvenance, MappedPreprocSource, + TokenProvenance, macro_expansion_provenances_at, +}; use syntax::{ SyntaxElement, SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, WalkEvent, has_text_range::HasTextRange, @@ -9,31 +12,114 @@ use vfs::FileId; use crate::db::root_db::RootDb; #[derive(Debug, Clone)] -pub(crate) struct SourceTokenSelection<'tree> { +pub(crate) enum SourceTokenSelection<'tree> { + NormalSyntax(NormalSyntaxSelection<'tree>), + Preproc(PreprocTokenSelection<'tree>), + Unavailable(PreprocTokenUnavailable), + Ambiguous(PreprocTokenAmbiguity), +} + +#[derive(Debug, Clone)] +pub(crate) struct NormalSyntaxSelection<'tree> { pub range: TextRange, pub tokens: Vec>, } -pub(crate) fn token_candidates_at_offset<'tree>( +#[derive(Debug, Clone)] +pub(crate) struct PreprocTokenSelection<'tree> { + pub range: TextRange, + pub hits: Vec, + pub tokens: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PreprocTokenUnavailable { + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PreprocTokenAmbiguity { + pub range: TextRange, + pub hits: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PreprocTokenHit { + pub expansion: usize, + pub call: usize, + pub emitted_token: usize, + pub virtual_range: TextRange, + pub source_range: TextRange, + pub provenance: PreprocTokenProvenance, + target: PreprocSemanticTarget, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PreprocTokenProvenance { + SourceToken { + source: MappedPreprocSource, + range: TextRange, + }, + MacroBody { + call: usize, + definition_id: MacroDefinitionId, + source: MappedPreprocSource, + range: TextRange, + }, + MacroArgument { + call: usize, + argument_index: usize, + source: MappedPreprocSource, + range: TextRange, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum PreprocSemanticTarget { + SourceToken { source: MappedPreprocSource, range: TextRange }, + MacroBody { definition_id: MacroDefinitionId, source: MappedPreprocSource, range: TextRange }, +} + +pub(crate) fn token_candidates_at_offset<'tree, F>( db: &RootDb, file_id: FileId, root: SyntaxNode<'tree>, offset: TextSize, - precedence: impl Fn(TokenKind) -> usize, -) -> Option> { - match provenance_token_candidates_at_offset(db, file_id, root, offset) { - ProvenanceTokenLookup::Available(selection) => return Some(selection), - ProvenanceTokenLookup::Unavailable => return None, + precedence: F, +) -> Option> +where + F: Fn(TokenKind) -> usize, +{ + match provenance_token_candidates_at_offset(db, file_id, root, offset, &precedence) { + ProvenanceTokenLookup::Available(selection) => { + return Some(SourceTokenSelection::Preproc(selection)); + } + ProvenanceTokenLookup::Unavailable(unavailable) => { + return Some(SourceTokenSelection::Unavailable(unavailable)); + } + ProvenanceTokenLookup::Ambiguous(ambiguous) => { + return Some(SourceTokenSelection::Ambiguous(ambiguous)); + } ProvenanceTokenLookup::NotApplicable => {} } + normal_syntax_selection_at_offset(root, offset, &precedence) + .map(SourceTokenSelection::NormalSyntax) +} + +fn normal_syntax_selection_at_offset<'tree>( + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, +) -> Option> { let token = root.token_at_offset(offset).pick_bext_token(precedence)?; - Some(SourceTokenSelection { range: token.text_range()?, tokens: vec![token] }) + Some(NormalSyntaxSelection { range: token.text_range()?, tokens: vec![token] }) } enum ProvenanceTokenLookup<'tree> { - Available(SourceTokenSelection<'tree>), - Unavailable, + Available(PreprocTokenSelection<'tree>), + Unavailable(PreprocTokenUnavailable), + Ambiguous(PreprocTokenAmbiguity), NotApplicable, } @@ -42,81 +128,185 @@ fn provenance_token_candidates_at_offset<'tree>( file_id: FileId, root: SyntaxNode<'tree>, offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, ) -> ProvenanceTokenLookup<'tree> { let Ok(provenances) = macro_expansion_provenances_at(db, file_id, offset) else { return ProvenanceTokenLookup::NotApplicable; }; + if provenances.is_empty() { + return ProvenanceTokenLookup::NotApplicable; + } - let mut source_ranges = Vec::new(); - for provenance in provenances { - for token in provenance.tokens { - let Some(range) = source_token_range_for_offset(&token.provenance, file_id, offset) + match preproc_hits_at_offset(&provenances, file_id, offset) { + PreprocHitLookup::Available { range, hits } => { + let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &hits) else { - continue; + return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }); }; - if !source_ranges.contains(&range) { - source_ranges.push(range); - } + ProvenanceTokenLookup::Available(PreprocTokenSelection { range, hits, tokens }) + } + PreprocHitLookup::Unavailable { range } => { + ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }) + } + PreprocHitLookup::Ambiguous { range, hits } => { + ProvenanceTokenLookup::Ambiguous(PreprocTokenAmbiguity { range, hits }) } } +} - if source_ranges.is_empty() { - return ProvenanceTokenLookup::NotApplicable; +enum PreprocHitLookup { + Available { range: TextRange, hits: Vec }, + Unavailable { range: TextRange }, + Ambiguous { range: TextRange, hits: Vec }, +} + +fn preproc_hits_at_offset( + provenances: &[MacroExpansionProvenance], + file_id: FileId, + offset: TextSize, +) -> PreprocHitLookup { + let mut hits = Vec::new(); + for expansion in provenances { + for token in &expansion.tokens { + let Some(hit) = preproc_hit_for_token(expansion, token, file_id, offset) else { + continue; + }; + push_unique_preproc_hit(&mut hits, hit); + } } - let tokens = tokens_with_exact_ranges(root, &source_ranges); - if tokens.is_empty() { - return ProvenanceTokenLookup::Unavailable; + if hits.is_empty() { + return PreprocHitLookup::Unavailable { + range: covering_range( + &provenances + .iter() + .map(|provenance| provenance.expansion.call.range) + .collect::>(), + ) + .unwrap_or_else(|| TextRange::empty(offset)), + }; } - let range = covering_range(&source_ranges); - ProvenanceTokenLookup::Available(SourceTokenSelection { range, tokens }) + let range = covering_range(&hits.iter().map(|hit| hit.source_range).collect::>()) + .unwrap_or_else(|| TextRange::empty(offset)); + match hits.len() { + 0 => unreachable!(), + 1 => PreprocHitLookup::Available { range, hits }, + _ => PreprocHitLookup::Ambiguous { range, hits }, + } } -fn source_token_range_for_offset( - provenance: &TokenProvenance, +fn preproc_hit_for_token( + expansion: &MacroExpansionProvenance, + token: &EmittedTokenProvenance, file_id: FileId, offset: TextSize, -) -> Option { - let (source, range) = match provenance { - TokenProvenance::SourceToken { source, range } - | TokenProvenance::MacroArgument { source, range, .. } => (source, *range), - TokenProvenance::MacroBody { .. } - | TokenProvenance::Predefine { .. } +) -> Option { + let (source, range, provenance, target, call) = match &token.provenance { + TokenProvenance::SourceToken { source, range } => ( + source.clone(), + *range, + PreprocTokenProvenance::SourceToken { source: source.clone(), range: *range }, + PreprocSemanticTarget::SourceToken { source: source.clone(), range: *range }, + expansion.expansion.call.id.raw(), + ), + TokenProvenance::MacroBody { call, definition_id, source, range } => ( + source.clone(), + *range, + PreprocTokenProvenance::MacroBody { + call: call.id.raw(), + definition_id: *definition_id, + source: source.clone(), + range: *range, + }, + PreprocSemanticTarget::MacroBody { + definition_id: *definition_id, + source: source.clone(), + range: *range, + }, + call.id.raw(), + ), + TokenProvenance::MacroArgument { call, argument_index, source, range } => ( + source.clone(), + *range, + PreprocTokenProvenance::MacroArgument { + call: call.id.raw(), + argument_index: *argument_index, + source: source.clone(), + range: *range, + }, + PreprocSemanticTarget::SourceToken { source: source.clone(), range: *range }, + call.id.raw(), + ), + TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } | TokenProvenance::Unavailable(_) => return None, }; - (source.file_id() == Some(file_id) && range.contains(offset)).then_some(range) + + if source.file_id() != Some(file_id) || !range.contains(offset) { + return None; + } + + Some(PreprocTokenHit { + expansion: expansion.expansion.id.raw(), + call, + emitted_token: token.token.raw(), + virtual_range: token.virtual_range, + source_range: range, + provenance, + target, + }) +} + +fn push_unique_preproc_hit(hits: &mut Vec, hit: PreprocTokenHit) { + if hits.iter().any(|existing| existing.target == hit.target) { + return; + } + hits.push(hit); } -fn tokens_with_exact_ranges<'tree>( +fn syntax_tokens_for_preproc_hit<'tree>( root: SyntaxNode<'tree>, - ranges: &[TextRange], -) -> Vec> { + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, + hits: &[PreprocTokenHit], +) -> Option>> { let mut tokens = Vec::new(); + let mut best_precedence = 0; for event in root.elem_preorder() { let WalkEvent::Enter(SyntaxElement::Token(token)) = event else { continue; }; - let Some(range) = token.text_range() else { + let Some(token_range) = token.text_range() else { continue; }; - if ranges.contains(&range) && !tokens.contains(&token) { + if !token_range.contains(offset) + || !hits.iter().any(|hit| hit.source_range.intersect(token_range).is_some()) + { + continue; + } + + let token_precedence = precedence(token.kind()); + if token_precedence > best_precedence { + tokens.clear(); + best_precedence = token_precedence; + } + if token_precedence == best_precedence && !tokens.contains(&token) { tokens.push(token); } } - tokens + (!tokens.is_empty()).then_some(tokens) } -fn covering_range(ranges: &[TextRange]) -> TextRange { - let start = ranges.iter().map(|range| range.start()).min().unwrap_or_default(); - let end = ranges.iter().map(|range| range.end()).max().unwrap_or_default(); - TextRange::new(start, end) +fn covering_range(ranges: &[TextRange]) -> Option { + let start = ranges.iter().map(|range| range.start()).min()?; + let end = ranges.iter().map(|range| range.end()).max()?; + Some(TextRange::new(start, end)) } #[cfg(test)] mod tests { - use hir::preproc::{MappedPreprocSource, TokenProvenance}; + use syntax::{SyntaxTree, token::TokenKindExt}; use super::*; @@ -129,8 +319,185 @@ mod tests { range, }; - assert_eq!(source_token_range_for_offset(&provenance, file_id, 5.into()), Some(range)); - assert_eq!(source_token_range_for_offset(&provenance, file_id, 9.into()), Some(range)); - assert_eq!(source_token_range_for_offset(&provenance, file_id, 10.into()), None); + assert!( + preproc_hit_for_raw_provenance(&provenance, file_id, 5.into()).is_some(), + "range start should hit" + ); + assert!( + preproc_hit_for_raw_provenance(&provenance, file_id, 9.into()).is_some(), + "offset before range end should hit" + ); + assert!( + preproc_hit_for_raw_provenance(&provenance, file_id, 10.into()).is_none(), + "range end should not hit" + ); + } + + #[test] + fn source_tokens_preproc_range_mismatch_still_selects_by_identity() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 2); + let file_id = FileId(0); + let provenance_range = TextRange::new( + parser_range.start() + TextSize::from(1), + parser_range.end() - TextSize::from(1), + ); + let hit = test_source_hit(file_id, provenance_range, 0); + + let ProvenanceTokenLookup::Available(selection) = preproc_selection_from_hits( + root, + offset, + &test_precedence, + vec![hit], + provenance_range, + ) else { + panic!("preproc identity hit should select without exact parser range equality"); + }; + + assert_eq!(selection.range, provenance_range); + assert_eq!(selection.hits.len(), 1); + assert_eq!(selection.tokens.len(), 1); + assert_eq!(selection.tokens[0].text_range(), Some(parser_range)); + assert_ne!(selection.tokens[0].text_range(), Some(provenance_range)); + } + + #[test] + fn source_tokens_preproc_owned_unresolved_does_not_use_normal_syntax_fallback() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); + assert!( + normal_syntax_selection_at_offset(root, offset, &test_precedence).is_some(), + "test setup must have an ordinary syntax token that fallback could have selected" + ); + + let lookup = + preproc_selection_from_hits(root, offset, &test_precedence, Vec::new(), parser_range); + assert!(matches!(lookup, ProvenanceTokenLookup::Unavailable(_))); + } + + #[test] + fn source_tokens_normal_syntax_path_still_selects_non_preproc_offsets() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); + let selection = normal_syntax_selection_at_offset(root, offset, &test_precedence) + .expect("normal syntax token expected"); + + assert_eq!(selection.range, parser_range); + assert_eq!(selection.tokens.len(), 1); + } + + #[test] + fn source_tokens_dedups_preproc_hits_for_same_semantic_target() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); + let file_id = FileId(0); + let hits = vec![ + test_source_hit(file_id, parser_range, 0), + test_source_hit(file_id, parser_range, 1), + ]; + + let ProvenanceTokenLookup::Available(selection) = + preproc_selection_from_hits(root, offset, &test_precedence, hits, parser_range) + else { + panic!("same semantic target should dedup to one available preproc hit"); + }; + + assert_eq!(selection.hits.len(), 1); + } + + #[test] + fn source_tokens_reports_ambiguous_preproc_hits_for_conflicting_targets() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 2); + let file_id = FileId(0); + let first = TextRange::new(parser_range.start(), parser_range.start() + TextSize::from(4)); + let second = TextRange::new(parser_range.start() + TextSize::from(1), parser_range.end()); + let hits = vec![test_source_hit(file_id, first, 0), test_source_hit(file_id, second, 1)]; + + let ProvenanceTokenLookup::Ambiguous(ambiguous) = + preproc_selection_from_hits(root, offset, &test_precedence, hits, parser_range) + else { + panic!("conflicting preproc targets should be ambiguous"); + }; + + assert_eq!(ambiguous.hits.len(), 2); + } + + fn root_and_offset<'tree>( + text: &str, + needle: &str, + delta: u32, + ) -> (SyntaxNode<'tree>, TextSize, TextRange) { + let tree = Box::leak(Box::new(SyntaxTree::from_text(text, "test", "test.sv"))); + let root = tree.root().expect("test source should parse"); + let start = text.find(needle).expect("needle should exist"); + let range = TextRange::new( + TextSize::from(start as u32), + TextSize::from((start + needle.len()) as u32), + ); + (root, range.start() + TextSize::from(delta), range) + } + + fn test_source_hit(file_id: FileId, range: TextRange, emitted_token: usize) -> PreprocTokenHit { + let source = MappedPreprocSource::RealFile { file_id }; + PreprocTokenHit { + expansion: 0, + call: 0, + emitted_token, + virtual_range: range, + source_range: range, + provenance: PreprocTokenProvenance::SourceToken { source: source.clone(), range }, + target: PreprocSemanticTarget::SourceToken { source, range }, + } + } + + fn preproc_selection_from_hits<'tree>( + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, + hits: Vec, + fallback_range: TextRange, + ) -> ProvenanceTokenLookup<'tree> { + let mut unique_hits = Vec::new(); + for hit in hits { + if hit.source_range.contains(offset) { + push_unique_preproc_hit(&mut unique_hits, hit); + } + } + if unique_hits.is_empty() { + return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { + range: fallback_range, + }); + } + let range = + covering_range(&unique_hits.iter().map(|hit| hit.source_range).collect::>()) + .unwrap_or(fallback_range); + if unique_hits.len() > 1 { + return ProvenanceTokenLookup::Ambiguous(PreprocTokenAmbiguity { + range, + hits: unique_hits, + }); + } + let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &unique_hits) + else { + return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }); + }; + ProvenanceTokenLookup::Available(PreprocTokenSelection { range, hits: unique_hits, tokens }) + } + + fn preproc_hit_for_raw_provenance( + provenance: &TokenProvenance, + file_id: FileId, + offset: TextSize, + ) -> Option { + let (source, range) = match provenance { + TokenProvenance::SourceToken { source, range } => (source, *range), + _ => return None, + }; + (source.file_id() == Some(file_id) && range.contains(offset)).then_some(range) + } + + fn test_precedence(kind: TokenKind) -> usize { + usize::from(kind.name_like()) } } From 447a02996342c62554427e9ab8b82a4ecc4d268e Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 15:08:40 +0800 Subject: [PATCH 26/80] fix(hir): report ambiguous macro diagnostic provenance --- crates/hir/src/preproc.rs | 146 +++++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 18 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index f6673e37..2dcca94c 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1372,6 +1372,7 @@ pub fn macro_expansion_provenances_for_range( range: TextRange, ) -> PreprocResult> { let mut provenances = Vec::new(); + let mut ambiguous_contexts = 0; let mut first_error = None; for model_file_id in source_preproc_query_model_file_ids(db, file_id) { let mapped = db.source_preproc_model(model_file_id); @@ -1382,14 +1383,27 @@ pub fn macro_expansion_provenances_for_range( continue; } }; - let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { - continue; - }; - if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { - push_unique_macro_expansion_provenance(&mut provenances, provenance); + let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); + match call_facts.as_slice() { + [] => continue, + [call_fact] => { + if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { + push_unique_macro_expansion_provenance(&mut provenances, provenance); + } + } + call_facts => { + ambiguous_contexts += call_facts.len(); + } } } + if ambiguous_contexts > 0 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { + contexts: ambiguous_contexts + provenances.len(), + }, + }); + } if !provenances.is_empty() { return Ok(provenances); } @@ -1406,6 +1420,7 @@ pub fn diagnostic_provenance_for_range( range: TextRange, ) -> PreprocResult> { let mut provenances = Vec::new(); + let mut ambiguous_targets = 0; let mut first_error = None; for model_file_id in source_preproc_query_model_file_ids(db, file_id) { @@ -1417,11 +1432,17 @@ pub fn diagnostic_provenance_for_range( continue; } }; - let Some(call_fact) = source_macro_call_intersecting_range(mapped, file_id, range) else { - continue; - }; - let provenance = diagnostic_provenance_for_call(mapped, call_fact)?; - push_unique_diagnostic_provenance(&mut provenances, provenance); + let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); + match call_facts.as_slice() { + [] => continue, + [call_fact] => { + let provenance = diagnostic_provenance_for_call(mapped, call_fact)?; + push_unique_diagnostic_provenance(&mut provenances, provenance); + } + call_facts => { + ambiguous_targets += call_facts.len(); + } + } } let precise = provenances @@ -1429,6 +1450,13 @@ pub fn diagnostic_provenance_for_range( .filter(|provenance| !matches!(provenance, DiagnosticProvenance::Unavailable(_))) .cloned() .collect::>(); + if ambiguous_targets > 0 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { + targets: ambiguous_targets + precise.len(), + }, + ))); + } if precise.len() == 1 { return Ok(Some(precise.into_iter().next().unwrap())); } @@ -2142,17 +2170,25 @@ fn source_macro_call_at( }) } -fn source_macro_call_intersecting_range( +fn source_macro_calls_intersecting_range( mapped: &MappedSourcePreprocModel, file_id: FileId, source_range: TextRange, -) -> Option<&SourceMacroCallFact> { - mapped.model.macro_calls().iter().find(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) && range.intersect(source_range).is_some() - }) +) -> Vec<&SourceMacroCallFact> { + mapped + .model + .macro_calls() + .iter() + .filter(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == Some(file_id) + && range + .intersect(source_range) + .is_some_and(|intersection| !intersection.is_empty()) + }) + .collect() } fn immediate_macro_expansion_for_call( @@ -3157,6 +3193,80 @@ endmodule ); } + #[test] + fn diagnostic_provenance_for_range_spanning_two_macro_calls_is_ambiguous() { + let root_text = r#"`define A 1 +`define B 2 +module top; +localparam int W = `A + `B; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let range = TextRange::new(offset(root_text, "`A"), offset_after(root_text, "`B")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, range).unwrap().unwrap(); + + assert!(matches!( + provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::AmbiguousDiagnosticProvenance { + targets: 2 + }) + )); + let expansion_error = macro_expansion_provenances_for_range(&db, TOP, range).unwrap_err(); + assert!(matches!( + expansion_error, + PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + } + )); + } + + #[test] + fn diagnostic_provenance_for_adjacent_macro_calls_only_hits_intersecting_call() { + let root_text = r#"`define ID(x) x +module top; +localparam int W = `ID(1)`ID(2); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let two_range = + TextRange::new(offset(root_text, "`ID(2)"), offset_after(root_text, "`ID(2)")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, two_range).unwrap().unwrap(); + + let DiagnosticProvenance::MacroArgument { call, argument_index, source, range } = + provenance + else { + panic!("adjacent single-call range should resolve precisely: {provenance:?}"); + }; + assert_eq!(text_at_range(root_text, call.range), "`ID(2)"); + assert_eq!(argument_index, 0); + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, range), "2"); + } + + #[test] + fn diagnostic_provenance_for_nested_macro_call_range_is_precise() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module top; +localparam int W = `WRAP; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let leaf_range = + TextRange::new(offset(root_text, "`LEAF"), offset_after(root_text, "`LEAF")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, leaf_range).unwrap().unwrap(); + + let DiagnosticProvenance::MacroBody { call, source, range, .. } = provenance else { + panic!("nested macro call range should resolve precisely"); + }; + assert_eq!(text_at_range(root_text, call.range), "`LEAF"); + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, range), "3"); + } + #[test] fn diagnostic_provenance_returns_unavailable_for_unsupported_expansion_mapping() { let root_text = r#"`define JOIN(a,b) a``b From a0f5a063aa897a8accea7e67b4d5df8c2025f767 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 15:27:10 +0800 Subject: [PATCH 27/80] fix(hir): scope single-offset preproc queries to relevant contexts --- crates/hir/src/base_db/source_db.rs | 265 ++++++++++++++++++++++++++++ crates/hir/src/preproc.rs | 246 ++++++++++++++------------ crates/ide/src/references/search.rs | 16 +- crates/ide/src/source_tokens.rs | 98 +++++++++- 4 files changed, 507 insertions(+), 118 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index e3911576..02e72fde 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,6 +1,7 @@ use preproc::source::{ PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, + SourceTokenProvenance, }; use rustc_hash::{FxHashMap, FxHashSet}; use smol_str::SmolStr; @@ -146,6 +147,30 @@ pub struct PreprocSourceMap { range_offsets: FxHashMap, } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourcePreprocContextIndex { + contexts_by_file: FxHashMap>, + issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocContextIndexIssue { + pub model_file_id: FileId, + pub error: SourcePreprocQueryError, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocRelevantContexts { + pub model_file_ids: Vec, + pub status: SourcePreprocContextStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourcePreprocContextStatus { + Complete, + Partial { skipped_models: usize }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocSourceMapping { RealFile(FileId), @@ -441,6 +466,161 @@ impl PreprocSourceMap { } } +impl SourcePreprocContextIndex { + fn push_context(&mut self, file_id: FileId, model_file_id: FileId) { + let contexts = self.contexts_by_file.entry(file_id).or_default(); + if !contexts.contains(&model_file_id) { + contexts.push(model_file_id); + contexts.sort(); + } + } + + fn push_issue(&mut self, issue: SourcePreprocContextIndexIssue) { + self.issues.push(issue); + } + + pub fn relevant_contexts(&self, file_id: FileId) -> SourcePreprocRelevantContexts { + SourcePreprocRelevantContexts { + model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), + status: self.status(), + } + } + + pub fn status(&self) -> SourcePreprocContextStatus { + if self.issues.is_empty() { + SourcePreprocContextStatus::Complete + } else { + SourcePreprocContextStatus::Partial { skipped_models: self.issues.len() } + } + } + + pub fn issues(&self) -> &[SourcePreprocContextIndexIssue] { + &self.issues + } +} + +fn preproc_context_file_ids( + mapped: &MappedSourcePreprocModel, + model_file_id: FileId, +) -> Vec { + let mut file_ids = Vec::new(); + let mut seen = FxHashSet::default(); + push_unique_file_id(&mut file_ids, &mut seen, model_file_id); + + for definition in mapped.model.macro_definitions().iter() { + collect_context_source_range(mapped, definition.directive_range, &mut file_ids, &mut seen); + collect_context_source_range(mapped, definition.name_range, &mut file_ids, &mut seen); + if let Some(params) = &definition.params { + for param in params { + if let Some(range) = param.name_range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + if let Some(range) = param.range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + if let Some(default) = ¶m.default { + for token in default { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + } + } + } + } + for token in &definition.body_tokens { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + } + } + + for reference in mapped.model.macro_references().iter() { + collect_context_source_range(mapped, reference.directive_range, &mut file_ids, &mut seen); + collect_context_source_range(mapped, reference.name_range, &mut file_ids, &mut seen); + } + + for call in mapped.model.macro_calls().iter() { + collect_context_source_range(mapped, call.call_range, &mut file_ids, &mut seen); + for argument in &call.arguments { + if let Some(range) = argument.argument_range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + for token in &argument.tokens { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + } + } + } + + for include in mapped.model.include_graph().directives() { + collect_context_source_range(mapped, include.directive_range, &mut file_ids, &mut seen); + if let Some(range) = include.target_range { + collect_context_source_range(mapped, range, &mut file_ids, &mut seen); + } + if let Some(source) = include.resolved_source { + collect_context_source(mapped, source, &mut file_ids, &mut seen); + } + } + + for range in mapped.model.inactive_ranges() { + collect_context_source_range(mapped, *range, &mut file_ids, &mut seen); + } + + for provenance in mapped.model.token_provenance().iter() { + match provenance { + SourceTokenProvenance::Source { token_range } + | SourceTokenProvenance::MacroBody { body_token_range: token_range, .. } => { + collect_context_source_range(mapped, *token_range, &mut file_ids, &mut seen); + } + SourceTokenProvenance::MacroArgument { + body_token_range, argument_token_range, .. + } => { + collect_context_source_range(mapped, *body_token_range, &mut file_ids, &mut seen); + collect_context_source_range( + mapped, + *argument_token_range, + &mut file_ids, + &mut seen, + ); + } + SourceTokenProvenance::TokenPaste { .. } + | SourceTokenProvenance::Stringification { .. } + | SourceTokenProvenance::Builtin { .. } + | SourceTokenProvenance::Unavailable(_) => {} + SourceTokenProvenance::Predefine { source } => { + collect_context_source(mapped, *source, &mut file_ids, &mut seen); + } + } + } + + file_ids.sort(); + file_ids +} + +fn collect_context_source_range( + mapped: &MappedSourcePreprocModel, + range: SourceRange, + file_ids: &mut Vec, + seen: &mut FxHashSet, +) { + collect_context_source(mapped, range.source, file_ids, seen); +} + +fn collect_context_source( + mapped: &MappedSourcePreprocModel, + source: PreprocSourceId, + file_ids: &mut Vec, + seen: &mut FxHashSet, +) { + if let Ok(file_id) = mapped.source_map.file_id(source) { + push_unique_file_id(file_ids, seen, file_id); + } + if let Some(manifest_source) = mapped.source_map.predefine_manifest_source(source) { + push_unique_file_id(file_ids, seen, manifest_source.file_id); + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourcePreprocQueryError { UnsupportedFileKind(SourceFileKind), @@ -482,6 +662,12 @@ fn insert_buffer_file_ids( } } +fn push_unique_file_id(file_ids: &mut Vec, seen: &mut FxHashSet, file_id: FileId) { + if seen.insert(file_id) { + file_ids.push(file_id); + } +} + fn source_preproc_file_ids( db: &dyn SourceRootDb, file_id: FileId, @@ -997,6 +1183,10 @@ pub trait SourceRootDb: SourceDb { &self, file_id: FileId, ) -> Arc>; + fn source_preproc_context_index_for_profile( + &self, + profile_id: Option, + ) -> Arc; fn macro_reference_index_for_profile( &self, profile_id: Option, @@ -1063,6 +1253,54 @@ fn include_buffers_for_profile( Arc::new(compilation_plan::include_buffers_for_plan(db, &plan)) } +pub(crate) fn workspace_preproc_model_file_ids( + db: &dyn SourceRootDb, + profile_id: Option, +) -> Vec { + let plan = db.compilation_plan_for_profile(profile_id); + let mut file_ids = FxHashSet::default(); + + for root in plan.roots.iter().copied() { + if matches!( + db.file_kind(root), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + file_ids.insert(root); + } + } + file_ids.extend(plan.include_only.iter().copied()); + + for source_root_id in &plan.source_roots { + for candidate in db.source_root(*source_root_id).iter() { + if db.file_is_project_ignored(candidate) { + continue; + } + if matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { + file_ids.insert(candidate); + } + } + } + + for candidate in db.files().iter().copied() { + if db.file_is_project_ignored(candidate) { + continue; + } + if !matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { + continue; + } + let Some(path) = db.file_path(candidate) else { + continue; + }; + if plan.include_dirs.iter().any(|include_dir| path.starts_with(include_dir)) { + file_ids.insert(candidate); + } + } + + let mut file_ids = file_ids.into_iter().collect::>(); + file_ids.sort(); + file_ids +} + fn source_preproc_model( db: &dyn SourceRootDb, file_id: FileId, @@ -1097,6 +1335,33 @@ fn source_preproc_model( Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) } +fn source_preproc_context_index_for_profile( + db: &dyn SourceRootDb, + profile_id: Option, +) -> Arc { + let mut index = SourcePreprocContextIndex::default(); + + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { + index.push_context(model_file_id, model_file_id); + let mapped = db.source_preproc_model(model_file_id); + match mapped.as_ref() { + Ok(mapped) => { + for file_id in preproc_context_file_ids(mapped, model_file_id) { + index.push_context(file_id, model_file_id); + } + } + Err(error) => { + index.push_issue(SourcePreprocContextIndexIssue { + model_file_id, + error: error.clone(), + }); + } + } + } + + Arc::new(index) +} + fn macro_reference_index_for_profile( db: &dyn SourceRootDb, profile_id: Option, diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 2dcca94c..4168ee77 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -28,7 +28,8 @@ use crate::base_db::{ project::{CompilationProfileId, Predefine}, source_db::{ MappedSourcePreprocModel, PreprocSourceMapError, PreprocSourceMapping, - PreprocVirtualOrigin, SourceFileKind, SourcePreprocQueryError, SourceRootDb, + PreprocVirtualOrigin, SourceFileKind, SourcePreprocContextStatus, SourcePreprocQueryError, + SourceRootDb, workspace_preproc_model_file_ids, }, }; @@ -69,6 +70,7 @@ pub enum PreprocUnavailable { AmbiguousMacroDefinitionContexts { contexts: usize }, AmbiguousDiagnosticProvenance { targets: usize }, AmbiguousIncludeTargets { targets: usize }, + PartialPreprocContextIndex { skipped_models: usize }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -563,7 +565,8 @@ pub fn visible_macros_at( ) -> PreprocResult> { let mut definitions = Vec::new(); let mut first_error = None; - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -584,7 +587,7 @@ pub fn visible_macros_at( } if definitions.is_empty() - && let Some(error) = first_error + && let Err(error) = finish_empty_single_query(&contexts, first_error) { return Err(error); } @@ -658,9 +661,10 @@ fn configured_predefine_definitions_at( db: &dyn SourceRootDb, file_id: FileId, offset: TextSize, -) -> Vec { +) -> PreprocResult> { let mut definitions = Vec::new(); - for context_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for context_file_id in contexts.model_file_ids.iter().copied() { let profile_id = db.file_compilation_profile(context_file_id); let project_preprocess = db.project_config().preprocess_for_profile(profile_id); for predefine in &project_preprocess.predefines { @@ -678,7 +682,10 @@ fn configured_predefine_definitions_at( } } } - definitions + if definitions.is_empty() { + finish_empty_single_query(&contexts, None)?; + } + Ok(definitions) } fn configured_predefine_definition_at( @@ -730,7 +737,8 @@ pub fn macro_definition_at( offset: TextSize, ) -> PreprocResult> { let mut first_error = None; - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -749,7 +757,7 @@ pub fn macro_definition_at( } } - let mut configured_definitions = configured_predefine_definitions_at(db, file_id, offset); + let mut configured_definitions = configured_predefine_definitions_at(db, file_id, offset)?; match configured_definitions.len() { 0 => {} 1 => return Ok(configured_definitions.pop()), @@ -760,9 +768,7 @@ pub fn macro_definition_at( } } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(None) } @@ -789,8 +795,9 @@ pub fn macro_param_definitions_at( ) -> PreprocResult> { let mut definitions = Vec::new(); let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -820,7 +827,7 @@ pub fn macro_param_definitions_at( } if definitions.is_empty() - && let Some(error) = first_error + && let Err(error) = finish_empty_single_query(&contexts, first_error) { return Err(error); } @@ -837,8 +844,9 @@ pub fn macro_param_reference_definitions_at( let mut references = Vec::new(); let mut query_range = None; let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -891,9 +899,7 @@ pub fn macro_param_reference_definitions_at( } let Some(range) = query_range else { - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; return Ok(None); }; @@ -928,8 +934,9 @@ pub fn macro_usage_resolutions_at( let mut resolutions = Vec::new(); let mut first_error = None; let mut unavailable_contexts = 0; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1010,9 +1017,7 @@ pub fn macro_usage_resolutions_at( }, }); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1066,8 +1071,9 @@ pub fn macro_reference_definitions_at( let mut references = Vec::new(); let mut query_range = None; let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1140,9 +1146,7 @@ pub fn macro_reference_definitions_at( } let Some(range) = query_range else { - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; return Ok(None); }; @@ -1192,8 +1196,9 @@ pub fn macro_expansion_queries_at( ) -> PreprocResult> { let mut queries = Vec::new(); let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1212,9 +1217,7 @@ pub fn macro_expansion_queries_at( if !queries.is_empty() { return Ok(queries); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1241,8 +1244,9 @@ pub fn recursive_macro_expansions_at( ) -> PreprocResult> { let mut expansions = Vec::new(); let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1261,9 +1265,7 @@ pub fn recursive_macro_expansions_at( if !expansions.is_empty() { return Ok(expansions); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1275,8 +1277,9 @@ pub fn recursive_macro_expansion_provenances_at( ) -> PreprocResult> { let mut expansions = Vec::new(); let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1295,9 +1298,7 @@ pub fn recursive_macro_expansion_provenances_at( if !expansions.is_empty() { return Ok(expansions); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1324,7 +1325,8 @@ pub fn macro_expansion_provenances_at( ) -> PreprocResult> { let mut provenances = Vec::new(); let mut first_error = None; - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1344,9 +1346,7 @@ pub fn macro_expansion_provenances_at( if !provenances.is_empty() { return Ok(provenances); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1374,7 +1374,8 @@ pub fn macro_expansion_provenances_for_range( let mut provenances = Vec::new(); let mut ambiguous_contexts = 0; let mut first_error = None; - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1407,9 +1408,7 @@ pub fn macro_expansion_provenances_for_range( if !provenances.is_empty() { return Ok(provenances); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1422,8 +1421,9 @@ pub fn diagnostic_provenance_for_range( let mut provenances = Vec::new(); let mut ambiguous_targets = 0; let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1473,9 +1473,7 @@ pub fn diagnostic_provenance_for_range( PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: provenances.len() }, ))); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(None) } @@ -1503,7 +1501,7 @@ pub fn macro_param_references( let mut references = Vec::new(); let mut first_error = None; - for model_file_id in preproc_reference_model_file_ids(db, profile_id) { + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1570,7 +1568,7 @@ pub(crate) fn build_macro_reference_index( ) -> MacroReferenceIndex { let mut index = MacroReferenceIndex::default(); - for model_file_id in preproc_reference_model_file_ids(db, profile_id) { + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped.as_ref() { Ok(mapped) => mapped, @@ -1689,7 +1687,8 @@ pub fn include_directives_at( ) -> PreprocResult> { let mut directives = Vec::new(); let mut first_error = None; - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1739,9 +1738,7 @@ pub fn include_directives_at( if !directives.is_empty() { return Ok(directives); } - if let Some(error) = first_error { - return Err(error); - } + finish_empty_single_query(&contexts, first_error)?; Ok(Vec::new()) } @@ -1752,8 +1749,9 @@ pub fn inactive_branches( ) -> PreprocResult> { let mut branches = Vec::new(); let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in source_preproc_query_model_file_ids(db, file_id) { + for model_file_id in contexts.model_file_ids.iter().copied() { let mapped = db.source_preproc_model(model_file_id); let mapped = match mapped_result(mapped.as_ref()) { Ok(mapped) => mapped, @@ -1789,7 +1787,7 @@ pub fn inactive_branches( } if branches.is_empty() - && let Some(error) = first_error + && let Err(error) = finish_empty_single_query(&contexts, first_error) { return Err(error); } @@ -1803,15 +1801,55 @@ fn mapped_result( result.as_ref().map_err(|err| err.clone().into()) } -fn source_preproc_query_model_file_ids(db: &dyn SourceRootDb, file_id: FileId) -> Vec { +#[derive(Debug, Clone, PartialEq, Eq)] +struct SourcePreprocQueryContexts { + model_file_ids: Vec, + status: SourcePreprocContextStatus, +} + +impl SourcePreprocQueryContexts { + fn partial_error(&self) -> Option { + let SourcePreprocContextStatus::Partial { skipped_models } = self.status else { + return None; + }; + Some(PreprocError::Unavailable { + reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models }, + }) + } +} + +fn source_preproc_single_query_contexts( + db: &dyn SourceRootDb, + file_id: FileId, +) -> SourcePreprocQueryContexts { let profile_id = db.file_compilation_profile(file_id); + let index = db.source_preproc_context_index_for_profile(profile_id); + let relevant = index.relevant_contexts(file_id); let mut file_ids = Vec::new(); let mut seen = FxHashSet::default(); - push_unique_file_id(&mut file_ids, &mut seen, file_id); - for model_file_id in preproc_reference_model_file_ids(db, profile_id) { + if matches!( + db.file_kind(file_id), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + push_unique_file_id(&mut file_ids, &mut seen, file_id); + } + for model_file_id in relevant.model_file_ids { push_unique_file_id(&mut file_ids, &mut seen, model_file_id); } - file_ids + SourcePreprocQueryContexts { model_file_ids: file_ids, status: relevant.status } +} + +fn finish_empty_single_query( + contexts: &SourcePreprocQueryContexts, + first_error: Option, +) -> PreprocResult<()> { + if let Some(error) = first_error { + return Err(error); + } + if let Some(error) = contexts.partial_error() { + return Err(error); + } + Ok(()) } fn push_unique_file_id(file_ids: &mut Vec, seen: &mut FxHashSet, file_id: FileId) { @@ -2804,54 +2842,6 @@ fn macro_param_reference_context_capability( .unwrap_or(PreprocAvailability::Complete) } -fn preproc_reference_model_file_ids( - db: &dyn SourceRootDb, - profile_id: Option, -) -> Vec { - let plan = db.compilation_plan_for_profile(profile_id); - let mut file_ids = FxHashSet::default(); - - for root in plan.roots.iter().copied() { - if matches!( - db.file_kind(root), - SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader - ) { - file_ids.insert(root); - } - } - file_ids.extend(plan.include_only.iter().copied()); - - for source_root_id in &plan.source_roots { - for candidate in db.source_root(*source_root_id).iter() { - if db.file_is_project_ignored(candidate) { - continue; - } - if matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { - file_ids.insert(candidate); - } - } - } - - for candidate in db.files().iter().copied() { - if db.file_is_project_ignored(candidate) { - continue; - } - if !matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { - continue; - } - let Some(path) = db.file_path(candidate) else { - continue; - }; - if plan.include_dirs.iter().any(|include_dir| path.starts_with(include_dir)) { - file_ids.insert(candidate); - } - } - - let mut file_ids = file_ids.into_iter().collect::>(); - file_ids.sort(); - file_ids -} - #[cfg(test)] mod tests { use std::fmt; @@ -3395,6 +3385,44 @@ endmodule assert!(names.iter().any(|name| name == "A005_MAGIC"), "{names:?}"); } + #[test] + fn preproc_single_offset_contexts_exclude_unrelated_profile_models() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `HEADER_WIDTH; +endmodule +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let unrelated_header_text = "`define UNUSED_WIDTH 16\n"; + let db = db_with_nested_files(root_text, header_text, unrelated_header_text); + + let contexts = source_preproc_single_query_contexts(&db, HEADER); + + assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); + assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); + assert!( + !contexts.model_file_ids.contains(&LEAF), + "single-offset query contexts should not include unrelated profile model: {contexts:?}" + ); + } + + #[test] + fn preproc_partial_context_index_is_structured_unavailable() { + let contexts = SourcePreprocQueryContexts { + model_file_ids: Vec::new(), + status: SourcePreprocContextStatus::Partial { skipped_models: 2 }, + }; + + let error = finish_empty_single_query(&contexts, None).unwrap_err(); + + assert!(matches!( + error, + PreprocError::Unavailable { + reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models: 2 } + } + )); + } + #[test] fn preproc_manifest_predefine_definition_uses_manifest_provenance() { let root_text = r#"`ifdef FROM_MANIFEST diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index b8e975d2..9d1074c9 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -27,7 +27,7 @@ use crate::{ ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, - source_tokens::SourceTokenSelection, + source_tokens::{SourceTokenRequestCache, SourceTokenSelection}, }; /// A search scope is a set of files and ranges within those files that should @@ -205,6 +205,7 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { self.def.origins().into_iter().filter_map(|def| def.name_range(db)).collect(); let finder = &Finder::new(&name); + let mut source_token_cache = SourceTokenRequestCache::default(); for (text, file_id, range) in self.scope_files() { self.sema.db.unwind_if_cancelled(); @@ -214,7 +215,14 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { let Some(root) = (*parsed_file).root() else { return Vec::new(); }; - Self::filter_tokens(sema.db, root, file_id, &def_ranges, offset) + Self::filter_tokens( + sema.db, + root, + file_id, + &def_ranges, + offset, + &mut source_token_cache, + ) }) .filter(|tp| self.classify_and_filter(sema, file_id.into(), tp)) .for_each(|token| { @@ -266,13 +274,15 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { file_id: FileId, names: &[InFile], offset: TextSize, + source_token_cache: &mut SourceTokenRequestCache, ) -> Vec> { - let Some(selection) = crate::source_tokens::token_candidates_at_offset( + let Some(selection) = crate::source_tokens::token_candidates_at_offset_with_cache( db, file_id, node, offset, super::token_precedence, + source_token_cache, ) else { return Vec::new(); }; diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index 2b87b40c..f9263c5c 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -1,7 +1,11 @@ -use hir::preproc::{ - EmittedTokenProvenance, MacroDefinitionId, MacroExpansionProvenance, MappedPreprocSource, - TokenProvenance, macro_expansion_provenances_at, +use hir::{ + base_db::source_db::SourcePreprocQueryError, + preproc::{ + EmittedTokenProvenance, MacroDefinitionId, MacroExpansionProvenance, MappedPreprocSource, + PreprocError, TokenProvenance, macro_expansion_provenances_at, + }, }; +use rustc_hash::FxHashMap; use syntax::{ SyntaxElement, SyntaxNode, SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, WalkEvent, has_text_range::HasTextRange, @@ -43,6 +47,36 @@ pub(crate) struct PreprocTokenAmbiguity { pub hits: Vec, } +#[derive(Debug, Default)] +pub(crate) struct SourceTokenRequestCache { + provenance_by_offset: + FxHashMap<(FileId, TextSize), Result, PreprocError>>, +} + +impl SourceTokenRequestCache { + fn macro_expansion_provenances_at( + &mut self, + db: &RootDb, + file_id: FileId, + offset: TextSize, + ) -> Result, PreprocError> { + self.provenance_by_offset + .entry((file_id, offset)) + .or_insert_with(|| macro_expansion_provenances_at(db, file_id, offset)) + .clone() + } + + #[cfg(test)] + fn macro_expansion_provenances_at_with( + &mut self, + file_id: FileId, + offset: TextSize, + compute: impl FnOnce() -> Result, PreprocError>, + ) -> Result, PreprocError> { + self.provenance_by_offset.entry((file_id, offset)).or_insert_with(compute).clone() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PreprocTokenHit { pub expansion: usize, @@ -90,7 +124,22 @@ pub(crate) fn token_candidates_at_offset<'tree, F>( where F: Fn(TokenKind) -> usize, { - match provenance_token_candidates_at_offset(db, file_id, root, offset, &precedence) { + let mut cache = SourceTokenRequestCache::default(); + token_candidates_at_offset_with_cache(db, file_id, root, offset, precedence, &mut cache) +} + +pub(crate) fn token_candidates_at_offset_with_cache<'tree, F>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: F, + cache: &mut SourceTokenRequestCache, +) -> Option> +where + F: Fn(TokenKind) -> usize, +{ + match provenance_token_candidates_at_offset(db, file_id, root, offset, &precedence, cache) { ProvenanceTokenLookup::Available(selection) => { return Some(SourceTokenSelection::Preproc(selection)); } @@ -129,9 +178,18 @@ fn provenance_token_candidates_at_offset<'tree>( root: SyntaxNode<'tree>, offset: TextSize, precedence: &impl Fn(TokenKind) -> usize, + cache: &mut SourceTokenRequestCache, ) -> ProvenanceTokenLookup<'tree> { - let Ok(provenances) = macro_expansion_provenances_at(db, file_id, offset) else { - return ProvenanceTokenLookup::NotApplicable; + let provenances = match cache.macro_expansion_provenances_at(db, file_id, offset) { + Ok(provenances) => provenances, + Err(PreprocError::SourceQuery(SourcePreprocQueryError::UnsupportedFileKind(_))) => { + return ProvenanceTokenLookup::NotApplicable; + } + Err(_) => { + return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { + range: TextRange::empty(offset), + }); + } }; if provenances.is_empty() { return ProvenanceTokenLookup::NotApplicable; @@ -423,6 +481,34 @@ mod tests { assert_eq!(ambiguous.hits.len(), 2); } + #[test] + fn source_token_request_cache_reuses_provenance_lookup_for_repeated_reference_hits() { + let mut cache = SourceTokenRequestCache::default(); + let mut lookups = 0usize; + let file_id = FileId(0); + let offset = TextSize::from(12); + + for _ in 0..3 { + let result = cache + .macro_expansion_provenances_at_with(file_id, offset, || { + lookups += 1; + Ok(Vec::new()) + }) + .unwrap(); + assert!(result.is_empty()); + } + + assert_eq!(lookups, 1, "repeated text hits at one offset should reuse the request cache"); + + let _ = cache + .macro_expansion_provenances_at_with(file_id, offset + TextSize::from(1), || { + lookups += 1; + Ok(Vec::new()) + }) + .unwrap(); + assert_eq!(lookups, 2, "different offsets should remain distinct cache entries"); + } + fn root_and_offset<'tree>( text: &str, needle: &str, From 545d62ed67802ec5a2f59ce93af17e678853e36f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 15:39:40 +0800 Subject: [PATCH 28/80] test(project-model): lock manifest semantic participation --- crates/hir/src/base_db/source_db.rs | 67 ++++++++++++++++++++++++++++- crates/ide/src/diagnostics.rs | 48 +++++++++++++++++++++ crates/project-model/src/lib.rs | 38 ++++++++++++---- 3 files changed, 143 insertions(+), 10 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 02e72fde..4f81908d 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1588,9 +1588,13 @@ mod tests { use vfs::{FileSet, VfsPath}; use super::*; - use crate::base_db::salsa::{self, Durability}; + use crate::base_db::{ + project::CompilationProfile, + salsa::{self, Durability}, + }; const TOP: FileId = FileId(0); + const MANIFEST: FileId = FileId(1); const ROOT: SourceRootId = SourceRootId(0); #[salsa::database(SourceDbStorage, SourceRootDbStorage)] @@ -1624,6 +1628,10 @@ mod tests { let mut db = TestDb::default(); db.set_files_with_durability(Box::new(files), Durability::HIGH); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::LOW, + ); db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); db.set_source_root_id_with_durability(TOP, ROOT, Durability::LOW); db.set_file_path_with_durability(TOP, Some(top_path), Durability::LOW); @@ -1666,6 +1674,63 @@ mod tests { assert!(!kind.is_slang_parse_unit()); } + #[test] + fn project_manifests_are_loadable_but_not_semantic_or_preproc_inputs() { + let top_path = abs_path("rtl/top.sv"); + let manifest_path = abs_path("vide.toml"); + let mut file_set = FileSet::default(); + file_set.insert(TOP, VfsPath::from(top_path.clone())); + file_set.insert(MANIFEST, VfsPath::from(manifest_path.clone())); + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + + let mut files = FxHashSet::default(); + files.insert(TOP); + files.insert(MANIFEST); + + let mut db = TestDb::default(); + db.set_files_with_durability(Box::new(files), Durability::HIGH); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::LOW, + ); + db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); + for (file_id, path, kind, text) in [ + (TOP, top_path, SourceFileKind::SystemVerilog, "module top; endmodule\n"), + (MANIFEST, manifest_path, SourceFileKind::ProjectManifest, "defines = [\"M=1\"]\n"), + ] { + db.set_source_root_id_with_durability(file_id, ROOT, Durability::LOW); + db.set_file_path_with_durability(file_id, Some(path), Durability::LOW); + db.set_file_kind_with_durability(file_id, kind, Durability::LOW); + db.set_file_text_with_durability(file_id, Arc::from(text), Durability::LOW); + } + db.set_project_config_with_durability( + Arc::new(ProjectConfig::new( + vec![Some(CompilationProfileId(0))], + vec![CompilationProfile { + source_roots: vec![ROOT], + top_modules: Vec::new(), + preprocess: PreprocessConfig::default(), + }], + )), + Durability::LOW, + ); + + assert_eq!(db.file_kind(MANIFEST), SourceFileKind::ProjectManifest); + assert!(db.parse_diagnostics(MANIFEST).is_empty()); + + let plan = db.compilation_plan_for_root(ROOT); + assert_eq!(plan.roots, vec![TOP]); + assert!(!plan.include_only.contains(&MANIFEST)); + + let preproc_model_files = + workspace_preproc_model_file_ids(&db, Some(CompilationProfileId(0))); + assert_eq!(preproc_model_files, vec![TOP]); + assert_eq!( + db.source_preproc_model(MANIFEST).as_ref(), + &Err(SourcePreprocQueryError::UnsupportedFileKind(SourceFileKind::ProjectManifest)) + ); + } + #[test] fn source_preproc_mapping_reports_unmapped_included_source() { let db = db_with_root_file(); diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 2da49330..f052bc5e 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -848,6 +848,54 @@ mod tests { ); } + #[test] + fn syntax_only_manifest_does_not_disable_open_file_syntax_diagnostics() { + let manifest_id = FileId(0); + let open_file_id = FileId(1); + let mut manifest_files = FileSet::default(); + manifest_files.insert(manifest_id, VfsPath::new_virtual_path("/project/vide.toml".into())); + let mut open_files = FileSet::default(); + open_files.insert(open_file_id, VfsPath::new_virtual_path("/scratch/open.sv".into())); + + let mut change = Change::new(); + change.set_roots(vec![ + SourceRoot::new_local(manifest_files), + SourceRoot::new_ignored(open_files), + ]); + change.set_project_config(Arc::new(ProjectConfig::new(vec![None, None], Vec::new()))); + change.add_changed_file(ChangedFile { + file_id: manifest_id, + change_kind: ChangeKind::Create(Arc::from(""), LineEnding::Unix), + }); + change.add_changed_file(ChangedFile { + file_id: open_file_id, + change_kind: ChangeKind::Create( + Arc::from("module open(;\nendmodule\n"), + LineEnding::Unix, + ), + }); + + let mut db = RootDb::new(None); + db.apply_change(change); + + assert!(!db.project_config().has_compilation_profiles()); + assert_eq!(db.project_config().profile_for_root(SourceRootId(0)), None); + assert_eq!(db.project_config().profile_for_root(SourceRootId(1)), None); + assert!(diagnostics(&db, manifest_id).is_empty()); + + let diagnostics = diagnostics(&db, open_file_id); + assert!( + diagnostics.iter().any(|diag| diag.source == DiagnosticSource::SlangParse), + "profile-less open files should keep syntax diagnostics: {diagnostics:?}" + ); + assert!( + diagnostics.iter().all(|diag| { + diag.file_id == open_file_id && diag.source != DiagnosticSource::SlangSemantic + }), + "syntax-only manifest must not create semantic diagnostic ownership: {diagnostics:?}" + ); + } + #[test] fn best_effort_index_root_does_not_produce_fallback_compilation_plan() { let mut db = RootDb::new(None); diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 3d568839..13c910df 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -97,6 +97,13 @@ impl WorkspaceRoot { || !self.include_dirs.is_empty() || !self.source_directories.is_empty() } + + fn contributes_semantic_profile(&self) -> bool { + self.role.participates_in_semantic_profile() + && (!self.source.is_empty() + || !self.source_directories.is_empty() + || !self.source_files.is_empty()) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -222,7 +229,7 @@ impl Workspace { let roots = workspace_roots(kind, &source_policy, has_source_paths, root_parts); let semantic_profile = roots .iter() - .any(|root| root.role.participates_in_semantic_profile()) + .any(WorkspaceRoot::contributes_semantic_profile) .then(|| semantic_profile(top_modules, macro_defs, include_dirs, Some(manifest_path))); Ok(Self { workspace_root, library_paths, kind, roots, semantic_profile }) @@ -244,7 +251,7 @@ impl Workspace { let roots = workspace_roots(kind, &ManifestSourcePolicy::DefaultIndex, true, root_parts); let semantic_profile = roots .iter() - .any(|root| root.role.participates_in_semantic_profile()) + .any(WorkspaceRoot::contributes_semantic_profile) .then(|| semantic_profile(Vec::new(), MacroDef::default(), include_dirs, None)); Self { @@ -957,7 +964,9 @@ libraries = ["../pkg/rtl"] matches!(&load[0], vfs::loader::Entry::Files(files) if files == std::slice::from_ref(&manifest_path)) ); assert!(matches!(&load[1], vfs::loader::Entry::Directories(_))); - assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); + assert!(!project_config.has_compilation_profiles()); + assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); + assert_eq!(project_config.profile_for_root(SourceRootId(1)), None); } #[test] @@ -982,7 +991,8 @@ libraries = ["../pkg/rtl"] source_root_config.fileset_roles, vec![SourceRootRole::Local, SourceRootRole::Ignored] ); - assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); + assert!(!project_config.has_compilation_profiles()); + assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); } #[test] @@ -1040,9 +1050,8 @@ include_dirs = ["include"] vec![SourceRootRole::Local, SourceRootRole::BestEffortIndex, SourceRootRole::Ignored] ); - let include_profile_id = project_config.profile_for_root(SourceRootId(0)).unwrap(); - let include_profile = project_config.profile(include_profile_id).unwrap(); - assert_eq!(include_profile.source_roots, vec![SourceRootId(0)]); + assert!(!project_config.has_compilation_profiles()); + assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); assert_eq!(project_config.profile_for_root(SourceRootId(1)), None); let mut vfs = Vfs::default(); @@ -1101,7 +1110,8 @@ include_dirs = ["include"] let project_manifest = ProjectManifest::from_path(&root.path().to_path_buf()).unwrap(); let (model, errors) = ProjectModel::load(vec![project_manifest]); - let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); + let (load, _, source_root_config, project_config) = + get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); assert!(load.iter().any(|entry| { @@ -1121,6 +1131,11 @@ include_dirs = ["include"] let top_id = roots[0].file_for_path(&VfsPath::from(top)).unwrap(); assert_eq!(roots[0].file_kind(manifest_id), SourceFileKind::ProjectManifest); assert_eq!(roots[0].file_kind(top_id), SourceFileKind::SystemVerilog); + assert!(project_config.has_compilation_profiles()); + let profile_id = project_config.profile_for_root(SourceRootId(0)).unwrap(); + let profile = project_config.profile(profile_id).unwrap(); + assert_eq!(profile.source_roots, vec![SourceRootId(0)]); + assert_eq!(profile.preprocess.predefines.len(), 1); } #[test] @@ -1148,6 +1163,7 @@ exclude = ["rtl/**"] other => panic!("expected directory loader entry, got {other:?}"), }; assert!(!dirs.contains_file(top.as_path())); + assert!(project_config.has_compilation_profiles()); assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); } @@ -1172,7 +1188,8 @@ exclude = ["rtl/excluded/**"] let excluded_top = excluded.join("top.sv"); let manifest = ProjectManifest::from_path(&root).unwrap(); let (model, errors) = ProjectModel::load(vec![manifest]); - let (load, _, source_root_config, _) = get_workspace_folder(&model.workspaces, &[]); + let (load, _, source_root_config, project_config) = + get_workspace_folder(&model.workspaces, &[]); assert!(errors.is_empty(), "{errors:#?}"); assert_eq!(load.len(), 2); @@ -1195,6 +1212,9 @@ exclude = ["rtl/excluded/**"] assert!(roots[0].file_for_path(&VfsPath::from(top)).is_some()); assert_eq!(roots[1].role(), SourceRootRole::Ignored); assert!(roots[1].file_for_path(&VfsPath::from(excluded_top)).is_some()); + assert!(project_config.has_compilation_profiles()); + assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); + assert_eq!(project_config.profile_for_root(SourceRootId(1)), None); } #[test] From 4aba1c82cc45787719e0dca5631c72615e21fb08 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 15:56:20 +0800 Subject: [PATCH 29/80] fix(hir): validate configured predefine source mapping --- crates/hir/src/base_db/source_db.rs | 329 ++++++++++++++++++++-- crates/hir/src/preproc.rs | 30 +- crates/preproc/src/source/model.rs | 3 + crates/preproc/src/source/provenance.rs | 2 + crates/slang/bindings/rust/ffi.rs | 2 + crates/slang/bindings/rust/ffi/wrapper.cc | 14 +- crates/slang/bindings/rust/lib.rs | 3 + crates/slang/bindings/rust/tests.rs | 11 +- 8 files changed, 357 insertions(+), 37 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 4f81908d..722aed48 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -685,7 +685,10 @@ fn source_preproc_file_ids( .source_buffers .iter() .filter(|source| source.origin == SourceBufferOrigin::Predefine) - .map(|source| PreprocSourceId::from(source.buffer_id)) + .map(|source| PredefineSourceBuffer { + source: PreprocSourceId::from(source.buffer_id), + text: source.text.as_deref(), + }) .collect::>(); let predefine_map = PredefineVirtualMapping::new(db, profile_id, &preprocess.predefines, predefine_sources); @@ -729,6 +732,13 @@ fn source_preproc_file_ids( } SourceBufferOrigin::Predefine => { if let Some(entry) = predefine_map.entry(source_id) { + let manifest_source = match entry.manifest_source(db, &path_file_ids) { + Ok(manifest_source) => manifest_source, + Err(reason) => { + source_map.insert_unmapped(source_id, reason); + continue; + } + }; source_map.insert_virtual_file_with_offset( source_id, entry.file_id, @@ -737,9 +747,11 @@ fn source_preproc_file_ids( entry.text_len, entry.range_offset, ); - if let Some(manifest_source) = entry.manifest_source(&path_file_ids) { + if let Some(manifest_source) = manifest_source { source_map.insert_predefine_manifest_source(source_id, manifest_source); } + } else if let Some(reason) = predefine_map.unavailable_reason(source_id) { + source_map.insert_unmapped(source_id, reason.clone()); } else { source_map.insert_unmapped( source_id, @@ -851,9 +863,11 @@ fn materialized_predefine_text(predefine: &str) -> String { struct PredefineVirtualMapping { entries: FxHashMap, + unavailable: FxHashMap, } struct PredefineVirtualEntry { + source: PreprocSourceId, file_id: Option, path: VfsPath, text_len: usize, @@ -861,18 +875,25 @@ struct PredefineVirtualEntry { predefine: Predefine, } +struct PredefineSourceBuffer<'a> { + source: PreprocSourceId, + text: Option<&'a str>, +} + +struct PredefineConfigEntry { + text: String, + name: SmolStr, + range_offset: usize, + predefine: Predefine, +} + impl PredefineVirtualMapping { fn new( db: &dyn SourceRootDb, profile_id: Option, predefines: &[Predefine], - mut sources: Vec, + sources: Vec>, ) -> Self { - sources.sort_by_key(|source| source.raw()); - if sources.len() != predefines.len() || sources.is_empty() { - return Self { entries: FxHashMap::default() }; - } - let texts = predefines .iter() .map(|predefine| materialized_predefine_text(predefine.as_str())) @@ -881,40 +902,141 @@ impl PredefineVirtualMapping { let path = preproc_virtual_predefines_path(profile_id); let file_id = materialized_preproc_virtual_file_id(db, &path); let mut range_offset = 0usize; + let mut configs = Vec::new(); + for (index, predefine) in predefines.iter().enumerate() { + let text = &texts[index]; + if let Some(name) = materialized_predefine_name(text) { + configs.push(PredefineConfigEntry { + text: text.clone(), + name, + range_offset, + predefine: predefine.clone(), + }); + } + range_offset += text.len(); + } + + let mut configs_by_text = FxHashMap::>::default(); + for (index, config) in configs.iter().enumerate() { + let slot = configs_by_text.entry(config.text.clone()).or_insert(Some(index)); + if *slot != Some(index) { + *slot = None; + } + } + let mut entries = FxHashMap::default(); - for (index, (source, text)) in sources.into_iter().zip(texts).enumerate() { + let mut unavailable = FxHashMap::default(); + for source in sources { + let Some(source_text) = source.text else { + unavailable.insert( + source.source, + SourcePreprocUnavailable::MissingPredefineSourceText { source: source.source }, + ); + continue; + }; + let Some(config_index) = configs_by_text.get(source_text).and_then(|index| *index) + else { + unavailable.insert( + source.source, + SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, + ); + continue; + }; + let config = &configs[config_index]; + if materialized_predefine_name(source_text).as_ref() != Some(&config.name) { + unavailable.insert( + source.source, + SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, + ); + continue; + } entries.insert( - source, + source.source, PredefineVirtualEntry { + source: source.source, file_id, path: path.clone(), text_len, - range_offset, - predefine: predefines[index].clone(), + range_offset: config.range_offset, + predefine: config.predefine.clone(), }, ); - range_offset += text.len(); } - Self { entries } + Self { entries, unavailable } } fn entry(&self, source: PreprocSourceId) -> Option<&PredefineVirtualEntry> { self.entries.get(&source) } + + fn unavailable_reason(&self, source: PreprocSourceId) -> Option<&SourcePreprocUnavailable> { + self.unavailable.get(&source) + } } impl PredefineVirtualEntry { fn manifest_source( &self, + db: &dyn SourceRootDb, path_file_ids: &PathIdentityIndex, - ) -> Option { - let source = self.predefine.source.as_ref()?; - let file_id = path_file_ids.get_path(source.path.as_path())?; - Some(PreprocManifestSource { file_id, range: source.range }) + ) -> Result, SourcePreprocUnavailable> { + let Some(source) = self.predefine.source.as_ref() else { + return Ok(None); + }; + let Some(file_id) = path_file_ids.get_path(source.path.as_path()) else { + return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { + source: self.source, + }); + }; + if !manifest_predefine_source_matches( + db.file_text(file_id).as_ref(), + source.range, + &self.predefine, + ) { + return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { + source: self.source, + }); + } + Ok(Some(PreprocManifestSource { file_id, range: source.range })) } } +fn materialized_predefine_name(text: &str) -> Option { + let rest = text.trim_start().strip_prefix("`define")?.trim_start(); + let name = + rest.split(|ch: char| ch.is_whitespace() || ch == '(').next().unwrap_or_default().trim(); + let name = name.strip_prefix('`').unwrap_or(name); + if name.is_empty() { None } else { Some(SmolStr::new(name)) } +} + +fn manifest_predefine_source_matches(text: &str, range: TextRange, predefine: &Predefine) -> bool { + let start = usize::from(range.start()); + let end = usize::from(range.end()); + let Some(raw_source) = text.get(start..end) else { + return false; + }; + let Some(source_definition) = unquote_manifest_predefine(raw_source) else { + return false; + }; + source_definition == predefine.as_str() + && predefine_definition_name(source_definition) + == predefine_definition_name(predefine.as_str()) +} + +fn unquote_manifest_predefine(text: &str) -> Option<&str> { + let text = text.trim(); + text.strip_prefix('"') + .and_then(|text| text.strip_suffix('"')) + .or_else(|| text.strip_prefix('\'').and_then(|text| text.strip_suffix('\''))) +} + +fn predefine_definition_name(predefine: &str) -> Option { + let name = predefine.split_once('=').map_or(predefine, |(name, _)| name); + let name = name.trim().strip_prefix('`').unwrap_or(name.trim()); + if name.is_empty() { None } else { Some(SmolStr::new(name)) } +} + fn materialized_preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> Option { file_id_for_vfs_path(db, path) } @@ -1589,7 +1711,7 @@ mod tests { use super::*; use crate::base_db::{ - project::CompilationProfile, + project::{CompilationProfile, PredefineSource}, salsa::{self, Durability}, }; @@ -1644,11 +1766,49 @@ mod tests { db } + fn db_with_root_and_manifest(manifest_text: &str) -> TestDb { + let top_path = abs_path("rtl/top.v"); + let manifest_path = abs_path("vide.toml"); + let mut file_set = FileSet::default(); + file_set.insert(TOP, VfsPath::from(top_path.clone())); + file_set.insert(MANIFEST, VfsPath::from(manifest_path.clone())); + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + let mut files = FxHashSet::default(); + files.insert(TOP); + files.insert(MANIFEST); + + let mut db = TestDb::default(); + db.set_files_with_durability(Box::new(files), Durability::HIGH); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::LOW, + ); + db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); + for (file_id, path, kind, text) in [ + (TOP, top_path, SourceFileKind::SystemVerilog, "module top; endmodule\n"), + (MANIFEST, manifest_path, SourceFileKind::ProjectManifest, manifest_text), + ] { + db.set_source_root_id_with_durability(file_id, ROOT, Durability::LOW); + db.set_file_path_with_durability(file_id, Some(path), Durability::LOW); + db.set_file_kind_with_durability(file_id, kind, Durability::LOW); + db.set_file_text_with_durability(file_id, Arc::from(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 offset(text: &str, needle: &str) -> TextSize { + TextSize::try_from(text.find(needle).expect("needle must exist")).unwrap() + } + + fn offset_after(text: &str, needle: &str) -> TextSize { + offset(text, needle) + TextSize::try_from(needle.len()).unwrap() + } + #[test] fn include_headers_are_not_standalone_parse_diagnostic_units() { let kind = @@ -1739,11 +1899,13 @@ mod tests { source_buffers: vec![ SourceBufferId { path: abs_path("rtl/top.v").to_string(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }, SourceBufferId { path: abs_path("include/missing.vh").to_string(), + text: None, buffer_id: 2, origin: SourceBufferOrigin::Source, }, @@ -1770,24 +1932,35 @@ mod tests { } #[test] - fn source_preproc_mapping_records_predefines_as_display_virtual_source_without_backing() { + fn source_preproc_mapping_records_predefines_by_verified_source_text() { let db = db_with_root_file(); + let first_text = materialized_predefine_text("FIRST=1"); + let second_text = materialized_predefine_text("SECOND"); let trace = PreprocessorTrace { root_buffer_id: 1, source_buffers: vec![ SourceBufferId { path: abs_path("rtl/top.v").to_string(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }, SourceBufferId { path: "".to_owned(), + text: Some(second_text.clone()), buffer_id: 2, origin: SourceBufferOrigin::Predefine, }, SourceBufferId { path: "".to_owned(), - buffer_id: 3, + text: Some(first_text.clone()), + buffer_id: 9, + origin: SourceBufferOrigin::Predefine, + }, + SourceBufferId { + path: "".to_owned(), + text: Some(materialized_predefine_text("EXTRA=9")), + buffer_id: 4, origin: SourceBufferOrigin::Predefine, }, ], @@ -1804,10 +1977,10 @@ mod tests { let source_map = source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); - let first = PreprocSourceId::from(2); - let second = PreprocSourceId::from(3); + let first = PreprocSourceId::from(9); + let second = PreprocSourceId::from(2); + let extra = PreprocSourceId::from(4); let expected_path = preproc_virtual_predefines_path(None); - let first_text = materialized_predefine_text("FIRST=1"); let Some(PreprocSourceMapping::VirtualDisplay { path, origin }) = source_map.get(first) else { @@ -1823,6 +1996,12 @@ mod tests { origin: PreprocVirtualOrigin::Predefines { profile: None }, }) ); + assert_eq!( + source_map.get(extra), + Some(&PreprocSourceMapping::Unmapped( + SourcePreprocUnavailable::UnverifiedPredefineSource { source: extra } + )) + ); assert!(matches!( source_map.file_id(first), Err(PreprocSourceMapError::DisplayOnlyVirtualSource { .. }) @@ -1841,6 +2020,106 @@ mod tests { ); } + #[test] + fn source_preproc_mapping_rejects_predefine_source_text_mismatch() { + let db = db_with_root_file(); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![ + SourceBufferId { + path: abs_path("rtl/top.v").to_string(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: "".to_owned(), + text: Some(materialized_predefine_text("SECOND=2")), + buffer_id: 2, + origin: SourceBufferOrigin::Predefine, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let options = SyntaxTreeOptions { + predefines: vec!["FIRST=1".to_owned()], + ..SyntaxTreeOptions::default() + }; + let preprocess = PreprocessConfig::with_predefine_strings(["FIRST=1"], Vec::new()); + + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); + let source = PreprocSourceId::from(2); + + assert_eq!( + source_map.get(source), + Some(&PreprocSourceMapping::Unmapped( + SourcePreprocUnavailable::UnverifiedPredefineSource { source } + )) + ); + assert!(matches!( + source_map.map_range(SourceRange { + source, + range: TextRange::new(TextSize::from(0), TextSize::from(1)), + }), + Err(PreprocSourceMapError::UnmappedSource { .. }) + )); + } + + #[test] + fn source_preproc_mapping_rejects_manifest_range_mismatch() { + let manifest_text = "defines = [\"RIGHT=1\", \"WRONG=2\"]\n"; + let db = db_with_root_and_manifest(manifest_text); + let wrong_range = TextRange::new( + offset(manifest_text, "\"WRONG=2\""), + offset_after(manifest_text, "\"WRONG=2\""), + ); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![ + SourceBufferId { + path: abs_path("rtl/top.v").to_string(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: "".to_owned(), + text: Some(materialized_predefine_text("RIGHT=1")), + buffer_id: 2, + origin: SourceBufferOrigin::Predefine, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let options = SyntaxTreeOptions { + predefines: vec!["RIGHT=1".to_owned()], + ..SyntaxTreeOptions::default() + }; + let preprocess = PreprocessConfig { + predefines: vec![Predefine::with_source( + "RIGHT=1", + PredefineSource { path: abs_path("vide.toml"), range: wrong_range }, + )], + include_dirs: Vec::new(), + }; + + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); + let source = PreprocSourceId::from(2); + + assert_eq!( + source_map.get(source), + Some(&PreprocSourceMapping::Unmapped( + SourcePreprocUnavailable::UnverifiedPredefineSource { source } + )) + ); + } + #[test] fn source_preproc_mapping_records_external_include_buffer_as_display_virtual_source() { let db = db_with_root_file(); @@ -1850,11 +2129,13 @@ mod tests { source_buffers: vec![ SourceBufferId { path: abs_path("rtl/top.v").to_string(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }, SourceBufferId { path: external_path.clone(), + text: None, buffer_id: 4, origin: SourceBufferOrigin::Source, }, diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 4168ee77..11ffd0ba 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -3425,28 +3425,36 @@ endmodule #[test] fn preproc_manifest_predefine_definition_uses_manifest_provenance() { - let root_text = r#"`ifdef FROM_MANIFEST + let root_text = r#"`ifdef Z_FROM_MANIFEST module top; -localparam int W = `FROM_MANIFEST; +localparam int W = `Z_FROM_MANIFEST; endmodule `endif "#; - let manifest_text = "defines = [\"FROM_MANIFEST=1\"]\n"; + let manifest_text = "defines = [\"A_OTHER=2\", \"Z_FROM_MANIFEST=1\"]\n"; let manifest_range = TextRange::new( - offset(manifest_text, "\"FROM_MANIFEST=1\""), - offset_after(manifest_text, "\"FROM_MANIFEST=1\""), + offset(manifest_text, "\"Z_FROM_MANIFEST=1\""), + offset_after(manifest_text, "\"Z_FROM_MANIFEST=1\""), + ); + let other_range = TextRange::new( + offset(manifest_text, "\"A_OTHER=2\""), + offset_after(manifest_text, "\"A_OTHER=2\""), ); let predefine = Predefine::with_source( - "FROM_MANIFEST=1", + "Z_FROM_MANIFEST=1", PredefineSource { path: abs_path("vide.toml"), range: manifest_range }, ); + let other_predefine = Predefine::with_source( + "A_OTHER=2", + PredefineSource { path: abs_path("vide.toml"), range: other_range }, + ); let db = db_with_entries_and_predefine_entries( &[(TOP, "rtl/top.v", root_text), (MANIFEST, "vide.toml", manifest_text)], - vec![predefine], + vec![other_predefine, predefine], ); let resolution = - macro_reference_definitions_at(&db, TOP, offset(root_text, "FROM_MANIFEST;")) + macro_reference_definitions_at(&db, TOP, offset(root_text, "Z_FROM_MANIFEST;")) .unwrap() .unwrap(); assert!( @@ -3459,15 +3467,15 @@ endmodule let definition = macro_definition_at(&db, MANIFEST, manifest_range.start()).unwrap().unwrap(); assert_eq!(definition.file_id, MANIFEST); - assert_eq!(definition.name.as_str(), "FROM_MANIFEST"); + assert_eq!(definition.name.as_str(), "Z_FROM_MANIFEST"); assert_eq!(definition.name_range, manifest_range); - assert_eq!(text_at_range(manifest_text, definition.name_range), "\"FROM_MANIFEST=1\""); + assert_eq!(text_at_range(manifest_text, definition.name_range), "\"Z_FROM_MANIFEST=1\""); let references = macro_references(&db, MANIFEST, &definition).unwrap(); assert!( references.references.iter().any(|reference| { reference.file_id == TOP - && text_at_range(root_text, reference.range) == "FROM_MANIFEST" + && text_at_range(root_text, reference.range) == "Z_FROM_MANIFEST" }), "manifest predefine definition should find source references: {references:?}" ); diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index d33fa4b7..2c5eb851 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -818,6 +818,7 @@ endmodule root_buffer_id: 1, source_buffers: vec![SourceBufferId { path: ROOT_PATH.to_owned(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }], @@ -1235,6 +1236,7 @@ endmodule root_buffer_id: 1, source_buffers: vec![SourceBufferId { path: ROOT_PATH.to_owned(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }], @@ -1485,6 +1487,7 @@ logic [`LEAF_WIDTH-1:0] data; root_buffer_id: 1, source_buffers: vec![SourceBufferId { path: ROOT_PATH.to_owned(), + text: None, buffer_id: 1, origin: SourceBufferOrigin::Source, }], diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index acaacef3..9b4f471b 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -327,6 +327,8 @@ pub enum SourcePreprocUnavailable { IncludeEdgeNotInclude { include_event_id: SourcePreprocEventId }, IncludeChainUnavailable { source: PreprocSourceId }, DetachedSource { source: PreprocSourceId }, + MissingPredefineSourceText { source: PreprocSourceId }, + UnverifiedPredefineSource { source: PreprocSourceId }, MacroCallAuthorityUnavailable, EmittedTokenAuthorityUnavailable, TokenProvenanceAuthorityUnavailable, diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index d17a0d95..0f40e10c 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -41,6 +41,8 @@ mod slang_ffi { #[derive(Debug, Clone, PartialEq, Eq)] struct RawSourceBufferId { path: String, + text: String, + has_text: bool, buffer_id: u32, origin: u8, } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index ceb2f2c0..fd7721f2 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -658,8 +658,20 @@ rust::Vec<::RawSourceBufferId> collectSourceBufferIds( ::RawSourceBufferId sourceBuffer; sourceBuffer.path = rust::String(fullPath.string()); + sourceBuffer.text = rust::String(); + sourceBuffer.has_text = false; sourceBuffer.buffer_id = buffer.getId(); - sourceBuffer.origin = predefineBufferIds.contains(buffer.getId()) ? 1 : 0; + if (predefineBufferIds.contains(buffer.getId())) { + auto text = sourceManager.getSourceText(buffer); + if (!text.empty() && text.back() == '\0') + text.remove_suffix(1); + sourceBuffer.text = rust::String(std::string(text)); + sourceBuffer.has_text = true; + sourceBuffer.origin = 1; + } + else { + sourceBuffer.origin = 0; + } sourceBuffers.emplace_back(std::move(sourceBuffer)); } diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 76c826d0..a574307a 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -103,6 +103,7 @@ pub struct SyntaxTreeBufferIds { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceBufferId { pub path: String, + pub text: Option, pub buffer_id: u32, pub origin: SourceBufferOrigin, } @@ -338,6 +339,7 @@ impl SyntaxTreeBufferIds { .into_iter() .map(|buffer| SourceBufferId { path: buffer.path, + text: buffer.has_text.then_some(buffer.text), buffer_id: buffer.buffer_id, origin: SourceBufferOrigin::from_raw(buffer.origin), }) @@ -364,6 +366,7 @@ impl PreprocessorTrace { .into_iter() .map(|buffer| SourceBufferId { path: buffer.path, + text: buffer.has_text.then_some(buffer.text), buffer_id: buffer.buffer_id, origin: SourceBufferOrigin::from_raw(buffer.origin), }) diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index c2b4cd0a..7f8a811b 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1398,6 +1398,15 @@ endmodule ) .expect("trace should include emitted tokens"); + let predefine_source = trace + .source_buffers + .iter() + .find(|source| { + source.origin == SourceBufferOrigin::Predefine + && source.text.as_deref() == Some("`define FROM_API 11\n") + }) + .expect("configured predefine source buffer should expose materialized text"); + let from_api = trace .emitted_tokens .iter() @@ -1408,7 +1417,7 @@ endmodule else { unreachable!(); }; - assert_ne!(body_token_range.buffer_id, trace.root_buffer_id); + assert_eq!(body_token_range.buffer_id, predefine_source.buffer_id); let intrinsic = trace .emitted_tokens From b0a444d012c908a883dd992775f405f605c82ea4 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 16:20:08 +0800 Subject: [PATCH 30/80] fix(hir): separate expansion display text from virtual source --- crates/hir/src/base_db/source_db.rs | 177 +++++++++++++++++++++------ crates/hir/src/preproc.rs | 182 +++++++++++++++++++++++----- crates/ide/src/source_tokens.rs | 6 +- 3 files changed, 297 insertions(+), 68 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 722aed48..c6b2972d 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -181,14 +181,32 @@ pub enum PreprocSourceMapping { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocExpansionMapping { - pub file_id: Option, - pub path: VfsPath, pub origin: PreprocVirtualOrigin, - pub text: String, pub emitted_range: SourceEmittedTokenRange, + pub display: PreprocExpansionDisplay, + pub source_buffer: PreprocExpansionSourceBuffer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocExpansionDisplay { + pub path: VfsPath, + pub text: String, token_ranges: FxHashMap, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocExpansionSourceBuffer { + ParseStable { + file_id: FileId, + path: VfsPath, + text: String, + token_ranges: FxHashMap, + }, + DisplayOnly { + path: VfsPath, + }, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PreprocManifestSource { pub file_id: FileId, @@ -301,24 +319,25 @@ impl PreprocSourceMap { self.predefine_sources.get(&source).copied() } - pub fn insert_expansion_virtual_file( + pub fn insert_expansion_display_only( &mut self, expansion: SourceMacroExpansionId, - file_id: Option, path: VfsPath, - text: String, + display_text: String, emitted_range: SourceEmittedTokenRange, - token_ranges: FxHashMap, + display_token_ranges: FxHashMap, ) { self.expansion_entries.insert( expansion, PreprocExpansionMapping { - file_id, - path, origin: PreprocVirtualOrigin::Expansion { expansion }, - text, emitted_range, - token_ranges, + display: PreprocExpansionDisplay { + path: path.clone(), + text: display_text, + token_ranges: display_token_ranges, + }, + source_buffer: PreprocExpansionSourceBuffer::DisplayOnly { path }, }, ); } @@ -327,27 +346,56 @@ impl PreprocSourceMap { self.expansion_entries.get(&expansion) } - pub fn expansion_source( + pub fn expansion_display_source( &self, expansion: SourceMacroExpansionId, ) -> Result { let entry = self .expansion(expansion) .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - Ok(match entry.file_id { - Some(file_id) => PreprocSourceMapping::VirtualFile { - file_id, - path: entry.path.clone(), - origin: entry.origin.clone(), - }, - None => PreprocSourceMapping::VirtualDisplay { - path: entry.path.clone(), - origin: entry.origin.clone(), - }, + Ok(PreprocSourceMapping::VirtualDisplay { + path: entry.display.path.clone(), + origin: entry.origin.clone(), }) } - pub fn emitted_token_range( + pub fn expansion_source_buffer( + &self, + expansion: SourceMacroExpansionId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + Ok(match &entry.source_buffer { + PreprocExpansionSourceBuffer::ParseStable { file_id, path, .. } => { + PreprocSourceMapping::VirtualFile { + file_id: *file_id, + path: path.clone(), + origin: entry.origin.clone(), + } + } + PreprocExpansionSourceBuffer::DisplayOnly { path } => { + PreprocSourceMapping::VirtualDisplay { + path: path.clone(), + origin: entry.origin.clone(), + } + } + }) + } + + pub fn emitted_display_range( + &self, + expansion: SourceMacroExpansionId, + emitted_range: SourceEmittedTokenRange, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + emitted_range_from_token_ranges(&entry.display.token_ranges, emitted_range) + .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) + } + + pub fn emitted_source_buffer_range( &self, expansion: SourceMacroExpansionId, emitted_range: SourceEmittedTokenRange, @@ -355,11 +403,15 @@ impl PreprocSourceMap { let entry = self .expansion(expansion) .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - expansion_text_range(entry, emitted_range) + let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer + else { + return Err(display_only_expansion_source_buffer_error(entry)); + }; + emitted_range_from_token_ranges(token_ranges, emitted_range) .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) } - pub fn emitted_token_text_range( + pub fn emitted_token_display_range( &self, expansion: SourceMacroExpansionId, token: SourceEmittedTokenId, @@ -368,12 +420,52 @@ impl PreprocSourceMap { .expansion(expansion) .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; entry + .display .token_ranges .get(&token) .copied() .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) } + pub fn emitted_token_source_buffer_range( + &self, + expansion: SourceMacroExpansionId, + token: SourceEmittedTokenId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer + else { + return Err(display_only_expansion_source_buffer_error(entry)); + }; + token_ranges + .get(&token) + .copied() + .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) + } + + pub fn insert_expansion_parse_stable_source_buffer( + &mut self, + expansion: SourceMacroExpansionId, + file_id: FileId, + path: VfsPath, + text: String, + token_ranges: FxHashMap, + ) -> Result<(), PreprocSourceMapError> { + let entry = self + .expansion_entries + .get_mut(&expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + entry.source_buffer = + PreprocExpansionSourceBuffer::ParseStable { file_id, path, text, token_ranges }; + Ok(()) + } + + pub fn expansion_display_text(&self, expansion: SourceMacroExpansionId) -> Option<&str> { + self.expansion(expansion).map(|entry| entry.display.text.as_str()) + } + pub fn file_id(&self, source: PreprocSourceId) -> Result { match self.get(source) { Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), @@ -1066,8 +1158,8 @@ fn unshift_text_size(offset: TextSize, range_offset: usize) -> Option Some(TextSize::from(u32::try_from(offset).ok()?)) } -fn expansion_text_range( - entry: &PreprocExpansionMapping, +fn emitted_range_from_token_ranges( + token_ranges: &FxHashMap, emitted_range: SourceEmittedTokenRange, ) -> Option { if emitted_range.len == 0 { @@ -1076,28 +1168,37 @@ fn expansion_text_range( let start = emitted_range.start; let end = SourceEmittedTokenId::new(start.raw().checked_add(emitted_range.len - 1)?); - let start_range = entry.token_ranges.get(&start)?; - let end_range = entry.token_ranges.get(&end)?; + let start_range = token_ranges.get(&start)?; + let end_range = token_ranges.get(&end)?; Some(TextRange::new(start_range.start(), end_range.end())) } -fn materialize_expansion_virtual_files( - db: &dyn SourceRootDb, +fn display_only_expansion_source_buffer_error( + entry: &PreprocExpansionMapping, +) -> PreprocSourceMapError { + PreprocSourceMapError::DisplayOnlyVirtualSource { + path: match &entry.source_buffer { + PreprocExpansionSourceBuffer::ParseStable { path, .. } + | PreprocExpansionSourceBuffer::DisplayOnly { path } => path.clone(), + }, + origin: entry.origin.clone(), + } +} + +fn record_expansion_display_texts( profile_id: Option, model: &SourcePreprocModel, source_map: &mut PreprocSourceMap, ) { for expansion in model.macro_expansions().iter() { let Some((text, token_ranges)) = - materialized_expansion_text_and_ranges(model, expansion.emitted_token_range) + expansion_display_text_and_ranges(model, expansion.emitted_token_range) else { continue; }; let path = preproc_virtual_expansion_path(profile_id, expansion.id); - let file_id = materialized_preproc_virtual_file_id(db, &path); - source_map.insert_expansion_virtual_file( + source_map.insert_expansion_display_only( expansion.id, - file_id, path, text, expansion.emitted_token_range, @@ -1106,13 +1207,15 @@ fn materialize_expansion_virtual_files( } } -fn materialized_expansion_text_and_ranges( +fn expansion_display_text_and_ranges( model: &SourcePreprocModel, emitted_range: SourceEmittedTokenRange, ) -> Option<(String, FxHashMap)> { let mut text = String::new(); let mut token_ranges = FxHashMap::default(); + // This is intentionally a readable display form. It is not a + // parse-stable SystemVerilog source buffer or source-map authority. for raw in emitted_range.start.raw()..emitted_range.start.raw().checked_add(emitted_range.len)? { @@ -1452,7 +1555,7 @@ fn source_preproc_model( Ok(model) => model, Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), }; - materialize_expansion_virtual_files(db, profile_id, &model, &mut source_map); + record_expansion_display_texts(profile_id, &model, &mut source_map); Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) } diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 11ffd0ba..5de2df05 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -22,7 +22,7 @@ use utils::{ line_index::{TextRange, TextSize}, uniq_vec::UniqVec, }; -use vfs::FileId; +use vfs::{FileId, VfsPath}; use crate::base_db::{ project::{CompilationProfileId, Predefine}, @@ -71,6 +71,7 @@ pub enum PreprocUnavailable { AmbiguousDiagnosticProvenance { targets: usize }, AmbiguousIncludeTargets { targets: usize }, PartialPreprocContextIndex { skipped_models: usize }, + DisplayOnlyVirtualExpansion { path: VfsPath, origin: PreprocVirtualOrigin }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -302,8 +303,8 @@ pub struct MacroExpansion { pub definition_id: MacroDefinitionId, pub definition: MacroDefinition, pub emitted_token_range: SourceEmittedTokenRange, - pub virtual_source: MappedPreprocSource, - pub virtual_range: TextRange, + pub display_source: MappedPreprocSource, + pub display_range: TextRange, pub child_calls: Vec, pub capability: PreprocAvailability, } @@ -318,7 +319,7 @@ pub struct MacroExpansionProvenance { pub struct EmittedTokenProvenance { pub token: SourceEmittedTokenId, pub text: SmolStr, - pub virtual_range: TextRange, + pub display_range: TextRange, pub provenance: TokenProvenance, } @@ -2167,21 +2168,21 @@ fn map_macro_expansion( definition_id: expansion.definition.into(), definition: map_macro_definition(mapped, definition)?, emitted_token_range: expansion.emitted_token_range, - virtual_source: map_expansion_virtual_source(mapped, expansion.id)?, - virtual_range: mapped + display_source: map_expansion_display_source(mapped, expansion.id)?, + display_range: mapped .source_map - .emitted_token_range(expansion.id, expansion.emitted_token_range) + .emitted_display_range(expansion.id, expansion.emitted_token_range) .map_err(PreprocError::SourceMap)?, child_calls: expansion.child_calls.iter().copied().map(Into::into).collect(), capability: macro_expansion_availability(&expansion.status), }) } -fn map_expansion_virtual_source( +fn map_expansion_display_source( mapped: &MappedSourcePreprocModel, expansion: SourceMacroExpansionId, ) -> PreprocResult { - match mapped.source_map.expansion_source(expansion).map_err(PreprocError::SourceMap)? { + match mapped.source_map.expansion_display_source(expansion).map_err(PreprocError::SourceMap)? { PreprocSourceMapping::VirtualFile { file_id, path, origin } => { Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) } @@ -2195,6 +2196,38 @@ fn map_expansion_virtual_source( } } +fn map_expansion_source_buffer( + mapped: &MappedSourcePreprocModel, + expansion: SourceMacroExpansionId, +) -> PreprocResult { + match mapped.source_map.expansion_source_buffer(expansion).map_err(PreprocError::SourceMap)? { + PreprocSourceMapping::VirtualFile { file_id, path, origin } => { + Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) + } + PreprocSourceMapping::VirtualDisplay { path, origin } => { + Ok(MappedPreprocSource::VirtualDisplay { path, origin }) + } + PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), + PreprocSourceMapping::Unmapped(reason) => { + Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) + } + } +} + +fn display_only_virtual_expansion_unavailable(source: &MappedPreprocSource) -> PreprocUnavailable { + match source { + MappedPreprocSource::VirtualDisplay { path, origin } => { + PreprocUnavailable::DisplayOnlyVirtualExpansion { + path: path.clone(), + origin: origin.clone(), + } + } + MappedPreprocSource::RealFile { .. } | MappedPreprocSource::VirtualFile { .. } => { + PreprocUnavailable::Source(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) + } + } +} + fn source_macro_call_at( mapped: &MappedSourcePreprocModel, file_id: FileId, @@ -2376,9 +2409,9 @@ fn macro_expansion_provenance_for_expansion( tokens.push(EmittedTokenProvenance { token: token_id, text: token.text.clone(), - virtual_range: mapped + display_range: mapped .source_map - .emitted_token_text_range(expansion_id, token_id) + .emitted_token_display_range(expansion_id, token_id) .map_err(PreprocError::SourceMap)?, provenance: map_token_provenance(mapped, provenance)?, }); @@ -2447,11 +2480,6 @@ fn diagnostic_target_for_source_expansion( mapped: &MappedSourcePreprocModel, expansion: &SourceMacroExpansionFact, ) -> PreprocResult { - let virtual_source = map_expansion_virtual_source(mapped, expansion.id)?; - let virtual_range = mapped - .source_map - .emitted_token_range(expansion.id, expansion.emitted_token_range) - .map_err(PreprocError::SourceMap)?; let mut saw_unavailable = None; for token_id in emitted_token_ids(expansion.emitted_token_range) { let Some(token) = mapped.model.emitted_tokens().get(token_id) else { @@ -2486,10 +2514,24 @@ fn diagnostic_target_for_source_expansion( } } - Ok(saw_unavailable.map_or_else( - || DiagnosticProvenance::VirtualExpansion { source: virtual_source, range: virtual_range }, - DiagnosticProvenance::Unavailable, - )) + if let Some(reason) = saw_unavailable { + return Ok(DiagnosticProvenance::Unavailable(reason)); + } + + let source_buffer_source = map_expansion_source_buffer(mapped, expansion.id)?; + let MappedPreprocSource::VirtualFile { .. } = &source_buffer_source else { + return Ok(DiagnosticProvenance::Unavailable(display_only_virtual_expansion_unavailable( + &source_buffer_source, + ))); + }; + let source_buffer_range = mapped + .source_map + .emitted_source_buffer_range(expansion.id, expansion.emitted_token_range) + .map_err(PreprocError::SourceMap)?; + Ok(DiagnosticProvenance::VirtualExpansion { + source: source_buffer_source, + range: source_buffer_range, + }) } fn map_macro_resolution( @@ -2865,8 +2907,8 @@ mod tests { }, salsa::{self, Durability}, source_db::{ - FileLoader, PreprocVirtualOrigin, SourceDb, SourceDbStorage, SourceFileKind, - SourceRootDb, SourceRootDbStorage, + FileLoader, PreprocExpansionSourceBuffer, PreprocVirtualOrigin, SourceDb, + SourceDbStorage, SourceFileKind, SourceRootDb, SourceRootDbStorage, }, source_root::{SourceRoot, SourceRootId}, }, @@ -3019,6 +3061,24 @@ mod tests { &text[usize::from(range.start())..usize::from(range.end())] } + fn assert_expansion_is_display_only_source_buffer( + mapped: &MappedSourcePreprocModel, + expansion: &MacroExpansion, + ) { + let expansion_id = SourceMacroExpansionId::new(expansion.id.raw()); + let entry = mapped + .source_map + .expansion(expansion_id) + .expect("expansion should have a display entry"); + assert!(matches!(&entry.source_buffer, PreprocExpansionSourceBuffer::DisplayOnly { .. })); + assert!(matches!( + mapped + .source_map + .emitted_source_buffer_range(expansion_id, expansion.emitted_token_range), + Err(PreprocSourceMapError::DisplayOnlyVirtualSource { .. }) + )); + } + #[test] fn preproc_include_usage_resolves_to_header_define() { let root_text = r#"`include "defs.vh" @@ -3099,7 +3159,7 @@ endmodule .unwrap() .unwrap(); let MappedPreprocSource::VirtualDisplay { path, origin } = - &provenance.expansion.virtual_source + &provenance.expansion.display_source else { panic!("macro expansion should expose a display-only virtual expansion source"); }; @@ -3114,9 +3174,10 @@ endmodule let mapped = db.source_preproc_model(TOP); let mapped = mapped.as_ref().as_ref().unwrap(); - let virtual_file = mapped.source_map.expansion(SourceMacroExpansionId::new(0)).unwrap(); - assert_eq!(virtual_file.text, "logic generated ;"); - assert_eq!(provenance.expansion.virtual_range, TextRange::new(0.into(), 17.into())); + let expansion_display = + mapped.source_map.expansion_display_text(SourceMacroExpansionId::new(0)).unwrap(); + assert_eq!(expansion_display, "logic generated ;"); + assert_eq!(provenance.expansion.display_range, TextRange::new(0.into(), 17.into())); let logic = provenance .tokens @@ -3128,7 +3189,7 @@ endmodule }; assert_eq!(source.file_id(), Some(TOP)); assert_eq!(text_at_range(root_text, *range), "logic"); - assert_eq!(logic.virtual_range, TextRange::new(0.into(), 5.into())); + assert_eq!(logic.display_range, TextRange::new(0.into(), 5.into())); let generated = provenance .tokens @@ -3143,7 +3204,55 @@ endmodule assert_eq!(*argument_index, 0); assert_eq!(source.file_id(), Some(TOP)); assert_eq!(text_at_range(root_text, *range), "generated"); - assert_eq!(generated.virtual_range, TextRange::new(6.into(), 15.into())); + assert_eq!(generated.display_range, TextRange::new(6.into(), 15.into())); + } + + #[test] + fn preproc_numeric_literal_expansion_display_is_not_source_buffer() { + let root_text = r#"`define ONE 12'd1 +module top; +localparam int W = `ONE; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ONE")).unwrap().unwrap(); + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); + + let display_text = mapped + .source_map + .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) + .unwrap(); + assert!(display_text.contains("12")); + assert!(display_text.contains("'d")); + assert!(display_text.contains("1")); + } + + #[test] + fn preproc_escaped_identifier_expansion_display_is_not_source_buffer() { + let root_text = concat!( + "`define ESCAPED \\escaped.name \n", + "module top;\n", + "wire `ESCAPED;\n", + "endmodule\n", + ); + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ESCAPED")) + .unwrap() + .unwrap(); + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); + + let display_text = mapped + .source_map + .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) + .unwrap(); + assert!(display_text.contains("\\escaped.name")); } #[test] @@ -3260,8 +3369,10 @@ endmodule #[test] fn diagnostic_provenance_returns_unavailable_for_unsupported_expansion_mapping() { let root_text = r#"`define JOIN(a,b) a``b +`define STR(x) `"x`" module top; wire `JOIN(foo,bar); +string s = `STR(foo); endmodule "#; let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); @@ -3273,6 +3384,21 @@ endmodule provenance, DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(_)) )); + + let stringification_range = + TextRange::new(offset(root_text, "`STR"), offset_after(root_text, "`STR(foo)")); + let provenance = + diagnostic_provenance_for_range(&db, TOP, stringification_range).unwrap().unwrap(); + assert!( + matches!( + &provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + | SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + )) + ), + "stringification should be unsupported or unavailable, got {provenance:?}" + ); } #[test] diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index f9263c5c..fa191067 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -82,7 +82,7 @@ pub(crate) struct PreprocTokenHit { pub expansion: usize, pub call: usize, pub emitted_token: usize, - pub virtual_range: TextRange, + pub display_range: TextRange, pub source_range: TextRange, pub provenance: PreprocTokenProvenance, target: PreprocSemanticTarget, @@ -309,7 +309,7 @@ fn preproc_hit_for_token( expansion: expansion.expansion.id.raw(), call, emitted_token: token.token.raw(), - virtual_range: token.virtual_range, + display_range: token.display_range, source_range: range, provenance, target, @@ -530,7 +530,7 @@ mod tests { expansion: 0, call: 0, emitted_token, - virtual_range: range, + display_range: range, source_range: range, provenance: PreprocTokenProvenance::SourceToken { source: source.clone(), range }, target: PreprocSemanticTarget::SourceToken { source, range }, From 4ab3e2cf9a7f5dbf5aa0681bf7e459cbc815b1b5 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 17:33:59 +0800 Subject: [PATCH 31/80] fix(preproc): trace runtime macro usages --- crates/hir/src/preproc.rs | 77 ++++++-- crates/ide/src/verilog_2005.rs | 53 ++++++ crates/preproc/src/source/model.rs | 118 ++++++++++--- crates/preproc/src/source/provenance.rs | 74 +++++++- crates/preproc/src/source/trace.rs | 27 ++- crates/preproc/src/source/types.rs | 11 ++ crates/slang/bindings/rust/ffi.rs | 11 ++ crates/slang/bindings/rust/ffi/wrapper.cc | 165 ++++++++++++++++-- crates/slang/bindings/rust/lib.rs | 30 ++++ crates/slang/bindings/rust/tests.rs | 75 ++++++++ .../include/slang/parsing/Preprocessor.h | 30 +++- crates/slang/source/parsing/Preprocessor.cpp | 10 +- .../source/parsing/Preprocessor_macros.cpp | 41 ++++- 13 files changed, 645 insertions(+), 77 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 5de2df05..0e00b751 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -2447,7 +2447,11 @@ fn map_token_provenance( .. } => { let call = mapped_macro_call(mapped, *call)?; - let (source, range) = map_mapped_source_range(mapped, *argument_token_range)?; + let Ok((source, range)) = map_mapped_source_range(mapped, *argument_token_range) else { + return Ok(TokenProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + ))); + }; TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } } SourceTokenProvenanceFact::TokenPaste { .. } @@ -3134,16 +3138,21 @@ endmodule recursive_macro_expansion_at(&db, TOP, offset(root_text, "`WRAP")).unwrap().unwrap(); assert_eq!(recursive.root_call.file_id, TOP); assert_eq!(text_at_range(root_text, recursive.root_call.range), "`WRAP"); - assert!(recursive.expansions.is_empty()); - assert!(matches!( - recursive.unavailable.as_slice(), - [MacroExpansionUnavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - ), - .. - }] - )); + assert!(recursive.unavailable.is_empty()); + assert_eq!(recursive.expansions.len(), 2); + let wrap_expansion = recursive + .expansions + .iter() + .find(|expansion| expansion.definition.name.as_str() == "WRAP") + .expect("outer expansion should be mapped"); + let leaf_expansion = recursive + .expansions + .iter() + .find(|expansion| expansion.definition.name.as_str() == "LEAF") + .expect("nested expansion should be mapped"); + assert_eq!(text_at_range(root_text, wrap_expansion.call.range), "`WRAP"); + assert_eq!(text_at_range(root_text, leaf_expansion.call.range), "`LEAF"); + assert_eq!(wrap_expansion.child_calls, vec![leaf_expansion.call.id]); } #[test] @@ -3207,6 +3216,52 @@ endmodule assert_eq!(generated.display_range, TextRange::new(6.into(), 15.into())); } + #[test] + fn preproc_maps_nested_actual_argument_macro_usage_without_dropping_expansion() { + let root_text = r#"`define PAYL payload_i +`define NEXT(x) ((x) + 12'd1) +module top(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`PAYL); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let payl = macro_reference_definitions_at(&db, TOP, offset_after(root_text, "`NEXT(")) + .unwrap() + .expect("nested actual-argument macro reference should be mapped"); + assert_eq!(text_at_range(root_text, payl.range), "`PAYL"); + assert!( + payl.definitions.iter().any(|definition| { + definition.file_id == TOP && definition.name.as_str() == "PAYL" + }) + ); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`NEXT")).unwrap().unwrap(); + let argument = provenance + .expansion + .call + .arguments + .iter() + .find(|argument| argument.argument_index == 0) + .expect("NEXT call should expose its written actual argument"); + assert_eq!(argument.source.as_ref().and_then(MappedPreprocSource::file_id), Some(TOP)); + assert_eq!(text_at_range(root_text, argument.range.unwrap()), "`PAYL"); + assert_eq!(argument.tokens, vec![SmolStr::new("`PAYL")]); + + let payload = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "payload_i") + .expect("expanded payload token should stay in NEXT expansion provenance"); + assert!(matches!( + payload.provenance, + TokenProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + )) + )); + } + #[test] fn preproc_numeric_literal_expansion_display_is_not_source_buffer() { let root_text = r#"`define ONE 12'd1 diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 563f4781..a492e923 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1549,6 +1549,59 @@ endmodule ); } +#[test] +fn preproc_macro_hover_keeps_nested_actual_argument_macro_reference() { + let text = r#" +`define /*marker:payl_def*/PAYL payload_i +`define MATH_ONE 12'd1 +`define DEMO_NEXT(value) ((value) + `MATH_ONE) +module top(input logic payload_i); + assign active_data = `/*marker:call*/DEMO_NEXT(`/*marker:payl*/PAYL); +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + let call_hover = analysis + .hover(position(file_id, &markers, "call"), HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("outer macro call hover expected"); + let call_info = call_hover.info.as_str(); + assert!( + call_info.contains("`` `DEMO_NEXT(value) ``") + && call_info.contains("`value` = `` `PAYL ``") + && call_info.contains("Expanded result") + && call_info.contains("payload_i") + && call_info.contains("12") + && call_info.contains("Expansion steps") + && call_info.contains("1. `` `DEMO_NEXT(`PAYL) `` from `` `DEMO_NEXT(value) ``"), + "outer macro hover should keep expansion and written argument spelling: {call_info}" + ); + + let payl_position = position(file_id, &markers, "payl"); + let payl_def_range = marked_range(&markers, "payl_def", TextSize::of("PAYL")); + let nav = analysis + .goto_definition(payl_position) + .unwrap() + .expect("nested actual-argument macro navigation expected"); + assert!( + nav.info.iter().any(|target| { + target.name.as_deref() == Some("PAYL") && target.focus_range == Some(payl_def_range) + }), + "PAYL should navigate to its macro definition: {nav:?}" + ); + + let payl_hover = analysis + .hover(payl_position, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("nested actual-argument macro hover expected"); + let payl_info = payl_hover.info.as_str(); + assert!( + payl_info.contains("Macro") && payl_info.contains("PAYL"), + "PAYL hover should identify the nested macro reference: {payl_info}" + ); +} + #[test] fn preproc_macro_hover_reports_unavailable_expansion() { let text = r#" diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 2c5eb851..d38d7700 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -812,6 +812,65 @@ endmodule assert_eq!(expansion.emitted_token_range.len, 1); } + #[test] + fn source_model_maps_nested_macro_usage_in_actual_argument_to_source_spelling() { + let root_text = r#"`define PAYL payload_i +`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`PAYL); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let next_usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("NEXT")) + .expect("outer function macro usage should be traced"); + let next_usage = &model.usages()[next_usage_index]; + assert_eq!(next_usage.arguments.len(), 1); + let next_argument_range = next_usage.arguments[0] + .argument_range + .expect("actual argument should keep written source range"); + assert_eq!(next_argument_range.source, root_source); + assert_eq!(text_at_range(root_text, next_argument_range.range), "`PAYL"); + assert_eq!( + next_usage.arguments[0] + .tokens + .iter() + .map(|token| token.raw.as_str()) + .collect::>(), + vec!["`PAYL"] + ); + + let next_reference = reference_for_usage(&model, next_usage_index); + let next_call = model + .macro_calls() + .iter() + .find(|call| call.reference == next_reference.id) + .expect("outer macro usage should create a call"); + assert_eq!(next_call.arguments[0].argument_range, Some(next_argument_range)); + assert!(matches!( + model.immediate_macro_expansion(next_call.id), + SourceMacroExpansionQuery::Available(_) + )); + + let payl_usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("PAYL")) + .expect("nested actual-argument macro usage should be traced"); + let payl_usage = &model.usages()[payl_usage_index]; + assert_eq!(payl_usage.range.source, root_source); + assert_eq!(text_at_range(root_text, payl_usage.range.range), "`PAYL"); + let payl_reference = reference_for_usage(&model, payl_usage_index); + let SourceMacroResolution::Resolved { definition, .. } = &payl_reference.resolution else { + panic!("PAYL usage should resolve through its runtime definition identity"); + }; + assert_eq!(model.macro_definitions().get(*definition).unwrap().name.as_str(), "PAYL"); + assert!(model.macro_calls().iter().any(|call| call.reference == payl_reference.id)); + } + #[test] fn source_model_uses_direct_definition_identity_when_body_ranges_collide() { let trace = PreprocessorTrace { @@ -829,6 +888,8 @@ endmodule range: Some(SourceBufferRange { buffer_id: 1, range: 0..12 }), macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(10)), macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "A".to_owned(), @@ -838,6 +899,7 @@ endmodule }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: vec![PreprocessorTraceToken { raw_text: "1".to_owned(), value_text: "1".to_owned(), @@ -853,6 +915,8 @@ endmodule range: Some(SourceBufferRange { buffer_id: 1, range: 13..25 }), macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(20)), macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "B".to_owned(), @@ -862,6 +926,7 @@ endmodule }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: vec![PreprocessorTraceToken { raw_text: "2".to_owned(), value_text: "2".to_owned(), @@ -877,6 +942,8 @@ endmodule range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), macro_definition_id: None, macro_call_id: Some(PreprocessorTraceMacroCallId(200)), + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "`B".to_owned(), @@ -886,6 +953,7 @@ endmodule }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: Vec::new(), expr_tokens: Vec::new(), disabled_ranges: Vec::new(), @@ -1052,8 +1120,12 @@ endmodule usages: vec![SourceMacroUsage { event_id: SourcePreprocEventId(1), identity: Some(SourceMacroCallKey::new(20)), + definition_identity: None, + expansion_identity: None, + parent_expansion_identity: None, name: Some(SmolStr::new("A")), name_range: Some(usage_range), + arguments: Vec::new(), range: usage_range, }], ..SourcePreprocIndex::default() @@ -1072,7 +1144,7 @@ endmodule } #[test] - fn source_model_keeps_nested_macro_identity_without_range_recovery() { + fn source_model_builds_nested_expansion_graph_from_runtime_usage_records() { let root_text = r#"`define LEAF 3 `define WRAP `LEAF module m; @@ -1099,31 +1171,25 @@ endmodule .find(|call| { let reference = model.macro_references().get(call.reference).unwrap(); reference.name.as_str() == "LEAF" - && matches!( - reference.site, - SourceMacroReferenceSite::ExpansionToken { emitted_token: _ } - ) + && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) }) - .expect("nested macro invocation should create an expansion-token call"); + .expect("nested macro invocation should create a runtime usage call"); let leaf_reference = model.macro_references().get(leaf_call.reference).unwrap(); assert_eq!(text_at_range(root_text, leaf_reference.name_range.range), "`LEAF"); + assert_eq!(leaf_call.parent_expansion_identity, wrap_call.expansion_identity); let SourceMacroExpansionQuery::Available(wrap_expansion_id) = model.immediate_macro_expansion(wrap_call.id) else { - assert!(matches!( - model.immediate_macro_expansion(wrap_call.id), - SourceMacroExpansionQuery::Unavailable( - SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - ) - )); - assert_eq!(wrap_call.expansion_identity, None); - assert!(matches!(model.capabilities().macro_expansions, CapabilityStatus::Partial)); - return; + panic!("outer macro should have an expansion identity from the runtime usage record"); }; - panic!( - "outer macro should not recover tokenless nested expansion by range: {wrap_expansion_id:?}" - ); + let wrap_expansion = model.macro_expansions().get(wrap_expansion_id).unwrap(); + assert_eq!(wrap_expansion.child_calls, vec![leaf_call.id]); + + let recursive = model.recursive_macro_expansion(wrap_call.id); + assert_eq!(recursive.expansions.len(), 2); + assert!(recursive.expansions.contains(&wrap_expansion_id)); + assert!(recursive.unavailable.is_empty()); } #[test] @@ -1142,12 +1208,9 @@ endmodule .find(|call| { let reference = model.macro_references().get(call.reference).unwrap(); reference.name.as_str() == "LEAF" - && matches!( - reference.site, - SourceMacroReferenceSite::ExpansionToken { emitted_token: _ } - ) + && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) }) - .expect("nested macro invocation should create an expansion-token call"); + .expect("nested macro invocation should create a runtime usage call"); assert!(leaf_call.identity.is_some()); assert!(leaf_call.expansion_identity.is_some()); assert!(leaf_call.parent_expansion_identity.is_some()); @@ -1250,6 +1313,8 @@ endmodule }), macro_definition_id: None, macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "A".to_owned(), @@ -1259,6 +1324,7 @@ endmodule }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: vec![PreprocessorTraceToken { raw_text: "1".to_owned(), value_text: "1".to_owned(), @@ -1277,6 +1343,8 @@ endmodule }), macro_definition_id: None, macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "`A".to_owned(), @@ -1289,6 +1357,7 @@ endmodule }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: Vec::new(), expr_tokens: Vec::new(), disabled_ranges: Vec::new(), @@ -1497,6 +1566,8 @@ logic [`LEAF_WIDTH-1:0] data; range: None, macro_definition_id: None, macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, directive: None, name: Some(PreprocessorTraceToken { raw_text: "WIDTH".to_owned(), @@ -1506,6 +1577,7 @@ logic [`LEAF_WIDTH-1:0] data; }), include_file_name: None, params: Vec::new(), + arguments: Vec::new(), body_tokens: Vec::new(), expr_tokens: Vec::new(), disabled_ranges: Vec::new(), diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 9b4f471b..9ef9888f 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -336,6 +336,7 @@ pub enum SourcePreprocUnavailable { MissingMacroCall { call: SourceMacroCallId }, MissingMacroExpansion { call: SourceMacroCallId }, MissingEmittedTokenMacroCall { source: PreprocSourceId }, + UnknownMacroUsageDefinitionIdentity { identity: SourceMacroDefinitionKey }, MissingEmittedTokenMacroCallIdentity, UnknownEmittedTokenMacroCallIdentity { identity: SourceMacroCallKey }, MissingEmittedTokenMacroDefinitionIdentity, @@ -904,7 +905,11 @@ impl<'a> SourcePreprocModelBuilder<'a> { }; let event_id = usage.event_id; let directive_range = usage.range; - let resolution = self.resolve_visible_reference(name.as_str()); + let definition_identity = usage.definition_identity; + let expansion_identity = usage.expansion_identity; + let parent_expansion_identity = usage.parent_expansion_identity; + let arguments = usage.arguments.clone(); + let resolution = self.resolve_usage_reference(name.as_str(), definition_identity); let reference = self.push_reference( event_id, SourceMacroReferenceSite::Usage { usage_index: directive.index }, @@ -913,7 +918,17 @@ impl<'a> SourcePreprocModelBuilder<'a> { directive_range, resolution.clone(), ); - self.push_call(reference, directive_range, resolution, usage.identity, None, None); + let call = self.push_call( + reference, + directive_range, + resolution, + usage.identity, + expansion_identity, + parent_expansion_identity, + ); + for argument in arguments { + self.record_macro_actual_argument(call, argument); + } } fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { @@ -1337,6 +1352,34 @@ impl<'a> SourcePreprocModelBuilder<'a> { call.arguments.sort_by_key(|argument| argument.argument_index); } + fn record_macro_actual_argument( + &mut self, + call: SourceMacroCallId, + argument: SourceMacroActualArgument, + ) { + let Some(call) = self.tables.macro_calls.get_mut(call) else { + return; + }; + if let Some(existing) = call + .arguments + .iter_mut() + .find(|existing| existing.argument_index == argument.argument_index) + { + existing.argument_range = + merge_optional_source_ranges(existing.argument_range, argument.argument_range); + if existing.tokens.is_empty() { + existing.tokens = argument.tokens; + } + return; + } + call.arguments.push(SourceMacroArgument { + argument_index: argument.argument_index, + argument_range: argument.argument_range, + tokens: argument.tokens, + }); + call.arguments.sort_by_key(|argument| argument.argument_index); + } + fn build_macro_expansion_graph(&mut self) { if self.tables.macro_calls.is_empty() { return; @@ -1578,6 +1621,23 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) } + fn resolve_usage_reference( + &mut self, + name: &str, + identity: Option, + ) -> SourceMacroResolution { + let Some(identity) = identity else { + return self.resolve_visible_reference(name); + }; + let Some(definition) = self.definition_ids_by_identity.get(&identity).copied() else { + self.references_partial = true; + return SourceMacroResolution::Unavailable( + SourcePreprocUnavailable::UnknownMacroUsageDefinitionIdentity { identity }, + ); + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + fn resolve_visible_reference_at_position( &mut self, name: &str, @@ -1813,3 +1873,13 @@ fn merge_source_ranges(existing: Option, next: SourceRange) -> Opti ), }) } + +fn merge_optional_source_ranges( + existing: Option, + next: Option, +) -> Option { + match next { + Some(next) => merge_source_ranges(existing, next), + None => existing, + } +} diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index c1653fa9..c6c79352 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -2,8 +2,8 @@ use std::collections::BTreeMap; use smol_str::{SmolStr, ToSmolStr}; use syntax::{ - PreprocessorTrace, PreprocessorTraceEmittedToken, PreprocessorTraceEvent, - PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, + PreprocessorTrace, PreprocessorTraceActualArgument, PreprocessorTraceEmittedToken, + PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, PreprocessorTraceMacroParam, PreprocessorTraceToken, PreprocessorTraceTokenProvenance, @@ -230,8 +230,21 @@ fn collect_trace_event( index.usages.push(SourceMacroUsage { event_id, identity: directive.macro_call_id.map(SourceMacroCallKey::from), + definition_identity: directive + .macro_definition_id + .map(SourceMacroDefinitionKey::from), + expansion_identity: directive.macro_expansion_id.map(SourceMacroExpansionKey::from), + parent_expansion_identity: directive + .parent_macro_expansion_id + .map(SourceMacroExpansionKey::from), name: directive.name.as_ref().map(|token| macro_name(token.value_text.as_str())), name_range: directive.name.as_ref().and_then(trace_token_range), + arguments: directive + .arguments + .into_iter() + .enumerate() + .map(macro_actual_argument_from_trace) + .collect(), range, }); push_source_event_record(index, event_id, kind, event_index, range); @@ -269,6 +282,16 @@ fn macro_param_from_trace(param: PreprocessorTraceMacroParam) -> SourceMacroPara } } +fn macro_actual_argument_from_trace( + (argument_index, argument): (usize, PreprocessorTraceActualArgument), +) -> SourceMacroActualArgument { + SourceMacroActualArgument { + argument_index, + argument_range: argument.range.as_ref().and_then(source_range_from_trace), + tokens: argument.tokens.into_iter().map(macro_token_from_trace).collect(), + } +} + fn macro_token_from_trace(token: PreprocessorTraceToken) -> SourceMacroToken { SourceMacroToken { raw: token.raw_text.to_smolstr(), diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index 0194fa26..fe195be8 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -186,11 +186,22 @@ pub struct SourceMacroConditional { pub struct SourceMacroUsage { pub event_id: SourcePreprocEventId, pub identity: Option, + pub definition_identity: Option, + pub expansion_identity: Option, + pub parent_expansion_identity: Option, pub name: Option, pub name_range: Option, + pub arguments: Vec, pub range: SourceRange, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroActualArgument { + pub argument_index: usize, + pub argument_range: Option, + pub tokens: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroToken { pub raw: SmolStr, diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index 0f40e10c..86b2a510 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -101,6 +101,12 @@ mod slang_ffi { range: RawSourceBufferRange, } + #[derive(Debug, Clone, PartialEq, Eq)] + struct RawPreprocessorTraceActualArgument { + tokens: Vec, + range: RawSourceBufferRange, + } + #[derive(Debug, Clone, PartialEq, Eq)] struct RawPreprocessorTraceEvent { event_id: u32, @@ -110,10 +116,15 @@ mod slang_ffi { has_macro_definition_id: bool, macro_call_id: u32, has_macro_call_id: bool, + macro_expansion_id: u32, + has_macro_expansion_id: bool, + parent_macro_expansion_id: u32, + has_parent_macro_expansion_id: bool, directive: RawPreprocessorTraceToken, name: RawPreprocessorTraceToken, include_file_name: RawPreprocessorTraceToken, params: Vec, + arguments: Vec, body_tokens: Vec, expr_tokens: Vec, disabled_ranges: Vec, diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index fd7721f2..a1a9077f 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -4,6 +4,7 @@ #include "slang/parsing/ParserMetadata.h" #include "slang/syntax/AllSyntax.h" +#include #include #include #include @@ -346,6 +347,21 @@ ::RawPreprocessorTraceToken to_rust_preprocessor_trace_token(slang::parsing::Tok return result; } +::RawPreprocessorTraceToken to_rust_preprocessor_trace_written_token( + slang::parsing::Token token, + const slang::SourceManager& sourceManager) { + auto result = empty_preprocessor_trace_token(); + if (!token) + return result; + + result.raw_text = rust::String(std::string(token.rawText())); + result.value_text = rust::String(std::string(token.valueText())); + result.token_kind = static_cast(token.kind); + result.range = to_rust_written_source_range(sourceManager, token.range()); + result.has_token = true; + return result; +} + template rust::Vec<::RawPreprocessorTraceToken> to_rust_preprocessor_trace_tokens( const TTokens& tokens) { @@ -355,6 +371,43 @@ rust::Vec<::RawPreprocessorTraceToken> to_rust_preprocessor_trace_tokens( return result; } +template +rust::Vec<::RawPreprocessorTraceToken> to_rust_preprocessor_trace_written_tokens( + const TTokens& tokens, + const slang::SourceManager& sourceManager) { + rust::Vec<::RawPreprocessorTraceToken> result; + for (auto token : tokens) + result.emplace_back(to_rust_preprocessor_trace_written_token(token, sourceManager)); + return result; +} + +template +::RawSourceBufferRange to_rust_written_token_range( + const TTokens& tokens, + const slang::SourceManager& sourceManager) { + std::optional<::RawSourceBufferRange> merged; + for (auto token : tokens) { + auto range = to_rust_written_source_range(sourceManager, token.range()); + if (!range.has_range) + continue; + + if (!merged) { + merged = range; + continue; + } + + if (merged->buffer_id != range.buffer_id) + return empty_source_buffer_range(); + + merged->range_start = std::min(merged->range_start, range.range_start); + merged->range_end = std::max(merged->range_end, range.range_end); + } + + if (merged && merged->range_start < merged->range_end) + return *merged; + return empty_source_buffer_range(); +} + ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( slang::parsing::Token token, const slang::SourceManager& sourceManager) { @@ -470,10 +523,9 @@ rust::Vec<::RawSourceBufferRange> to_rust_trace_disabled_ranges(const TTokens& t } // Directive syntax node ranges are payload ranges, not trace event ranges. For example, -// generated MacroUsageSyntax ignores the inherited directive token and is NoLocation when -// there are no actual args; EndIf/Else ranges are based on disabledTokens and can also be -// empty. The trace contract needs the event's own source span, so anchor every directive -// event at DirectiveSyntax::directive and extend only through that event's semantic payload. +// EndIf/Else ranges are based on disabledTokens and can also be empty. The trace contract +// needs the event's own source span, so anchor every directive event at +// DirectiveSyntax::directive and extend only through that event's semantic payload. slang::SourceRange trace_event_source_range(const slang::syntax::SyntaxNode& syntax) { auto* directiveSyntax = syntax.as_if(); if (!directiveSyntax) @@ -528,12 +580,6 @@ slang::SourceRange trace_event_source_range(const slang::syntax::SyntaxNode& syn case slang::syntax::SyntaxKind::ElseDirective: case slang::syntax::SyntaxKind::EndIfDirective: break; - case slang::syntax::SyntaxKind::MacroUsage: { - const auto& usage = syntax.as(); - if (usage.args) - extend(usage.args->sourceRange()); - break; - } default: extend(syntax.sourceRange()); break; @@ -556,6 +602,37 @@ ::RawPreprocessorTraceMacroParam to_rust_trace_macro_param( return result; } +::RawPreprocessorTraceActualArgument empty_preprocessor_trace_actual_argument() { + ::RawPreprocessorTraceActualArgument result; + result.tokens = rust::Vec<::RawPreprocessorTraceToken>(); + result.range = empty_source_buffer_range(); + return result; +} + +::RawPreprocessorTraceActualArgument to_rust_trace_actual_argument( + const slang::syntax::MacroActualArgumentSyntax& argument, + const slang::SourceManager& sourceManager) { + auto result = empty_preprocessor_trace_actual_argument(); + result.tokens = to_rust_preprocessor_trace_written_tokens(argument.tokens, sourceManager); + result.range = to_rust_written_token_range(argument.tokens, sourceManager); + return result; +} + +rust::Vec<::RawPreprocessorTraceActualArgument> to_rust_trace_actual_arguments( + const slang::syntax::MacroActualArgumentListSyntax* arguments, + const slang::SourceManager& sourceManager) { + rust::Vec<::RawPreprocessorTraceActualArgument> result; + if (!arguments) + return result; + + for (const auto* argument : arguments->args) { + if (!argument) + continue; + result.emplace_back(to_rust_trace_actual_argument(*argument, sourceManager)); + } + return result; +} + ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( const slang::syntax::SyntaxNode& syntax, uint32_t eventId, @@ -568,10 +645,15 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( directive.has_macro_definition_id = false; directive.macro_call_id = 0; directive.has_macro_call_id = false; + directive.macro_expansion_id = 0; + directive.has_macro_expansion_id = false; + directive.parent_macro_expansion_id = 0; + directive.has_parent_macro_expansion_id = false; directive.directive = empty_preprocessor_trace_token(); directive.name = empty_preprocessor_trace_token(); directive.include_file_name = empty_preprocessor_trace_token(); directive.params = rust::Vec<::RawPreprocessorTraceMacroParam>(); + directive.arguments = rust::Vec<::RawPreprocessorTraceActualArgument>(); directive.body_tokens = rust::Vec<::RawPreprocessorTraceToken>(); directive.expr_tokens = rust::Vec<::RawPreprocessorTraceToken>(); directive.disabled_ranges = rust::Vec<::RawSourceBufferRange>(); @@ -617,14 +699,6 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( directive.disabled_ranges = to_rust_trace_disabled_ranges(branch.disabledTokens); break; } - case slang::syntax::SyntaxKind::MacroUsage: { - const auto& usage = syntax.as(); - auto callId = preprocessor.getMacroCallId(usage); - directive.macro_call_id = callId; - directive.has_macro_call_id = callId != 0; - directive.name = to_rust_preprocessor_trace_token(usage.directive); - break; - } default: break; } @@ -632,6 +706,33 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( return directive; } +::RawPreprocessorTraceEvent to_rust_preprocessor_trace_macro_usage_record( + const slang::parsing::Preprocessor::MacroUsageTraceRecord& record, + uint32_t eventId, + const slang::SourceManager& sourceManager) { + ::RawPreprocessorTraceEvent directive; + directive.event_id = eventId; + directive.kind = static_cast(slang::syntax::SyntaxKind::MacroUsage); + directive.range = to_rust_written_source_range(sourceManager, record.range); + directive.macro_definition_id = record.definitionId; + directive.has_macro_definition_id = record.definitionId != 0; + directive.macro_call_id = record.callId; + directive.has_macro_call_id = record.callId != 0; + directive.macro_expansion_id = record.expansionId; + directive.has_macro_expansion_id = record.expansionId != 0; + directive.parent_macro_expansion_id = record.parentExpansionId; + directive.has_parent_macro_expansion_id = record.parentExpansionId != 0; + directive.directive = to_rust_preprocessor_trace_written_token(record.directive, sourceManager); + directive.name = directive.directive; + directive.include_file_name = empty_preprocessor_trace_token(); + directive.params = rust::Vec<::RawPreprocessorTraceMacroParam>(); + directive.arguments = to_rust_trace_actual_arguments(record.actualArgs, sourceManager); + directive.body_tokens = rust::Vec<::RawPreprocessorTraceToken>(); + directive.expr_tokens = rust::Vec<::RawPreprocessorTraceToken>(); + directive.disabled_ranges = rust::Vec<::RawSourceBufferRange>(); + return directive; +} + std::optional mapSourceRangeToContext( const slang::DiagnosticEngine& engine, slang::SourceLocation context, @@ -1126,9 +1227,30 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( if (!sourceBufferIds.contains(bufferId)) predefineBufferIds.insert(bufferId); } + + for (auto* define : preprocessor.getDefinedMacros()) { + if (!define) + continue; + auto location = define->directive.location(); + if (!location.valid() || !predefineBufferIds.contains(location.buffer().getId())) + continue; + + auto eventId = static_cast(result.events.size()); + result.events.emplace_back(to_rust_preprocessor_trace_event(*define, eventId, preprocessor)); + } + preprocessor.pushSource(rootBuffer); std::unordered_map includeEventIdsByLocation; + size_t macroUsageTraceRecordCount = 0; + auto flushMacroUsageTraceRecords = [&]() { + auto records = preprocessor.getMacroUsageTraceRecords(); + for (; macroUsageTraceRecordCount < records.size(); macroUsageTraceRecordCount++) { + auto eventId = static_cast(result.events.size()); + result.events.emplace_back(to_rust_preprocessor_trace_macro_usage_record( + records[macroUsageTraceRecordCount], eventId, sourceManager)); + } + }; while (true) { auto token = preprocessor.next(); @@ -1137,15 +1259,20 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( continue; if (auto* syntax = trivia.syntax()) { + if (syntax->kind == slang::syntax::SyntaxKind::MacroUsage) + continue; + auto eventId = static_cast(result.events.size()); if (syntax->kind == slang::syntax::SyntaxKind::IncludeDirective) { const auto& include = syntax->as(); if (auto key = trace_source_location_key(include.directive.location())) includeEventIdsByLocation.emplace(*key, eventId); } - result.events.emplace_back(to_rust_preprocessor_trace_event(*syntax, eventId, preprocessor)); + auto event = to_rust_preprocessor_trace_event(*syntax, eventId, preprocessor); + result.events.emplace_back(std::move(event)); } } + flushMacroUsageTraceRecords(); if (token.kind == slang::parsing::TokenKind::EndOfFile) break; diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index a574307a..5bc67a0d 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -164,10 +164,13 @@ pub struct PreprocessorTraceEvent { pub range: Option, pub macro_definition_id: Option, pub macro_call_id: Option, + pub macro_expansion_id: Option, + pub parent_macro_expansion_id: Option, pub directive: Option, pub name: Option, pub include_file_name: Option, pub params: Vec, + pub arguments: Vec, pub body_tokens: Vec, pub expr_tokens: Vec, pub disabled_ranges: Vec, @@ -237,6 +240,12 @@ pub struct PreprocessorTraceMacroParam { pub range: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceActualArgument { + pub tokens: Vec, + pub range: Option, +} + #[derive(Clone, Copy)] pub struct SyntaxTrivia<'a> { _ptr: Pin<&'a ffi::SyntaxTrivia>, @@ -402,10 +411,21 @@ impl PreprocessorTraceEvent { macro_call_id: raw .has_macro_call_id .then_some(PreprocessorTraceMacroCallId(raw.macro_call_id)), + macro_expansion_id: raw + .has_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.macro_expansion_id)), + parent_macro_expansion_id: raw + .has_parent_macro_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.parent_macro_expansion_id)), directive: PreprocessorTraceToken::from_raw(raw.directive), name: PreprocessorTraceToken::from_raw(raw.name), include_file_name: PreprocessorTraceToken::from_raw(raw.include_file_name), params: raw.params.into_iter().map(PreprocessorTraceMacroParam::from_raw).collect(), + arguments: raw + .arguments + .into_iter() + .map(PreprocessorTraceActualArgument::from_raw) + .collect(), body_tokens: raw .body_tokens .into_iter() @@ -1483,6 +1503,16 @@ impl hash::Hash for SyntaxNode<'_> { } } +impl PreprocessorTraceActualArgument { + #[inline] + fn from_raw(raw: ffi::RawPreprocessorTraceActualArgument) -> Self { + Self { + tokens: raw.tokens.into_iter().filter_map(PreprocessorTraceToken::from_raw).collect(), + range: SourceBufferRange::from_raw(raw.range), + } + } +} + impl SyntaxTree { #[inline] pub fn from_text(text: &str, name: &str, path: &str) -> SyntaxTree { diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index 7f8a811b..f12fcee4 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1308,6 +1308,56 @@ endmodule } } +#[test] +fn preprocessor_trace_reports_nested_macro_usage_in_actual_argument() { + let source = r#"`define PAYL payload_i +`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`PAYL); +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include macro usage events"); + + let next = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`NEXT") + }) + .expect("outer NEXT usage should be traced by the preprocessor runtime"); + assert!(next.macro_definition_id.is_some()); + assert!(next.macro_call_id.is_some()); + assert!(next.macro_expansion_id.is_some()); + assert_eq!(next.parent_macro_expansion_id, None); + assert_eq!(&source[next.range.as_ref().unwrap().range.clone()], "`NEXT(`PAYL)"); + assert_eq!(next.arguments.len(), 1); + assert_eq!(&source[next.arguments[0].range.as_ref().unwrap().range.clone()], "`PAYL"); + assert_eq!( + next.arguments[0].tokens.iter().map(|token| token.raw_text.as_str()).collect::>(), + vec!["`PAYL"] + ); + + let payl = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`PAYL") + }) + .expect("nested PAYL usage in the actual argument should be traced"); + assert!(payl.macro_definition_id.is_some()); + assert!(payl.macro_call_id.is_some()); + assert!(payl.macro_expansion_id.is_some()); + assert_eq!(&source[payl.range.as_ref().unwrap().range.clone()], "`PAYL"); +} + #[test] fn preprocessor_trace_reports_escaped_identifier_macro_body_identity() { let source = concat!( @@ -1407,6 +1457,31 @@ endmodule }) .expect("configured predefine source buffer should expose materialized text"); + let predefine_event = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::DEFINE_DIRECTIVE + && event.name.as_ref().is_some_and(|token| token.value_text == "FROM_API") + }) + .expect("configured predefine should be traced as a define event"); + assert_eq!( + predefine_event.range.as_ref().map(|range| range.buffer_id), + Some(predefine_source.buffer_id) + ); + let predefine_definition_id = + predefine_event.macro_definition_id.expect("predefine should carry definition identity"); + + let predefine_usage = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|token| token.value_text == "`FROM_API") + }) + .expect("configured predefine usage should be traced as a runtime macro usage"); + assert_eq!(predefine_usage.macro_definition_id, Some(predefine_definition_id)); + let from_api = trace .emitted_tokens .iter() diff --git a/crates/slang/include/slang/parsing/Preprocessor.h b/crates/slang/include/slang/parsing/Preprocessor.h index c1a39ffc..73e0cf36 100644 --- a/crates/slang/include/slang/parsing/Preprocessor.h +++ b/crates/slang/include/slang/parsing/Preprocessor.h @@ -155,8 +155,22 @@ class SLANG_EXPORT Preprocessor { /// Gets the frontend identity assigned to a macro definition syntax node. uint32_t getMacroDefinitionId(const syntax::DefineDirectiveSyntax& syntax) const; - /// Gets the frontend identity assigned to a macro usage syntax node. - uint32_t getMacroCallId(const syntax::MacroUsageSyntax& syntax) const; + /// A macro usage observed by the preprocessor while expanding source tokens. + struct MacroUsageTraceRecord { + Token directive; + syntax::MacroActualArgumentListSyntax* actualArgs = nullptr; + SourceRange range; + uint32_t callId = 0; + uint32_t definitionId = 0; + uint32_t expansionId = 0; + uint32_t parentExpansionId = 0; + }; + + /// Gets all macro usages observed while preprocessing, including usages expanded from + /// macro replacement lists that do not become directive trivia in the parsed token stream. + std::span getMacroUsageTraceRecords() const { + return macroUsageTraceRecords; + } private: Preprocessor(const Preprocessor& other); @@ -287,6 +301,8 @@ class SLANG_EXPORT Preprocessor { SourceRange getRange() const; const SourceManager::MacroExpansionMetadata& getMetadata() const { return metadata; } + uint32_t getExpansionId() const { return expansionId; } + void setExpansionLoc(SourceLocation location); SourceManager::MacroTokenProvenance tokenProvenance( uint32_t bodyTokenIndex, uint32_t argumentIndex = SourceManager::MacroTokenProvenance::InvalidIndex, @@ -309,18 +325,22 @@ class SLANG_EXPORT Preprocessor { bool any = false; bool isTopLevel = false; SourceManager::MacroExpansionMetadata metadata; + uint32_t expansionId = 0; }; // Macro handling methods MacroDef findMacro(Token directive); - std::pair handleTopLevelMacro( - Token directive, uint32_t* callId = nullptr); + std::pair handleTopLevelMacro(Token directive); bool expandMacro(MacroDef macro, MacroExpansion& expansion, syntax::MacroActualArgumentListSyntax* actualArgs); bool expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& expansion); bool expandReplacementList(std::span& tokens, SmallSet& alreadyExpanded); bool applyMacroOps(std::span tokens, SmallVectorBase& dest); + void recordMacroUsageTrace(Token directive, syntax::MacroActualArgumentListSyntax* actualArgs, + MacroDef macro, + const SourceManager::MacroExpansionMetadata& metadata, + uint32_t expansionId); void createBuiltInMacro(std::string_view name, int value, std::string_view valueStr = {}); void splitTokens(Token sourceToken, size_t offset, SmallVectorBase& results); Token getLastConsumed() const { return lastConsumed; } @@ -414,7 +434,7 @@ class SLANG_EXPORT Preprocessor { // map from macro name to macro definition flat_hash_map macros; flat_hash_map macroDefinitionIds; - flat_hash_map macroCallIds; + std::vector macroUsageTraceRecords; uint32_t nextMacroDefinitionId = 1; uint32_t nextMacroCallId = 1; diff --git a/crates/slang/source/parsing/Preprocessor.cpp b/crates/slang/source/parsing/Preprocessor.cpp index f7984475..c0b1e95f 100644 --- a/crates/slang/source/parsing/Preprocessor.cpp +++ b/crates/slang/source/parsing/Preprocessor.cpp @@ -220,11 +220,6 @@ uint32_t Preprocessor::getMacroDefinitionId(const DefineDirectiveSyntax& syntax) return it == macroDefinitionIds.end() ? 0 : it->second; } -uint32_t Preprocessor::getMacroCallId(const MacroUsageSyntax& syntax) const { - auto it = macroCallIds.find(&syntax); - return it == macroCallIds.end() ? 0 : it->second; -} - uint32_t Preprocessor::allocateMacroDefinitionId(const DefineDirectiveSyntax* syntax) { if (!syntax) return 0; @@ -720,13 +715,10 @@ Trivia Preprocessor::handleDefineDirective(Token directive) { std::pair Preprocessor::handleMacroUsage(Token directive) { // delegate to a nested function to simplify the error handling paths inMacroBody = true; - uint32_t callId = 0; - auto [actualArgs, extraTrivia] = handleTopLevelMacro(directive, &callId); + auto [actualArgs, extraTrivia] = handleTopLevelMacro(directive); inMacroBody = false; auto syntax = alloc.emplace(directive, actualArgs); - if (callId != 0) - macroCallIds.emplace(syntax, callId); return std::make_pair(Trivia(TriviaKind::Directive, syntax), extraTrivia); } diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index b08bdf8c..4bfc9bcb 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -58,10 +58,7 @@ void Preprocessor::createBuiltInMacro(std::string_view name, int value, std::str } std::pair Preprocessor::handleTopLevelMacro( - Token directive, uint32_t* callId) { - if (callId) - *callId = 0; - + Token directive) { auto macro = findMacro(directive); if (!macro.valid()) { if (options.ignoreDirectives.find(directive.valueText().substr(1)) != @@ -102,12 +99,11 @@ std::pair Preprocessor::handleTopLevelMa metadata.definitionId = macro.definitionId; if (sourceManager.isMacroLoc(directive.location())) metadata.parentExpansionId = directive.location().buffer().getId(); - if (callId) - *callId = metadata.callId; MacroExpansion expansion{sourceManager, alloc, buffer, directive, true, metadata}; if (!expandMacro(macro, expansion, actualArgs)) return {actualArgs, Trivia()}; + recordMacroUsageTrace(directive, actualArgs, macro, metadata, expansion.getExpansionId()); // The macro is now expanded out into tokens, but some of those tokens might // be more macros that need to be expanded, or special characters that @@ -416,6 +412,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansion.getRange(), macroName, expansion.getMetadata()); + expansion.setExpansionLoc(expansionLoc); // simple macro; just take body tokens uint32_t bodyTokenIndex = 0; @@ -481,6 +478,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, macroName, expansion.getMetadata()); + expansion.setExpansionLoc(expansionLoc); auto append = [&](Token token, uint32_t bodyTokenIndex) { expansion.append(token, expansionLoc, start, expansionRange, false, @@ -665,6 +663,11 @@ SourceRange Preprocessor::MacroExpansion::getRange() const { return {usageSite.location(), usageSite.location() + usageSite.rawText().length()}; } +void Preprocessor::MacroExpansion::setExpansionLoc(SourceLocation location) { + if (location.valid()) + expansionId = location.buffer().getId(); +} + SourceManager::MacroTokenProvenance Preprocessor::MacroExpansion::tokenProvenance( uint32_t bodyTokenIndex, uint32_t argumentIndex, uint32_t argumentTokenIndex) const { SourceManager::MacroTokenProvenance provenance; @@ -775,6 +778,7 @@ bool Preprocessor::expandReplacementList( MacroExpansion expansion{sourceManager, alloc, expansionBuffer, token, false, metadata}; if (!expandMacro(macro, expansion, actualArgs)) return false; + recordMacroUsageTrace(token, actualArgs, macro, metadata, expansion.getExpansionId()); // Recursively expand out nested macros; this ensures that we detect // any potentially recursive macros. @@ -799,6 +803,7 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp auto macroLoc = sourceManager.createExpansionLoc( loc, expansion.getRange(), SourceManager::MacroExpansionKind::Body, expansion.getMetadata()); + expansion.setExpansionLoc(macroLoc); SmallVector text; switch (intrinsic) { case MacroIntrinsic::File: { @@ -828,6 +833,30 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp return true; } +void Preprocessor::recordMacroUsageTrace( + Token directive, MacroActualArgumentListSyntax* actualArgs, MacroDef macro, + const SourceManager::MacroExpansionMetadata& metadata, uint32_t expansionId) { + if (metadata.callId == 0) + return; + + SourceRange range = {directive.location(), directive.location() + directive.rawText().length()}; + if (actualArgs) { + Token last = actualArgs->getLastToken(); + if (last) + range = {directive.location(), last.location() + last.rawText().length()}; + } + + macroUsageTraceRecords.push_back(MacroUsageTraceRecord{ + directive, + actualArgs, + range, + metadata.callId, + macro.definitionId, + expansionId, + metadata.parentExpansionId, + }); +} + bool Preprocessor::MacroDef::needsArgs() const { return syntax && syntax->formalArguments; } From eb1e6713da37f8da20eab019c53e80962927d28a Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 17:52:38 +0800 Subject: [PATCH 32/80] fix(hir): preserve ambiguous macro expansion contexts --- crates/hir/src/preproc.rs | 180 +++++++++++++++++++++++++------------- 1 file changed, 119 insertions(+), 61 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 0e00b751..bd452c8a 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1165,28 +1165,24 @@ pub fn immediate_macro_expansion_at( offset: TextSize, ) -> PreprocResult> { let mut queries = macro_expansion_queries_at(db, file_id, offset)?; - let available = queries - .iter() - .filter_map(|query| match query { - MacroExpansionQuery::Available(expansion) => Some(expansion.as_ref().clone()), - MacroExpansionQuery::Ambiguous(expansions) => Some(expansions.first()?.clone()), - MacroExpansionQuery::Unavailable(_) => None, - }) - .collect::>(); - if available.len() > 1 { - return Ok(Some(MacroExpansionQuery::Ambiguous(available))); - } - if available.len() == 1 { - return Ok(Some(MacroExpansionQuery::Available(Box::new( - available.into_iter().next().unwrap(), - )))); - } match queries.len() { 0 => Ok(None), 1 => Ok(queries.pop()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }), + contexts => { + let available = queries + .iter() + .filter_map(|query| match query { + MacroExpansionQuery::Available(expansion) => Some(expansion.as_ref().clone()), + MacroExpansionQuery::Ambiguous(_) | MacroExpansionQuery::Unavailable(_) => None, + }) + .collect::>(); + if available.len() == contexts { + return Ok(Some(MacroExpansionQuery::Ambiguous(available))); + } + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }) + } } } @@ -1208,11 +1204,10 @@ pub fn macro_expansion_queries_at( continue; } }; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - continue; - }; - let query = immediate_macro_expansion_for_call(mapped, call_fact)?; - push_unique_macro_expansion_query(&mut queries, query); + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let query = immediate_macro_expansion_for_call(mapped, call_fact)?; + push_unique_macro_expansion_query(&mut queries, query); + } } if !queries.is_empty() { @@ -1256,11 +1251,10 @@ pub fn recursive_macro_expansions_at( continue; } }; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - continue; - }; - let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; - push_unique_recursive_macro_expansion(&mut expansions, recursive); + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; + push_unique_recursive_macro_expansion(&mut expansions, recursive); + } } if !expansions.is_empty() { @@ -1289,11 +1283,10 @@ pub fn recursive_macro_expansion_provenances_at( continue; } }; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - continue; - }; - let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; - push_unique_recursive_macro_expansion_provenance(&mut expansions, recursive); + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; + push_unique_recursive_macro_expansion_provenance(&mut expansions, recursive); + } } if !expansions.is_empty() { @@ -1325,6 +1318,7 @@ pub fn macro_expansion_provenances_at( offset: TextSize, ) -> PreprocResult> { let mut provenances = Vec::new(); + let mut unavailable = Vec::new(); let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); for model_file_id in contexts.model_file_ids.iter().copied() { @@ -1336,14 +1330,19 @@ pub fn macro_expansion_provenances_at( continue; } }; - let Some(call_fact) = source_macro_call_at(mapped, file_id, offset) else { - continue; - }; - if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { - push_unique_macro_expansion_provenance(&mut provenances, provenance); + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + match macro_expansion_provenance_for_call(mapped, call_fact)? { + MacroExpansionProvenanceForCall::Available(provenance) => { + push_unique_macro_expansion_provenance(&mut provenances, provenance); + } + MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), + } } } + if !unavailable.is_empty() { + return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); + } if !provenances.is_empty() { return Ok(provenances); } @@ -1373,6 +1372,7 @@ pub fn macro_expansion_provenances_for_range( range: TextRange, ) -> PreprocResult> { let mut provenances = Vec::new(); + let mut unavailable = Vec::new(); let mut ambiguous_contexts = 0; let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); @@ -1388,11 +1388,12 @@ pub fn macro_expansion_provenances_for_range( let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); match call_facts.as_slice() { [] => continue, - [call_fact] => { - if let Some(provenance) = macro_expansion_provenance_for_call(mapped, call_fact)? { + [call_fact] => match macro_expansion_provenance_for_call(mapped, call_fact)? { + MacroExpansionProvenanceForCall::Available(provenance) => { push_unique_macro_expansion_provenance(&mut provenances, provenance); } - } + MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), + }, call_facts => { ambiguous_contexts += call_facts.len(); } @@ -1402,10 +1403,13 @@ pub fn macro_expansion_provenances_for_range( if ambiguous_contexts > 0 { return Err(PreprocError::Unavailable { reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { - contexts: ambiguous_contexts + provenances.len(), + contexts: ambiguous_contexts + provenances.len() + unavailable.len(), }, }); } + if !unavailable.is_empty() { + return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); + } if !provenances.is_empty() { return Ok(provenances); } @@ -1414,6 +1418,19 @@ pub fn macro_expansion_provenances_for_range( Ok(Vec::new()) } +fn unavailable_or_ambiguous_macro_expansion_provenance( + available_contexts: usize, + mut unavailable: Vec, +) -> PreprocResult> { + let contexts = available_contexts + unavailable.len(); + if contexts == 1 { + return Err(PreprocError::Unavailable { reason: unavailable.pop().unwrap() }); + } + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }) +} + pub fn diagnostic_provenance_for_range( db: &dyn SourceRootDb, file_id: FileId, @@ -2228,17 +2245,22 @@ fn display_only_virtual_expansion_unavailable(source: &MappedPreprocSource) -> P } } -fn source_macro_call_at( +fn source_macro_calls_at( mapped: &MappedSourcePreprocModel, file_id: FileId, offset: TextSize, -) -> Option<&SourceMacroCallFact> { - mapped.model.macro_calls().iter().find(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) && range.contains(offset) - }) +) -> Vec<&SourceMacroCallFact> { + mapped + .model + .macro_calls() + .iter() + .filter(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == Some(file_id) && range.contains(offset) + }) + .collect() } fn source_macro_calls_intersecting_range( @@ -2373,19 +2395,32 @@ fn diagnostic_provenance_for_call( } } +enum MacroExpansionProvenanceForCall { + Available(MacroExpansionProvenance), + Unavailable(PreprocUnavailable), +} + fn macro_expansion_provenance_for_call( mapped: &MappedSourcePreprocModel, call_fact: &SourceMacroCallFact, -) -> PreprocResult> { - let SourceMacroExpansionQueryFact::Available(expansion_id) = - mapped.model.immediate_macro_expansion(call_fact.id) - else { - return Ok(None); - }; - let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { - return Ok(None); - }; - Ok(Some(macro_expansion_provenance_for_expansion(mapped, expansion)?)) +) -> PreprocResult { + Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion_id) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { + return Ok(MacroExpansionProvenanceForCall::Unavailable( + PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroExpansion { + call: call_fact.id, + }), + )); + }; + MacroExpansionProvenanceForCall::Available(macro_expansion_provenance_for_expansion( + mapped, expansion, + )?) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + MacroExpansionProvenanceForCall::Unavailable(PreprocUnavailable::Source(reason)) + } + }) } fn macro_expansion_provenance_for_expansion( @@ -3260,6 +3295,27 @@ endmodule SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance )) )); + + let payl_offset = offset(root_text, "`PAYL"); + let queries = macro_expansion_queries_at(&db, TOP, payl_offset).unwrap(); + assert!(queries.iter().any(|query| matches!( + query, + MacroExpansionQuery::Available(expansion) + if expansion.definition.name.as_str() == "NEXT" + ))); + assert!(queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); + assert!(matches!( + immediate_macro_expansion_at(&db, TOP, payl_offset), + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + }) + )); + assert!(matches!( + macro_expansion_provenance_at(&db, TOP, payl_offset), + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + }) + )); } #[test] @@ -3450,6 +3506,7 @@ endmodule DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance | SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + | SourcePreprocUnavailable::ExpansionAuthorityUnavailable )) ), "stringification should be unsupported or unavailable, got {provenance:?}" @@ -3486,6 +3543,7 @@ endmodule provenance, DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + | SourcePreprocUnavailable::ExpansionAuthorityUnavailable )) )); } From 4e86f7fc97c774197be4916d3fa61d81219f640c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 18:30:13 +0800 Subject: [PATCH 33/80] feat(ide): compact macro expansion hover --- crates/hir/src/preproc.rs | 3 + crates/ide/src/hover.rs | 306 +++++++++++++++++++-------------- crates/ide/src/verilog_2005.rs | 105 ++++++----- 3 files changed, 248 insertions(+), 166 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index bd452c8a..7ce485c9 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -161,6 +161,7 @@ pub struct MacroDefinition { pub file_id: FileId, pub name: SmolStr, pub params: Option>, + pub body_tokens: Vec, pub define_index: usize, pub event_id: u32, pub directive_range: TextRange, @@ -718,6 +719,7 @@ fn configured_predefine_definition( file_id, name: predefine_name, params: None, + body_tokens: Vec::new(), define_index: CONFIGURED_PREDEFINE_DEFINE_INDEX, event_id: CONFIGURED_PREDEFINE_EVENT_ID, directive_range: source.range, @@ -2007,6 +2009,7 @@ fn map_macro_definition( capability: capability_status(&mapped.model.capabilities().definition_name_ranges), name: definition.name.clone(), params, + body_tokens: definition.body_tokens.iter().map(|token| token.raw.clone()).collect(), define_index: define_index_for_definition(mapped, definition)?, event_id: definition.event_id.raw(), directive_range, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 4613eedb..af8fb757 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -1,11 +1,13 @@ use hir::{ - base_db::source_db::{SourceDb, SourceRootDb}, + base_db::{ + source_db::{SourceDb, SourceRootDb}, + source_root::SourceRootRole, + }, container::InContainer, file::HirFileId, hir_def::expr::Expr, preproc::{ - EmittedTokenProvenance, IncludeTarget, MacroArgument, MacroDefinition, - MacroExpansionProvenance, MacroExpansionUnavailable, MacroParamDefinition, + EmittedTokenProvenance, IncludeTarget, MacroDefinition, MacroParamDefinition, RecursiveMacroExpansionProvenance, include_directives_at, macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, macro_reference_definitions_at, recursive_macro_expansion_provenances_at, @@ -32,6 +34,13 @@ use crate::{ source_tokens::{PreprocTokenSelection, SourceTokenSelection}, }; +const MACRO_EXPANSION_SEPARATOR: &str = "--------------------"; + +struct MacroSourceLink { + label: String, + target: String, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HoverFormat { Markdown, @@ -49,7 +58,7 @@ pub(crate) fn hover( _config: HoverConfig, ) -> Option> { if let Some(macro_hover) = handle_preproc_macro(db, file_id, offset) { - return Some(with_expanded_macro_hover(db, file_id, offset, macro_hover)); + return Some(expanded_macro_hover(db, file_id, offset).unwrap_or(macro_hover)); } if let Some(include) = handle_preproc_include(db, file_id, offset) { @@ -176,8 +185,26 @@ fn expanded_macro_hover( file_id: FileId, offset: TextSize, ) -> Option> { + let reference_ids = macro_reference_definitions_at(db, file_id, offset) + .ok() + .flatten()? + .references + .into_iter() + .map(|reference| reference.id) + .collect::>(); + if reference_ids.is_empty() { + return None; + } + let expansions = recursive_macro_expansion_provenances_at(db, file_id, offset).ok().unwrap_or_default(); + let expansions = expansions + .into_iter() + .filter(|expansion| { + reference_ids.contains(&expansion.root_call.reference_id) + && !expansion.expansions.is_empty() + }) + .collect::>(); if expansions.is_empty() { return None; } @@ -190,14 +217,8 @@ fn expanded_macro_hover( fn expanded_macro_markup(db: &RootDb, expansions: &[RecursiveMacroExpansionProvenance]) -> Markup { let mut markup = Markup::new(); - markup.print("Macro expansion"); - for (index, expansion) in expansions.iter().enumerate() { - if expansions.len() > 1 { - markup.newline(); - markup.print("Context "); - markup.print(&(index + 1).to_string()); - } + for expansion in expansions { render_recursive_expansion(db, &mut markup, expansion); } @@ -210,105 +231,30 @@ fn render_recursive_expansion( expansion: &RecursiveMacroExpansionProvenance, ) { let Some(root) = expansion.expansions.first() else { - render_unavailable_expansion(db, markup, &expansion.unavailable); return; }; - markup.newline(); - markup.print("Signature"); - markup.newline(); - render_signature_line(db, markup, &root.expansion.definition); - - if !root.expansion.call.arguments.is_empty() { + if !markup.is_empty() { markup.newline(); - markup.print("Arguments"); - render_arguments(db, markup, &root.expansion.definition, &root.expansion.call.arguments); } - - markup.newline(); - markup.print("Expanded result"); - markup.newline(); + markup.push_with_code_fence(¯o_definition_line(&root.expansion.definition)); + render_macro_expansion_separator(markup); + render_macro_expansion_label(markup); markup.push_with_code_fence(&expanded_text_from_tokens(&root.tokens)); - - markup.print("Expansion steps"); - for (index, step) in expansion.expansions.iter().enumerate() { - render_expansion_step(db, markup, index + 1, step); - } - render_unavailable_expansion(db, markup, &expansion.unavailable); + render_macro_expansion_separator(markup); + render_macro_source_link(db, markup, &root.expansion.definition, root.expansion.call.file_id); } -fn render_signature_line(db: &RootDb, markup: &mut Markup, definition: &MacroDefinition) { - markup.push_with_backticks(¯o_signature(definition)); - if let Some(source) = macro_definition_source_label(db, definition) { - markup.print(" from "); - markup.push_with_backticks(&source); - } -} - -fn render_arguments( - db: &RootDb, - markup: &mut Markup, - definition: &MacroDefinition, - arguments: &[MacroArgument], -) { - for argument in arguments { - markup.print("\n- "); - let name = definition - .params - .as_ref() - .and_then(|params| params.get(argument.argument_index)) - .and_then(|param| param.name.as_ref()) - .map_or_else(|| format!("${}", argument.argument_index), ToString::to_string); - markup.push_with_backticks(&name); - markup.print(" = "); - markup.push_with_backticks(&argument_text(db, argument)); - } -} - -fn render_expansion_step( - db: &RootDb, - markup: &mut Markup, - index: usize, - provenance: &MacroExpansionProvenance, -) { +fn render_macro_expansion_label(markup: &mut Markup) { markup.newline(); - if let Some(call_text) = - text_at_file_range(db, provenance.expansion.call.file_id, provenance.expansion.call.range) - { - markup.print(&index.to_string()); - markup.print(". "); - markup.push_with_backticks(call_text.trim()); - markup.print(" from "); - markup.push_with_backticks(¯o_signature(&provenance.expansion.definition)); - if let Some(source) = macro_definition_source_label(db, &provenance.expansion.definition) { - markup.print(" in "); - markup.push_with_backticks(&source); - } - } else { - markup.print(&index.to_string()); - markup.print(". Expansion from "); - markup.push_with_backticks(¯o_signature(&provenance.expansion.definition)); - } + markup.print("Expands to"); markup.newline(); - markup.push_with_code_fence(&expanded_text_from_tokens(&provenance.tokens)); } -fn render_unavailable_expansion( - db: &RootDb, - markup: &mut Markup, - unavailable: &[MacroExpansionUnavailable], -) { - for unavailable in unavailable { - markup.newline(); - if let Some(call_text) = - text_at_file_range(db, unavailable.call.file_id, unavailable.call.range) - { - markup.push_with_backticks(call_text.trim()); - markup.print(" expansion unavailable."); - } else { - markup.print("Expansion unavailable."); - } - } +fn render_macro_expansion_separator(markup: &mut Markup) { + markup.newline(); + markup.print(MACRO_EXPANSION_SEPARATOR); + markup.newline(); } fn macro_signature(definition: &MacroDefinition) -> String { @@ -326,28 +272,100 @@ fn macro_signature(definition: &MacroDefinition) -> String { signature } -fn macro_definition_source_label(db: &RootDb, definition: &MacroDefinition) -> Option { +fn macro_definition_line(definition: &MacroDefinition) -> String { + let mut line = String::from("`define "); + line.push_str(¯o_signature(definition)); + let body = macro_definition_body_text(definition); + if !body.is_empty() { + line.push(' '); + line.push_str(&body); + } + line +} + +fn macro_definition_source_link( + db: &RootDb, + definition: &MacroDefinition, + anchor_file_id: FileId, +) -> Option { match &definition.source { hir::preproc::MappedPreprocSource::RealFile { file_id } => { - db.file_path(*file_id).map(|path| path.to_string()).or_else(|| { - db.source_root(db.source_root_id(*file_id)) - .path_for_file(file_id) - .map(|path| path.to_string()) - }) + macro_file_source_link(db, *file_id, anchor_file_id) } hir::preproc::MappedPreprocSource::VirtualFile { .. } | hir::preproc::MappedPreprocSource::VirtualDisplay { .. } => None, } } -fn argument_text(db: &RootDb, argument: &MacroArgument) -> String { - if let (Some(source), Some(range)) = (&argument.source, argument.range) - && let Some(file_id) = source.file_id() - && let Some(text) = text_at_file_range(db, file_id, range) +fn macro_file_source_link( + db: &RootDb, + file_id: FileId, + anchor_file_id: FileId, +) -> Option { + let source_root = db.source_root(db.source_root_id(file_id)); + let label = if matches!(source_root.role(), SourceRootRole::Local) + && let Some(label) = local_source_root_path_label(db, file_id, anchor_file_id) { - return text.trim().to_owned(); + label + } else { + source_root + .path_for_file(&file_id) + .map(|path| display_hover_path(path.to_string())) + .or_else(|| db.file_path(file_id).map(|path| display_hover_path(path.to_string())))? + }; + let target = db + .file_path(file_id) + .map(|path| file_link_target(&path.to_string())) + .unwrap_or_else(|| label.clone()); + Some(MacroSourceLink { label, target }) +} + +fn local_source_root_path_label( + db: &RootDb, + file_id: FileId, + anchor_file_id: FileId, +) -> Option { + let source_root = db.source_root(db.source_root_id(file_id)); + let source_path = source_root.path_for_file(&file_id)?; + let Some(target_path) = source_path.as_abs_path() else { + return Some(display_project_path(source_path.to_string())); + }; + + let anchor_source_root = db.source_root(db.source_root_id(anchor_file_id)); + let anchor_path = anchor_source_root.path_for_file(&anchor_file_id)?.as_abs_path()?; + let mut common_dir = anchor_path.parent()?.to_path_buf(); + while !target_path.starts_with(common_dir.as_path()) { + if !common_dir.pop() { + return None; + } + } + if !has_normal_path_component(common_dir.as_path()) { + return None; } - argument.tokens.iter().map(|token| token.as_str()).collect::>().join(" ") + + target_path + .strip_prefix(common_dir.as_path()) + .map(|path| display_project_path(path.as_ref().display().to_string())) +} + +fn has_normal_path_component(path: &utils::paths::AbsPath) -> bool { + path.components().any(|component| matches!(component, utils::paths::Utf8Component::Normal(_))) +} + +fn display_project_path(mut path: String) -> String { + while path.starts_with('/') { + path.remove(0); + } + display_hover_path(path) +} + +fn display_hover_path(path: String) -> String { + path.replace('\\', "/") +} + +fn file_link_target(path: &str) -> String { + let path = display_hover_path(path.to_owned()); + if path.starts_with('/') { format!("file://{path}") } else { format!("file:///{path}") } } fn expanded_text_from_tokens(tokens: &[EmittedTokenProvenance]) -> String { @@ -361,13 +379,6 @@ fn expanded_text_from_tokens(tokens: &[EmittedTokenProvenance]) -> String { text } -fn text_at_file_range(db: &RootDb, file_id: FileId, range: TextRange) -> Option { - let text = db.file_text(file_id); - let start = usize::from(range.start()); - let end = usize::from(range.end()); - text.get(start..end).map(ToOwned::to_owned) -} - fn covering_range(ranges: &[TextRange]) -> Option { let start = ranges.iter().map(|range| range.start()).min()?; let end = ranges.iter().map(|range| range.end()).max()?; @@ -396,7 +407,7 @@ fn handle_preproc_macro( if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { return Some(RangeInfo::new( definition.name_range, - macro_definition_markup(db, &definition), + macro_definition_markup(db, file_id, &definition), )); } @@ -406,7 +417,7 @@ fn handle_preproc_macro( } return Some(RangeInfo::new( resolution.range, - macro_definitions_markup(db, &resolution.definitions), + macro_definitions_markup(db, file_id, &resolution.definitions), )); } @@ -438,16 +449,22 @@ fn macro_param_definitions_markup(definitions: &[MacroParamDefinition]) -> Marku markup } -fn macro_definition_markup(db: &RootDb, definition: &MacroDefinition) -> Markup { - macro_definitions_markup(db, std::slice::from_ref(definition)) +fn macro_definition_markup( + db: &RootDb, + anchor_file_id: FileId, + definition: &MacroDefinition, +) -> Markup { + macro_definitions_markup(db, anchor_file_id, std::slice::from_ref(definition)) } -fn macro_definitions_markup(db: &RootDb, definitions: &[MacroDefinition]) -> Markup { +fn macro_definitions_markup( + db: &RootDb, + anchor_file_id: FileId, + definitions: &[MacroDefinition], +) -> Markup { let mut markup = Markup::new(); if definitions.len() == 1 { - markup.print("Macro"); - markup.newline(); - markup.push_with_backticks(definitions[0].name.as_str()); + render_macro_definition_display(db, &mut markup, anchor_file_id, &definitions[0]); return markup; } @@ -463,6 +480,45 @@ fn macro_definitions_markup(db: &RootDb, definitions: &[MacroDefinition]) -> Mar markup } +fn render_macro_definition_display( + db: &RootDb, + markup: &mut Markup, + anchor_file_id: FileId, + definition: &MacroDefinition, +) { + markup.push_with_code_fence(¯o_definition_line(definition)); + render_macro_expansion_separator(markup); + render_macro_source_link(db, markup, definition, anchor_file_id); +} + +fn render_macro_source_link( + db: &RootDb, + markup: &mut Markup, + definition: &MacroDefinition, + anchor_file_id: FileId, +) { + let Some(source) = macro_definition_source_link(db, definition, anchor_file_id) else { + return; + }; + markup.print("From ["); + markup.print(&markdown_link_label(&source.label)); + markup.print("](<"); + markup.print(&markdown_link_destination(&source.target)); + markup.print(">)"); +} + +fn markdown_link_label(label: &str) -> String { + label.replace('\\', "\\\\").replace('[', "\\[").replace(']', "\\]") +} + +fn markdown_link_destination(destination: &str) -> String { + destination.replace('>', "%3E") +} + +fn macro_definition_body_text(definition: &MacroDefinition) -> String { + definition.body_tokens.iter().map(|token| token.as_str()).collect::>().join(" ") +} + fn handle_preproc_include( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index a492e923..145d6937 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1316,8 +1316,8 @@ endmodule .hover(position, HoverConfig { format: HoverFormat::PlainText }) .unwrap() .expect("macro hover expected"); - assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); assert!(hover.info.as_str().contains("WIDTH"), "hover should mention macro name"); + assert!(hover.info.as_str().contains("8"), "hover should show macro expansion"); } #[test] @@ -1433,11 +1433,9 @@ endmodule hover.info.as_str() ); assert!( - hover.info.as_str().contains("Macro expansion") - && hover.info.as_str().contains("payload_i + 1") - && hover.info.as_str().contains("Expanded result") - && hover.info.as_str().contains("Expansion steps"), - "macro argument hover should show macro expansion: {}", + !hover.info.as_str().contains("`define `NEXT(value)") + && !hover.info.as_str().contains("--------------------"), + "macro argument hover should not show macro expansion away from the macro name: {}", hover.info.as_str() ); @@ -1491,17 +1489,16 @@ endmodule .expect("macro call hover expected"); let info = hover.info.as_str(); assert!( - info.contains("Macro") - && info.contains("MAKE_DECL") - && info.contains("Macro expansion") - && info.contains("Signature") - && info.contains("`` `MAKE_DECL(name) ``") - && info.contains("Arguments") - && info.contains("`name` = `generated`") - && info.contains("Expanded result") - && info.contains("Expansion steps") - && info.contains("1. `` `MAKE_DECL(generated) `` from `` `MAKE_DECL(name) ``") + info.contains("```systemverilog") + && info.contains("`define `MAKE_DECL(name) logic name ;") + && info.contains("Expands to") + && info.contains("--------------------") && info.contains("logic generated ;") + && info.contains("From [feature.v]") + && !info.contains("Context ") + && !info.contains("Signature") + && !info.contains("Arguments") + && !info.contains("Expansion steps") && !info.contains("Virtual expansion source") && !info.contains("Token provenance"), "macro call hover should include the expanded macro text: {info}" @@ -1513,13 +1510,15 @@ endmodule .expect("macro argument hover expected"); let arg_info = arg_hover.info.as_str(); assert!( - arg_info.contains("Macro expansion") && arg_info.contains("logic generated ;"), - "macro argument hover should include expanded macro text: {arg_info}" + arg_info.contains("generated") + && !arg_info.contains("`define `MAKE_DECL(name)") + && !arg_info.contains("--------------------"), + "macro argument hover should stay on the source token away from the macro name: {arg_info}" ); } #[test] -fn preproc_macro_hover_shows_nested_expansion_steps() { +fn preproc_macro_hover_shows_nested_compact_expansion() { let text = r#" `define MATH_ONE 12'd1 `define DEMO_NEXT(value) ((value) + `MATH_ONE) @@ -1536,16 +1535,19 @@ endmodule .expect("nested macro call hover expected"); let info = hover.info.as_str(); assert!( - info.contains("`` `DEMO_NEXT(value) ``") - && info.contains("`value` = `payload_i`") - && info.contains("Expanded result") + info.contains("```systemverilog") + && info.contains("`define `DEMO_NEXT(value)") + && info.contains("`MATH_ONE") + && info.contains("Expands to") + && info.contains("--------------------") + && info.contains("( ( payload_i ) + 12 'd 1 )") && info.contains("payload_i") && info.contains("12") && info.contains("'d") - && info.contains("Expansion steps") - && info.contains("1. `` `DEMO_NEXT(payload_i) `` from `` `DEMO_NEXT(value) ``") - && info.contains("2. `` `MATH_ONE `` from `` `MATH_ONE ``"), - "nested macro hover should show signature, arguments, result, and steps: {info}" + && info.contains("From [feature.v]") + && !info.contains("Context ") + && !info.contains("Expansion steps"), + "nested macro hover should show compact signature, result, and source: {info}" ); } @@ -1568,14 +1570,18 @@ endmodule .expect("outer macro call hover expected"); let call_info = call_hover.info.as_str(); assert!( - call_info.contains("`` `DEMO_NEXT(value) ``") - && call_info.contains("`value` = `` `PAYL ``") - && call_info.contains("Expanded result") + call_info.contains("```systemverilog") + && call_info.contains("`define `DEMO_NEXT(value)") + && call_info.contains("`MATH_ONE") + && call_info.contains("Expands to") + && call_info.contains("--------------------") + && call_info.contains("( ( payload_i ) + 12 'd 1 )") && call_info.contains("payload_i") && call_info.contains("12") - && call_info.contains("Expansion steps") - && call_info.contains("1. `` `DEMO_NEXT(`PAYL) `` from `` `DEMO_NEXT(value) ``"), - "outer macro hover should keep expansion and written argument spelling: {call_info}" + && call_info.contains("From [feature.v]") + && !call_info.contains("Context ") + && !call_info.contains("Expansion steps"), + "outer macro hover should keep compact expansion facts: {call_info}" ); let payl_position = position(file_id, &markers, "payl"); @@ -1597,13 +1603,16 @@ endmodule .expect("nested actual-argument macro hover expected"); let payl_info = payl_hover.info.as_str(); assert!( - payl_info.contains("Macro") && payl_info.contains("PAYL"), - "PAYL hover should identify the nested macro reference: {payl_info}" + payl_info.contains("```systemverilog") + && payl_info.contains("`define `PAYL payload_i") + && payl_info.contains("From [feature.v]") + && !payl_info.contains("unavailable"), + "PAYL hover should show the macro definition display without unavailable text: {payl_info}" ); } #[test] -fn preproc_macro_hover_reports_unavailable_expansion() { +fn preproc_macro_hover_falls_back_to_definition_body_for_unsupported_expansion() { let text = r#" `define JOIN(a,b) a``b module top; @@ -1619,9 +1628,11 @@ endmodule .expect("macro call hover expected"); let info = hover.info.as_str(); assert!( - info.contains("Macro expansion") - && info.contains("`` `JOIN(foo,bar) `` expansion unavailable."), - "unsupported expansion should be explicit in hover: {info}" + info.contains("```systemverilog") + && info.contains("`define `JOIN(a, b) a `` b") + && info.contains("From [feature.v]") + && !info.contains("unavailable"), + "unsupported expansion hover should show the macro definition display: {info}" ); } @@ -1657,8 +1668,11 @@ endmodule .hover(definition, HoverConfig { format: HoverFormat::PlainText }) .unwrap() .expect("macro definition hover expected"); - assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); - assert!(hover.info.as_str().contains("LOCAL_WIDTH"), "hover should mention macro name"); + assert!( + hover.info.as_str().contains("`define `LOCAL_WIDTH 8"), + "hover should show macro definition" + ); + assert!(hover.info.as_str().contains("From [feature.v]"), "hover should show macro source"); let conditional_nav = analysis .goto_definition(position(file_id, &markers, "conditional")) @@ -1781,8 +1795,17 @@ endmodule .hover(usage, HoverConfig { format: HoverFormat::PlainText }) .unwrap() .expect("included macro hover expected"); - assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); assert!(hover.info.as_str().contains("HEADER_WIDTH"), "hover should mention macro name"); + assert!( + hover.info.as_str().contains("`define `HEADER_WIDTH 8"), + "hover should show macro definition" + ); + assert!(hover.info.as_str().contains("8"), "hover should show macro expansion"); + assert!( + hover.info.as_str().contains("From [include/defs.vh]"), + "hover should show project-relative macro source path: {}", + hover.info.as_str() + ); let completion_items = analysis .completions_with_trigger( From 98522ce3e7f165b456e5793d236603e30bd0cd1d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 18:43:43 +0800 Subject: [PATCH 34/80] chore: resolve provenance clippy lints --- crates/hir/src/preproc.rs | 22 +- crates/preproc/src/source/provenance.rs | 66 +++--- crates/slang/bindings/rust/lib.rs | 258 ++++++++++++------------ 3 files changed, 176 insertions(+), 170 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 7ce485c9..efe63eae 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -380,7 +380,7 @@ pub enum DiagnosticProvenance { pub enum MacroExpansionQuery { Available(Box), Ambiguous(Vec), - Unavailable(MacroExpansionUnavailable), + Unavailable(Box), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1335,7 +1335,7 @@ pub fn macro_expansion_provenances_at( for call_fact in source_macro_calls_at(mapped, file_id, offset) { match macro_expansion_provenance_for_call(mapped, call_fact)? { MacroExpansionProvenanceForCall::Available(provenance) => { - push_unique_macro_expansion_provenance(&mut provenances, provenance); + push_unique_macro_expansion_provenance(&mut provenances, *provenance); } MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), } @@ -1392,7 +1392,7 @@ pub fn macro_expansion_provenances_for_range( [] => continue, [call_fact] => match macro_expansion_provenance_for_call(mapped, call_fact)? { MacroExpansionProvenanceForCall::Available(provenance) => { - push_unique_macro_expansion_provenance(&mut provenances, provenance); + push_unique_macro_expansion_provenance(&mut provenances, *provenance); } MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), }, @@ -2295,20 +2295,20 @@ fn immediate_macro_expansion_for_call( Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { SourceMacroExpansionQueryFact::Available(expansion) => { let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { - return Ok(MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + return Ok(MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { call, reason: PreprocUnavailable::Source( SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, ), - })); + }))); }; MacroExpansionQuery::Available(Box::new(map_macro_expansion(mapped, expansion)?)) } SourceMacroExpansionQueryFact::Unavailable(reason) => { - MacroExpansionQuery::Unavailable(MacroExpansionUnavailable { + MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { call, reason: PreprocUnavailable::Source(reason), - }) + })) } }) } @@ -2399,7 +2399,7 @@ fn diagnostic_provenance_for_call( } enum MacroExpansionProvenanceForCall { - Available(MacroExpansionProvenance), + Available(Box), Unavailable(PreprocUnavailable), } @@ -2416,9 +2416,9 @@ fn macro_expansion_provenance_for_call( }), )); }; - MacroExpansionProvenanceForCall::Available(macro_expansion_provenance_for_expansion( - mapped, expansion, - )?) + MacroExpansionProvenanceForCall::Available(Box::new( + macro_expansion_provenance_for_expansion(mapped, expansion)?, + )) } SourceMacroExpansionQueryFact::Unavailable(reason) => { MacroExpansionProvenanceForCall::Unavailable(PreprocUnavailable::Source(reason)) diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 9ef9888f..e0c8cf4b 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -251,6 +251,16 @@ pub enum SourceTokenProvenance { Unavailable(SourcePreprocUnavailable), } +struct EmittedTokenMacroCall { + token_id: SourceEmittedTokenId, + macro_name: SmolStr, + call_identity: SourceMacroCallKey, + definition: SourceMacroDefinitionId, + call_range: SourceRange, + expansion_identity: SourceMacroExpansionKey, + parent_expansion_identity: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocTables { pub macro_definitions: SourceMacroDefinitionTable, @@ -1126,15 +1136,15 @@ impl<'a> SourcePreprocModelBuilder<'a> { }, ); }; - let Ok(call) = self.call_for_emitted_token( + let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { token_id, macro_name, - identity.call, + call_identity: identity.call, definition, call_range, - identity.expansion, - identity.parent_expansion, - ) else { + expansion_identity: identity.expansion, + parent_expansion_identity: identity.parent_expansion, + }) else { return self.unavailable_token_provenance( SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { identity: identity.call, @@ -1173,15 +1183,15 @@ impl<'a> SourcePreprocModelBuilder<'a> { ); }; let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); - let Ok(call) = self.call_for_emitted_token( + let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { token_id, macro_name, - identity.call, + call_identity: identity.call, definition, call_range, - call_expansion_identity, - None, - ) else { + expansion_identity: call_expansion_identity, + parent_expansion_identity: None, + }) else { return self.unavailable_token_provenance( SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { identity: identity.call, @@ -1211,19 +1221,13 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn call_for_emitted_token( &mut self, - token_id: SourceEmittedTokenId, - macro_name: SmolStr, - call_identity: SourceMacroCallKey, - definition: SourceMacroDefinitionId, - call_range: SourceRange, - expansion_identity: SourceMacroExpansionKey, - parent_expansion_identity: Option, + request: EmittedTokenMacroCall, ) -> Result { - if let Some(call) = self.call_ids_by_identity.get(&call_identity).copied() { + if let Some(call) = self.call_ids_by_identity.get(&request.call_identity).copied() { self.record_call_expansion_identity( call, - expansion_identity, - parent_expansion_identity, + request.expansion_identity, + request.parent_expansion_identity, )?; return Ok(call); } @@ -1231,26 +1235,26 @@ impl<'a> SourcePreprocModelBuilder<'a> { let event_id = self .tables .macro_definitions - .get(definition) + .get(request.definition) .expect("definition id should point at inserted definition") .event_id; - let resolution = - self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition); + let resolution = self + .resolve_definition(request.definition, SourceMacroResolutionReason::VisibleDefinition); let reference = self.push_reference( event_id, - SourceMacroReferenceSite::ExpansionToken { emitted_token: token_id }, - macro_name.clone(), - call_range, - call_range, + SourceMacroReferenceSite::ExpansionToken { emitted_token: request.token_id }, + request.macro_name.clone(), + request.call_range, + request.call_range, resolution.clone(), ); Ok(self.push_call( reference, - call_range, + request.call_range, resolution, - Some(call_identity), - Some(expansion_identity), - parent_expansion_identity, + Some(request.call_identity), + Some(request.expansion_identity), + request.parent_expansion_identity, )) } diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 5bc67a0d..7a1a7448 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -448,36 +448,92 @@ impl PreprocessorTraceEvent { impl PreprocessorTraceEmittedToken { #[inline] fn from_raw(raw: ffi::RawPreprocessorTraceEmittedToken) -> Self { + let ffi::RawPreprocessorTraceEmittedToken { + raw_text, + value_text, + token_kind, + provenance_kind, + macro_name, + macro_call_id, + has_macro_call_id, + macro_definition_id, + has_macro_definition_id, + macro_expansion_id, + has_macro_expansion_id, + parent_macro_expansion_id, + has_parent_macro_expansion_id, + body_token_index, + has_body_token_index, + argument_index, + has_argument_index, + argument_token_index, + has_argument_token_index, + token_range, + call_range, + body_token_range, + argument_token_range, + } = raw; Self { - raw_text: raw.raw_text, - value_text: raw.value_text, - token_kind: TokenKind::from_id(raw.token_kind), + raw_text, + value_text, + token_kind: TokenKind::from_id(token_kind), provenance: PreprocessorTraceTokenProvenance::from_raw( - raw.provenance_kind, - raw.macro_name, - raw.macro_call_id, - raw.has_macro_call_id, - raw.macro_definition_id, - raw.has_macro_definition_id, - raw.macro_expansion_id, - raw.has_macro_expansion_id, - raw.parent_macro_expansion_id, - raw.has_parent_macro_expansion_id, - raw.body_token_index, - raw.has_body_token_index, - raw.argument_index, - raw.has_argument_index, - raw.argument_token_index, - raw.has_argument_token_index, - raw.token_range, - raw.call_range, - raw.body_token_range, - raw.argument_token_range, + RawPreprocessorTraceTokenProvenance { + kind: provenance_kind, + macro_name, + identity: RawPreprocessorTraceMacroIdentity { + call_id: macro_call_id, + has_call_id: has_macro_call_id, + definition_id: macro_definition_id, + has_definition_id: has_macro_definition_id, + expansion_id: macro_expansion_id, + has_expansion_id: has_macro_expansion_id, + parent_expansion_id: parent_macro_expansion_id, + has_parent_expansion_id: has_parent_macro_expansion_id, + body_token_index, + has_body_token_index, + argument_index, + has_argument_index, + argument_token_index, + has_argument_token_index, + }, + token_range, + call_range, + body_token_range, + argument_token_range, + }, ), } } } +struct RawPreprocessorTraceTokenProvenance { + kind: u8, + macro_name: String, + identity: RawPreprocessorTraceMacroIdentity, + token_range: ffi::RawSourceBufferRange, + call_range: ffi::RawSourceBufferRange, + body_token_range: ffi::RawSourceBufferRange, + argument_token_range: ffi::RawSourceBufferRange, +} + +struct RawPreprocessorTraceMacroIdentity { + call_id: u32, + has_call_id: bool, + definition_id: u32, + has_definition_id: bool, + expansion_id: u32, + has_expansion_id: bool, + parent_expansion_id: u32, + has_parent_expansion_id: bool, + body_token_index: u32, + has_body_token_index: bool, + argument_index: u32, + has_argument_index: bool, + argument_token_index: u32, + has_argument_token_index: bool, +} + impl PreprocessorTraceTokenProvenance { const MACRO_ARGUMENT: u8 = 3; const MACRO_BODY: u8 = 2; @@ -485,86 +541,50 @@ impl PreprocessorTraceTokenProvenance { const UNAVAILABLE: u8 = 0; #[inline] - fn from_raw( - kind: u8, - macro_name: String, - macro_call_id: u32, - has_macro_call_id: bool, - macro_definition_id: u32, - has_macro_definition_id: bool, - macro_expansion_id: u32, - has_macro_expansion_id: bool, - parent_macro_expansion_id: u32, - has_parent_macro_expansion_id: bool, - body_token_index: u32, - has_body_token_index: bool, - argument_index: u32, - has_argument_index: bool, - argument_token_index: u32, - has_argument_token_index: bool, - token_range: ffi::RawSourceBufferRange, - call_range: ffi::RawSourceBufferRange, - body_token_range: ffi::RawSourceBufferRange, - argument_token_range: ffi::RawSourceBufferRange, - ) -> Self { - match kind { - Self::SOURCE => SourceBufferRange::from_raw(token_range) + fn from_raw(raw: RawPreprocessorTraceTokenProvenance) -> Self { + match raw.kind { + Self::SOURCE => SourceBufferRange::from_raw(raw.token_range) .map(|token_range| Self::Source { token_range }) .unwrap_or(Self::Unavailable), Self::MACRO_BODY => { - let Some(call_range) = SourceBufferRange::from_raw(call_range) else { + let Some(call_range) = SourceBufferRange::from_raw(raw.call_range) else { return Self::Unavailable; }; - let Some(body_token_range) = SourceBufferRange::from_raw(body_token_range) else { + let Some(body_token_range) = SourceBufferRange::from_raw(raw.body_token_range) + else { return Self::Unavailable; }; - let Some(identity) = PreprocessorTraceMacroBodyIdentity::from_raw( - macro_call_id, - has_macro_call_id, - macro_definition_id, - has_macro_definition_id, - macro_expansion_id, - has_macro_expansion_id, - parent_macro_expansion_id, - has_parent_macro_expansion_id, - body_token_index, - has_body_token_index, - ) else { + let Some(identity) = PreprocessorTraceMacroBodyIdentity::from_raw(&raw.identity) + else { return Self::Unavailable; }; - Self::MacroBody { macro_name, identity, call_range, body_token_range } + Self::MacroBody { + macro_name: raw.macro_name, + identity, + call_range, + body_token_range, + } } Self::MACRO_ARGUMENT => { - let Some(call_range) = SourceBufferRange::from_raw(call_range) else { + let Some(call_range) = SourceBufferRange::from_raw(raw.call_range) else { return Self::Unavailable; }; - let Some(body_token_range) = SourceBufferRange::from_raw(body_token_range) else { + let Some(body_token_range) = SourceBufferRange::from_raw(raw.body_token_range) + else { return Self::Unavailable; }; - let Some(argument_token_range) = SourceBufferRange::from_raw(argument_token_range) + let Some(argument_token_range) = + SourceBufferRange::from_raw(raw.argument_token_range) else { return Self::Unavailable; }; - let Some(identity) = PreprocessorTraceMacroArgumentIdentity::from_raw( - macro_call_id, - has_macro_call_id, - macro_definition_id, - has_macro_definition_id, - macro_expansion_id, - has_macro_expansion_id, - parent_macro_expansion_id, - has_parent_macro_expansion_id, - body_token_index, - has_body_token_index, - argument_index, - has_argument_index, - argument_token_index, - has_argument_token_index, - ) else { + let Some(identity) = + PreprocessorTraceMacroArgumentIdentity::from_raw(&raw.identity) + else { return Self::Unavailable; }; Self::MacroArgument { - macro_name, + macro_name: raw.macro_name, identity, call_range, body_token_range, @@ -579,60 +599,42 @@ impl PreprocessorTraceTokenProvenance { impl PreprocessorTraceMacroBodyIdentity { #[inline] - fn from_raw( - macro_call_id: u32, - has_macro_call_id: bool, - macro_definition_id: u32, - has_macro_definition_id: bool, - macro_expansion_id: u32, - has_macro_expansion_id: bool, - parent_macro_expansion_id: u32, - has_parent_macro_expansion_id: bool, - body_token_index: u32, - has_body_token_index: bool, - ) -> Option { + fn from_raw(raw: &RawPreprocessorTraceMacroIdentity) -> Option { Some(Self { - call_id: has_macro_call_id.then_some(PreprocessorTraceMacroCallId(macro_call_id))?, - definition_id: has_macro_definition_id - .then_some(PreprocessorTraceMacroDefinitionId(macro_definition_id))?, - expansion_id: has_macro_expansion_id - .then_some(PreprocessorTraceMacroExpansionId(macro_expansion_id))?, - parent_expansion_id: has_parent_macro_expansion_id - .then_some(PreprocessorTraceMacroExpansionId(parent_macro_expansion_id)), - body_token_index: has_body_token_index.then_some(body_token_index)?, + call_id: raw.has_call_id.then_some(PreprocessorTraceMacroCallId(raw.call_id))?, + definition_id: raw + .has_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(raw.definition_id))?, + expansion_id: raw + .has_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.expansion_id))?, + parent_expansion_id: raw + .has_parent_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.parent_expansion_id)), + body_token_index: raw.has_body_token_index.then_some(raw.body_token_index)?, }) } } impl PreprocessorTraceMacroArgumentIdentity { #[inline] - fn from_raw( - macro_call_id: u32, - has_macro_call_id: bool, - macro_definition_id: u32, - has_macro_definition_id: bool, - macro_expansion_id: u32, - has_macro_expansion_id: bool, - parent_macro_expansion_id: u32, - has_parent_macro_expansion_id: bool, - body_token_index: u32, - has_body_token_index: bool, - argument_index: u32, - has_argument_index: bool, - argument_token_index: u32, - has_argument_token_index: bool, - ) -> Option { + fn from_raw(raw: &RawPreprocessorTraceMacroIdentity) -> Option { Some(Self { - call_id: has_macro_call_id.then_some(PreprocessorTraceMacroCallId(macro_call_id))?, - definition_id: has_macro_definition_id - .then_some(PreprocessorTraceMacroDefinitionId(macro_definition_id))?, - expansion_id: has_macro_expansion_id - .then_some(PreprocessorTraceMacroExpansionId(macro_expansion_id))?, - parent_expansion_id: has_parent_macro_expansion_id - .then_some(PreprocessorTraceMacroExpansionId(parent_macro_expansion_id)), - body_token_index: has_body_token_index.then_some(body_token_index)?, - argument_index: has_argument_index.then_some(argument_index)?, - argument_token_index: has_argument_token_index.then_some(argument_token_index)?, + call_id: raw.has_call_id.then_some(PreprocessorTraceMacroCallId(raw.call_id))?, + definition_id: raw + .has_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(raw.definition_id))?, + expansion_id: raw + .has_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.expansion_id))?, + parent_expansion_id: raw + .has_parent_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.parent_expansion_id)), + body_token_index: raw.has_body_token_index.then_some(raw.body_token_index)?, + argument_index: raw.has_argument_index.then_some(raw.argument_index)?, + argument_token_index: raw + .has_argument_token_index + .then_some(raw.argument_token_index)?, }) } } From dccb04f4f4bc8e099ac1c4ff8c1af43015981796 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 21:11:12 +0800 Subject: [PATCH 35/80] chore: remove redundant validate --- crates/preproc/src/source/trace.rs | 31 ------------------------------ 1 file changed, 31 deletions(-) diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index c6c79352..a5a6a948 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -108,8 +108,6 @@ impl SourcePreprocIndex { } index.emitted_tokens = emitted_tokens.into_iter().map(emitted_token_from_trace).collect(); - validate_include_edges(&index)?; - Ok(index) } } @@ -135,35 +133,6 @@ fn source_origin( .unwrap_or(PreprocSourceOrigin::Detached) } -fn validate_include_edges(index: &SourcePreprocIndex) -> Result<(), SourcePreprocError> { - for edge in &index.include_edges { - if !index.sources.iter().any(|source| source.id == edge.included_source) { - return Err(SourcePreprocError::MissingIncludedSource { - include_event_id: edge.include_event_id.raw(), - source: edge.included_source.raw(), - }); - } - - let Some(directive) = index - .event_records - .iter() - .find(|directive| directive.event_id == edge.include_event_id) - else { - return Err(SourcePreprocError::MissingIncludeEvent { - include_event_id: edge.include_event_id.raw(), - }); - }; - - if directive.kind != MacroEventKind::Include { - return Err(SourcePreprocError::IncludeEdgeNotInclude { - include_event_id: edge.include_event_id.raw(), - }); - } - } - - Ok(()) -} - fn collect_trace_event( index: &mut SourcePreprocIndex, source_order: usize, From 3261469029e8be98eb02e03edad01b8418f6d8a6 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 21:37:14 +0800 Subject: [PATCH 36/80] refactor(preproc): modularize source provenance code --- crates/preproc/src/source/model.rs | 1274 +------------- crates/preproc/src/source/model/tests.rs | 1246 ++++++++++++++ crates/preproc/src/source/provenance.rs | 1513 +---------------- .../preproc/src/source/provenance/builder.rs | 1257 ++++++++++++++ crates/preproc/src/source/trace.rs | 74 +- 5 files changed, 2631 insertions(+), 2733 deletions(-) create mode 100644 crates/preproc/src/source/model/tests.rs create mode 100644 crates/preproc/src/source/provenance/builder.rs diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index d38d7700..620abbbd 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -319,1276 +319,4 @@ impl SourcePreprocModel { } #[cfg(test)] -mod tests { - use smol_str::SmolStr; - use syntax::{ - PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, - PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, - PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, - PreprocessorTraceToken, PreprocessorTraceTokenProvenance, SourceBufferId, - SourceBufferOrigin, SourceBufferRange, SyntaxKind, SyntaxTree, SyntaxTreeBuffer, - SyntaxTreeOptions, TokenKind, - }; - use utils::line_index::{TextRange, TextSize}; - - use super::{super::SourceMacroReferenceSite, *}; - - const ROOT_PATH: &str = "sample/rtl/top.sv"; - const HEADER_PATH: &str = "sample/include/defs.vh"; - const INCLUDE_DIR: &str = "sample/include"; - - fn source_model( - root_text: &str, - header_text: &str, - ) -> (SourcePreprocModel, PreprocSourceId, PreprocSourceId) { - let options = SyntaxTreeOptions { - include_paths: vec![INCLUDE_DIR.to_owned()], - include_buffers: vec![SyntaxTreeBuffer { - path: HEADER_PATH.to_owned(), - text: header_text.to_owned(), - }], - expand_includes: true, - ..SyntaxTreeOptions::default() - }; - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); - let root_source = PreprocSourceId::from(trace.root_buffer_id); - let header_source = first_non_root_source(&trace, root_source); - let model = SourcePreprocModel::from_trace(trace).unwrap(); - (model, root_source, header_source) - } - - fn first_non_root_source( - trace: &PreprocessorTrace, - root_source: PreprocSourceId, - ) -> PreprocSourceId { - trace - .events - .iter() - .filter_map(|directive| directive.range.as_ref()) - .map(|range| PreprocSourceId::from(range.buffer_id)) - .find(|source| *source != root_source) - .expect("included source directive should be traced") - } - - fn source_by_path_suffix(model: &SourcePreprocModel, suffix: &str) -> PreprocSourceId { - model - .sources() - .iter() - .find(|source| { - matches!(source.origin, PreprocSourceOrigin::Included { .. }) - && source.path.as_str().replace('\\', "/").ends_with(suffix) - }) - .unwrap_or_else(|| panic!("source ending with {suffix} should be present")) - .id - } - - fn source_model_from_root( - root_text: &str, - options: SyntaxTreeOptions, - ) -> (SourcePreprocModel, PreprocSourceId) { - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); - let root_source = PreprocSourceId::from(trace.root_buffer_id); - let model = SourcePreprocModel::from_trace(trace).unwrap(); - (model, root_source) - } - - fn offset_before(text: &str, needle: &str) -> TextSize { - TextSize::from(u32::try_from(text.find(needle).unwrap()).unwrap()) - } - - fn offset_after(text: &str, needle: &str) -> TextSize { - TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) - } - - fn text_at_range(text: &str, range: TextRange) -> &str { - &text[usize::from(range.start())..usize::from(range.end())] - } - - fn source_range(source: PreprocSourceId, start: u32, end: u32) -> SourceRange { - SourceRange { source, range: TextRange::new(TextSize::from(start), TextSize::from(end)) } - } - - fn visible_macro_names( - model: &SourcePreprocModel, - source: PreprocSourceId, - offset: TextSize, - ) -> Vec { - model - .visible_macros_at(SourcePosition { source, offset }) - .into_iter() - .map(|definition| definition.name.clone()) - .collect() - } - - fn visible_macro_definition<'a>( - model: &'a SourcePreprocModel, - source: PreprocSourceId, - offset: TextSize, - name: &str, - ) -> Option<&'a SourceMacroDefinition> { - model - .visible_macros_at(SourcePosition { source, offset }) - .into_iter() - .find(|definition| definition.name == name) - } - - fn reference_for_usage( - model: &SourcePreprocModel, - usage_index: usize, - ) -> &SourceMacroReference { - model - .macro_references() - .iter() - .find(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::Usage { - usage_index: site_usage_index, - } if site_usage_index == usage_index - ) - }) - .expect("usage reference should be in resolved reference table") - } - - fn reference_for_conditional_token( - model: &SourcePreprocModel, - conditional_index: usize, - token_index: usize, - ) -> &SourceMacroReference { - model - .macro_references() - .iter() - .find(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::ConditionalToken { - conditional_index: site_conditional_index, - token_index: site_token_index, - } | SourceMacroReferenceSite::IncludeGuardIfNDef { - conditional_index: site_conditional_index, - token_index: site_token_index, - } if site_conditional_index == conditional_index - && site_token_index == token_index - ) - }) - .expect("conditional token reference should be in resolved reference table") - } - - #[test] - fn source_model_applies_include_define_after_include_point_only() { - let root_text = r#"`include "defs.vh" -logic [`HEADER_WIDTH-1:0] data; -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - assert!( - !visible_macro_names(&model, root_source, offset_before(root_text, "`include")) - .iter() - .any(|name| name == "HEADER_WIDTH") - ); - - let after_include = visible_macro_definition( - &model, - root_source, - offset_after(root_text, "`include \"defs.vh\"\n"), - "HEADER_WIDTH", - ) - .unwrap(); - assert_eq!(after_include.id.raw(), 0); - - let definition = model - .visible_macros_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`include \"defs.vh\"\n"), - }) - .into_iter() - .find(|definition| definition.name == "HEADER_WIDTH") - .unwrap(); - assert_eq!(definition.name_range.source, header_source); - } - - #[test] - fn source_model_undef_removes_included_define() { - let root_text = r#"`include "defs.vh" -`undef HEADER_WIDTH -logic [`HEADER_WIDTH-1:0] data; -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - let after_include = visible_macro_definition( - &model, - root_source, - offset_after(root_text, "`include \"defs.vh\"\n"), - "HEADER_WIDTH", - ) - .unwrap(); - assert_eq!(after_include.id.raw(), 0); - assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); - - assert!( - visible_macro_definition( - &model, - root_source, - offset_after(root_text, "`undef HEADER_WIDTH\n"), - "HEADER_WIDTH", - ) - .is_none() - ); - assert_eq!(model.undefs()[0].name.as_deref(), Some("HEADER_WIDTH")); - assert_eq!(model.undefs()[0].name_range.unwrap().source, root_source); - } - - #[test] - fn source_model_same_name_define_overrides_included_define() { - let root_text = r#"`include "defs.vh" -`define HEADER_WIDTH 16 -logic [`HEADER_WIDTH-1:0] data; -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); - assert_eq!(model.defines()[1].name_range.unwrap().source, root_source); - - let after_override = visible_macro_definition( - &model, - root_source, - offset_after(root_text, "`define HEADER_WIDTH 16\n"), - "HEADER_WIDTH", - ) - .unwrap(); - assert_eq!(after_override.id.raw(), 1); - - let definition = model - .visible_macros_at(SourcePosition { - source: root_source, - offset: offset_after(root_text, "`define HEADER_WIDTH 16\n"), - }) - .into_iter() - .find(|definition| definition.name == "HEADER_WIDTH") - .unwrap(); - assert_eq!(definition.body_tokens[0].value.as_str(), "16"); - assert_eq!(definition.name_range.source, root_source); - } - - #[test] - fn visible_macro_query_reads_timeline_without_event_records() { - let root_text = r#"`define A 1 -`undef A -`define B 2 -"#; - let trace = SyntaxTree::preprocessor_trace( - root_text, - "source", - ROOT_PATH, - &SyntaxTreeOptions::default(), - ) - .expect("trace should include root source"); - let root_source = PreprocSourceId::from(trace.root_buffer_id); - let mut model = SourcePreprocModel::from_trace(trace).unwrap(); - - let names_after_define = - visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")); - let names_after_undef = - visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")); - let names_after_second_define = - visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")); - - assert_eq!(names_after_define, vec![SmolStr::new("A")]); - assert!(names_after_undef.is_empty(), "{names_after_undef:?}"); - assert_eq!(names_after_second_define, vec![SmolStr::new("B")]); - - model.index.event_records.clear(); - - assert_eq!( - visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")), - names_after_define - ); - assert_eq!( - visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")), - names_after_undef - ); - assert_eq!( - visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")), - names_after_second_define - ); - } - - #[test] - fn source_model_preserves_inactive_range_sources() { - let root_text = r#"`include "defs.vh" -`ifndef HEADER_FLAG -wire disabled_by_header; -`endif -"#; - let header_text = r#"`define HEADER_FLAG -`ifdef NEVER -wire disabled_from_header; -`endif -"#; - let (model, root_source, header_source) = source_model(root_text, header_text); - - let root_inactive = - model.inactive_ranges().iter().find(|range| range.source == root_source).unwrap(); - assert_eq!(text_at_range(root_text, root_inactive.range), "wire disabled_by_header;"); - - let header_inactive = - model.inactive_ranges().iter().find(|range| range.source == header_source).unwrap(); - assert_eq!(text_at_range(header_text, header_inactive.range), "wire disabled_from_header;"); - } - - #[test] - fn source_model_resolves_root_usage_to_included_define() { - let root_text = r#"`include "defs.vh" -logic [`HEADER_WIDTH-1:0] data; -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - let usage_index = model - .usages() - .iter() - .position(|usage| usage.name.as_deref() == Some("HEADER_WIDTH")) - .expect("root macro usage should be traced"); - let usage = &model.usages()[usage_index]; - assert_eq!(usage.range.source, root_source); - assert_eq!(usage.name_range.unwrap().source, root_source); - - let reference = reference_for_usage(&model, usage_index); - let SourceMacroResolution::Resolved { definition, include_chain, reason } = - &reference.resolution - else { - panic!("usage reference should resolve to included definition"); - }; - assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); - let definition = model.macro_definitions().get(*definition).unwrap(); - assert_eq!(definition.name.as_str(), "HEADER_WIDTH"); - assert_eq!(definition.name_range.source, header_source); - assert_eq!(definition.body_tokens[0].value.as_str(), "8"); - assert_eq!(include_chain.len(), 1); - assert_eq!(include_chain[0].include_range.source, root_source); - assert_eq!(include_chain[0].included_source, header_source); - } - - #[test] - fn source_model_exposes_expansion_provenance_skeleton_tables() { - let root_text = r#"`include "defs.vh" -logic [`HEADER_WIDTH-1:0] data; -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - let definition = model - .macro_definitions() - .iter() - .find(|definition| definition.name.as_str() == "HEADER_WIDTH") - .expect("definition table should include precise macro definition"); - assert_eq!(definition.directive_range.source, header_source); - assert_eq!(definition.name_range.source, header_source); - assert_ne!(definition.directive_range.range, definition.name_range.range); - assert_eq!(text_at_range(header_text, definition.name_range.range), "HEADER_WIDTH"); - - let reference = model - .macro_references() - .iter() - .find(|reference| { - reference.name.as_str() == "HEADER_WIDTH" - && matches!(reference.site, SourceMacroReferenceSite::Usage { usage_index: _ }) - }) - .expect("reference table should include resolved macro usage"); - assert_eq!(reference.name_range.source, root_source); - assert_eq!(reference.directive_range.source, root_source); - let SourceMacroResolution::Resolved { - definition: resolved_definition, - reason, - include_chain, - } = &reference.resolution - else { - panic!("macro usage should resolve to included definition"); - }; - assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); - assert_eq!(include_chain.len(), 1); - assert_eq!( - model.macro_definitions().get(*resolved_definition).unwrap().name.as_str(), - "HEADER_WIDTH" - ); - - assert_eq!(model.include_graph().directives().len(), 1); - assert!(matches!( - &model.include_graph().directives()[0].status, - SourceIncludeStatus::Resolved { source } if *source == header_source - )); - assert!(!model.state_timeline().checkpoints().is_empty()); - - let call = model - .macro_calls() - .iter() - .find(|call| call.reference == reference.id) - .expect("macro usage should create a call fact"); - assert_eq!(call.call_range.source, root_source); - assert_eq!(call.status, SourceMacroCallStatus::ExpansionAvailable); - let SourceMacroExpansionQuery::Available(expansion_id) = - model.immediate_macro_expansion(call.id) - else { - panic!("object-like macro call should have an immediate expansion"); - }; - assert_eq!(call.expansion, Some(expansion_id)); - let expansion = model.macro_expansions().get(expansion_id).unwrap(); - assert_eq!(expansion.call, call.id); - assert_eq!(*resolved_definition, expansion.definition); - assert!(expansion.child_calls.is_empty()); - assert_eq!(expansion.status, SourceMacroExpansionStatus::Complete); - - let emitted = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "8") - .expect("macro body token should be emitted by adapter authority"); - assert_eq!(expansion.emitted_token_range.start, emitted.id); - assert_eq!(expansion.emitted_token_range.len, 1); - let provenance = model.token_provenance().get(emitted.provenance).unwrap(); - assert!(matches!( - provenance, - SourceTokenProvenance::MacroBody { - definition: body_definition, - body_token_range, - call: body_call, - .. - } if *body_definition == *resolved_definition - && body_token_range.source == header_source - && *body_call == call.id - )); - let recursive = model.recursive_macro_expansion(call.id); - assert_eq!(recursive.expansions, vec![expansion_id]); - assert!(recursive.unavailable.is_empty()); - assert_eq!(model.capabilities().macro_calls, CapabilityStatus::Complete); - assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Complete); - assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); - assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Complete); - } - - #[test] - fn source_model_maps_function_macro_argument_emitted_token_to_argument() { - let root_text = r#"`define ID(x) x -module m; -localparam int W = `ID(7); -endmodule -"#; - let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let emitted = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "7") - .expect("argument replacement token should be emitted"); - let SourceTokenProvenance::MacroArgument { - call, argument_index, argument_token_range, .. - } = model.token_provenance().get(emitted.provenance).unwrap() - else { - panic!("argument replacement should map to MacroArgument provenance"); - }; - assert_eq!(*argument_index, 0); - assert_eq!(argument_token_range.source, root_source); - assert_eq!(text_at_range(root_text, argument_token_range.range), "7"); - - let call = model.macro_calls().get(*call).expect("call id should resolve"); - assert_eq!(call.call_range.source, root_source); - assert_eq!(text_at_range(root_text, call.call_range.range), "`ID(7)"); - assert_eq!(call.arguments.len(), 1); - assert_eq!(call.arguments[0].argument_index, 0); - assert_eq!(call.arguments[0].argument_range, Some(*argument_token_range)); - - let SourceMacroExpansionQuery::Available(expansion_id) = - model.immediate_macro_expansion(call.id) - else { - panic!("function-like macro call should have an immediate expansion"); - }; - let expansion = model.macro_expansions().get(expansion_id).unwrap(); - assert_eq!(expansion.emitted_token_range.start, emitted.id); - assert_eq!(expansion.emitted_token_range.len, 1); - } - - #[test] - fn source_model_maps_nested_macro_usage_in_actual_argument_to_source_spelling() { - let root_text = r#"`define PAYL payload_i -`define NEXT(x) ((x) + 12'd1) -module m(input logic [3:0] payload_i, output logic [3:0] y); -assign y = `NEXT(`PAYL); -endmodule -"#; - let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let next_usage_index = model - .usages() - .iter() - .position(|usage| usage.name.as_deref() == Some("NEXT")) - .expect("outer function macro usage should be traced"); - let next_usage = &model.usages()[next_usage_index]; - assert_eq!(next_usage.arguments.len(), 1); - let next_argument_range = next_usage.arguments[0] - .argument_range - .expect("actual argument should keep written source range"); - assert_eq!(next_argument_range.source, root_source); - assert_eq!(text_at_range(root_text, next_argument_range.range), "`PAYL"); - assert_eq!( - next_usage.arguments[0] - .tokens - .iter() - .map(|token| token.raw.as_str()) - .collect::>(), - vec!["`PAYL"] - ); - - let next_reference = reference_for_usage(&model, next_usage_index); - let next_call = model - .macro_calls() - .iter() - .find(|call| call.reference == next_reference.id) - .expect("outer macro usage should create a call"); - assert_eq!(next_call.arguments[0].argument_range, Some(next_argument_range)); - assert!(matches!( - model.immediate_macro_expansion(next_call.id), - SourceMacroExpansionQuery::Available(_) - )); - - let payl_usage_index = model - .usages() - .iter() - .position(|usage| usage.name.as_deref() == Some("PAYL")) - .expect("nested actual-argument macro usage should be traced"); - let payl_usage = &model.usages()[payl_usage_index]; - assert_eq!(payl_usage.range.source, root_source); - assert_eq!(text_at_range(root_text, payl_usage.range.range), "`PAYL"); - let payl_reference = reference_for_usage(&model, payl_usage_index); - let SourceMacroResolution::Resolved { definition, .. } = &payl_reference.resolution else { - panic!("PAYL usage should resolve through its runtime definition identity"); - }; - assert_eq!(model.macro_definitions().get(*definition).unwrap().name.as_str(), "PAYL"); - assert!(model.macro_calls().iter().any(|call| call.reference == payl_reference.id)); - } - - #[test] - fn source_model_uses_direct_definition_identity_when_body_ranges_collide() { - let trace = PreprocessorTrace { - root_buffer_id: 1, - source_buffers: vec![SourceBufferId { - path: ROOT_PATH.to_owned(), - text: None, - buffer_id: 1, - origin: SourceBufferOrigin::Source, - }], - events: vec![ - PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(0), - kind: SyntaxKind::DEFINE_DIRECTIVE, - range: Some(SourceBufferRange { buffer_id: 1, range: 0..12 }), - macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(10)), - macro_call_id: None, - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "A".to_owned(), - value_text: "A".to_owned(), - token_kind: TokenKind::IDENTIFIER, - range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: vec![PreprocessorTraceToken { - raw_text: "1".to_owned(), - value_text: "1".to_owned(), - token_kind: TokenKind::INTEGER_LITERAL, - range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), - }], - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }, - PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(1), - kind: SyntaxKind::DEFINE_DIRECTIVE, - range: Some(SourceBufferRange { buffer_id: 1, range: 13..25 }), - macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(20)), - macro_call_id: None, - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "B".to_owned(), - value_text: "B".to_owned(), - token_kind: TokenKind::IDENTIFIER, - range: Some(SourceBufferRange { buffer_id: 1, range: 21..22 }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: vec![PreprocessorTraceToken { - raw_text: "2".to_owned(), - value_text: "2".to_owned(), - token_kind: TokenKind::INTEGER_LITERAL, - range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), - }], - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }, - PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(2), - kind: SyntaxKind::MACRO_USAGE, - range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), - macro_definition_id: None, - macro_call_id: Some(PreprocessorTraceMacroCallId(200)), - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "`B".to_owned(), - value_text: "`B".to_owned(), - token_kind: TokenKind::DIRECTIVE, - range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: Vec::new(), - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }, - ], - include_edges: Vec::new(), - emitted_tokens: vec![syntax::PreprocessorTraceEmittedToken { - raw_text: "2".to_owned(), - value_text: "2".to_owned(), - token_kind: TokenKind::INTEGER_LITERAL, - provenance: PreprocessorTraceTokenProvenance::MacroBody { - macro_name: "B".to_owned(), - identity: PreprocessorTraceMacroBodyIdentity { - call_id: PreprocessorTraceMacroCallId(200), - definition_id: PreprocessorTraceMacroDefinitionId(20), - expansion_id: PreprocessorTraceMacroExpansionId(300), - parent_expansion_id: None, - body_token_index: 0, - }, - call_range: SourceBufferRange { buffer_id: 1, range: 40..42 }, - body_token_range: SourceBufferRange { buffer_id: 1, range: 8..9 }, - }, - }], - }; - let model = SourcePreprocModel::from_trace(trace).unwrap(); - let emitted = model.emitted_tokens().iter().find(|token| token.text == "2").unwrap(); - let SourceTokenProvenance::MacroBody { definition, call, identity, .. } = - model.token_provenance().get(emitted.provenance).unwrap() - else { - panic!("colliding range token should still resolve through direct body identity"); - }; - - let definition = model.macro_definitions().get(*definition).unwrap(); - assert_eq!(definition.name.as_str(), "B"); - assert_eq!(definition.identity, Some(identity.definition)); - assert_eq!(model.macro_calls().get(*call).unwrap().identity, Some(identity.call)); - } - - #[test] - fn source_model_preserves_multi_token_argument_direct_identity() { - let root_text = r#"`define NEXT(x) ((x) + 12'd1) -module m(input logic [3:0] payload_i, output logic [3:0] y); -assign y = `NEXT(payload_i[3:0]); -endmodule -"#; - let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let payload = model - .emitted_tokens() - .iter() - .find_map(|token| { - let SourceTokenProvenance::MacroArgument { - identity, - call, - argument_index, - body_token_range, - argument_token_range, - } = model.token_provenance().get(token.provenance)? - else { - return None; - }; - (token.text.as_str() == "payload_i").then_some(( - *identity, - *call, - *argument_index, - *body_token_range, - *argument_token_range, - )) - }) - .expect("payload identifier should be direct macro argument provenance"); - let slice = model - .emitted_tokens() - .iter() - .find_map(|token| { - let SourceTokenProvenance::MacroArgument { - identity, - call, - argument_index, - body_token_range, - argument_token_range, - } = model.token_provenance().get(token.provenance)? - else { - return None; - }; - (token.text.as_str() == "3").then_some(( - *identity, - *call, - *argument_index, - *body_token_range, - *argument_token_range, - )) - }) - .expect("slice index should be direct macro argument provenance"); - - assert_eq!(payload.0.call, slice.0.call); - assert_eq!(payload.1, slice.1); - assert_eq!(payload.2, 0); - assert_eq!(slice.2, 0); - assert_eq!(payload.0.argument_token_index, 0); - assert_eq!(slice.0.argument_token_index, 2); - assert_eq!(payload.3, slice.3); - assert_eq!(payload.4.source, root_source); - assert_eq!(slice.4.source, root_source); - let call = model.macro_calls().get(payload.1).unwrap(); - assert_eq!(call.arguments.len(), 1); - assert_eq!( - text_at_range(root_text, call.arguments[0].argument_range.unwrap().range), - "payload_i[3:0]" - ); - } - - #[test] - fn source_model_marks_missing_direct_identity_partial_without_range_fallback() { - let root_source = PreprocSourceId::from(1); - let define_range = source_range(root_source, 0, 11); - let name_range = source_range(root_source, 8, 9); - let body_range = source_range(root_source, 10, 11); - let usage_range = source_range(root_source, 24, 26); - let index = SourcePreprocIndex { - root_source: Some(root_source), - sources: vec![PreprocSource { - id: root_source, - path: SmolStr::new(ROOT_PATH), - origin: PreprocSourceOrigin::Root, - }], - event_records: vec![ - SourcePreprocEventRecord { - event_id: SourcePreprocEventId(0), - kind: MacroEventKind::Define, - range: define_range, - index: 0, - }, - SourcePreprocEventRecord { - event_id: SourcePreprocEventId(1), - kind: MacroEventKind::Usage, - range: usage_range, - index: 0, - }, - ], - emitted_tokens: vec![SourceEmittedTokenFact { - raw: SmolStr::new("1"), - value: SmolStr::new("1"), - kind: SourceTokenKind::Syntax(TokenKind::INTEGER_LITERAL), - provenance: SourceTokenProvenanceFact::MacroBody { - macro_name: SmolStr::new("A"), - identity: None, - call_range: usage_range, - body_token_range: body_range, - }, - }], - defines: vec![SourceMacroDefine { - event_id: SourcePreprocEventId(0), - identity: Some(SourceMacroDefinitionKey::new(10)), - name: Some(SmolStr::new("A")), - name_range: Some(name_range), - params: None, - body: vec![SourceMacroToken { - raw: SmolStr::new("1"), - value: SmolStr::new("1"), - range: Some(body_range), - }], - range: define_range, - }], - usages: vec![SourceMacroUsage { - event_id: SourcePreprocEventId(1), - identity: Some(SourceMacroCallKey::new(20)), - definition_identity: None, - expansion_identity: None, - parent_expansion_identity: None, - name: Some(SmolStr::new("A")), - name_range: Some(usage_range), - arguments: Vec::new(), - range: usage_range, - }], - ..SourcePreprocIndex::default() - }; - - let model = SourcePreprocModel::new(index); - let emitted = model.emitted_tokens().iter().next().unwrap(); - assert!(matches!( - model.token_provenance().get(emitted.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity - ) - )); - assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); - assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); - } - - #[test] - fn source_model_builds_nested_expansion_graph_from_runtime_usage_records() { - let root_text = r#"`define LEAF 3 -`define WRAP `LEAF -module m; -localparam int W = `WRAP; -endmodule -"#; - let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let wrap_reference = model - .macro_references() - .iter() - .find(|reference| reference.name.as_str() == "WRAP") - .expect("outer macro usage should create a reference"); - let wrap_call = model - .macro_calls() - .iter() - .find(|call| call.reference == wrap_reference.id) - .expect("outer macro usage should create a call"); - assert_eq!(wrap_call.call_range.source, root_source); - - let leaf_call = model - .macro_calls() - .iter() - .find(|call| { - let reference = model.macro_references().get(call.reference).unwrap(); - reference.name.as_str() == "LEAF" - && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) - }) - .expect("nested macro invocation should create a runtime usage call"); - let leaf_reference = model.macro_references().get(leaf_call.reference).unwrap(); - assert_eq!(text_at_range(root_text, leaf_reference.name_range.range), "`LEAF"); - assert_eq!(leaf_call.parent_expansion_identity, wrap_call.expansion_identity); - - let SourceMacroExpansionQuery::Available(wrap_expansion_id) = - model.immediate_macro_expansion(wrap_call.id) - else { - panic!("outer macro should have an expansion identity from the runtime usage record"); - }; - let wrap_expansion = model.macro_expansions().get(wrap_expansion_id).unwrap(); - assert_eq!(wrap_expansion.child_calls, vec![leaf_call.id]); - - let recursive = model.recursive_macro_expansion(wrap_call.id); - assert_eq!(recursive.expansions.len(), 2); - assert!(recursive.expansions.contains(&wrap_expansion_id)); - assert!(recursive.unavailable.is_empty()); - } - - #[test] - fn source_model_builds_nested_leaf_expansion_from_direct_identity() { - let root_text = r#"`define LEAF 3 -`define WRAP `LEAF -module m; -localparam int W = `WRAP; -endmodule -"#; - let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let leaf_call = model - .macro_calls() - .iter() - .find(|call| { - let reference = model.macro_references().get(call.reference).unwrap(); - reference.name.as_str() == "LEAF" - && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) - }) - .expect("nested macro invocation should create a runtime usage call"); - assert!(leaf_call.identity.is_some()); - assert!(leaf_call.expansion_identity.is_some()); - assert!(leaf_call.parent_expansion_identity.is_some()); - - let SourceMacroExpansionQuery::Available(leaf_expansion_id) = - model.immediate_macro_expansion(leaf_call.id) - else { - panic!("nested macro should have its own immediate expansion"); - }; - let emitted = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "3") - .expect("nested macro body token should be emitted"); - let SourceTokenProvenance::MacroBody { identity, definition, call, .. } = - model.token_provenance().get(emitted.provenance).unwrap() - else { - panic!("nested emitted token should keep macro body provenance"); - }; - assert_eq!(*call, leaf_call.id); - assert_eq!(Some(identity.call), leaf_call.identity); - assert_eq!(Some(identity.expansion), leaf_call.expansion_identity); - assert_eq!(identity.parent_expansion, leaf_call.parent_expansion_identity); - assert_eq!( - Some(identity.definition), - model.macro_definitions().get(*definition).unwrap().identity - ); - - let recursive = model.recursive_macro_expansion(leaf_call.id); - assert_eq!(recursive.expansions, vec![leaf_expansion_id]); - assert!(recursive.unavailable.is_empty()); - } - - #[test] - fn source_model_marks_unsupported_macro_ops_unavailable_without_dropping_tokens() { - let root_text = r#"`define JOIN(a,b) a``b -`define STR(x) `"x`" -module m; -wire `JOIN(foo,bar); -string s = `STR(foo); -endmodule -"#; - let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); - - let pasted = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "foobar") - .expect("token paste result should not be dropped"); - assert!(matches!( - model.token_provenance().get(pasted.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) - )); - - let stringified = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "\"foo\"") - .expect("stringification result should not be dropped"); - assert!(matches!( - model.token_provenance().get(stringified.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) - )); - assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); - assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); - assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); - for call in model.macro_calls().iter() { - assert!(matches!( - model.immediate_macro_expansion(call.id), - SourceMacroExpansionQuery::Unavailable(_) - )); - } - } - - #[test] - fn source_model_does_not_create_expansion_without_emitted_token_authority() { - let root_text = "`define A 1\nmodule m; localparam int W = `A; endmodule\n"; - let define_start = root_text.find("`define").unwrap(); - let define_end = root_text.find('\n').unwrap(); - let usage_start = root_text.find("`A").unwrap(); - let trace = PreprocessorTrace { - root_buffer_id: 1, - source_buffers: vec![SourceBufferId { - path: ROOT_PATH.to_owned(), - text: None, - buffer_id: 1, - origin: SourceBufferOrigin::Source, - }], - events: vec![ - PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(0), - kind: SyntaxKind::DEFINE_DIRECTIVE, - range: Some(SourceBufferRange { - buffer_id: 1, - range: define_start..define_end, - }), - macro_definition_id: None, - macro_call_id: None, - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "A".to_owned(), - value_text: "A".to_owned(), - token_kind: TokenKind::IDENTIFIER, - range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: vec![PreprocessorTraceToken { - raw_text: "1".to_owned(), - value_text: "1".to_owned(), - token_kind: TokenKind::INTEGER_LITERAL, - range: Some(SourceBufferRange { buffer_id: 1, range: 10..11 }), - }], - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }, - PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(1), - kind: SyntaxKind::MACRO_USAGE, - range: Some(SourceBufferRange { - buffer_id: 1, - range: usage_start..usage_start + 2, - }), - macro_definition_id: None, - macro_call_id: None, - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "`A".to_owned(), - value_text: "`A".to_owned(), - token_kind: TokenKind::DIRECTIVE, - range: Some(SourceBufferRange { - buffer_id: 1, - range: usage_start..usage_start + 2, - }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: Vec::new(), - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }, - ], - include_edges: Vec::new(), - emitted_tokens: Vec::new(), - }; - let model = SourcePreprocModel::from_trace(trace).unwrap(); - let call = model.macro_calls().iter().next().expect("usage should create a call"); - - assert!(model.macro_expansions().is_empty()); - assert!(matches!( - model.immediate_macro_expansion(call.id), - SourceMacroExpansionQuery::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable - ) - )); - assert!(matches!( - &model.capabilities().macro_expansions, - CapabilityStatus::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable - ) - )); - } - - #[test] - fn source_model_maps_predefine_and_marks_intrinsic_unavailable() { - let root_text = r#"module m; -localparam int P = `FROM_API; -localparam int L = `__LINE__; -endmodule -"#; - let (model, _root_source) = source_model_from_root( - root_text, - SyntaxTreeOptions { - predefines: vec!["FROM_API=11".to_owned()], - ..SyntaxTreeOptions::default() - }, - ); - - let predefine = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "11") - .expect("predefine expansion token should be emitted"); - let SourceTokenProvenance::Predefine { source } = - model.token_provenance().get(predefine.provenance).unwrap() - else { - panic!("configured predefine token should map to Predefine provenance"); - }; - assert!(model.sources().iter().any(|candidate| { - candidate.id == *source && candidate.origin == PreprocSourceOrigin::Predefine - })); - - let intrinsic = model - .emitted_tokens() - .iter() - .find(|token| token.text.as_str() == "3") - .expect("intrinsic macro token should stay in emitted stream"); - assert!(matches!( - model.token_provenance().get(intrinsic.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) - )); - } - - #[test] - fn source_model_resolves_conditional_tokens_to_visible_defines() { - let root_text = r#"`include "defs.vh" -`ifdef HEADER_FLAG -wire active; -`endif -"#; - let header_text = "`define HEADER_FLAG\n"; - let (model, root_source, header_source) = source_model(root_text, header_text); - - let conditional_index = model - .conditionals() - .iter() - .position(|conditional| conditional.kind == MacroConditionalKind::IfDef) - .expect("ifdef should be traced"); - let reference = reference_for_conditional_token(&model, conditional_index, 0); - - assert_eq!(reference.name.as_str(), "HEADER_FLAG"); - assert_eq!(reference.name_range.source, root_source); - let SourceMacroResolution::Resolved { definition, reason, .. } = reference.resolution - else { - panic!("conditional token reference should resolve to visible definition"); - }; - assert_eq!(reason, SourceMacroResolutionReason::VisibleDefinition); - assert_eq!( - model.macro_definitions().get(definition).unwrap().name_range.source, - header_source - ); - } - - #[test] - fn source_model_resolves_ifndef_include_guard_to_following_define() { - let root_text = r#"`include "defs.vh" -`ifdef HEADER_FLAG -wire active; -`endif -"#; - let header_text = r#"`ifndef HEADER_FLAG -`define HEADER_FLAG -`endif -"#; - let (model, _root_source, header_source) = source_model(root_text, header_text); - - let conditional_index = model - .conditionals() - .iter() - .position(|conditional| { - conditional.kind == MacroConditionalKind::IfNDef - && conditional.range.source == header_source - }) - .expect("ifndef guard should be traced"); - let reference = model - .macro_references() - .iter() - .find(|reference| { - matches!( - reference.site, - SourceMacroReferenceSite::IncludeGuardIfNDef { - conditional_index: site_conditional_index, - token_index: 0, - } if site_conditional_index == conditional_index - ) - }) - .expect("include guard token should be modeled as a resolved reference"); - assert_eq!(reference.name.as_str(), "HEADER_FLAG"); - assert_eq!(reference.name_range.source, header_source); - assert!(matches!( - reference.resolution, - SourceMacroResolution::Resolved { - reason: SourceMacroResolutionReason::IncludeGuardIfNDef, - .. - } - )); - } - - #[test] - fn source_model_nested_include_resolution_carries_definition_chain() { - let root_text = r#"`include "defs.vh" -logic [`LEAF_WIDTH-1:0] data; -"#; - let header_text = "`include \"leaf.vh\"\n"; - let leaf_path = "sample/include/leaf.vh"; - let options = SyntaxTreeOptions { - include_paths: vec![INCLUDE_DIR.to_owned()], - include_buffers: vec![ - SyntaxTreeBuffer { path: HEADER_PATH.to_owned(), text: header_text.to_owned() }, - SyntaxTreeBuffer { - path: leaf_path.to_owned(), - text: "`define LEAF_WIDTH 4\n".to_owned(), - }, - ], - expand_includes: true, - ..SyntaxTreeOptions::default() - }; - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); - let root_source = PreprocSourceId::from(trace.root_buffer_id); - let model = SourcePreprocModel::from_trace(trace).unwrap(); - let header_source = source_by_path_suffix(&model, "include/defs.vh"); - let leaf_source = source_by_path_suffix(&model, "include/leaf.vh"); - - let usage_index = model - .usages() - .iter() - .position(|usage| usage.name.as_deref() == Some("LEAF_WIDTH")) - .expect("root macro usage should be traced"); - let reference = reference_for_usage(&model, usage_index); - let SourceMacroResolution::Resolved { definition, include_chain, .. } = - &reference.resolution - else { - panic!("usage reference should resolve to nested included definition"); - }; - - assert_eq!( - model.macro_definitions().get(*definition).unwrap().name_range.source, - leaf_source - ); - assert_eq!(include_chain.len(), 2); - assert_eq!(include_chain[0].include_range.source, root_source); - assert_eq!(include_chain[0].included_source, header_source); - assert_eq!(include_chain[1].include_range.source, header_source); - assert_eq!(include_chain[1].included_source, leaf_source); - } - - #[test] - fn source_model_fails_closed_when_directive_event_range_is_missing() { - let trace = PreprocessorTrace { - root_buffer_id: 1, - source_buffers: vec![SourceBufferId { - path: ROOT_PATH.to_owned(), - text: None, - buffer_id: 1, - origin: SourceBufferOrigin::Source, - }], - events: vec![PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(0), - kind: SyntaxKind::DEFINE_DIRECTIVE, - range: None, - macro_definition_id: None, - macro_call_id: None, - macro_expansion_id: None, - parent_macro_expansion_id: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "WIDTH".to_owned(), - value_text: "WIDTH".to_owned(), - token_kind: TokenKind::IDENTIFIER, - range: Some(SourceBufferRange { buffer_id: 1, range: 8..13 }), - }), - include_file_name: None, - params: Vec::new(), - arguments: Vec::new(), - body_tokens: Vec::new(), - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }], - include_edges: Vec::new(), - emitted_tokens: Vec::new(), - }; - - assert_eq!( - SourcePreprocModel::from_trace(trace).unwrap_err(), - SourcePreprocError::MissingEventRange { source_order: 0, kind: MacroEventKind::Define } - ); - } -} +mod tests; diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs new file mode 100644 index 00000000..8777c195 --- /dev/null +++ b/crates/preproc/src/source/model/tests.rs @@ -0,0 +1,1246 @@ +use smol_str::SmolStr; +use syntax::{ + PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, + PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, + PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, PreprocessorTraceToken, + PreprocessorTraceTokenProvenance, SourceBufferId, SourceBufferOrigin, SourceBufferRange, + SyntaxKind, SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions, TokenKind, +}; +use utils::line_index::{TextRange, TextSize}; + +use super::{super::SourceMacroReferenceSite, *}; + +const ROOT_PATH: &str = "sample/rtl/top.sv"; +const HEADER_PATH: &str = "sample/include/defs.vh"; +const INCLUDE_DIR: &str = "sample/include"; + +fn source_model( + root_text: &str, + header_text: &str, +) -> (SourcePreprocModel, PreprocSourceId, PreprocSourceId) { + let options = SyntaxTreeOptions { + include_paths: vec![INCLUDE_DIR.to_owned()], + include_buffers: vec![SyntaxTreeBuffer { + path: HEADER_PATH.to_owned(), + text: header_text.to_owned(), + }], + expand_includes: true, + ..SyntaxTreeOptions::default() + }; + let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let header_source = first_non_root_source(&trace, root_source); + let model = SourcePreprocModel::from_trace(trace).unwrap(); + (model, root_source, header_source) +} + +fn first_non_root_source( + trace: &PreprocessorTrace, + root_source: PreprocSourceId, +) -> PreprocSourceId { + trace + .events + .iter() + .filter_map(|directive| directive.range.as_ref()) + .map(|range| PreprocSourceId::from(range.buffer_id)) + .find(|source| *source != root_source) + .expect("included source directive should be traced") +} + +fn source_by_path_suffix(model: &SourcePreprocModel, suffix: &str) -> PreprocSourceId { + model + .sources() + .iter() + .find(|source| { + matches!(source.origin, PreprocSourceOrigin::Included { .. }) + && source.path.as_str().replace('\\', "/").ends_with(suffix) + }) + .unwrap_or_else(|| panic!("source ending with {suffix} should be present")) + .id +} + +fn source_model_from_root( + root_text: &str, + options: SyntaxTreeOptions, +) -> (SourcePreprocModel, PreprocSourceId) { + let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let model = SourcePreprocModel::from_trace(trace).unwrap(); + (model, root_source) +} + +fn offset_before(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).unwrap()).unwrap()) +} + +fn offset_after(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) +} + +fn text_at_range(text: &str, range: TextRange) -> &str { + &text[usize::from(range.start())..usize::from(range.end())] +} + +fn source_range(source: PreprocSourceId, start: u32, end: u32) -> SourceRange { + SourceRange { source, range: TextRange::new(TextSize::from(start), TextSize::from(end)) } +} + +fn visible_macro_names( + model: &SourcePreprocModel, + source: PreprocSourceId, + offset: TextSize, +) -> Vec { + model + .visible_macros_at(SourcePosition { source, offset }) + .into_iter() + .map(|definition| definition.name.clone()) + .collect() +} + +fn visible_macro_definition<'a>( + model: &'a SourcePreprocModel, + source: PreprocSourceId, + offset: TextSize, + name: &str, +) -> Option<&'a SourceMacroDefinition> { + model + .visible_macros_at(SourcePosition { source, offset }) + .into_iter() + .find(|definition| definition.name == name) +} + +fn reference_for_usage(model: &SourcePreprocModel, usage_index: usize) -> &SourceMacroReference { + model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::Usage { + usage_index: site_usage_index, + } if site_usage_index == usage_index + ) + }) + .expect("usage reference should be in resolved reference table") +} + +fn reference_for_conditional_token( + model: &SourcePreprocModel, + conditional_index: usize, + token_index: usize, +) -> &SourceMacroReference { + model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::ConditionalToken { + conditional_index: site_conditional_index, + token_index: site_token_index, + } | SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: site_conditional_index, + token_index: site_token_index, + } if site_conditional_index == conditional_index + && site_token_index == token_index + ) + }) + .expect("conditional token reference should be in resolved reference table") +} + +#[test] +fn source_model_applies_include_define_after_include_point_only() { + let root_text = r#"`include "defs.vh" +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + assert!( + !visible_macro_names(&model, root_source, offset_before(root_text, "`include")) + .iter() + .any(|name| name == "HEADER_WIDTH") + ); + + let after_include = visible_macro_definition( + &model, + root_source, + offset_after(root_text, "`include \"defs.vh\"\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_include.id.raw(), 0); + + let definition = model + .visible_macros_at(SourcePosition { + source: root_source, + offset: offset_after(root_text, "`include \"defs.vh\"\n"), + }) + .into_iter() + .find(|definition| definition.name == "HEADER_WIDTH") + .unwrap(); + assert_eq!(definition.name_range.source, header_source); +} + +#[test] +fn source_model_undef_removes_included_define() { + let root_text = r#"`include "defs.vh" +`undef HEADER_WIDTH +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let after_include = visible_macro_definition( + &model, + root_source, + offset_after(root_text, "`include \"defs.vh\"\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_include.id.raw(), 0); + assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); + + assert!( + visible_macro_definition( + &model, + root_source, + offset_after(root_text, "`undef HEADER_WIDTH\n"), + "HEADER_WIDTH", + ) + .is_none() + ); + assert_eq!(model.undefs()[0].name.as_deref(), Some("HEADER_WIDTH")); + assert_eq!(model.undefs()[0].name_range.unwrap().source, root_source); +} + +#[test] +fn source_model_same_name_define_overrides_included_define() { + let root_text = r#"`include "defs.vh" +`define HEADER_WIDTH 16 +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + assert_eq!(model.defines()[0].name_range.unwrap().source, header_source); + assert_eq!(model.defines()[1].name_range.unwrap().source, root_source); + + let after_override = visible_macro_definition( + &model, + root_source, + offset_after(root_text, "`define HEADER_WIDTH 16\n"), + "HEADER_WIDTH", + ) + .unwrap(); + assert_eq!(after_override.id.raw(), 1); + + let definition = model + .visible_macros_at(SourcePosition { + source: root_source, + offset: offset_after(root_text, "`define HEADER_WIDTH 16\n"), + }) + .into_iter() + .find(|definition| definition.name == "HEADER_WIDTH") + .unwrap(); + assert_eq!(definition.body_tokens[0].value.as_str(), "16"); + assert_eq!(definition.name_range.source, root_source); +} + +#[test] +fn visible_macro_query_reads_timeline_without_event_records() { + let root_text = r#"`define A 1 +`undef A +`define B 2 +"#; + let trace = SyntaxTree::preprocessor_trace( + root_text, + "source", + ROOT_PATH, + &SyntaxTreeOptions::default(), + ) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let mut model = SourcePreprocModel::from_trace(trace).unwrap(); + + let names_after_define = + visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")); + let names_after_undef = + visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")); + let names_after_second_define = + visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")); + + assert_eq!(names_after_define, vec![SmolStr::new("A")]); + assert!(names_after_undef.is_empty(), "{names_after_undef:?}"); + assert_eq!(names_after_second_define, vec![SmolStr::new("B")]); + + model.index.event_records.clear(); + + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`define A 1\n")), + names_after_define + ); + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`undef A\n")), + names_after_undef + ); + assert_eq!( + visible_macro_names(&model, root_source, offset_after(root_text, "`define B 2\n")), + names_after_second_define + ); +} + +#[test] +fn source_model_preserves_inactive_range_sources() { + let root_text = r#"`include "defs.vh" +`ifndef HEADER_FLAG +wire disabled_by_header; +`endif +"#; + let header_text = r#"`define HEADER_FLAG +`ifdef NEVER +wire disabled_from_header; +`endif +"#; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let root_inactive = + model.inactive_ranges().iter().find(|range| range.source == root_source).unwrap(); + assert_eq!(text_at_range(root_text, root_inactive.range), "wire disabled_by_header;"); + + let header_inactive = + model.inactive_ranges().iter().find(|range| range.source == header_source).unwrap(); + assert_eq!(text_at_range(header_text, header_inactive.range), "wire disabled_from_header;"); +} + +#[test] +fn source_model_resolves_root_usage_to_included_define() { + let root_text = r#"`include "defs.vh" +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("HEADER_WIDTH")) + .expect("root macro usage should be traced"); + let usage = &model.usages()[usage_index]; + assert_eq!(usage.range.source, root_source); + assert_eq!(usage.name_range.unwrap().source, root_source); + + let reference = reference_for_usage(&model, usage_index); + let SourceMacroResolution::Resolved { definition, include_chain, reason } = + &reference.resolution + else { + panic!("usage reference should resolve to included definition"); + }; + assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); + let definition = model.macro_definitions().get(*definition).unwrap(); + assert_eq!(definition.name.as_str(), "HEADER_WIDTH"); + assert_eq!(definition.name_range.source, header_source); + assert_eq!(definition.body_tokens[0].value.as_str(), "8"); + assert_eq!(include_chain.len(), 1); + assert_eq!(include_chain[0].include_range.source, root_source); + assert_eq!(include_chain[0].included_source, header_source); +} + +#[test] +fn source_model_exposes_expansion_provenance_skeleton_tables() { + let root_text = r#"`include "defs.vh" +logic [`HEADER_WIDTH-1:0] data; +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let definition = model + .macro_definitions() + .iter() + .find(|definition| definition.name.as_str() == "HEADER_WIDTH") + .expect("definition table should include precise macro definition"); + assert_eq!(definition.directive_range.source, header_source); + assert_eq!(definition.name_range.source, header_source); + assert_ne!(definition.directive_range.range, definition.name_range.range); + assert_eq!(text_at_range(header_text, definition.name_range.range), "HEADER_WIDTH"); + + let reference = model + .macro_references() + .iter() + .find(|reference| { + reference.name.as_str() == "HEADER_WIDTH" + && matches!(reference.site, SourceMacroReferenceSite::Usage { usage_index: _ }) + }) + .expect("reference table should include resolved macro usage"); + assert_eq!(reference.name_range.source, root_source); + assert_eq!(reference.directive_range.source, root_source); + let SourceMacroResolution::Resolved { definition: resolved_definition, reason, include_chain } = + &reference.resolution + else { + panic!("macro usage should resolve to included definition"); + }; + assert_eq!(*reason, SourceMacroResolutionReason::VisibleDefinition); + assert_eq!(include_chain.len(), 1); + assert_eq!( + model.macro_definitions().get(*resolved_definition).unwrap().name.as_str(), + "HEADER_WIDTH" + ); + + assert_eq!(model.include_graph().directives().len(), 1); + assert!(matches!( + &model.include_graph().directives()[0].status, + SourceIncludeStatus::Resolved { source } if *source == header_source + )); + assert!(!model.state_timeline().checkpoints().is_empty()); + + let call = model + .macro_calls() + .iter() + .find(|call| call.reference == reference.id) + .expect("macro usage should create a call fact"); + assert_eq!(call.call_range.source, root_source); + assert_eq!(call.status, SourceMacroCallStatus::ExpansionAvailable); + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("object-like macro call should have an immediate expansion"); + }; + assert_eq!(call.expansion, Some(expansion_id)); + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!(expansion.call, call.id); + assert_eq!(*resolved_definition, expansion.definition); + assert!(expansion.child_calls.is_empty()); + assert_eq!(expansion.status, SourceMacroExpansionStatus::Complete); + + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "8") + .expect("macro body token should be emitted by adapter authority"); + assert_eq!(expansion.emitted_token_range.start, emitted.id); + assert_eq!(expansion.emitted_token_range.len, 1); + let provenance = model.token_provenance().get(emitted.provenance).unwrap(); + assert!(matches!( + provenance, + SourceTokenProvenance::MacroBody { + definition: body_definition, + body_token_range, + call: body_call, + .. + } if *body_definition == *resolved_definition + && body_token_range.source == header_source + && *body_call == call.id + )); + let recursive = model.recursive_macro_expansion(call.id); + assert_eq!(recursive.expansions, vec![expansion_id]); + assert!(recursive.unavailable.is_empty()); + assert_eq!(model.capabilities().macro_calls, CapabilityStatus::Complete); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Complete); + assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Complete); +} + +#[test] +fn source_model_maps_function_macro_argument_emitted_token_to_argument() { + let root_text = r#"`define ID(x) x +module m; +localparam int W = `ID(7); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "7") + .expect("argument replacement token should be emitted"); + let SourceTokenProvenance::MacroArgument { call, argument_index, argument_token_range, .. } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("argument replacement should map to MacroArgument provenance"); + }; + assert_eq!(*argument_index, 0); + assert_eq!(argument_token_range.source, root_source); + assert_eq!(text_at_range(root_text, argument_token_range.range), "7"); + + let call = model.macro_calls().get(*call).expect("call id should resolve"); + assert_eq!(call.call_range.source, root_source); + assert_eq!(text_at_range(root_text, call.call_range.range), "`ID(7)"); + assert_eq!(call.arguments.len(), 1); + assert_eq!(call.arguments[0].argument_index, 0); + assert_eq!(call.arguments[0].argument_range, Some(*argument_token_range)); + + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("function-like macro call should have an immediate expansion"); + }; + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!(expansion.emitted_token_range.start, emitted.id); + assert_eq!(expansion.emitted_token_range.len, 1); +} + +#[test] +fn source_model_maps_nested_macro_usage_in_actual_argument_to_source_spelling() { + let root_text = r#"`define PAYL payload_i +`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`PAYL); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let next_usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("NEXT")) + .expect("outer function macro usage should be traced"); + let next_usage = &model.usages()[next_usage_index]; + assert_eq!(next_usage.arguments.len(), 1); + let next_argument_range = next_usage.arguments[0] + .argument_range + .expect("actual argument should keep written source range"); + assert_eq!(next_argument_range.source, root_source); + assert_eq!(text_at_range(root_text, next_argument_range.range), "`PAYL"); + assert_eq!( + next_usage.arguments[0].tokens.iter().map(|token| token.raw.as_str()).collect::>(), + vec!["`PAYL"] + ); + + let next_reference = reference_for_usage(&model, next_usage_index); + let next_call = model + .macro_calls() + .iter() + .find(|call| call.reference == next_reference.id) + .expect("outer macro usage should create a call"); + assert_eq!(next_call.arguments[0].argument_range, Some(next_argument_range)); + assert!(matches!( + model.immediate_macro_expansion(next_call.id), + SourceMacroExpansionQuery::Available(_) + )); + + let payl_usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("PAYL")) + .expect("nested actual-argument macro usage should be traced"); + let payl_usage = &model.usages()[payl_usage_index]; + assert_eq!(payl_usage.range.source, root_source); + assert_eq!(text_at_range(root_text, payl_usage.range.range), "`PAYL"); + let payl_reference = reference_for_usage(&model, payl_usage_index); + let SourceMacroResolution::Resolved { definition, .. } = &payl_reference.resolution else { + panic!("PAYL usage should resolve through its runtime definition identity"); + }; + assert_eq!(model.macro_definitions().get(*definition).unwrap().name.as_str(), "PAYL"); + assert!(model.macro_calls().iter().any(|call| call.reference == payl_reference.id)); +} + +#[test] +fn source_model_uses_direct_definition_identity_when_body_ranges_collide() { + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![SourceBufferId { + path: ROOT_PATH.to_owned(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }], + events: vec![ + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(0), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 0..12 }), + macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(10)), + macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "A".to_owned(), + value_text: "A".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "1".to_owned(), + value_text: "1".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(1), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 13..25 }), + macro_definition_id: Some(PreprocessorTraceMacroDefinitionId(20)), + macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "B".to_owned(), + value_text: "B".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 21..22 }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "2".to_owned(), + value_text: "2".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(2), + kind: SyntaxKind::MACRO_USAGE, + range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), + macro_definition_id: None, + macro_call_id: Some(PreprocessorTraceMacroCallId(200)), + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "`B".to_owned(), + value_text: "`B".to_owned(), + token_kind: TokenKind::DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: 40..42 }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: Vec::new(), + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + ], + include_edges: Vec::new(), + emitted_tokens: vec![syntax::PreprocessorTraceEmittedToken { + raw_text: "2".to_owned(), + value_text: "2".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + provenance: PreprocessorTraceTokenProvenance::MacroBody { + macro_name: "B".to_owned(), + identity: PreprocessorTraceMacroBodyIdentity { + call_id: PreprocessorTraceMacroCallId(200), + definition_id: PreprocessorTraceMacroDefinitionId(20), + expansion_id: PreprocessorTraceMacroExpansionId(300), + parent_expansion_id: None, + body_token_index: 0, + }, + call_range: SourceBufferRange { buffer_id: 1, range: 40..42 }, + body_token_range: SourceBufferRange { buffer_id: 1, range: 8..9 }, + }, + }], + }; + let model = SourcePreprocModel::from_trace(trace).unwrap(); + let emitted = model.emitted_tokens().iter().find(|token| token.text == "2").unwrap(); + let SourceTokenProvenance::MacroBody { definition, call, identity, .. } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("colliding range token should still resolve through direct body identity"); + }; + + let definition = model.macro_definitions().get(*definition).unwrap(); + assert_eq!(definition.name.as_str(), "B"); + assert_eq!(definition.identity, Some(identity.definition)); + assert_eq!(model.macro_calls().get(*call).unwrap().identity, Some(identity.call)); +} + +#[test] +fn source_model_preserves_multi_token_argument_direct_identity() { + let root_text = r#"`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(payload_i[3:0]); +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let payload = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index, + body_token_range, + argument_token_range, + } = model.token_provenance().get(token.provenance)? + else { + return None; + }; + (token.text.as_str() == "payload_i").then_some(( + *identity, + *call, + *argument_index, + *body_token_range, + *argument_token_range, + )) + }) + .expect("payload identifier should be direct macro argument provenance"); + let slice = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index, + body_token_range, + argument_token_range, + } = model.token_provenance().get(token.provenance)? + else { + return None; + }; + (token.text.as_str() == "3").then_some(( + *identity, + *call, + *argument_index, + *body_token_range, + *argument_token_range, + )) + }) + .expect("slice index should be direct macro argument provenance"); + + assert_eq!(payload.0.call, slice.0.call); + assert_eq!(payload.1, slice.1); + assert_eq!(payload.2, 0); + assert_eq!(slice.2, 0); + assert_eq!(payload.0.argument_token_index, 0); + assert_eq!(slice.0.argument_token_index, 2); + assert_eq!(payload.3, slice.3); + assert_eq!(payload.4.source, root_source); + assert_eq!(slice.4.source, root_source); + let call = model.macro_calls().get(payload.1).unwrap(); + assert_eq!(call.arguments.len(), 1); + assert_eq!( + text_at_range(root_text, call.arguments[0].argument_range.unwrap().range), + "payload_i[3:0]" + ); +} + +#[test] +fn source_model_marks_missing_direct_identity_partial_without_range_fallback() { + let root_source = PreprocSourceId::from(1); + let define_range = source_range(root_source, 0, 11); + let name_range = source_range(root_source, 8, 9); + let body_range = source_range(root_source, 10, 11); + let usage_range = source_range(root_source, 24, 26); + let index = SourcePreprocIndex { + root_source: Some(root_source), + sources: vec![PreprocSource { + id: root_source, + path: SmolStr::new(ROOT_PATH), + origin: PreprocSourceOrigin::Root, + }], + event_records: vec![ + SourcePreprocEventRecord { + event_id: SourcePreprocEventId(0), + kind: MacroEventKind::Define, + range: define_range, + index: 0, + }, + SourcePreprocEventRecord { + event_id: SourcePreprocEventId(1), + kind: MacroEventKind::Usage, + range: usage_range, + index: 0, + }, + ], + emitted_tokens: vec![SourceEmittedTokenFact { + raw: SmolStr::new("1"), + value: SmolStr::new("1"), + kind: SourceTokenKind::Syntax(TokenKind::INTEGER_LITERAL), + provenance: SourceTokenProvenanceFact::MacroBody { + macro_name: SmolStr::new("A"), + identity: None, + call_range: usage_range, + body_token_range: body_range, + }, + }], + defines: vec![SourceMacroDefine { + event_id: SourcePreprocEventId(0), + identity: Some(SourceMacroDefinitionKey::new(10)), + name: Some(SmolStr::new("A")), + name_range: Some(name_range), + params: None, + body: vec![SourceMacroToken { + raw: SmolStr::new("1"), + value: SmolStr::new("1"), + range: Some(body_range), + }], + range: define_range, + }], + usages: vec![SourceMacroUsage { + event_id: SourcePreprocEventId(1), + identity: Some(SourceMacroCallKey::new(20)), + definition_identity: None, + expansion_identity: None, + parent_expansion_identity: None, + name: Some(SmolStr::new("A")), + name_range: Some(usage_range), + arguments: Vec::new(), + range: usage_range, + }], + ..SourcePreprocIndex::default() + }; + + let model = SourcePreprocModel::new(index); + let emitted = model.emitted_tokens().iter().next().unwrap(); + assert!(matches!( + model.token_provenance().get(emitted.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity + ) + )); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); +} + +#[test] +fn source_model_builds_nested_expansion_graph_from_runtime_usage_records() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int W = `WRAP; +endmodule +"#; + let (model, root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let wrap_reference = model + .macro_references() + .iter() + .find(|reference| reference.name.as_str() == "WRAP") + .expect("outer macro usage should create a reference"); + let wrap_call = model + .macro_calls() + .iter() + .find(|call| call.reference == wrap_reference.id) + .expect("outer macro usage should create a call"); + assert_eq!(wrap_call.call_range.source, root_source); + + let leaf_call = model + .macro_calls() + .iter() + .find(|call| { + let reference = model.macro_references().get(call.reference).unwrap(); + reference.name.as_str() == "LEAF" + && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) + }) + .expect("nested macro invocation should create a runtime usage call"); + let leaf_reference = model.macro_references().get(leaf_call.reference).unwrap(); + assert_eq!(text_at_range(root_text, leaf_reference.name_range.range), "`LEAF"); + assert_eq!(leaf_call.parent_expansion_identity, wrap_call.expansion_identity); + + let SourceMacroExpansionQuery::Available(wrap_expansion_id) = + model.immediate_macro_expansion(wrap_call.id) + else { + panic!("outer macro should have an expansion identity from the runtime usage record"); + }; + let wrap_expansion = model.macro_expansions().get(wrap_expansion_id).unwrap(); + assert_eq!(wrap_expansion.child_calls, vec![leaf_call.id]); + + let recursive = model.recursive_macro_expansion(wrap_call.id); + assert_eq!(recursive.expansions.len(), 2); + assert!(recursive.expansions.contains(&wrap_expansion_id)); + assert!(recursive.unavailable.is_empty()); +} + +#[test] +fn source_model_builds_nested_leaf_expansion_from_direct_identity() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int W = `WRAP; +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let leaf_call = model + .macro_calls() + .iter() + .find(|call| { + let reference = model.macro_references().get(call.reference).unwrap(); + reference.name.as_str() == "LEAF" + && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) + }) + .expect("nested macro invocation should create a runtime usage call"); + assert!(leaf_call.identity.is_some()); + assert!(leaf_call.expansion_identity.is_some()); + assert!(leaf_call.parent_expansion_identity.is_some()); + + let SourceMacroExpansionQuery::Available(leaf_expansion_id) = + model.immediate_macro_expansion(leaf_call.id) + else { + panic!("nested macro should have its own immediate expansion"); + }; + let emitted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "3") + .expect("nested macro body token should be emitted"); + let SourceTokenProvenance::MacroBody { identity, definition, call, .. } = + model.token_provenance().get(emitted.provenance).unwrap() + else { + panic!("nested emitted token should keep macro body provenance"); + }; + assert_eq!(*call, leaf_call.id); + assert_eq!(Some(identity.call), leaf_call.identity); + assert_eq!(Some(identity.expansion), leaf_call.expansion_identity); + assert_eq!(identity.parent_expansion, leaf_call.parent_expansion_identity); + assert_eq!( + Some(identity.definition), + model.macro_definitions().get(*definition).unwrap().identity + ); + + let recursive = model.recursive_macro_expansion(leaf_call.id); + assert_eq!(recursive.expansions, vec![leaf_expansion_id]); + assert!(recursive.unavailable.is_empty()); +} + +#[test] +fn source_model_marks_unsupported_macro_ops_unavailable_without_dropping_tokens() { + let root_text = r#"`define JOIN(a,b) a``b +`define STR(x) `"x`" +module m; +wire `JOIN(foo,bar); +string s = `STR(foo); +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let pasted = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "foobar") + .expect("token paste result should not be dropped"); + assert!(matches!( + model.token_provenance().get(pasted.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + ) + )); + + let stringified = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "\"foo\"") + .expect("stringification result should not be dropped"); + assert!(matches!( + model.token_provenance().get(stringified.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + ) + )); + assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); + for call in model.macro_calls().iter() { + assert!(matches!( + model.immediate_macro_expansion(call.id), + SourceMacroExpansionQuery::Unavailable(_) + )); + } +} + +#[test] +fn source_model_does_not_create_expansion_without_emitted_token_authority() { + let root_text = "`define A 1\nmodule m; localparam int W = `A; endmodule\n"; + let define_start = root_text.find("`define").unwrap(); + let define_end = root_text.find('\n').unwrap(); + let usage_start = root_text.find("`A").unwrap(); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![SourceBufferId { + path: ROOT_PATH.to_owned(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }], + events: vec![ + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(0), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: Some(SourceBufferRange { buffer_id: 1, range: define_start..define_end }), + macro_definition_id: None, + macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "A".to_owned(), + value_text: "A".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..9 }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: vec![PreprocessorTraceToken { + raw_text: "1".to_owned(), + value_text: "1".to_owned(), + token_kind: TokenKind::INTEGER_LITERAL, + range: Some(SourceBufferRange { buffer_id: 1, range: 10..11 }), + }], + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(1), + kind: SyntaxKind::MACRO_USAGE, + range: Some(SourceBufferRange { + buffer_id: 1, + range: usage_start..usage_start + 2, + }), + macro_definition_id: None, + macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "`A".to_owned(), + value_text: "`A".to_owned(), + token_kind: TokenKind::DIRECTIVE, + range: Some(SourceBufferRange { + buffer_id: 1, + range: usage_start..usage_start + 2, + }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: Vec::new(), + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }, + ], + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let model = SourcePreprocModel::from_trace(trace).unwrap(); + let call = model.macro_calls().iter().next().expect("usage should create a call"); + + assert!(model.macro_expansions().is_empty()); + assert!(matches!( + model.immediate_macro_expansion(call.id), + SourceMacroExpansionQuery::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + ) + )); + assert!(matches!( + &model.capabilities().macro_expansions, + CapabilityStatus::Unavailable(SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable) + )); +} + +#[test] +fn source_model_maps_predefine_and_marks_intrinsic_unavailable() { + let root_text = r#"module m; +localparam int P = `FROM_API; +localparam int L = `__LINE__; +endmodule +"#; + let (model, _root_source) = source_model_from_root( + root_text, + SyntaxTreeOptions { + predefines: vec!["FROM_API=11".to_owned()], + ..SyntaxTreeOptions::default() + }, + ); + + let predefine = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "11") + .expect("predefine expansion token should be emitted"); + let SourceTokenProvenance::Predefine { source } = + model.token_provenance().get(predefine.provenance).unwrap() + else { + panic!("configured predefine token should map to Predefine provenance"); + }; + assert!(model.sources().iter().any(|candidate| { + candidate.id == *source && candidate.origin == PreprocSourceOrigin::Predefine + })); + + let intrinsic = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "3") + .expect("intrinsic macro token should stay in emitted stream"); + assert!(matches!( + model.token_provenance().get(intrinsic.provenance).unwrap(), + SourceTokenProvenance::Unavailable( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + ) + )); +} + +#[test] +fn source_model_resolves_conditional_tokens_to_visible_defines() { + let root_text = r#"`include "defs.vh" +`ifdef HEADER_FLAG +wire active; +`endif +"#; + let header_text = "`define HEADER_FLAG\n"; + let (model, root_source, header_source) = source_model(root_text, header_text); + + let conditional_index = model + .conditionals() + .iter() + .position(|conditional| conditional.kind == MacroConditionalKind::IfDef) + .expect("ifdef should be traced"); + let reference = reference_for_conditional_token(&model, conditional_index, 0); + + assert_eq!(reference.name.as_str(), "HEADER_FLAG"); + assert_eq!(reference.name_range.source, root_source); + let SourceMacroResolution::Resolved { definition, reason, .. } = reference.resolution else { + panic!("conditional token reference should resolve to visible definition"); + }; + assert_eq!(reason, SourceMacroResolutionReason::VisibleDefinition); + assert_eq!(model.macro_definitions().get(definition).unwrap().name_range.source, header_source); +} + +#[test] +fn source_model_resolves_ifndef_include_guard_to_following_define() { + let root_text = r#"`include "defs.vh" +`ifdef HEADER_FLAG +wire active; +`endif +"#; + let header_text = r#"`ifndef HEADER_FLAG +`define HEADER_FLAG +`endif +"#; + let (model, _root_source, header_source) = source_model(root_text, header_text); + + let conditional_index = model + .conditionals() + .iter() + .position(|conditional| { + conditional.kind == MacroConditionalKind::IfNDef + && conditional.range.source == header_source + }) + .expect("ifndef guard should be traced"); + let reference = model + .macro_references() + .iter() + .find(|reference| { + matches!( + reference.site, + SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: site_conditional_index, + token_index: 0, + } if site_conditional_index == conditional_index + ) + }) + .expect("include guard token should be modeled as a resolved reference"); + assert_eq!(reference.name.as_str(), "HEADER_FLAG"); + assert_eq!(reference.name_range.source, header_source); + assert!(matches!( + reference.resolution, + SourceMacroResolution::Resolved { + reason: SourceMacroResolutionReason::IncludeGuardIfNDef, + .. + } + )); +} + +#[test] +fn source_model_nested_include_resolution_carries_definition_chain() { + let root_text = r#"`include "defs.vh" +logic [`LEAF_WIDTH-1:0] data; +"#; + let header_text = "`include \"leaf.vh\"\n"; + let leaf_path = "sample/include/leaf.vh"; + let options = SyntaxTreeOptions { + include_paths: vec![INCLUDE_DIR.to_owned()], + include_buffers: vec![ + SyntaxTreeBuffer { path: HEADER_PATH.to_owned(), text: header_text.to_owned() }, + SyntaxTreeBuffer { + path: leaf_path.to_owned(), + text: "`define LEAF_WIDTH 4\n".to_owned(), + }, + ], + expand_includes: true, + ..SyntaxTreeOptions::default() + }; + let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) + .expect("trace should include root source"); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + let model = SourcePreprocModel::from_trace(trace).unwrap(); + let header_source = source_by_path_suffix(&model, "include/defs.vh"); + let leaf_source = source_by_path_suffix(&model, "include/leaf.vh"); + + let usage_index = model + .usages() + .iter() + .position(|usage| usage.name.as_deref() == Some("LEAF_WIDTH")) + .expect("root macro usage should be traced"); + let reference = reference_for_usage(&model, usage_index); + let SourceMacroResolution::Resolved { definition, include_chain, .. } = &reference.resolution + else { + panic!("usage reference should resolve to nested included definition"); + }; + + assert_eq!(model.macro_definitions().get(*definition).unwrap().name_range.source, leaf_source); + assert_eq!(include_chain.len(), 2); + assert_eq!(include_chain[0].include_range.source, root_source); + assert_eq!(include_chain[0].included_source, header_source); + assert_eq!(include_chain[1].include_range.source, header_source); + assert_eq!(include_chain[1].included_source, leaf_source); +} + +#[test] +fn source_model_fails_closed_when_directive_event_range_is_missing() { + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![SourceBufferId { + path: ROOT_PATH.to_owned(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }], + events: vec![PreprocessorTraceEvent { + event_id: PreprocessorTraceEventId(0), + kind: SyntaxKind::DEFINE_DIRECTIVE, + range: None, + macro_definition_id: None, + macro_call_id: None, + macro_expansion_id: None, + parent_macro_expansion_id: None, + directive: None, + name: Some(PreprocessorTraceToken { + raw_text: "WIDTH".to_owned(), + value_text: "WIDTH".to_owned(), + token_kind: TokenKind::IDENTIFIER, + range: Some(SourceBufferRange { buffer_id: 1, range: 8..13 }), + }), + include_file_name: None, + params: Vec::new(), + arguments: Vec::new(), + body_tokens: Vec::new(), + expr_tokens: Vec::new(), + disabled_ranges: Vec::new(), + }], + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + + assert_eq!( + SourcePreprocModel::from_trace(trace).unwrap_err(), + SourcePreprocError::MissingEventRange { source_order: 0, kind: MacroEventKind::Define } + ); +} diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index e0c8cf4b..d5f6cd4e 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -21,6 +21,67 @@ macro_rules! source_table_id { }; } +macro_rules! source_table { + ($table:ident, $field:ident, $id:ident, $item:ty) => { + #[derive(Debug, Clone, PartialEq, Eq, Default)] + pub struct $table { + $field: Vec<$item>, + } + + impl $table { + pub fn get(&self, id: $id) -> Option<&$item> { + self.$field.get(id.raw()) + } + + pub fn iter(&self) -> std::slice::Iter<'_, $item> { + self.$field.iter() + } + + pub fn len(&self) -> usize { + self.$field.len() + } + + pub fn is_empty(&self) -> bool { + self.$field.is_empty() + } + + fn push(&mut self, item: $item) { + self.$field.push(item); + } + } + }; + + ($table:ident, $field:ident, $id:ident, $item:ty,mutable) => { + source_table!($table, $field, $id, $item); + + impl $table { + fn get_mut(&mut self, id: $id) -> Option<&mut $item> { + self.$field.get_mut(id.raw()) + } + } + }; +} + +macro_rules! impl_source_ranges { + ($ty:ty,directive = $directive:ident) => { + impl HasDirectiveRange for $ty { + fn directive_range(&self) -> SourceRange { + self.$directive + } + } + }; + + ($ty:ty,directive = $directive:ident,name = $name:ident) => { + impl_source_ranges!($ty, directive = $directive); + + impl HasNameRange for $ty { + fn name_range(&self) -> Option { + Some(self.$name) + } + } + }; +} + source_table_id!(SourceMacroDefinitionId); source_table_id!(SourceMacroReferenceId); source_table_id!(SourceIncludeDirectiveId); @@ -276,35 +337,22 @@ pub struct SourcePreprocTables { pub issues: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroDefinitionTable { - definitions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroReferenceTable { - references: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroCallTable { - calls: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroExpansionTable { - expansions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceEmittedTokenTable { - tokens: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceTokenProvenanceTable { - provenance: Vec, -} +source_table!( + SourceMacroDefinitionTable, + definitions, + SourceMacroDefinitionId, + SourceMacroDefinition +); +source_table!(SourceMacroReferenceTable, references, SourceMacroReferenceId, SourceMacroReference); +source_table!(SourceMacroCallTable, calls, SourceMacroCallId, SourceMacroCall, mutable); +source_table!(SourceMacroExpansionTable, expansions, SourceMacroExpansionId, SourceMacroExpansion); +source_table!(SourceEmittedTokenTable, tokens, SourceEmittedTokenId, SourceEmittedToken); +source_table!( + SourceTokenProvenanceTable, + provenance, + SourceTokenProvenanceId, + SourceTokenProvenance +); #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocCapabilities { @@ -484,1406 +532,9 @@ impl SourcePreprocCapabilities { } } -impl SourceMacroDefinitionTable { - pub fn get(&self, id: SourceMacroDefinitionId) -> Option<&SourceMacroDefinition> { - self.definitions.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.definitions.iter() - } - - pub fn len(&self) -> usize { - self.definitions.len() - } - - pub fn is_empty(&self) -> bool { - self.definitions.is_empty() - } - - fn push(&mut self, definition: SourceMacroDefinition) { - self.definitions.push(definition); - } -} - -impl SourceMacroReferenceTable { - pub fn get(&self, id: SourceMacroReferenceId) -> Option<&SourceMacroReference> { - self.references.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.references.iter() - } - - pub fn len(&self) -> usize { - self.references.len() - } - - pub fn is_empty(&self) -> bool { - self.references.is_empty() - } - - fn push(&mut self, reference: SourceMacroReference) { - self.references.push(reference); - } -} - -impl SourceMacroCallTable { - pub fn get(&self, id: SourceMacroCallId) -> Option<&SourceMacroCall> { - self.calls.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.calls.iter() - } - - pub fn len(&self) -> usize { - self.calls.len() - } - - pub fn is_empty(&self) -> bool { - self.calls.is_empty() - } - - fn push(&mut self, call: SourceMacroCall) { - self.calls.push(call); - } - - fn get_mut(&mut self, id: SourceMacroCallId) -> Option<&mut SourceMacroCall> { - self.calls.get_mut(id.raw()) - } -} - -impl SourceMacroExpansionTable { - pub fn get(&self, id: SourceMacroExpansionId) -> Option<&SourceMacroExpansion> { - self.expansions.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.expansions.iter() - } - - pub fn len(&self) -> usize { - self.expansions.len() - } - - pub fn is_empty(&self) -> bool { - self.expansions.is_empty() - } - - fn push(&mut self, expansion: SourceMacroExpansion) { - self.expansions.push(expansion); - } -} - -impl SourceEmittedTokenTable { - pub fn get(&self, id: SourceEmittedTokenId) -> Option<&SourceEmittedToken> { - self.tokens.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.tokens.iter() - } - - pub fn len(&self) -> usize { - self.tokens.len() - } - - pub fn is_empty(&self) -> bool { - self.tokens.is_empty() - } - - fn push(&mut self, token: SourceEmittedToken) { - self.tokens.push(token); - } -} - -impl SourceTokenProvenanceTable { - pub fn get(&self, id: SourceTokenProvenanceId) -> Option<&SourceTokenProvenance> { - self.provenance.get(id.raw()) - } - - pub fn iter(&self) -> impl Iterator { - self.provenance.iter() - } - - pub fn len(&self) -> usize { - self.provenance.len() - } - - pub fn is_empty(&self) -> bool { - self.provenance.is_empty() - } - - fn push(&mut self, provenance: SourceTokenProvenance) { - self.provenance.push(provenance); - } -} - -impl HasDirectiveRange for SourceMacroDefinition { - fn directive_range(&self) -> SourceRange { - self.directive_range - } -} - -impl HasNameRange for SourceMacroDefinition { - fn name_range(&self) -> Option { - Some(self.name_range) - } -} - -impl HasDirectiveRange for SourceMacroReference { - fn directive_range(&self) -> SourceRange { - self.directive_range - } -} - -impl HasNameRange for SourceMacroReference { - fn name_range(&self) -> Option { - Some(self.name_range) - } -} - -impl HasDirectiveRange for SourceIncludeDirective { - fn directive_range(&self) -> SourceRange { - self.directive_range - } -} - -pub struct SourcePreprocModelBuilder<'a> { - index: &'a SourcePreprocIndex, - tables: SourcePreprocTables, - definition_ids_by_define_index: BTreeMap, - definition_ids_by_identity: BTreeMap, - call_ids_by_identity: BTreeMap, - call_ids_by_expansion_identity: BTreeMap, - current_state: BTreeMap, - definition_ranges_partial: bool, - include_edges_partial: bool, - references_partial: bool, - macro_calls_partial: bool, - token_provenance_partial: bool, - expansions_partial: bool, -} - -impl<'a> SourcePreprocModelBuilder<'a> { - pub fn new(index: &'a SourcePreprocIndex) -> Self { - Self { - index, - tables: SourcePreprocTables::default(), - definition_ids_by_define_index: BTreeMap::new(), - definition_ids_by_identity: BTreeMap::new(), - call_ids_by_identity: BTreeMap::new(), - call_ids_by_expansion_identity: BTreeMap::new(), - current_state: BTreeMap::new(), - definition_ranges_partial: false, - include_edges_partial: false, - references_partial: false, - macro_calls_partial: false, - token_provenance_partial: false, - expansions_partial: false, - } - } - - pub fn build(mut self) -> SourcePreprocTables { - self.build_tables(); - self.tables - } - - fn build_tables(&mut self) { - self.build_definition_table(); - self.build_include_graph(); - self.record_position_boundaries(); - self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); - self.scan_references_and_state(); - self.build_emitted_token_tables(); - self.build_macro_expansion_graph(); - self.record_macro_body_references_for_calls(); - let macro_expansions = if self.tables.macro_calls.is_empty() { - CapabilityStatus::Complete - } else if self.index.emitted_tokens.is_empty() { - CapabilityStatus::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, - ) - } else { - partial_status(self.expansions_partial) - }; - self.tables.capabilities = SourcePreprocCapabilities { - source_events: CapabilityStatus::Complete, - definition_name_ranges: partial_status(self.definition_ranges_partial), - include_edges: partial_status(self.include_edges_partial), - inactive_ranges: CapabilityStatus::Complete, - macro_reference_resolution: partial_status(self.references_partial), - macro_calls: partial_status(self.references_partial || self.macro_calls_partial), - macro_expansions, - emitted_tokens: CapabilityStatus::Complete, - emitted_token_provenance: partial_status(self.token_provenance_partial), - }; - } - - fn build_definition_table(&mut self) { - for (define_index, define) in self.index.defines.iter().enumerate() { - let Some(name) = define.name.clone() else { - self.definition_ranges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionName { - event_id: define.event_id, - }); - continue; - }; - let Some(name_range) = define.name_range else { - self.definition_ranges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionNameRange { - event_id: define.event_id, - }); - continue; - }; - - let id = SourceMacroDefinitionId::new(self.tables.macro_definitions.len()); - self.tables.macro_definitions.push(SourceMacroDefinition { - id, - event_id: define.event_id, - identity: define.identity, - name, - name_range, - directive_range: define.range, - params: define.params.clone(), - body_tokens: define.body.clone(), - }); - self.definition_ids_by_define_index.insert(define_index, id); - if let Some(identity) = define.identity { - self.definition_ids_by_identity.insert(identity, id); - } - } - } - - fn record_position_boundaries(&mut self) { - self.tables.state_timeline.final_source_order = self.index.event_records.len(); - for (source_order, directive) in self.index.event_records.iter().enumerate() { - self.tables - .state_timeline - .source_order_boundaries - .entry(directive.range.source) - .or_default() - .push(SourceMacroStatePositionBoundary { - source_order, - boundary: boundary_after(directive.range), - }); - } - - for boundaries in self.tables.state_timeline.source_order_boundaries.values_mut() { - boundaries.sort_by_key(|boundary| (boundary.boundary.offset, boundary.source_order)); - } - } - - fn build_include_graph(&mut self) { - self.tables.inactive_ranges = self.index.inactive_ranges.clone(); - let mut resolved_sources_by_event = BTreeMap::new(); - let mut unavailable_by_event = BTreeMap::new(); - let mut valid_edges = Vec::new(); - - for edge in &self.index.include_edges { - if let Some(unavailable) = self.validate_include_edge(edge) { - unavailable_by_event.insert(edge.include_event_id, unavailable); - continue; - } - - valid_edges.push(*edge); - resolved_sources_by_event.insert(edge.include_event_id, edge.included_source); - } - - for source in &self.index.sources { - if source.origin == PreprocSourceOrigin::Detached { - self.include_edges_partial = true; - self.tables - .issues - .push(SourcePreprocFactIssue::DetachedSource { source: source.id }); - } - } - - self.tables.include_graph.edges = valid_edges; - for include in &self.index.includes { - let id = SourceIncludeDirectiveId::new(self.tables.include_graph.directives.len()); - let resolved_source = resolved_sources_by_event.get(&include.event_id).copied(); - let status = match resolved_source { - Some(source) => SourceIncludeStatus::Resolved { source }, - None => unavailable_by_event - .remove(&include.event_id) - .map(SourceIncludeStatus::Unavailable) - .unwrap_or(SourceIncludeStatus::Unresolved), - }; - self.tables.include_graph.directives.push(SourceIncludeDirective { - id, - event_id: include.event_id, - directive_range: include.range, - target: include.target.clone(), - target_range: include.target_range, - resolved_source, - status, - }); - } - } - - fn validate_include_edge( - &mut self, - edge: &SourceIncludeEdge, - ) -> Option { - if !self.index.sources.iter().any(|source| source.id == edge.included_source) { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingIncludedSource { - include_event_id: edge.include_event_id, - source: edge.included_source, - }); - return Some(SourcePreprocUnavailable::MissingIncludedSource { - include_event_id: edge.include_event_id, - source: edge.included_source, - }); - } - - let Some(directive) = self - .index - .event_records - .iter() - .find(|directive| directive.event_id == edge.include_event_id) - else { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingIncludeEvent { - include_event_id: edge.include_event_id, - }); - return Some(SourcePreprocUnavailable::MissingIncludeEvent { - include_event_id: edge.include_event_id, - }); - }; - - if directive.kind != MacroEventKind::Include { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::IncludeEdgeNotInclude { - include_event_id: edge.include_event_id, - }); - return Some(SourcePreprocUnavailable::IncludeEdgeNotInclude { - include_event_id: edge.include_event_id, - }); - } - - None - } - - fn scan_references_and_state(&mut self) { - for (source_order, directive) in self.index.event_records.iter().enumerate() { - match directive.kind { - MacroEventKind::Define => self.apply_define(source_order, directive), - MacroEventKind::Undef => self.apply_undef(source_order, directive), - MacroEventKind::Conditional => self.record_conditional_references(directive), - MacroEventKind::Usage => self.record_usage_reference(directive), - MacroEventKind::Include | MacroEventKind::Branch => {} - } - } - } - - fn apply_define(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { - if let Some(definition_id) = self.definition_ids_by_define_index.get(&directive.index) { - let definition = self - .tables - .macro_definitions - .get(*definition_id) - .expect("definition id should point at inserted definition"); - self.current_state.insert(definition.name.clone(), *definition_id); - self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); - } - } - - fn apply_undef(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { - let Some(undef) = self.index.undefs.get(directive.index) else { - return; - }; - if let Some(name) = undef.name.as_ref() { - self.current_state.remove(name.as_str()); - self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); - } - } - - fn record_usage_reference(&mut self, directive: &SourcePreprocEventRecord) { - let Some(usage) = self.index.usages.get(directive.index) else { - return; - }; - let Some(name) = usage.name.clone() else { - self.record_missing_reference_name(usage.event_id); - return; - }; - let Some(name_range) = usage.name_range else { - self.record_missing_reference_name_range(usage.event_id); - return; - }; - let event_id = usage.event_id; - let directive_range = usage.range; - let definition_identity = usage.definition_identity; - let expansion_identity = usage.expansion_identity; - let parent_expansion_identity = usage.parent_expansion_identity; - let arguments = usage.arguments.clone(); - let resolution = self.resolve_usage_reference(name.as_str(), definition_identity); - let reference = self.push_reference( - event_id, - SourceMacroReferenceSite::Usage { usage_index: directive.index }, - name.clone(), - name_range, - directive_range, - resolution.clone(), - ); - let call = self.push_call( - reference, - directive_range, - resolution, - usage.identity, - expansion_identity, - parent_expansion_identity, - ); - for argument in arguments { - self.record_macro_actual_argument(call, argument); - } - } - - fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { - let Some(conditional) = self.index.conditionals.get(directive.index) else { - return; - }; - let event_id = conditional.event_id; - let directive_range = conditional.range; - for (token_index, token) in conditional.expr.iter().enumerate() { - let name = token.value.clone(); - let Some(name_range) = token.range else { - self.record_missing_reference_name_range(event_id); - continue; - }; - let (site, resolution) = - if let Some(definition) = self.current_state.get(name.as_str()).copied() { - ( - SourceMacroReferenceSite::ConditionalToken { - conditional_index: directive.index, - token_index, - }, - self.resolve_definition( - definition, - SourceMacroResolutionReason::VisibleDefinition, - ), - ) - } else if let Some(definition) = - self.include_guard_definition_after_ifndef(directive.index, name.as_str()) - { - ( - SourceMacroReferenceSite::IncludeGuardIfNDef { - conditional_index: directive.index, - token_index, - }, - self.resolve_definition( - definition, - SourceMacroResolutionReason::IncludeGuardIfNDef, - ), - ) - } else { - ( - SourceMacroReferenceSite::ConditionalToken { - conditional_index: directive.index, - token_index, - }, - SourceMacroResolution::Undefined, - ) - }; - self.push_reference(event_id, site, name, name_range, directive_range, resolution); - } - } - - fn push_reference( - &mut self, - event_id: SourcePreprocEventId, - site: SourceMacroReferenceSite, - name: SmolStr, - name_range: SourceRange, - directive_range: SourceRange, - resolution: SourceMacroResolution, - ) -> SourceMacroReferenceId { - let id = SourceMacroReferenceId::new(self.tables.macro_references.len()); - self.tables.macro_references.push(SourceMacroReference { - id, - event_id, - site, - name, - name_range, - directive_range, - resolution, - }); - id - } - - fn push_call( - &mut self, - reference: SourceMacroReferenceId, - call_range: SourceRange, - callee: SourceMacroResolution, - identity: Option, - expansion_identity: Option, - parent_expansion_identity: Option, - ) -> SourceMacroCallId { - let id = SourceMacroCallId::new(self.tables.macro_calls.len()); - self.tables.macro_calls.push(SourceMacroCall { - id, - identity, - expansion_identity, - parent_expansion_identity, - reference, - call_range, - callee, - arguments: Vec::new(), - expansion: None, - status: SourceMacroCallStatus::ExpansionUnavailable( - SourcePreprocUnavailable::ExpansionAuthorityUnavailable, - ), - }); - if let Some(identity) = identity { - self.call_ids_by_identity.insert(identity, id); - } else { - self.macro_calls_partial = true; - } - if let Some(expansion_identity) = expansion_identity { - self.call_ids_by_expansion_identity.insert(expansion_identity, id); - } - id - } - - fn build_emitted_token_tables(&mut self) { - for index in 0..self.index.emitted_tokens.len() { - let token = self.index.emitted_tokens[index].clone(); - let token_id = SourceEmittedTokenId::new(self.tables.emitted_tokens.len()); - let provenance = self.resolve_emitted_token_provenance(token_id, &token); - let provenance_id = SourceTokenProvenanceId::new(self.tables.token_provenance.len()); - self.tables.token_provenance.push(provenance); - - self.tables.emitted_tokens.push(SourceEmittedToken { - id: token_id, - text: token.raw, - kind: token.kind, - emitted_range: SourceEmittedTokenRange { start: token_id, len: 1 }, - provenance: provenance_id, - }); - } - } - - fn resolve_emitted_token_provenance( - &mut self, - token_id: SourceEmittedTokenId, - token: &SourceEmittedTokenFact, - ) -> SourceTokenProvenance { - match &token.provenance { - SourceTokenProvenanceFact::Source { token_range } => { - SourceTokenProvenance::Source { token_range: *token_range } - } - SourceTokenProvenanceFact::MacroBody { - macro_name, - identity, - call_range, - body_token_range, - } => self.resolve_macro_body_token_provenance( - token_id, - macro_name.clone(), - *identity, - *call_range, - *body_token_range, - ), - SourceTokenProvenanceFact::MacroArgument { - macro_name, - identity, - call_range, - body_token_range, - argument_token_range, - } => self.resolve_macro_argument_token_provenance( - token_id, - macro_name.clone(), - *identity, - *call_range, - *body_token_range, - *argument_token_range, - ), - SourceTokenProvenanceFact::Builtin { name } if !name.is_empty() => { - SourceTokenProvenance::Builtin { name: name.clone() } - } - SourceTokenProvenanceFact::Builtin { .. } | SourceTokenProvenanceFact::Unavailable => { - self.unavailable_token_provenance( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, - ) - } - } - } - - fn resolve_macro_body_token_provenance( - &mut self, - token_id: SourceEmittedTokenId, - macro_name: SmolStr, - identity: Option, - call_range: SourceRange, - body_token_range: SourceRange, - ) -> SourceTokenProvenance { - if self.source_is_predefine(body_token_range.source) { - return SourceTokenProvenance::Predefine { source: body_token_range.source }; - } - - let Some(identity) = identity else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, - ); - }; - let Ok(definition) = self.definition_for_identity(identity.definition) else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { - identity: identity.definition, - }, - ); - }; - let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { - token_id, - macro_name, - call_identity: identity.call, - definition, - call_range, - expansion_identity: identity.expansion, - parent_expansion_identity: identity.parent_expansion, - }) else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { - identity: identity.call, - }, - ); - }; - - if !self.definition_body_token_exists(definition, identity.body_token_index) { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, - ); - } - - SourceTokenProvenance::MacroBody { identity, definition, body_token_range, call } - } - - fn resolve_macro_argument_token_provenance( - &mut self, - token_id: SourceEmittedTokenId, - macro_name: SmolStr, - identity: Option, - call_range: SourceRange, - body_token_range: SourceRange, - argument_token_range: SourceRange, - ) -> SourceTokenProvenance { - let Some(identity) = identity else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, - ); - }; - let Ok(definition) = self.definition_for_identity(identity.definition) else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { - identity: identity.definition, - }, - ); - }; - let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); - let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { - token_id, - macro_name, - call_identity: identity.call, - definition, - call_range, - expansion_identity: call_expansion_identity, - parent_expansion_identity: None, - }) else { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { - identity: identity.call, - }, - ); - }; - if !self.definition_body_token_exists(definition, identity.body_token_index) { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, - ); - } - if !self.definition_parameter_exists(definition, identity.argument_index) { - return self.unavailable_token_provenance( - SourcePreprocUnavailable::MissingEmittedTokenMacroArgument { call }, - ); - }; - self.record_macro_argument(call, identity.argument_index, argument_token_range); - - SourceTokenProvenance::MacroArgument { - identity, - call, - argument_index: identity.argument_index, - body_token_range, - argument_token_range, - } - } - - fn call_for_emitted_token( - &mut self, - request: EmittedTokenMacroCall, - ) -> Result { - if let Some(call) = self.call_ids_by_identity.get(&request.call_identity).copied() { - self.record_call_expansion_identity( - call, - request.expansion_identity, - request.parent_expansion_identity, - )?; - return Ok(call); - } - - let event_id = self - .tables - .macro_definitions - .get(request.definition) - .expect("definition id should point at inserted definition") - .event_id; - let resolution = self - .resolve_definition(request.definition, SourceMacroResolutionReason::VisibleDefinition); - let reference = self.push_reference( - event_id, - SourceMacroReferenceSite::ExpansionToken { emitted_token: request.token_id }, - request.macro_name.clone(), - request.call_range, - request.call_range, - resolution.clone(), - ); - Ok(self.push_call( - reference, - request.call_range, - resolution, - Some(request.call_identity), - Some(request.expansion_identity), - request.parent_expansion_identity, - )) - } - - fn definition_for_call(&self, call: SourceMacroCallId) -> Result { - let Some(call) = self.tables.macro_calls.get(call) else { - return Err(()); - }; - match &call.callee { - SourceMacroResolution::Resolved { definition, .. } => Ok(*definition), - SourceMacroResolution::Undefined | SourceMacroResolution::Unavailable(_) => Err(()), - } - } - - fn definition_for_identity( - &self, - identity: SourceMacroDefinitionKey, - ) -> Result { - self.definition_ids_by_identity.get(&identity).copied().ok_or(()) - } - - fn definition_body_token_exists( - &self, - definition: SourceMacroDefinitionId, - body_token_index: usize, - ) -> bool { - let Some(definition) = self.tables.macro_definitions.get(definition) else { - return false; - }; - definition.body_tokens.get(body_token_index).is_some() - } - - fn definition_parameter_exists( - &self, - definition: SourceMacroDefinitionId, - argument_index: usize, - ) -> bool { - let Some(definition) = self.tables.macro_definitions.get(definition) else { - return false; - }; - definition.params.as_ref().is_some_and(|params| params.get(argument_index).is_some()) - } - - fn record_call_expansion_identity( - &mut self, - call: SourceMacroCallId, - expansion_identity: SourceMacroExpansionKey, - parent_expansion_identity: Option, - ) -> Result<(), SourcePreprocUnavailable> { - let Some(call_fact) = self.tables.macro_calls.get_mut(call) else { - return Err(SourcePreprocUnavailable::MissingMacroCall { call }); - }; - if let Some(existing) = call_fact.expansion_identity { - if existing != expansion_identity { - self.expansions_partial = true; - return Err(SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { - call, - }); - } - } else { - call_fact.expansion_identity = Some(expansion_identity); - self.call_ids_by_expansion_identity.insert(expansion_identity, call); - } - if let Some(parent_expansion_identity) = parent_expansion_identity { - match call_fact.parent_expansion_identity { - Some(existing) if existing != parent_expansion_identity => { - self.expansions_partial = true; - return Err(SourcePreprocUnavailable::UnmappedParentMacroExpansionIdentity { - identity: parent_expansion_identity, - }); - } - Some(_) => {} - None => call_fact.parent_expansion_identity = Some(parent_expansion_identity), - } - } - Ok(()) - } - - fn record_macro_argument( - &mut self, - call: SourceMacroCallId, - argument_index: usize, - argument_token_range: SourceRange, - ) { - let Some(call) = self.tables.macro_calls.get_mut(call) else { - return; - }; - if let Some(argument) = - call.arguments.iter_mut().find(|argument| argument.argument_index == argument_index) - { - argument.argument_range = - merge_source_ranges(argument.argument_range, argument_token_range); - return; - } - call.arguments.push(SourceMacroArgument { - argument_index, - argument_range: Some(argument_token_range), - tokens: Vec::new(), - }); - call.arguments.sort_by_key(|argument| argument.argument_index); - } - - fn record_macro_actual_argument( - &mut self, - call: SourceMacroCallId, - argument: SourceMacroActualArgument, - ) { - let Some(call) = self.tables.macro_calls.get_mut(call) else { - return; - }; - if let Some(existing) = call - .arguments - .iter_mut() - .find(|existing| existing.argument_index == argument.argument_index) - { - existing.argument_range = - merge_optional_source_ranges(existing.argument_range, argument.argument_range); - if existing.tokens.is_empty() { - existing.tokens = argument.tokens; - } - return; - } - call.arguments.push(SourceMacroArgument { - argument_index: argument.argument_index, - argument_range: argument.argument_range, - tokens: argument.tokens, - }); - call.arguments.sort_by_key(|argument| argument.argument_index); - } - - fn build_macro_expansion_graph(&mut self) { - if self.tables.macro_calls.is_empty() { - return; - } - - if self.index.emitted_tokens.is_empty() { - self.mark_all_calls_unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, - ); - return; - } - - let direct_tokens_by_call = self.direct_emitted_tokens_by_call(); - let child_calls_by_parent = self.child_calls_by_parent(); - let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); - let mut expansion_tokens_by_call = BTreeMap::new(); - for call in &call_ids { - let mut visiting = Vec::new(); - let tokens = self.recursive_emitted_tokens_for_call( - *call, - &direct_tokens_by_call, - &child_calls_by_parent, - &mut visiting, - ); - expansion_tokens_by_call.insert(*call, tokens); - } - - for call in call_ids { - let tokens = expansion_tokens_by_call.remove(&call).unwrap_or_default(); - let Some(expansion_identity) = - self.tables.macro_calls.get(call).and_then(|call| call.expansion_identity) - else { - self.mark_call_unavailable( - call, - SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { call }, - ); - continue; - }; - let Some(emitted_token_range) = emitted_token_range_from_ids(&tokens) else { - self.mark_call_unavailable( - call, - if tokens.is_empty() { - SourcePreprocUnavailable::ExpansionAuthorityUnavailable - } else { - SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call } - }, - ); - continue; - }; - let Ok(definition) = self.definition_for_call(call) else { - self.mark_call_unavailable( - call, - SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, - ); - continue; - }; - - let expansion = SourceMacroExpansionId::new(self.tables.macro_expansions.len()); - self.tables.macro_expansions.push(SourceMacroExpansion { - id: expansion, - identity: Some(expansion_identity), - call, - definition, - emitted_token_range, - child_calls: child_calls_by_parent.get(&call).cloned().unwrap_or_default(), - status: SourceMacroExpansionStatus::Complete, - }); - if let Some(call) = self.tables.macro_calls.get_mut(call) { - call.expansion = Some(expansion); - call.status = SourceMacroCallStatus::ExpansionAvailable; - } - } - } - - fn record_macro_body_references_for_calls(&mut self) { - let calls = self.tables.macro_calls.iter().cloned().collect::>(); - for call in calls { - let SourceMacroResolution::Resolved { definition, .. } = call.callee else { - continue; - }; - let Some(definition) = self.tables.macro_definitions.get(definition).cloned() else { - continue; - }; - let call_position = SourcePosition { - source: call.call_range.source, - offset: call.call_range.range.start(), - }; - for (token_index, token) in definition.body_tokens.iter().enumerate() { - let Some(name) = macro_reference_name_from_body_token(token) else { - continue; - }; - let Some(name_range) = token.range else { - self.record_missing_reference_name_range(definition.event_id); - continue; - }; - let resolution = - self.resolve_visible_reference_at_position(name.as_str(), call_position); - if self.macro_reference_exists(name.as_str(), name_range, &resolution) { - continue; - } - self.push_reference( - definition.event_id, - SourceMacroReferenceSite::MacroBodyToken { call: call.id, token_index }, - name, - name_range, - definition.directive_range, - resolution, - ); - } - } - } - - fn macro_reference_exists( - &self, - name: &str, - name_range: SourceRange, - resolution: &SourceMacroResolution, - ) -> bool { - self.tables.macro_references.iter().any(|reference| { - reference.name.as_str() == name - && reference.name_range == name_range - && &reference.resolution == resolution - }) - } - - fn direct_emitted_tokens_by_call( - &self, - ) -> BTreeMap> { - let mut tokens_by_call = BTreeMap::>::new(); - for token in self.tables.emitted_tokens.iter() { - let Some(provenance) = self.tables.token_provenance.get(token.provenance) else { - continue; - }; - let call = match provenance { - SourceTokenProvenance::MacroBody { call, .. } - | SourceTokenProvenance::MacroArgument { call, .. } - | SourceTokenProvenance::TokenPaste { call, .. } - | SourceTokenProvenance::Stringification { call, .. } => *call, - SourceTokenProvenance::Source { .. } - | SourceTokenProvenance::Predefine { .. } - | SourceTokenProvenance::Builtin { .. } - | SourceTokenProvenance::Unavailable(_) => continue, - }; - tokens_by_call.entry(call).or_default().push(token.id); - } - tokens_by_call - } - - fn child_calls_by_parent(&mut self) -> BTreeMap> { - let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); - let mut children = BTreeMap::>::new(); - for child in &call_ids { - let Some(child_call) = self.tables.macro_calls.get(*child) else { - self.expansions_partial = true; - continue; - }; - let Some(parent_expansion_identity) = child_call.parent_expansion_identity else { - continue; - }; - match self.call_ids_by_expansion_identity.get(&parent_expansion_identity).copied() { - Some(parent) if parent != *child => { - children.entry(parent).or_default().push(*child); - } - Some(_) | None => { - self.expansions_partial = true; - } - } - } - for child_calls in children.values_mut() { - child_calls.sort_by_key(|call| call.raw()); - child_calls.dedup(); - } - children - } - - fn recursive_emitted_tokens_for_call( - &mut self, - call: SourceMacroCallId, - direct_tokens_by_call: &BTreeMap>, - child_calls_by_parent: &BTreeMap>, - visiting: &mut Vec, - ) -> Vec { - if visiting.contains(&call) { - self.expansions_partial = true; - return Vec::new(); - } - - visiting.push(call); - let mut tokens = direct_tokens_by_call.get(&call).cloned().unwrap_or_default(); - if let Some(children) = child_calls_by_parent.get(&call) { - for child in children { - tokens.extend(self.recursive_emitted_tokens_for_call( - *child, - direct_tokens_by_call, - child_calls_by_parent, - visiting, - )); - } - } - visiting.pop(); - tokens.sort_by_key(|token| token.raw()); - tokens.dedup(); - tokens - } - - fn mark_all_calls_unavailable(&mut self, reason: SourcePreprocUnavailable) { - let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); - for call in call_ids { - self.mark_call_unavailable(call, reason.clone()); - } - } - - fn mark_call_unavailable(&mut self, call: SourceMacroCallId, reason: SourcePreprocUnavailable) { - self.expansions_partial = true; - if let Some(call) = self.tables.macro_calls.get_mut(call) { - call.expansion = None; - call.status = SourceMacroCallStatus::ExpansionUnavailable(reason); - } - } - - fn source_is_predefine(&self, source: PreprocSourceId) -> bool { - self.index.sources.iter().any(|candidate| { - candidate.id == source && candidate.origin == PreprocSourceOrigin::Predefine - }) - } - - fn unavailable_token_provenance( - &mut self, - reason: SourcePreprocUnavailable, - ) -> SourceTokenProvenance { - self.token_provenance_partial = true; - SourceTokenProvenance::Unavailable(reason) - } - - fn resolve_visible_reference(&mut self, name: &str) -> SourceMacroResolution { - let Some(definition) = self.current_state.get(name).copied() else { - return SourceMacroResolution::Undefined; - }; - self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) - } - - fn resolve_usage_reference( - &mut self, - name: &str, - identity: Option, - ) -> SourceMacroResolution { - let Some(identity) = identity else { - return self.resolve_visible_reference(name); - }; - let Some(definition) = self.definition_ids_by_identity.get(&identity).copied() else { - self.references_partial = true; - return SourceMacroResolution::Unavailable( - SourcePreprocUnavailable::UnknownMacroUsageDefinitionIdentity { identity }, - ); - }; - self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) - } - - fn resolve_visible_reference_at_position( - &mut self, - name: &str, - position: SourcePosition, - ) -> SourceMacroResolution { - let Some(definition) = self - .tables - .state_timeline - .state_at_position(position) - .and_then(|state| state.definitions.get(name).copied()) - else { - return SourceMacroResolution::Undefined; - }; - self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) - } - - fn resolve_definition( - &mut self, - definition: SourceMacroDefinitionId, - reason: SourceMacroResolutionReason, - ) -> SourceMacroResolution { - let definition_source = self - .tables - .macro_definitions - .get(definition) - .expect("definition id should point at inserted definition") - .directive_range - .source; - match self.include_chain_for_source(definition_source) { - Ok(include_chain) => { - SourceMacroResolution::Resolved { definition, reason, include_chain } - } - Err(_) => { - self.references_partial = true; - if self.source_is_detached(definition_source) { - self.tables - .issues - .push(SourcePreprocFactIssue::DetachedSource { source: definition_source }); - SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { - source: definition_source, - }) - } else { - self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { - source: definition_source, - }); - SourceMacroResolution::Unavailable( - SourcePreprocUnavailable::IncludeChainUnavailable { - source: definition_source, - }, - ) - } - } - } - } - - fn source_is_detached(&self, source: PreprocSourceId) -> bool { - self.index.sources.iter().any(|candidate| { - candidate.id == source && candidate.origin == PreprocSourceOrigin::Detached - }) - } - - fn include_chain_for_source( - &self, - source: PreprocSourceId, - ) -> Result, SourcePreprocError> { - let mut chain = Vec::new(); - let mut current = source; - let mut visited = BTreeMap::new(); - - loop { - if visited.insert(current, ()).is_some() { - return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); - } - - let Some(source) = self.index.sources.iter().find(|candidate| candidate.id == current) - else { - return Err(SourcePreprocError::MissingIncludedSource { - include_event_id: 0, - source: current.raw(), - }); - }; - - match source.origin { - PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, - PreprocSourceOrigin::Detached => { - return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); - } - PreprocSourceOrigin::Included { .. } => { - let edge = self - .tables - .include_graph - .edges() - .iter() - .find(|edge| edge.included_source == current) - .ok_or(SourcePreprocError::MissingIncludeEdge { source: current.raw() })?; - let directive = self - .tables - .include_graph - .directives() - .iter() - .find(|directive| directive.event_id == edge.include_event_id) - .ok_or(SourcePreprocError::MissingIncludeEvent { - include_event_id: edge.include_event_id.raw(), - })?; - chain.push(SourceIncludeChainEntry { - include_event_id: edge.include_event_id, - include_range: directive.directive_range, - included_source: current, - }); - current = directive.directive_range.source; - } - } - } - - chain.reverse(); - Ok(chain) - } - - fn include_guard_definition_after_ifndef( - &self, - conditional_index: usize, - name: &str, - ) -> Option { - let conditional = self.index.conditionals.get(conditional_index)?; - if conditional.kind != MacroConditionalKind::IfNDef { - return None; - } - - let source = conditional.range.source; - let (conditional_order, _) = - self.index.event_records.iter().enumerate().find(|(_, directive)| { - directive.kind == MacroEventKind::Conditional - && directive.index == conditional_index - })?; - for directive in self.index.event_records.iter().skip(conditional_order + 1) { - if directive.range.source != source { - continue; - } - match directive.kind { - MacroEventKind::Define => { - let define = self.index.defines.get(directive.index)?; - if define.name.as_deref() == Some(name) { - return self.definition_ids_by_define_index.get(&directive.index).copied(); - } - } - MacroEventKind::Branch => break, - MacroEventKind::Undef - | MacroEventKind::Include - | MacroEventKind::Conditional - | MacroEventKind::Usage => {} - } - } - None - } - - fn record_missing_reference_name(&mut self, event_id: SourcePreprocEventId) { - self.references_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceName { event_id }); - } - - fn record_missing_reference_name_range(&mut self, event_id: SourcePreprocEventId) { - self.references_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceNameRange { event_id }); - } - - fn record_state_checkpoint(&mut self, source_order: usize, boundary: SourcePosition) { - let id = SourceMacroStateId::new(self.tables.state_timeline.states.len()); - self.tables - .state_timeline - .states - .push(SourceMacroState { id, definitions: self.current_state.clone() }); - self.tables.state_timeline.checkpoints.push(SourceMacroStateCheckpoint { - source_order, - boundary, - state: id, - }); - } -} - -impl SourcePosition { - fn from_first_event(index: &SourcePreprocIndex) -> Self { - index - .event_records - .first() - .map(|record| SourcePosition { - source: record.range.source, - offset: record.range.range.start(), - }) - .unwrap_or(SourcePosition { - source: index.root_source.unwrap_or_else(|| PreprocSourceId::new(0)), - offset: 0.into(), - }) - } -} - -fn boundary_after(directive_range: SourceRange) -> SourcePosition { - SourcePosition { source: directive_range.source, offset: directive_range.range.end() } -} - -fn partial_status(is_partial: bool) -> CapabilityStatus { - if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } -} +impl_source_ranges!(SourceMacroDefinition, directive = directive_range, name = name_range); +impl_source_ranges!(SourceMacroReference, directive = directive_range, name = name_range); +impl_source_ranges!(SourceIncludeDirective, directive = directive_range); -fn macro_reference_name_from_body_token(token: &SourceMacroToken) -> Option { - if !token.raw.starts_with('`') { - return None; - } - let name = token.value.strip_prefix('`').unwrap_or(token.value.as_str()); - (!name.is_empty()).then(|| SmolStr::new(name)) -} - -fn emitted_token_range_from_ids( - tokens: &[SourceEmittedTokenId], -) -> Option { - let first = *tokens.first()?; - let last = *tokens.last()?; - let len = last.raw().checked_sub(first.raw())? + 1; - (len == tokens.len()).then_some(SourceEmittedTokenRange { start: first, len }) -} - -fn merge_source_ranges(existing: Option, next: SourceRange) -> Option { - let Some(existing) = existing else { - return Some(next); - }; - if existing.source != next.source { - return Some(existing); - } - Some(SourceRange { - source: existing.source, - range: utils::line_index::TextRange::new( - existing.range.start().min(next.range.start()), - existing.range.end().max(next.range.end()), - ), - }) -} - -fn merge_optional_source_ranges( - existing: Option, - next: Option, -) -> Option { - match next { - Some(next) => merge_source_ranges(existing, next), - None => existing, - } -} +mod builder; +pub use builder::SourcePreprocModelBuilder; diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs new file mode 100644 index 00000000..d3f83598 --- /dev/null +++ b/crates/preproc/src/source/provenance/builder.rs @@ -0,0 +1,1257 @@ +use std::collections::BTreeMap; + +use smol_str::SmolStr; + +use super::*; + +pub struct SourcePreprocModelBuilder<'a> { + index: &'a SourcePreprocIndex, + tables: SourcePreprocTables, + definition_ids_by_define_index: BTreeMap, + definition_ids_by_identity: BTreeMap, + call_ids_by_identity: BTreeMap, + call_ids_by_expansion_identity: BTreeMap, + current_state: BTreeMap, + definition_ranges_partial: bool, + include_edges_partial: bool, + references_partial: bool, + macro_calls_partial: bool, + token_provenance_partial: bool, + expansions_partial: bool, +} + +impl<'a> SourcePreprocModelBuilder<'a> { + pub fn new(index: &'a SourcePreprocIndex) -> Self { + Self { + index, + tables: SourcePreprocTables::default(), + definition_ids_by_define_index: BTreeMap::new(), + definition_ids_by_identity: BTreeMap::new(), + call_ids_by_identity: BTreeMap::new(), + call_ids_by_expansion_identity: BTreeMap::new(), + current_state: BTreeMap::new(), + definition_ranges_partial: false, + include_edges_partial: false, + references_partial: false, + macro_calls_partial: false, + token_provenance_partial: false, + expansions_partial: false, + } + } + + pub fn build(mut self) -> SourcePreprocTables { + self.build_tables(); + self.tables + } + + fn build_tables(&mut self) { + self.build_definition_table(); + self.build_include_graph(); + self.record_position_boundaries(); + self.record_state_checkpoint(0, SourcePosition::from_first_event(self.index)); + self.scan_references_and_state(); + self.build_emitted_token_tables(); + self.build_macro_expansion_graph(); + self.record_macro_body_references_for_calls(); + let macro_expansions = if self.tables.macro_calls.is_empty() { + CapabilityStatus::Complete + } else if self.index.emitted_tokens.is_empty() { + CapabilityStatus::Unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ) + } else { + partial_status(self.expansions_partial) + }; + self.tables.capabilities = SourcePreprocCapabilities { + source_events: CapabilityStatus::Complete, + definition_name_ranges: partial_status(self.definition_ranges_partial), + include_edges: partial_status(self.include_edges_partial), + inactive_ranges: CapabilityStatus::Complete, + macro_reference_resolution: partial_status(self.references_partial), + macro_calls: partial_status(self.references_partial || self.macro_calls_partial), + macro_expansions, + emitted_tokens: CapabilityStatus::Complete, + emitted_token_provenance: partial_status(self.token_provenance_partial), + }; + } + + fn build_definition_table(&mut self) { + for (define_index, define) in self.index.defines.iter().enumerate() { + let Some(name) = define.name.clone() else { + self.definition_ranges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionName { + event_id: define.event_id, + }); + continue; + }; + let Some(name_range) = define.name_range else { + self.definition_ranges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingDefinitionNameRange { + event_id: define.event_id, + }); + continue; + }; + + let id = SourceMacroDefinitionId::new(self.tables.macro_definitions.len()); + self.tables.macro_definitions.push(SourceMacroDefinition { + id, + event_id: define.event_id, + identity: define.identity, + name, + name_range, + directive_range: define.range, + params: define.params.clone(), + body_tokens: define.body.clone(), + }); + self.definition_ids_by_define_index.insert(define_index, id); + if let Some(identity) = define.identity { + self.definition_ids_by_identity.insert(identity, id); + } + } + } + + fn record_position_boundaries(&mut self) { + self.tables.state_timeline.final_source_order = self.index.event_records.len(); + for (source_order, directive) in self.index.event_records.iter().enumerate() { + self.tables + .state_timeline + .source_order_boundaries + .entry(directive.range.source) + .or_default() + .push(SourceMacroStatePositionBoundary { + source_order, + boundary: boundary_after(directive.range), + }); + } + + for boundaries in self.tables.state_timeline.source_order_boundaries.values_mut() { + boundaries.sort_by_key(|boundary| (boundary.boundary.offset, boundary.source_order)); + } + } + + fn build_include_graph(&mut self) { + self.tables.inactive_ranges = self.index.inactive_ranges.clone(); + let mut resolved_sources_by_event = BTreeMap::new(); + let mut unavailable_by_event = BTreeMap::new(); + let mut valid_edges = Vec::new(); + + for edge in &self.index.include_edges { + if let Some(unavailable) = self.validate_include_edge(edge) { + unavailable_by_event.insert(edge.include_event_id, unavailable); + continue; + } + + valid_edges.push(*edge); + resolved_sources_by_event.insert(edge.include_event_id, edge.included_source); + } + + for source in &self.index.sources { + if source.origin == PreprocSourceOrigin::Detached { + self.include_edges_partial = true; + self.tables + .issues + .push(SourcePreprocFactIssue::DetachedSource { source: source.id }); + } + } + + self.tables.include_graph.edges = valid_edges; + for include in &self.index.includes { + let id = SourceIncludeDirectiveId::new(self.tables.include_graph.directives.len()); + let resolved_source = resolved_sources_by_event.get(&include.event_id).copied(); + let status = match resolved_source { + Some(source) => SourceIncludeStatus::Resolved { source }, + None => unavailable_by_event + .remove(&include.event_id) + .map(SourceIncludeStatus::Unavailable) + .unwrap_or(SourceIncludeStatus::Unresolved), + }; + self.tables.include_graph.directives.push(SourceIncludeDirective { + id, + event_id: include.event_id, + directive_range: include.range, + target: include.target.clone(), + target_range: include.target_range, + resolved_source, + status, + }); + } + } + + fn validate_include_edge( + &mut self, + edge: &SourceIncludeEdge, + ) -> Option { + if !self.index.sources.iter().any(|source| source.id == edge.included_source) { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingIncludedSource { + include_event_id: edge.include_event_id, + source: edge.included_source, + }); + return Some(SourcePreprocUnavailable::MissingIncludedSource { + include_event_id: edge.include_event_id, + source: edge.included_source, + }); + } + + let Some(directive) = self + .index + .event_records + .iter() + .find(|directive| directive.event_id == edge.include_event_id) + else { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingIncludeEvent { + include_event_id: edge.include_event_id, + }); + return Some(SourcePreprocUnavailable::MissingIncludeEvent { + include_event_id: edge.include_event_id, + }); + }; + + if directive.kind != MacroEventKind::Include { + self.include_edges_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::IncludeEdgeNotInclude { + include_event_id: edge.include_event_id, + }); + return Some(SourcePreprocUnavailable::IncludeEdgeNotInclude { + include_event_id: edge.include_event_id, + }); + } + + None + } + + fn scan_references_and_state(&mut self) { + for (source_order, directive) in self.index.event_records.iter().enumerate() { + match directive.kind { + MacroEventKind::Define => self.apply_define(source_order, directive), + MacroEventKind::Undef => self.apply_undef(source_order, directive), + MacroEventKind::Conditional => self.record_conditional_references(directive), + MacroEventKind::Usage => self.record_usage_reference(directive), + MacroEventKind::Include | MacroEventKind::Branch => {} + } + } + } + + fn apply_define(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { + if let Some(definition_id) = self.definition_ids_by_define_index.get(&directive.index) { + let definition = self + .tables + .macro_definitions + .get(*definition_id) + .expect("definition id should point at inserted definition"); + self.current_state.insert(definition.name.clone(), *definition_id); + self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); + } + } + + fn apply_undef(&mut self, source_order: usize, directive: &SourcePreprocEventRecord) { + let Some(undef) = self.index.undefs.get(directive.index) else { + return; + }; + if let Some(name) = undef.name.as_ref() { + self.current_state.remove(name.as_str()); + self.record_state_checkpoint(source_order + 1, boundary_after(directive.range)); + } + } + + fn record_usage_reference(&mut self, directive: &SourcePreprocEventRecord) { + let Some(usage) = self.index.usages.get(directive.index) else { + return; + }; + let Some(name) = usage.name.clone() else { + self.record_missing_reference_name(usage.event_id); + return; + }; + let Some(name_range) = usage.name_range else { + self.record_missing_reference_name_range(usage.event_id); + return; + }; + let event_id = usage.event_id; + let directive_range = usage.range; + let definition_identity = usage.definition_identity; + let expansion_identity = usage.expansion_identity; + let parent_expansion_identity = usage.parent_expansion_identity; + let arguments = usage.arguments.clone(); + let resolution = self.resolve_usage_reference(name.as_str(), definition_identity); + let reference = self.push_reference( + event_id, + SourceMacroReferenceSite::Usage { usage_index: directive.index }, + name.clone(), + name_range, + directive_range, + resolution.clone(), + ); + let call = self.push_call( + reference, + directive_range, + resolution, + usage.identity, + expansion_identity, + parent_expansion_identity, + ); + for argument in arguments { + self.record_macro_actual_argument(call, argument); + } + } + + fn record_conditional_references(&mut self, directive: &SourcePreprocEventRecord) { + let Some(conditional) = self.index.conditionals.get(directive.index) else { + return; + }; + let event_id = conditional.event_id; + let directive_range = conditional.range; + for (token_index, token) in conditional.expr.iter().enumerate() { + let name = token.value.clone(); + let Some(name_range) = token.range else { + self.record_missing_reference_name_range(event_id); + continue; + }; + let (site, resolution) = + if let Some(definition) = self.current_state.get(name.as_str()).copied() { + ( + SourceMacroReferenceSite::ConditionalToken { + conditional_index: directive.index, + token_index, + }, + self.resolve_definition( + definition, + SourceMacroResolutionReason::VisibleDefinition, + ), + ) + } else if let Some(definition) = + self.include_guard_definition_after_ifndef(directive.index, name.as_str()) + { + ( + SourceMacroReferenceSite::IncludeGuardIfNDef { + conditional_index: directive.index, + token_index, + }, + self.resolve_definition( + definition, + SourceMacroResolutionReason::IncludeGuardIfNDef, + ), + ) + } else { + ( + SourceMacroReferenceSite::ConditionalToken { + conditional_index: directive.index, + token_index, + }, + SourceMacroResolution::Undefined, + ) + }; + self.push_reference(event_id, site, name, name_range, directive_range, resolution); + } + } + + fn push_reference( + &mut self, + event_id: SourcePreprocEventId, + site: SourceMacroReferenceSite, + name: SmolStr, + name_range: SourceRange, + directive_range: SourceRange, + resolution: SourceMacroResolution, + ) -> SourceMacroReferenceId { + let id = SourceMacroReferenceId::new(self.tables.macro_references.len()); + self.tables.macro_references.push(SourceMacroReference { + id, + event_id, + site, + name, + name_range, + directive_range, + resolution, + }); + id + } + + fn push_call( + &mut self, + reference: SourceMacroReferenceId, + call_range: SourceRange, + callee: SourceMacroResolution, + identity: Option, + expansion_identity: Option, + parent_expansion_identity: Option, + ) -> SourceMacroCallId { + let id = SourceMacroCallId::new(self.tables.macro_calls.len()); + self.tables.macro_calls.push(SourceMacroCall { + id, + identity, + expansion_identity, + parent_expansion_identity, + reference, + call_range, + callee, + arguments: Vec::new(), + expansion: None, + status: SourceMacroCallStatus::ExpansionUnavailable( + SourcePreprocUnavailable::ExpansionAuthorityUnavailable, + ), + }); + if let Some(identity) = identity { + self.call_ids_by_identity.insert(identity, id); + } else { + self.macro_calls_partial = true; + } + if let Some(expansion_identity) = expansion_identity { + self.call_ids_by_expansion_identity.insert(expansion_identity, id); + } + id + } + + fn build_emitted_token_tables(&mut self) { + for index in 0..self.index.emitted_tokens.len() { + let token = self.index.emitted_tokens[index].clone(); + let token_id = SourceEmittedTokenId::new(self.tables.emitted_tokens.len()); + let provenance = self.resolve_emitted_token_provenance(token_id, &token); + let provenance_id = SourceTokenProvenanceId::new(self.tables.token_provenance.len()); + self.tables.token_provenance.push(provenance); + + self.tables.emitted_tokens.push(SourceEmittedToken { + id: token_id, + text: token.raw, + kind: token.kind, + emitted_range: SourceEmittedTokenRange { start: token_id, len: 1 }, + provenance: provenance_id, + }); + } + } + + fn resolve_emitted_token_provenance( + &mut self, + token_id: SourceEmittedTokenId, + token: &SourceEmittedTokenFact, + ) -> SourceTokenProvenance { + match &token.provenance { + SourceTokenProvenanceFact::Source { token_range } => { + SourceTokenProvenance::Source { token_range: *token_range } + } + SourceTokenProvenanceFact::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } => self.resolve_macro_body_token_provenance( + token_id, + macro_name.clone(), + *identity, + *call_range, + *body_token_range, + ), + SourceTokenProvenanceFact::MacroArgument { + macro_name, + identity, + call_range, + body_token_range, + argument_token_range, + } => self.resolve_macro_argument_token_provenance( + token_id, + macro_name.clone(), + *identity, + *call_range, + *body_token_range, + *argument_token_range, + ), + SourceTokenProvenanceFact::Builtin { name } if !name.is_empty() => { + SourceTokenProvenance::Builtin { name: name.clone() } + } + SourceTokenProvenanceFact::Builtin { .. } | SourceTokenProvenanceFact::Unavailable => { + self.unavailable_token_provenance( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + ) + } + } + } + + fn resolve_macro_body_token_provenance( + &mut self, + token_id: SourceEmittedTokenId, + macro_name: SmolStr, + identity: Option, + call_range: SourceRange, + body_token_range: SourceRange, + ) -> SourceTokenProvenance { + if self.source_is_predefine(body_token_range.source) { + return SourceTokenProvenance::Predefine { source: body_token_range.source }; + } + + let Some(identity) = identity else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + let Ok(definition) = self.definition_for_identity(identity.definition) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { + identity: identity.definition, + }, + ); + }; + let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { + token_id, + macro_name, + call_identity: identity.call, + definition, + call_range, + expansion_identity: identity.expansion, + parent_expansion_identity: identity.parent_expansion, + }) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, + }, + ); + }; + + if !self.definition_body_token_exists(definition, identity.body_token_index) { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, + ); + } + + SourceTokenProvenance::MacroBody { identity, definition, body_token_range, call } + } + + fn resolve_macro_argument_token_provenance( + &mut self, + token_id: SourceEmittedTokenId, + macro_name: SmolStr, + identity: Option, + call_range: SourceRange, + body_token_range: SourceRange, + argument_token_range: SourceRange, + ) -> SourceTokenProvenance { + let Some(identity) = identity else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + let Ok(definition) = self.definition_for_identity(identity.definition) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { + identity: identity.definition, + }, + ); + }; + let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); + let Ok(call) = self.call_for_emitted_token(EmittedTokenMacroCall { + token_id, + macro_name, + call_identity: identity.call, + definition, + call_range, + expansion_identity: call_expansion_identity, + parent_expansion_identity: None, + }) else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, + }, + ); + }; + if !self.definition_body_token_exists(definition, identity.body_token_index) { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroBody { call }, + ); + } + if !self.definition_parameter_exists(definition, identity.argument_index) { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroArgument { call }, + ); + }; + self.record_macro_argument(call, identity.argument_index, argument_token_range); + + SourceTokenProvenance::MacroArgument { + identity, + call, + argument_index: identity.argument_index, + body_token_range, + argument_token_range, + } + } + + fn call_for_emitted_token( + &mut self, + request: EmittedTokenMacroCall, + ) -> Result { + if let Some(call) = self.call_ids_by_identity.get(&request.call_identity).copied() { + self.record_call_expansion_identity( + call, + request.expansion_identity, + request.parent_expansion_identity, + )?; + return Ok(call); + } + + let event_id = self + .tables + .macro_definitions + .get(request.definition) + .expect("definition id should point at inserted definition") + .event_id; + let resolution = self + .resolve_definition(request.definition, SourceMacroResolutionReason::VisibleDefinition); + let reference = self.push_reference( + event_id, + SourceMacroReferenceSite::ExpansionToken { emitted_token: request.token_id }, + request.macro_name.clone(), + request.call_range, + request.call_range, + resolution.clone(), + ); + Ok(self.push_call( + reference, + request.call_range, + resolution, + Some(request.call_identity), + Some(request.expansion_identity), + request.parent_expansion_identity, + )) + } + + fn definition_for_call(&self, call: SourceMacroCallId) -> Result { + let Some(call) = self.tables.macro_calls.get(call) else { + return Err(()); + }; + match &call.callee { + SourceMacroResolution::Resolved { definition, .. } => Ok(*definition), + SourceMacroResolution::Undefined | SourceMacroResolution::Unavailable(_) => Err(()), + } + } + + fn definition_for_identity( + &self, + identity: SourceMacroDefinitionKey, + ) -> Result { + self.definition_ids_by_identity.get(&identity).copied().ok_or(()) + } + + fn definition_body_token_exists( + &self, + definition: SourceMacroDefinitionId, + body_token_index: usize, + ) -> bool { + let Some(definition) = self.tables.macro_definitions.get(definition) else { + return false; + }; + definition.body_tokens.get(body_token_index).is_some() + } + + fn definition_parameter_exists( + &self, + definition: SourceMacroDefinitionId, + argument_index: usize, + ) -> bool { + let Some(definition) = self.tables.macro_definitions.get(definition) else { + return false; + }; + definition.params.as_ref().is_some_and(|params| params.get(argument_index).is_some()) + } + + fn record_call_expansion_identity( + &mut self, + call: SourceMacroCallId, + expansion_identity: SourceMacroExpansionKey, + parent_expansion_identity: Option, + ) -> Result<(), SourcePreprocUnavailable> { + let Some(call_fact) = self.tables.macro_calls.get_mut(call) else { + return Err(SourcePreprocUnavailable::MissingMacroCall { call }); + }; + if let Some(existing) = call_fact.expansion_identity { + if existing != expansion_identity { + self.expansions_partial = true; + return Err(SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { + call, + }); + } + } else { + call_fact.expansion_identity = Some(expansion_identity); + self.call_ids_by_expansion_identity.insert(expansion_identity, call); + } + if let Some(parent_expansion_identity) = parent_expansion_identity { + match call_fact.parent_expansion_identity { + Some(existing) if existing != parent_expansion_identity => { + self.expansions_partial = true; + return Err(SourcePreprocUnavailable::UnmappedParentMacroExpansionIdentity { + identity: parent_expansion_identity, + }); + } + Some(_) => {} + None => call_fact.parent_expansion_identity = Some(parent_expansion_identity), + } + } + Ok(()) + } + + fn record_macro_argument( + &mut self, + call: SourceMacroCallId, + argument_index: usize, + argument_token_range: SourceRange, + ) { + let Some(call) = self.tables.macro_calls.get_mut(call) else { + return; + }; + if let Some(argument) = + call.arguments.iter_mut().find(|argument| argument.argument_index == argument_index) + { + argument.argument_range = + argument.argument_range.merge_same_source(argument_token_range); + return; + } + call.arguments.push(SourceMacroArgument { + argument_index, + argument_range: Some(argument_token_range), + tokens: Vec::new(), + }); + call.arguments.sort_by_key(|argument| argument.argument_index); + } + + fn record_macro_actual_argument( + &mut self, + call: SourceMacroCallId, + argument: SourceMacroActualArgument, + ) { + let Some(call) = self.tables.macro_calls.get_mut(call) else { + return; + }; + if let Some(existing) = call + .arguments + .iter_mut() + .find(|existing| existing.argument_index == argument.argument_index) + { + existing.argument_range = + existing.argument_range.merge_optional_same_source(argument.argument_range); + if existing.tokens.is_empty() { + existing.tokens = argument.tokens; + } + return; + } + call.arguments.push(SourceMacroArgument { + argument_index: argument.argument_index, + argument_range: argument.argument_range, + tokens: argument.tokens, + }); + call.arguments.sort_by_key(|argument| argument.argument_index); + } + + fn build_macro_expansion_graph(&mut self) { + if self.tables.macro_calls.is_empty() { + return; + } + + if self.index.emitted_tokens.is_empty() { + self.mark_all_calls_unavailable( + SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, + ); + return; + } + + let direct_tokens_by_call = self.direct_emitted_tokens_by_call(); + let child_calls_by_parent = self.child_calls_by_parent(); + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + let mut expansion_tokens_by_call = BTreeMap::new(); + for call in &call_ids { + let mut visiting = Vec::new(); + let tokens = self.recursive_emitted_tokens_for_call( + *call, + &direct_tokens_by_call, + &child_calls_by_parent, + &mut visiting, + ); + expansion_tokens_by_call.insert(*call, tokens); + } + + for call in call_ids { + let tokens = expansion_tokens_by_call.remove(&call).unwrap_or_default(); + let Some(expansion_identity) = + self.tables.macro_calls.get(call).and_then(|call| call.expansion_identity) + else { + self.mark_call_unavailable( + call, + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { call }, + ); + continue; + }; + let Some(emitted_token_range) = tokens.contiguous_emitted_range() else { + self.mark_call_unavailable( + call, + if tokens.is_empty() { + SourcePreprocUnavailable::ExpansionAuthorityUnavailable + } else { + SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call } + }, + ); + continue; + }; + let Ok(definition) = self.definition_for_call(call) else { + self.mark_call_unavailable( + call, + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, + ); + continue; + }; + + let expansion = SourceMacroExpansionId::new(self.tables.macro_expansions.len()); + self.tables.macro_expansions.push(SourceMacroExpansion { + id: expansion, + identity: Some(expansion_identity), + call, + definition, + emitted_token_range, + child_calls: child_calls_by_parent.get(&call).cloned().unwrap_or_default(), + status: SourceMacroExpansionStatus::Complete, + }); + if let Some(call) = self.tables.macro_calls.get_mut(call) { + call.expansion = Some(expansion); + call.status = SourceMacroCallStatus::ExpansionAvailable; + } + } + } + + fn record_macro_body_references_for_calls(&mut self) { + let calls = self.tables.macro_calls.iter().cloned().collect::>(); + for call in calls { + let SourceMacroResolution::Resolved { definition, .. } = call.callee else { + continue; + }; + let Some(definition) = self.tables.macro_definitions.get(definition).cloned() else { + continue; + }; + let call_position = SourcePosition { + source: call.call_range.source, + offset: call.call_range.range.start(), + }; + for (token_index, token) in definition.body_tokens.iter().enumerate() { + let Some(name) = token.macro_reference_name() else { + continue; + }; + let Some(name_range) = token.range else { + self.record_missing_reference_name_range(definition.event_id); + continue; + }; + let resolution = + self.resolve_visible_reference_at_position(name.as_str(), call_position); + if self.macro_reference_exists(name.as_str(), name_range, &resolution) { + continue; + } + self.push_reference( + definition.event_id, + SourceMacroReferenceSite::MacroBodyToken { call: call.id, token_index }, + name, + name_range, + definition.directive_range, + resolution, + ); + } + } + } + + fn macro_reference_exists( + &self, + name: &str, + name_range: SourceRange, + resolution: &SourceMacroResolution, + ) -> bool { + self.tables.macro_references.iter().any(|reference| { + reference.name.as_str() == name + && reference.name_range == name_range + && &reference.resolution == resolution + }) + } + + fn direct_emitted_tokens_by_call( + &self, + ) -> BTreeMap> { + let mut tokens_by_call = BTreeMap::>::new(); + for token in self.tables.emitted_tokens.iter() { + let Some(provenance) = self.tables.token_provenance.get(token.provenance) else { + continue; + }; + let call = match provenance { + SourceTokenProvenance::MacroBody { call, .. } + | SourceTokenProvenance::MacroArgument { call, .. } + | SourceTokenProvenance::TokenPaste { call, .. } + | SourceTokenProvenance::Stringification { call, .. } => *call, + SourceTokenProvenance::Source { .. } + | SourceTokenProvenance::Predefine { .. } + | SourceTokenProvenance::Builtin { .. } + | SourceTokenProvenance::Unavailable(_) => continue, + }; + tokens_by_call.entry(call).or_default().push(token.id); + } + tokens_by_call + } + + fn child_calls_by_parent(&mut self) -> BTreeMap> { + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + let mut children = BTreeMap::>::new(); + for child in &call_ids { + let Some(child_call) = self.tables.macro_calls.get(*child) else { + self.expansions_partial = true; + continue; + }; + let Some(parent_expansion_identity) = child_call.parent_expansion_identity else { + continue; + }; + match self.call_ids_by_expansion_identity.get(&parent_expansion_identity).copied() { + Some(parent) if parent != *child => { + children.entry(parent).or_default().push(*child); + } + Some(_) | None => { + self.expansions_partial = true; + } + } + } + for child_calls in children.values_mut() { + child_calls.sort_by_key(|call| call.raw()); + child_calls.dedup(); + } + children + } + + fn recursive_emitted_tokens_for_call( + &mut self, + call: SourceMacroCallId, + direct_tokens_by_call: &BTreeMap>, + child_calls_by_parent: &BTreeMap>, + visiting: &mut Vec, + ) -> Vec { + if visiting.contains(&call) { + self.expansions_partial = true; + return Vec::new(); + } + + visiting.push(call); + let mut tokens = direct_tokens_by_call.get(&call).cloned().unwrap_or_default(); + if let Some(children) = child_calls_by_parent.get(&call) { + for child in children { + tokens.extend(self.recursive_emitted_tokens_for_call( + *child, + direct_tokens_by_call, + child_calls_by_parent, + visiting, + )); + } + } + visiting.pop(); + tokens.sort_by_key(|token| token.raw()); + tokens.dedup(); + tokens + } + + fn mark_all_calls_unavailable(&mut self, reason: SourcePreprocUnavailable) { + let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); + for call in call_ids { + self.mark_call_unavailable(call, reason.clone()); + } + } + + fn mark_call_unavailable(&mut self, call: SourceMacroCallId, reason: SourcePreprocUnavailable) { + self.expansions_partial = true; + if let Some(call) = self.tables.macro_calls.get_mut(call) { + call.expansion = None; + call.status = SourceMacroCallStatus::ExpansionUnavailable(reason); + } + } + + fn source_is_predefine(&self, source: PreprocSourceId) -> bool { + self.index.sources.iter().any(|candidate| { + candidate.id == source && candidate.origin == PreprocSourceOrigin::Predefine + }) + } + + fn unavailable_token_provenance( + &mut self, + reason: SourcePreprocUnavailable, + ) -> SourceTokenProvenance { + self.token_provenance_partial = true; + SourceTokenProvenance::Unavailable(reason) + } + + fn resolve_visible_reference(&mut self, name: &str) -> SourceMacroResolution { + let Some(definition) = self.current_state.get(name).copied() else { + return SourceMacroResolution::Undefined; + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + + fn resolve_usage_reference( + &mut self, + name: &str, + identity: Option, + ) -> SourceMacroResolution { + let Some(identity) = identity else { + return self.resolve_visible_reference(name); + }; + let Some(definition) = self.definition_ids_by_identity.get(&identity).copied() else { + self.references_partial = true; + return SourceMacroResolution::Unavailable( + SourcePreprocUnavailable::UnknownMacroUsageDefinitionIdentity { identity }, + ); + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + + fn resolve_visible_reference_at_position( + &mut self, + name: &str, + position: SourcePosition, + ) -> SourceMacroResolution { + let Some(definition) = self + .tables + .state_timeline + .state_at_position(position) + .and_then(|state| state.definitions.get(name).copied()) + else { + return SourceMacroResolution::Undefined; + }; + self.resolve_definition(definition, SourceMacroResolutionReason::VisibleDefinition) + } + + fn resolve_definition( + &mut self, + definition: SourceMacroDefinitionId, + reason: SourceMacroResolutionReason, + ) -> SourceMacroResolution { + let definition_source = self + .tables + .macro_definitions + .get(definition) + .expect("definition id should point at inserted definition") + .directive_range + .source; + match self.include_chain_for_source(definition_source) { + Ok(include_chain) => { + SourceMacroResolution::Resolved { definition, reason, include_chain } + } + Err(_) => { + self.references_partial = true; + if self.source_is_detached(definition_source) { + self.tables + .issues + .push(SourcePreprocFactIssue::DetachedSource { source: definition_source }); + SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { + source: definition_source, + }) + } else { + self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { + source: definition_source, + }); + SourceMacroResolution::Unavailable( + SourcePreprocUnavailable::IncludeChainUnavailable { + source: definition_source, + }, + ) + } + } + } + } + + fn source_is_detached(&self, source: PreprocSourceId) -> bool { + self.index.sources.iter().any(|candidate| { + candidate.id == source && candidate.origin == PreprocSourceOrigin::Detached + }) + } + + fn include_chain_for_source( + &self, + source: PreprocSourceId, + ) -> Result, SourcePreprocError> { + let mut chain = Vec::new(); + let mut current = source; + let mut visited = BTreeMap::new(); + + loop { + if visited.insert(current, ()).is_some() { + return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); + } + + let Some(source) = self.index.sources.iter().find(|candidate| candidate.id == current) + else { + return Err(SourcePreprocError::MissingIncludedSource { + include_event_id: 0, + source: current.raw(), + }); + }; + + match source.origin { + PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, + PreprocSourceOrigin::Detached => { + return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); + } + PreprocSourceOrigin::Included { .. } => { + let edge = self + .tables + .include_graph + .edges() + .iter() + .find(|edge| edge.included_source == current) + .ok_or(SourcePreprocError::MissingIncludeEdge { source: current.raw() })?; + let directive = self + .tables + .include_graph + .directives() + .iter() + .find(|directive| directive.event_id == edge.include_event_id) + .ok_or(SourcePreprocError::MissingIncludeEvent { + include_event_id: edge.include_event_id.raw(), + })?; + chain.push(SourceIncludeChainEntry { + include_event_id: edge.include_event_id, + include_range: directive.directive_range, + included_source: current, + }); + current = directive.directive_range.source; + } + } + } + + chain.reverse(); + Ok(chain) + } + + fn include_guard_definition_after_ifndef( + &self, + conditional_index: usize, + name: &str, + ) -> Option { + let conditional = self.index.conditionals.get(conditional_index)?; + if conditional.kind != MacroConditionalKind::IfNDef { + return None; + } + + let source = conditional.range.source; + let (conditional_order, _) = + self.index.event_records.iter().enumerate().find(|(_, directive)| { + directive.kind == MacroEventKind::Conditional + && directive.index == conditional_index + })?; + for directive in self.index.event_records.iter().skip(conditional_order + 1) { + if directive.range.source != source { + continue; + } + match directive.kind { + MacroEventKind::Define => { + let define = self.index.defines.get(directive.index)?; + if define.name.as_deref() == Some(name) { + return self.definition_ids_by_define_index.get(&directive.index).copied(); + } + } + MacroEventKind::Branch => break, + MacroEventKind::Undef + | MacroEventKind::Include + | MacroEventKind::Conditional + | MacroEventKind::Usage => {} + } + } + None + } + + fn record_missing_reference_name(&mut self, event_id: SourcePreprocEventId) { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceName { event_id }); + } + + fn record_missing_reference_name_range(&mut self, event_id: SourcePreprocEventId) { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::MissingReferenceNameRange { event_id }); + } + + fn record_state_checkpoint(&mut self, source_order: usize, boundary: SourcePosition) { + let id = SourceMacroStateId::new(self.tables.state_timeline.states.len()); + self.tables + .state_timeline + .states + .push(SourceMacroState { id, definitions: self.current_state.clone() }); + self.tables.state_timeline.checkpoints.push(SourceMacroStateCheckpoint { + source_order, + boundary, + state: id, + }); + } +} + +impl SourcePosition { + fn from_first_event(index: &SourcePreprocIndex) -> Self { + index + .event_records + .first() + .map(|record| SourcePosition { + source: record.range.source, + offset: record.range.range.start(), + }) + .unwrap_or(SourcePosition { + source: index.root_source.unwrap_or_else(|| PreprocSourceId::new(0)), + offset: 0.into(), + }) + } +} + +fn boundary_after(directive_range: SourceRange) -> SourcePosition { + SourcePosition { source: directive_range.source, offset: directive_range.range.end() } +} + +fn partial_status(is_partial: bool) -> CapabilityStatus { + if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } +} + +trait SourceMacroTokenExt { + fn macro_reference_name(&self) -> Option; +} + +impl SourceMacroTokenExt for SourceMacroToken { + fn macro_reference_name(&self) -> Option { + if !self.raw.starts_with('`') { + return None; + } + let name = self.value.strip_prefix('`').unwrap_or(self.value.as_str()); + (!name.is_empty()).then(|| SmolStr::new(name)) + } +} + +trait SourceEmittedTokenIdSliceExt { + fn contiguous_emitted_range(&self) -> Option; +} + +impl SourceEmittedTokenIdSliceExt for [SourceEmittedTokenId] { + fn contiguous_emitted_range(&self) -> Option { + let first = *self.first()?; + let last = *self.last()?; + let len = last.raw().checked_sub(first.raw())? + 1; + (len == self.len()).then_some(SourceEmittedTokenRange { start: first, len }) + } +} + +trait SourceRangeOptionExt { + fn merge_same_source(self, next: SourceRange) -> Option; + fn merge_optional_same_source(self, next: Option) -> Option; +} + +impl SourceRangeOptionExt for Option { + fn merge_same_source(self, next: SourceRange) -> Option { + let Some(existing) = self else { + return Some(next); + }; + if existing.source != next.source { + return Some(existing); + } + Some(SourceRange { + source: existing.source, + range: utils::line_index::TextRange::new( + existing.range.start().min(next.range.start()), + existing.range.end().max(next.range.end()), + ), + }) + } + + fn merge_optional_same_source(self, next: Option) -> Option { + match next { + Some(next) => self.merge_same_source(next), + None => self, + } + } +} diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index a5a6a948..d7e1e190 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -163,23 +163,19 @@ fn collect_trace_event( let event_index = index.undefs.len(); index.undefs.push(SourceMacroUndef { event_id, - name: directive.name.as_ref().map(trace_token_value), - name_range: directive.name.as_ref().and_then(trace_token_range), + name: directive.name.value(), + name_range: directive.name.source_range(), range, }); push_source_event_record(index, event_id, kind, event_index, range); } MacroEventKind::Include => { let event_index = index.includes.len(); - let target = directive - .include_file_name - .as_ref() - .map(|token| include_target_from_raw(token.raw_text.to_smolstr())) - .unwrap_or_else(|| MacroIncludeTarget::Token { raw: SmolStr::new("") }); + let target = directive.include_file_name.include_target(); index.includes.push(SourceMacroInclude { event_id, target, - target_range: directive.include_file_name.as_ref().and_then(trace_token_range), + target_range: directive.include_file_name.source_range(), range, }); push_source_event_record(index, event_id, kind, event_index, range); @@ -206,8 +202,8 @@ fn collect_trace_event( parent_expansion_identity: directive .parent_macro_expansion_id .map(SourceMacroExpansionKey::from), - name: directive.name.as_ref().map(|token| macro_name(token.value_text.as_str())), - name_range: directive.name.as_ref().and_then(trace_token_range), + name: directive.name.macro_name(), + name_range: directive.name.source_range(), arguments: directive .arguments .into_iter() @@ -231,8 +227,8 @@ fn collect_trace_define( SourceMacroDefine { event_id, identity: directive.macro_definition_id.map(SourceMacroDefinitionKey::from), - name: directive.name.as_ref().map(trace_token_value), - name_range: directive.name.as_ref().and_then(trace_token_range), + name: directive.name.value(), + name_range: directive.name.source_range(), params: (!directive.params.is_empty()) .then(|| directive.params.into_iter().map(macro_param_from_trace).collect()), body: directive.body_tokens.into_iter().map(macro_token_from_trace).collect(), @@ -242,12 +238,12 @@ fn collect_trace_define( fn macro_param_from_trace(param: PreprocessorTraceMacroParam) -> SourceMacroParam { SourceMacroParam { - name: param.name.as_ref().map(trace_token_value), - name_range: param.name.as_ref().and_then(trace_token_range), + name: param.name.value(), + name_range: param.name.source_range(), default: param .default_tokens .map(|tokens| tokens.into_iter().map(macro_token_from_trace).collect()), - range: param.range.as_ref().and_then(source_range_from_trace), + range: trace_range(¶m.range), } } @@ -256,7 +252,7 @@ fn macro_actual_argument_from_trace( ) -> SourceMacroActualArgument { SourceMacroActualArgument { argument_index, - argument_range: argument.range.as_ref().and_then(source_range_from_trace), + argument_range: trace_range(&argument.range), tokens: argument.tokens.into_iter().map(macro_token_from_trace).collect(), } } @@ -265,7 +261,7 @@ fn macro_token_from_trace(token: PreprocessorTraceToken) -> SourceMacroToken { SourceMacroToken { raw: token.raw_text.to_smolstr(), value: token.value_text.to_smolstr(), - range: token.range.as_ref().and_then(source_range_from_trace), + range: trace_range(&token.range), } } @@ -334,26 +330,46 @@ fn emitted_token_provenance_from_trace( } } -fn trace_token_value(token: &PreprocessorTraceToken) -> SmolStr { - token.value_text.to_smolstr() -} - -fn trace_token_range(token: &PreprocessorTraceToken) -> Option { - token.range.as_ref().and_then(source_range_from_trace) -} - fn required_event_range( source_order: usize, kind: MacroEventKind, directive: &PreprocessorTraceEvent, ) -> Result { - directive - .range - .as_ref() - .and_then(source_range_from_trace) + trace_range(&directive.range) .ok_or(SourcePreprocError::MissingEventRange { source_order, kind }) } +trait TraceTokenOptionExt { + fn value(&self) -> Option; + fn macro_name(&self) -> Option; + fn source_range(&self) -> Option; + fn include_target(&self) -> MacroIncludeTarget; +} + +impl TraceTokenOptionExt for Option { + fn value(&self) -> Option { + self.as_ref().map(|token| token.value_text.to_smolstr()) + } + + fn macro_name(&self) -> Option { + self.as_ref().map(|token| macro_name(token.value_text.as_str())) + } + + fn source_range(&self) -> Option { + self.as_ref().and_then(|token| trace_range(&token.range)) + } + + fn include_target(&self) -> MacroIncludeTarget { + self.as_ref() + .map(|token| include_target_from_raw(token.raw_text.to_smolstr())) + .unwrap_or_else(|| MacroIncludeTarget::Token { raw: SmolStr::new("") }) + } +} + +fn trace_range(range: &Option) -> Option { + range.as_ref().and_then(source_range_from_trace) +} + fn source_range_from_trace(range: &SourceBufferRange) -> Option { Some(SourceRange { source: PreprocSourceId::from(range.buffer_id), From a2c0b6123cb0955a2235581b6a24e8320d1e3c37 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 22:05:37 +0800 Subject: [PATCH 37/80] refactor(preproc): simplify provenance plumbing --- crates/hir/src/preproc.rs | 213 +++++++++++------- crates/ide/src/document_highlight.rs | 17 +- crates/ide/src/goto_declaration.rs | 17 +- crates/ide/src/goto_definition.rs | 17 +- crates/ide/src/hover.rs | 13 +- crates/ide/src/references.rs | 17 +- crates/ide/src/references/search.rs | 18 +- crates/ide/src/source_tokens.rs | 23 ++ crates/preproc/src/source/provenance.rs | 8 - .../preproc/src/source/provenance/builder.rs | 127 ++--------- crates/preproc/src/source/types.rs | 5 - crates/utils/src/uniq_vec.rs | 13 ++ 12 files changed, 195 insertions(+), 293 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index efe63eae..c09e7ece 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, hash::Hash}; use preproc::source::{ CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenId, @@ -403,7 +403,7 @@ pub struct RecursiveMacroExpansionProvenance { pub unavailable: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] struct MacroDefinitionKey { file_id: FileId, range_start: TextSize, @@ -422,7 +422,7 @@ impl MacroDefinitionKey { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] struct MacroReferenceKey { file_id: FileId, range_start: TextSize, @@ -441,10 +441,73 @@ impl MacroReferenceKey { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct MacroParamDefinitionKey { + macro_definition: MacroDefinitionKey, + param_index: usize, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroParamDefinitionKey { + fn from_definition(definition: &MacroParamDefinition) -> Self { + Self { + macro_definition: MacroDefinitionKey::from_definition(&definition.macro_definition), + param_index: definition.param_index, + range_start: definition.range.start(), + range_end: definition.range.end(), + name: definition.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct MacroParamReferenceKey { + macro_definition: MacroDefinitionKey, + param_index: usize, + file_id: FileId, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroParamReferenceKey { + fn from_reference(reference: &MacroParamReference) -> Self { + Self { + macro_definition: MacroDefinitionKey::from_definition(&reference.macro_definition), + param_index: reference.param_index, + file_id: reference.file_id, + range_start: reference.range.start(), + range_end: reference.range.end(), + name: reference.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct InactiveBranchKey { + file_id: FileId, + range_start: TextSize, + range_end: TextSize, +} + +impl InactiveBranchKey { + fn from_branch(branch: &InactiveBranch) -> Self { + Self { + file_id: branch.file_id, + range_start: branch.range.start(), + range_end: branch.range.end(), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct MacroReferenceIndex { - references_by_definition: BTreeMap>, - definitions_by_reference: BTreeMap>, + references_by_definition: + BTreeMap>, + definitions_by_reference: + BTreeMap>, issues: Vec, } @@ -477,7 +540,7 @@ impl MacroReferenceIndex { pub fn references_for(&self, definition: &MacroDefinition) -> Vec { self.references_by_definition .get(&MacroDefinitionKey::from_definition(definition)) - .cloned() + .map(|references| references.as_slice().to_vec()) .unwrap_or_default() } @@ -487,7 +550,7 @@ impl MacroReferenceIndex { ) -> Option<&[MacroDefinition]> { self.definitions_by_reference .get(&MacroReferenceKey::from_reference(reference)) - .map(Vec::as_slice) + .map(UniqVec::as_slice) } pub fn status(&self) -> MacroReferenceIndexStatus { @@ -501,11 +564,19 @@ impl MacroReferenceIndex { fn push(&mut self, definition: MacroDefinition, reference: MacroReference) { let definition_key = MacroDefinitionKey::from_definition(&definition); let references = self.references_by_definition.entry(definition_key).or_default(); - push_unique_macro_reference(references, reference.clone()); + push_unique_value( + references, + MacroReferenceKey::from_reference(&reference), + reference.clone(), + ); let reference_key = MacroReferenceKey::from_reference(&reference); let definitions = self.definitions_by_reference.entry(reference_key).or_default(); - push_unique_macro_definition(definitions, definition); + push_unique_value( + definitions, + MacroDefinitionKey::from_definition(&definition), + definition, + ); } fn push_issue(&mut self, issue: MacroReferenceIndexIssue) { @@ -565,7 +636,7 @@ pub fn visible_macros_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mut definitions = Vec::new(); + let mut definitions = UniqVec::::default(); let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); for model_file_id in contexts.model_file_ids.iter().copied() { @@ -594,7 +665,7 @@ pub fn visible_macros_at( return Err(error); } - Ok(definitions) + Ok(definitions.into_vec()) } pub fn visible_macro_names_at( @@ -643,7 +714,7 @@ fn configured_predefine_definitions_for_name( context_file_id: FileId, name: &SmolStr, ) -> Vec { - let mut definitions = Vec::new(); + let mut definitions = UniqVec::::default(); let profile_id = db.file_compilation_profile(context_file_id); let project_preprocess = db.project_config().preprocess_for_profile(profile_id); for predefine in &project_preprocess.predefines { @@ -656,7 +727,7 @@ fn configured_predefine_definitions_for_name( push_unique_macro_definition(&mut definitions, definition); } } - definitions + definitions.into_vec() } fn configured_predefine_definitions_at( @@ -664,7 +735,7 @@ fn configured_predefine_definitions_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mut definitions = Vec::new(); + let mut definitions = UniqVec::::default(); let contexts = source_preproc_single_query_contexts(db, file_id); for context_file_id in contexts.model_file_ids.iter().copied() { let profile_id = db.file_compilation_profile(context_file_id); @@ -687,7 +758,7 @@ fn configured_predefine_definitions_at( if definitions.is_empty() { finish_empty_single_query(&contexts, None)?; } - Ok(definitions) + Ok(definitions.into_vec()) } fn configured_predefine_definition_at( @@ -796,7 +867,7 @@ pub fn macro_param_definitions_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mut definitions = Vec::new(); + let mut definitions = UniqVec::::default(); let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); @@ -835,7 +906,7 @@ pub fn macro_param_definitions_at( return Err(error); } - Ok(definitions) + Ok(definitions.into_vec()) } pub fn macro_param_reference_definitions_at( @@ -843,8 +914,8 @@ pub fn macro_param_reference_definitions_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mut definitions = Vec::new(); - let mut references = Vec::new(); + let mut definitions = UniqVec::::default(); + let mut references = UniqVec::::default(); let mut query_range = None; let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); @@ -906,6 +977,8 @@ pub fn macro_param_reference_definitions_at( return Ok(None); }; + let references = references.into_vec(); + let definitions = definitions.into_vec(); Ok(Some(MacroParamReferenceDefinitions { capability: macro_param_reference_context_capability(&references), references, @@ -1051,18 +1124,24 @@ pub fn macro_reference_resolution_at( let Some(mut resolution) = macro_reference_definitions_at(db, file_id, offset)? else { return Ok(None); }; - if resolution.references.len() != 1 || resolution.definitions.len() != 1 { + if resolution.references.len() != 1 { return Err(PreprocError::Unavailable { reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { - contexts: resolution.references.len().max(resolution.definitions.len()), + contexts: resolution.references.len(), }, }); } let reference = resolution.references.pop().unwrap(); - let Some(definition) = resolution.definitions.into_iter().next() else { - return Ok(None); - }; - Ok(Some(MacroReferenceResolution { reference, definition })) + match resolution.definitions.len() { + 0 => Ok(None), + 1 => { + let definition = resolution.definitions.pop().unwrap(); + Ok(Some(MacroReferenceResolution { reference, definition })) + } + contexts => Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts }, + }), + } } pub fn macro_reference_definitions_at( @@ -1070,7 +1149,7 @@ pub fn macro_reference_definitions_at( file_id: FileId, offset: TextSize, ) -> PreprocResult> { - let mut definitions = Vec::new(); + let mut definitions = UniqVec::::default(); let mut references = Vec::new(); let mut query_range = None; let mut first_error = None; @@ -1157,7 +1236,7 @@ pub fn macro_reference_definitions_at( capability: macro_reference_context_capability(&references), references, range, - definitions, + definitions: definitions.into_vec(), })) } @@ -1518,7 +1597,7 @@ pub fn macro_param_references( let profile_id = db .file_compilation_profile(file_id) .or_else(|| db.file_compilation_profile(definition.macro_definition.file_id)); - let mut references = Vec::new(); + let mut references = UniqVec::::default(); let mut first_error = None; for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { @@ -1579,7 +1658,7 @@ pub fn macro_param_references( return Err(error); } - Ok(MacroParamReferences { references }) + Ok(MacroParamReferences { references: references.into_vec() }) } pub(crate) fn build_macro_reference_index( @@ -1767,7 +1846,7 @@ pub fn inactive_branches( db: &dyn SourceRootDb, file_id: FileId, ) -> PreprocResult> { - let mut branches = Vec::new(); + let mut branches = UniqVec::::default(); let mut first_error = None; let contexts = source_preproc_single_query_contexts(db, file_id); @@ -1812,7 +1891,7 @@ pub fn inactive_branches( return Err(error); } - Ok(branches) + Ok(branches.into_vec()) } fn mapped_result( @@ -2728,17 +2807,6 @@ fn map_include_chain( .collect() } -fn push_unique_macro_reference(refs: &mut Vec, reference: MacroReference) { - if refs.iter().any(|existing| { - existing.file_id == reference.file_id - && existing.range == reference.range - && existing.name == reference.name - }) { - return; - } - refs.push(reference); -} - fn push_unique_macro_reference_context(refs: &mut Vec, reference: MacroReference) { if refs.iter().any(|existing| existing == &reference) { return; @@ -2821,14 +2889,11 @@ fn push_unique_include_directive( directives.push(directive); } -fn push_unique_inactive_branch(branches: &mut Vec, branch: InactiveBranch) { - if branches - .iter() - .any(|existing| existing.file_id == branch.file_id && existing.range == branch.range) - { - return; - } - branches.push(branch); +fn push_unique_inactive_branch( + branches: &mut UniqVec, + branch: InactiveBranch, +) { + push_unique_value(branches, InactiveBranchKey::from_branch(&branch), branch); } fn macro_reference_context_capability(references: &[MacroReference]) -> PreprocAvailability { @@ -2855,55 +2920,37 @@ fn macro_reference_context_capability(references: &[MacroReference]) -> PreprocA .unwrap_or(PreprocAvailability::Complete) } +fn push_unique_value(values: &mut UniqVec, key: K, value: T) { + values.push([key], value); +} + fn push_unique_macro_definition( - definitions: &mut Vec, + definitions: &mut UniqVec, definition: MacroDefinition, ) { - if definitions.iter().any(|existing| { - existing.file_id == definition.file_id - && existing.name_range == definition.name_range - && existing.name == definition.name - }) { - return; - } - definitions.push(definition); + push_unique_value(definitions, MacroDefinitionKey::from_definition(&definition), definition); } fn same_macro_definition(left: &MacroDefinition, right: &MacroDefinition) -> bool { - left.file_id == right.file_id && left.name_range == right.name_range && left.name == right.name -} - -fn same_macro_param_definition(left: &MacroParamDefinition, right: &MacroParamDefinition) -> bool { - same_macro_definition(&left.macro_definition, &right.macro_definition) - && left.param_index == right.param_index - && left.range == right.range - && left.name == right.name + MacroDefinitionKey::from_definition(left) == MacroDefinitionKey::from_definition(right) } fn push_unique_macro_param_definition( - definitions: &mut Vec, + definitions: &mut UniqVec, definition: MacroParamDefinition, ) { - if definitions.iter().any(|existing| same_macro_param_definition(existing, &definition)) { - return; - } - definitions.push(definition); + push_unique_value( + definitions, + MacroParamDefinitionKey::from_definition(&definition), + definition, + ); } fn push_unique_macro_param_reference( - refs: &mut Vec, + refs: &mut UniqVec, reference: MacroParamReference, ) { - if refs.iter().any(|existing| { - same_macro_definition(&existing.macro_definition, &reference.macro_definition) - && existing.param_index == reference.param_index - && existing.file_id == reference.file_id - && existing.range == reference.range - && existing.name == reference.name - }) { - return; - } - refs.push(reference); + push_unique_value(refs, MacroParamReferenceKey::from_reference(&reference), reference); } fn macro_param_reference_context_capability( diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index b94dba38..9ec7dd31 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -11,7 +11,6 @@ use crate::{ self, ReferenceCategory, ReferencesConfig, search::{ReferencesCtx, SearchScope}, }, - source_tokens::SourceTokenSelection, }; #[derive(Debug, Clone)] @@ -41,21 +40,7 @@ pub(crate) fn document_highlight( offset, token_precedence, )?; - let tokens = match selection { - SourceTokenSelection::NormalSyntax(selection) => selection.tokens, - SourceTokenSelection::Preproc(selection) => { - let _ = selection.hits.len(); - selection.tokens - } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - return None; - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - return None; - } - }; + let tokens = selection.tokens()?; let highlights = tokens .into_iter() .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 437f0601..8d7dd429 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -7,7 +7,6 @@ use crate::{ definitions::DefinitionClass, goto_definition, navigation_target::{NavTarget, ToNav}, - source_tokens::SourceTokenSelection, }; pub(crate) fn goto_declaration( @@ -25,21 +24,7 @@ pub(crate) fn goto_declaration( offset, goto_definition::token_precedence, )?; - let (range, tokens) = match selection { - SourceTokenSelection::NormalSyntax(selection) => (selection.range, selection.tokens), - SourceTokenSelection::Preproc(selection) => { - let _ = selection.hits.len(); - (selection.range, selection.tokens) - } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - return None; - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - return None; - } - }; + let (range, tokens) = selection.range_and_tokens()?; let origins = tokens .into_iter() diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 426d0e98..1c317966 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -22,7 +22,6 @@ use crate::{ db::root_db::RootDb, definitions::DefinitionClass, navigation_target::{NavTarget, ToNav}, - source_tokens::SourceTokenSelection, }; pub(crate) fn goto_definition( @@ -48,21 +47,7 @@ pub(crate) fn goto_definition( offset, token_precedence, )?; - let (range, tokens) = match selection { - SourceTokenSelection::NormalSyntax(selection) => (selection.range, selection.tokens), - SourceTokenSelection::Preproc(selection) => { - let _ = selection.hits.len(); - (selection.range, selection.tokens) - } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - return None; - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - return None; - } - }; + let (range, tokens) = selection.range_and_tokens()?; let navs = tokens .into_iter() .filter_map(|token| nav_targets_for_token(db, &sema, hir_file_id, token)) diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index af8fb757..aba578c2 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -83,14 +83,7 @@ pub(crate) fn hover( SourceTokenSelection::Preproc(selection) => { hover_for_preproc_selection(&sema, hir_file_id, selection) } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - None - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - None - } + SourceTokenSelection::Unavailable(_) | SourceTokenSelection::Ambiguous(_) => None, }?; Some(with_expanded_macro_hover(db, file_id, offset, hover)) } @@ -100,8 +93,8 @@ fn hover_for_preproc_selection( hir_file_id: HirFileId, selection: PreprocTokenSelection<'_>, ) -> Option> { - let _ = selection.hits.len(); - hover_for_token_selection(sema, hir_file_id, selection.range, selection.tokens) + let (range, tokens) = selection.range_and_tokens(); + hover_for_token_selection(sema, hir_file_id, range, tokens) } fn hover_for_token_selection( diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 0a7eec42..78e7f996 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -23,7 +23,6 @@ use crate::{ db::root_db::RootDb, definitions::{Definition, DefinitionClass}, navigation_target::{NavTarget, ToNav}, - source_tokens::SourceTokenSelection, }; pub(crate) mod search; @@ -85,21 +84,7 @@ pub(crate) fn references( offset, token_precedence, )?; - let tokens = match selection { - SourceTokenSelection::NormalSyntax(selection) => selection.tokens, - SourceTokenSelection::Preproc(selection) => { - let _ = selection.hits.len(); - selection.tokens - } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - return None; - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - return None; - } - }; + let tokens = selection.tokens()?; let references = tokens .into_iter() .filter_map(|token| references_for_token(&sema, hir_file_id, token, config.clone())) diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index 9d1074c9..b0c90477 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -27,7 +27,7 @@ use crate::{ ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, - source_tokens::{SourceTokenRequestCache, SourceTokenSelection}, + source_tokens::SourceTokenRequestCache, }; /// A search scope is a set of files and ranges within those files that should @@ -287,20 +287,8 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { return Vec::new(); }; - let tokens = match selection { - SourceTokenSelection::NormalSyntax(selection) => selection.tokens, - SourceTokenSelection::Preproc(selection) => { - let _ = selection.hits.len(); - selection.tokens - } - SourceTokenSelection::Unavailable(unavailable) => { - let _ = unavailable.range; - return Vec::new(); - } - SourceTokenSelection::Ambiguous(ambiguous) => { - let _ = (ambiguous.range, ambiguous.hits.len()); - return Vec::new(); - } + let Some(tokens) = selection.tokens() else { + return Vec::new(); }; tokens diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index fa191067..49214ecb 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -23,6 +23,21 @@ pub(crate) enum SourceTokenSelection<'tree> { Ambiguous(PreprocTokenAmbiguity), } +impl<'tree> SourceTokenSelection<'tree> { + pub(crate) fn range_and_tokens(self) -> Option<(TextRange, Vec>)> { + match self { + Self::NormalSyntax(selection) => Some((selection.range, selection.tokens)), + Self::Preproc(selection) => Some(selection.range_and_tokens()), + Self::Unavailable(PreprocTokenUnavailable { range: _ }) => None, + Self::Ambiguous(PreprocTokenAmbiguity { range: _, hits: _ }) => None, + } + } + + pub(crate) fn tokens(self) -> Option>> { + self.range_and_tokens().map(|(_, tokens)| tokens) + } +} + #[derive(Debug, Clone)] pub(crate) struct NormalSyntaxSelection<'tree> { pub range: TextRange, @@ -36,6 +51,14 @@ pub(crate) struct PreprocTokenSelection<'tree> { pub tokens: Vec>, } +impl<'tree> PreprocTokenSelection<'tree> { + pub(crate) fn range_and_tokens(self) -> (TextRange, Vec>) { + let Self { range, hits, tokens } = self; + let _hit_count = hits.len(); + (range, tokens) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PreprocTokenUnavailable { pub range: TextRange, diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index d5f6cd4e..61efd597 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -380,10 +380,6 @@ pub enum SourcePreprocUnavailable { MissingDefinitionNameRange { event_id: SourcePreprocEventId }, MissingReferenceName { event_id: SourcePreprocEventId }, MissingReferenceNameRange { event_id: SourcePreprocEventId }, - MissingIncludedSource { include_event_id: SourcePreprocEventId, source: PreprocSourceId }, - MissingIncludeEvent { include_event_id: SourcePreprocEventId }, - IncludeEdgeNotInclude { include_event_id: SourcePreprocEventId }, - IncludeChainUnavailable { source: PreprocSourceId }, DetachedSource { source: PreprocSourceId }, MissingPredefineSourceText { source: PreprocSourceId }, UnverifiedPredefineSource { source: PreprocSourceId }, @@ -414,10 +410,6 @@ pub enum SourcePreprocFactIssue { MissingDefinitionNameRange { event_id: SourcePreprocEventId }, MissingReferenceName { event_id: SourcePreprocEventId }, MissingReferenceNameRange { event_id: SourcePreprocEventId }, - MissingIncludedSource { include_event_id: SourcePreprocEventId, source: PreprocSourceId }, - MissingIncludeEvent { include_event_id: SourcePreprocEventId }, - IncludeEdgeNotInclude { include_event_id: SourcePreprocEventId }, - IncludeChainUnavailable { source: PreprocSourceId }, DetachedSource { source: PreprocSourceId }, } diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index d3f83598..800dd64c 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -132,16 +132,8 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn build_include_graph(&mut self) { self.tables.inactive_ranges = self.index.inactive_ranges.clone(); let mut resolved_sources_by_event = BTreeMap::new(); - let mut unavailable_by_event = BTreeMap::new(); - let mut valid_edges = Vec::new(); for edge in &self.index.include_edges { - if let Some(unavailable) = self.validate_include_edge(edge) { - unavailable_by_event.insert(edge.include_event_id, unavailable); - continue; - } - - valid_edges.push(*edge); resolved_sources_by_event.insert(edge.include_event_id, edge.included_source); } @@ -154,16 +146,13 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } - self.tables.include_graph.edges = valid_edges; + self.tables.include_graph.edges = self.index.include_edges.clone(); for include in &self.index.includes { let id = SourceIncludeDirectiveId::new(self.tables.include_graph.directives.len()); let resolved_source = resolved_sources_by_event.get(&include.event_id).copied(); let status = match resolved_source { Some(source) => SourceIncludeStatus::Resolved { source }, - None => unavailable_by_event - .remove(&include.event_id) - .map(SourceIncludeStatus::Unavailable) - .unwrap_or(SourceIncludeStatus::Unresolved), + None => SourceIncludeStatus::Unresolved, }; self.tables.include_graph.directives.push(SourceIncludeDirective { id, @@ -177,50 +166,6 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } - fn validate_include_edge( - &mut self, - edge: &SourceIncludeEdge, - ) -> Option { - if !self.index.sources.iter().any(|source| source.id == edge.included_source) { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingIncludedSource { - include_event_id: edge.include_event_id, - source: edge.included_source, - }); - return Some(SourcePreprocUnavailable::MissingIncludedSource { - include_event_id: edge.include_event_id, - source: edge.included_source, - }); - } - - let Some(directive) = self - .index - .event_records - .iter() - .find(|directive| directive.event_id == edge.include_event_id) - else { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::MissingIncludeEvent { - include_event_id: edge.include_event_id, - }); - return Some(SourcePreprocUnavailable::MissingIncludeEvent { - include_event_id: edge.include_event_id, - }); - }; - - if directive.kind != MacroEventKind::Include { - self.include_edges_partial = true; - self.tables.issues.push(SourcePreprocFactIssue::IncludeEdgeNotInclude { - include_event_id: edge.include_event_id, - }); - return Some(SourcePreprocUnavailable::IncludeEdgeNotInclude { - include_event_id: edge.include_event_id, - }); - } - - None - } - fn scan_references_and_state(&mut self) { for (source_order, directive) in self.index.event_records.iter().enumerate() { match directive.kind { @@ -1028,80 +973,46 @@ impl<'a> SourcePreprocModelBuilder<'a> { Ok(include_chain) => { SourceMacroResolution::Resolved { definition, reason, include_chain } } - Err(_) => { + Err(source) => { self.references_partial = true; - if self.source_is_detached(definition_source) { - self.tables - .issues - .push(SourcePreprocFactIssue::DetachedSource { source: definition_source }); - SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { - source: definition_source, - }) - } else { - self.tables.issues.push(SourcePreprocFactIssue::IncludeChainUnavailable { - source: definition_source, - }); - SourceMacroResolution::Unavailable( - SourcePreprocUnavailable::IncludeChainUnavailable { - source: definition_source, - }, - ) - } + self.tables.issues.push(SourcePreprocFactIssue::DetachedSource { source }); + SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { + source, + }) } } } - fn source_is_detached(&self, source: PreprocSourceId) -> bool { - self.index.sources.iter().any(|candidate| { - candidate.id == source && candidate.origin == PreprocSourceOrigin::Detached - }) - } - fn include_chain_for_source( &self, source: PreprocSourceId, - ) -> Result, SourcePreprocError> { + ) -> Result, PreprocSourceId> { let mut chain = Vec::new(); let mut current = source; - let mut visited = BTreeMap::new(); loop { - if visited.insert(current, ()).is_some() { - return Err(SourcePreprocError::IncludeCycle { source: current.raw() }); - } - - let Some(source) = self.index.sources.iter().find(|candidate| candidate.id == current) - else { - return Err(SourcePreprocError::MissingIncludedSource { - include_event_id: 0, - source: current.raw(), - }); - }; + let source = self + .index + .sources + .iter() + .find(|candidate| candidate.id == current) + .expect("source id should point at an indexed preprocessor source"); match source.origin { PreprocSourceOrigin::Root | PreprocSourceOrigin::Predefine => break, PreprocSourceOrigin::Detached => { - return Err(SourcePreprocError::MissingIncludeEdge { source: current.raw() }); + return Err(current); } - PreprocSourceOrigin::Included { .. } => { - let edge = self - .tables - .include_graph - .edges() - .iter() - .find(|edge| edge.included_source == current) - .ok_or(SourcePreprocError::MissingIncludeEdge { source: current.raw() })?; + PreprocSourceOrigin::Included { include_event_id } => { let directive = self .tables .include_graph .directives() .iter() - .find(|directive| directive.event_id == edge.include_event_id) - .ok_or(SourcePreprocError::MissingIncludeEvent { - include_event_id: edge.include_event_id.raw(), - })?; + .find(|directive| directive.event_id == include_event_id) + .expect("included source should point at an include directive"); chain.push(SourceIncludeChainEntry { - include_event_id: edge.include_event_id, + include_event_id, include_range: directive.directive_range, included_source: current, }); diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index fe195be8..5d94aad2 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -316,11 +316,6 @@ pub enum SourcePreprocError { MissingRootSource, MissingEventRange { source_order: usize, kind: MacroEventKind }, MissingEvent { event_id: u32 }, - MissingIncludedSource { include_event_id: u32, source: u32 }, - MissingIncludeEvent { include_event_id: u32 }, - IncludeEdgeNotInclude { include_event_id: u32 }, - MissingIncludeEdge { source: u32 }, - IncludeCycle { source: u32 }, } impl PreprocSourceId { diff --git a/crates/utils/src/uniq_vec.rs b/crates/utils/src/uniq_vec.rs index 40896c76..b3b4504e 100644 --- a/crates/utils/src/uniq_vec.rs +++ b/crates/utils/src/uniq_vec.rs @@ -2,11 +2,20 @@ use std::hash::Hash; use rustc_hash::FxHashSet; +#[derive(Debug, Clone)] pub struct UniqVec { items: Vec, seen: FxHashSet, } +impl PartialEq for UniqVec { + fn eq(&self, other: &Self) -> bool { + self.items == other.items && self.seen == other.seen + } +} + +impl Eq for UniqVec {} + impl Default for UniqVec { fn default() -> Self { Self { items: Vec::new(), seen: FxHashSet::default() } @@ -41,6 +50,10 @@ impl UniqVec { self.items.is_empty() } + pub fn as_slice(&self) -> &[T] { + &self.items + } + pub fn into_vec(self) -> Vec { self.items } From 96e7b8a78ff50e1e34dc3e91e4a3ff793bcc6215 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 23:20:00 +0800 Subject: [PATCH 38/80] refactor: hir::src::preproc --- crates/hir/src/base_db/source_db.rs | 1228 +---- crates/hir/src/base_db/source_db/preproc.rs | 726 +++ .../source_db/preproc/source_mapping.rs | 487 ++ crates/hir/src/preproc.rs | 4106 +---------------- crates/hir/src/preproc/conditionals.rs | 51 + crates/hir/src/preproc/definitions.rs | 235 + crates/hir/src/preproc/expansion.rs | 324 ++ crates/hir/src/preproc/helpers.rs | 8 + crates/hir/src/preproc/helpers/context.rs | 99 + crates/hir/src/preproc/helpers/expansion.rs | 419 ++ crates/hir/src/preproc/helpers/facts.rs | 407 ++ crates/hir/src/preproc/helpers/source.rs | 86 + crates/hir/src/preproc/includes.rs | 78 + crates/hir/src/preproc/predefines.rs | 122 + crates/hir/src/preproc/reference_index.rs | 190 + crates/hir/src/preproc/reference_queries.rs | 235 + crates/hir/src/preproc/tests.rs | 1091 +++++ crates/hir/src/preproc/types.rs | 607 +++ crates/utils/src/uniq_vec.rs | 25 + 19 files changed, 5244 insertions(+), 5280 deletions(-) create mode 100644 crates/hir/src/base_db/source_db/preproc.rs create mode 100644 crates/hir/src/base_db/source_db/preproc/source_mapping.rs create mode 100644 crates/hir/src/preproc/conditionals.rs create mode 100644 crates/hir/src/preproc/definitions.rs create mode 100644 crates/hir/src/preproc/expansion.rs create mode 100644 crates/hir/src/preproc/helpers.rs create mode 100644 crates/hir/src/preproc/helpers/context.rs create mode 100644 crates/hir/src/preproc/helpers/expansion.rs create mode 100644 crates/hir/src/preproc/helpers/facts.rs create mode 100644 crates/hir/src/preproc/helpers/source.rs create mode 100644 crates/hir/src/preproc/includes.rs create mode 100644 crates/hir/src/preproc/predefines.rs create mode 100644 crates/hir/src/preproc/reference_index.rs create mode 100644 crates/hir/src/preproc/reference_queries.rs create mode 100644 crates/hir/src/preproc/tests.rs create mode 100644 crates/hir/src/preproc/types.rs diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index c6b2972d..3ca5f095 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,28 +1,35 @@ -use preproc::source::{ - PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, - SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, - SourceTokenProvenance, -}; use rustc_hash::{FxHashMap, FxHashSet}; -use smol_str::SmolStr; use syntax::{ - Compilation, ParserExpectedSyntax, PreprocessorTrace, SourceBufferOrigin, SyntaxDiagnostic, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeBufferIds, SyntaxTreeOptions, + Compilation, ParserExpectedSyntax, SyntaxDiagnostic, SyntaxTree, SyntaxTreeBuffer, + SyntaxTreeBufferIds, }; use triomphe::Arc; -use utils::{ - line_index::{TextRange, TextSize}, - path_identity::PathIdentityIndex, -}; +use utils::{line_index::TextSize, path_identity::PathIdentityIndex}; use vfs::{FileId, VfsPath, anchored_path::AnchoredPath}; use crate::base_db::{ compilation_plan::{self, CompilationPlan}, diagnostics_config::{DiagnosticSource, DiagnosticsConfig}, - project::{CompilationProfileId, Predefine, PreprocessConfig, ProjectConfig}, + project::{CompilationProfileId, PreprocessConfig, ProjectConfig}, source_root::{SourceRoot, SourceRootId}, }; +mod preproc; + +pub(crate) use self::preproc::workspace_preproc_model_file_ids; +pub use self::preproc::{ + MappedSourcePreprocModel, PreprocExpansionDisplay, PreprocExpansionMapping, + PreprocExpansionSourceBuffer, PreprocManifestSource, PreprocSourceMap, PreprocSourceMapError, + PreprocSourceMapping, PreprocSpeculativeUniverseId, PreprocVirtualOrigin, + SourcePreprocContextIndex, SourcePreprocContextIndexIssue, SourcePreprocContextStatus, + SourcePreprocQueryError, SourcePreprocRelevantContexts, preproc_virtual_builtin_path, + preproc_virtual_expansion_path, preproc_virtual_predefines_path, + preproc_virtual_speculative_path, +}; +#[cfg(test)] +use self::preproc::{materialized_predefine_text, source_preproc_file_ids}; +use self::preproc::{source_preproc_context_index_for_profile, source_preproc_model}; + pub trait FileLoader { fn resolve_path(&self, path: AnchoredPath<'_>) -> Option; } @@ -132,595 +139,6 @@ pub struct CompilationDiagnostic { pub diagnostic: SyntaxDiagnostic, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MappedSourcePreprocModel { - pub model: SourcePreprocModel, - pub source_map: PreprocSourceMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PreprocSourceMap { - entries: FxHashMap, - expansion_entries: FxHashMap, - predefine_sources: FxHashMap, - text_lengths: FxHashMap, - range_offsets: FxHashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourcePreprocContextIndex { - contexts_by_file: FxHashMap>, - issues: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourcePreprocContextIndexIssue { - pub model_file_id: FileId, - pub error: SourcePreprocQueryError, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourcePreprocRelevantContexts { - pub model_file_ids: Vec, - pub status: SourcePreprocContextStatus, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SourcePreprocContextStatus { - Complete, - Partial { skipped_models: usize }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocSourceMapping { - RealFile(FileId), - VirtualFile { file_id: FileId, path: VfsPath, origin: PreprocVirtualOrigin }, - VirtualDisplay { path: VfsPath, origin: PreprocVirtualOrigin }, - Unmapped(SourcePreprocUnavailable), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PreprocExpansionMapping { - pub origin: PreprocVirtualOrigin, - pub emitted_range: SourceEmittedTokenRange, - pub display: PreprocExpansionDisplay, - pub source_buffer: PreprocExpansionSourceBuffer, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PreprocExpansionDisplay { - pub path: VfsPath, - pub text: String, - token_ranges: FxHashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocExpansionSourceBuffer { - ParseStable { - file_id: FileId, - path: VfsPath, - text: String, - token_ranges: FxHashMap, - }, - DisplayOnly { - path: VfsPath, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PreprocManifestSource { - pub file_id: FileId, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocVirtualOrigin { - Predefines { profile: Option }, - Builtin { name: SmolStr }, - ExternalIncludeBuffer { source: PreprocSourceId }, - Expansion { expansion: SourceMacroExpansionId }, - Speculative { universe: PreprocSpeculativeUniverseId }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct PreprocSpeculativeUniverseId(pub u32); - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocSourceMapError { - MissingSource { - source: PreprocSourceId, - }, - UnmappedSource { - source: PreprocSourceId, - reason: SourcePreprocUnavailable, - }, - RangeOutOfBounds { - source: PreprocSourceId, - range: TextRange, - mapped_range: TextRange, - text_len: usize, - }, - MissingExpansionVirtualFile { - expansion: SourceMacroExpansionId, - }, - MissingEmittedToken { - token: SourceEmittedTokenId, - }, - MissingEmittedTokenRange { - range: SourceEmittedTokenRange, - }, - DisplayOnlyVirtualSource { - path: VfsPath, - origin: PreprocVirtualOrigin, - }, -} - -impl PreprocSourceMap { - pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId, text_len: usize) { - self.entries.insert(source, PreprocSourceMapping::RealFile(file_id)); - self.predefine_sources.remove(&source); - self.text_lengths.insert(source, text_len); - self.range_offsets.insert(source, 0); - } - - pub fn insert_virtual_file( - &mut self, - source: PreprocSourceId, - file_id: Option, - path: VfsPath, - origin: PreprocVirtualOrigin, - text_len: usize, - ) { - self.insert_virtual_file_with_offset(source, file_id, path, origin, text_len, 0); - } - - fn insert_virtual_file_with_offset( - &mut self, - source: PreprocSourceId, - file_id: Option, - path: VfsPath, - origin: PreprocVirtualOrigin, - text_len: usize, - range_offset: usize, - ) { - let mapping = match file_id { - Some(file_id) => PreprocSourceMapping::VirtualFile { file_id, path, origin }, - None => PreprocSourceMapping::VirtualDisplay { path, origin }, - }; - self.entries.insert(source, mapping); - self.predefine_sources.remove(&source); - self.text_lengths.insert(source, text_len); - self.range_offsets.insert(source, range_offset); - } - - pub fn insert_unmapped(&mut self, source: PreprocSourceId, reason: SourcePreprocUnavailable) { - self.entries.insert(source, PreprocSourceMapping::Unmapped(reason)); - self.predefine_sources.remove(&source); - self.text_lengths.remove(&source); - self.range_offsets.remove(&source); - } - - fn insert_predefine_manifest_source( - &mut self, - source: PreprocSourceId, - manifest_source: PreprocManifestSource, - ) { - self.predefine_sources.insert(source, manifest_source); - } - - pub fn get(&self, source: PreprocSourceId) -> Option<&PreprocSourceMapping> { - self.entries.get(&source) - } - - pub fn predefine_manifest_source( - &self, - source: PreprocSourceId, - ) -> Option { - self.predefine_sources.get(&source).copied() - } - - pub fn insert_expansion_display_only( - &mut self, - expansion: SourceMacroExpansionId, - path: VfsPath, - display_text: String, - emitted_range: SourceEmittedTokenRange, - display_token_ranges: FxHashMap, - ) { - self.expansion_entries.insert( - expansion, - PreprocExpansionMapping { - origin: PreprocVirtualOrigin::Expansion { expansion }, - emitted_range, - display: PreprocExpansionDisplay { - path: path.clone(), - text: display_text, - token_ranges: display_token_ranges, - }, - source_buffer: PreprocExpansionSourceBuffer::DisplayOnly { path }, - }, - ); - } - - pub fn expansion(&self, expansion: SourceMacroExpansionId) -> Option<&PreprocExpansionMapping> { - self.expansion_entries.get(&expansion) - } - - pub fn expansion_display_source( - &self, - expansion: SourceMacroExpansionId, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - Ok(PreprocSourceMapping::VirtualDisplay { - path: entry.display.path.clone(), - origin: entry.origin.clone(), - }) - } - - pub fn expansion_source_buffer( - &self, - expansion: SourceMacroExpansionId, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - Ok(match &entry.source_buffer { - PreprocExpansionSourceBuffer::ParseStable { file_id, path, .. } => { - PreprocSourceMapping::VirtualFile { - file_id: *file_id, - path: path.clone(), - origin: entry.origin.clone(), - } - } - PreprocExpansionSourceBuffer::DisplayOnly { path } => { - PreprocSourceMapping::VirtualDisplay { - path: path.clone(), - origin: entry.origin.clone(), - } - } - }) - } - - pub fn emitted_display_range( - &self, - expansion: SourceMacroExpansionId, - emitted_range: SourceEmittedTokenRange, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - emitted_range_from_token_ranges(&entry.display.token_ranges, emitted_range) - .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) - } - - pub fn emitted_source_buffer_range( - &self, - expansion: SourceMacroExpansionId, - emitted_range: SourceEmittedTokenRange, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer - else { - return Err(display_only_expansion_source_buffer_error(entry)); - }; - emitted_range_from_token_ranges(token_ranges, emitted_range) - .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) - } - - pub fn emitted_token_display_range( - &self, - expansion: SourceMacroExpansionId, - token: SourceEmittedTokenId, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - entry - .display - .token_ranges - .get(&token) - .copied() - .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) - } - - pub fn emitted_token_source_buffer_range( - &self, - expansion: SourceMacroExpansionId, - token: SourceEmittedTokenId, - ) -> Result { - let entry = self - .expansion(expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer - else { - return Err(display_only_expansion_source_buffer_error(entry)); - }; - token_ranges - .get(&token) - .copied() - .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) - } - - pub fn insert_expansion_parse_stable_source_buffer( - &mut self, - expansion: SourceMacroExpansionId, - file_id: FileId, - path: VfsPath, - text: String, - token_ranges: FxHashMap, - ) -> Result<(), PreprocSourceMapError> { - let entry = self - .expansion_entries - .get_mut(&expansion) - .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; - entry.source_buffer = - PreprocExpansionSourceBuffer::ParseStable { file_id, path, text, token_ranges }; - Ok(()) - } - - pub fn expansion_display_text(&self, expansion: SourceMacroExpansionId) -> Option<&str> { - self.expansion(expansion).map(|entry| entry.display.text.as_str()) - } - - pub fn file_id(&self, source: PreprocSourceId) -> Result { - match self.get(source) { - Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), - Some(PreprocSourceMapping::VirtualFile { file_id, .. }) => Ok(*file_id), - Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { - Err(PreprocSourceMapError::DisplayOnlyVirtualSource { - path: path.clone(), - origin: origin.clone(), - }) - } - Some(PreprocSourceMapping::Unmapped(reason)) => { - Err(PreprocSourceMapError::UnmappedSource { source, reason: reason.clone() }) - } - None => Err(PreprocSourceMapError::MissingSource { source }), - } - } - - pub fn source_positions_for_file_offset( - &self, - file_id: FileId, - offset: TextSize, - ) -> Vec { - let mut positions = self - .entries - .iter() - .filter_map(|(source, mapping)| { - let mapped_file_id = match mapping { - PreprocSourceMapping::RealFile(mapped_file_id) - | PreprocSourceMapping::VirtualFile { file_id: mapped_file_id, .. } => { - *mapped_file_id - } - PreprocSourceMapping::VirtualDisplay { .. } => return None, - PreprocSourceMapping::Unmapped(_) => return None, - }; - if mapped_file_id != file_id { - return None; - } - - let range_offset = self.range_offsets.get(source).copied().unwrap_or(0); - let source_offset = unshift_text_size(offset, range_offset)?; - let text_len = self.text_lengths.get(source).copied()?; - (usize::from(source_offset) <= text_len) - .then_some(SourcePosition { source: *source, offset: source_offset }) - }) - .collect::>(); - positions.sort_by_key(|position| position.source.raw()); - positions - } - - pub fn map_range(&self, source_range: SourceRange) -> Result { - match self.get(source_range.source) { - Some(PreprocSourceMapping::RealFile(_)) - | Some(PreprocSourceMapping::VirtualFile { .. }) - | Some(PreprocSourceMapping::VirtualDisplay { .. }) => {} - Some(PreprocSourceMapping::Unmapped(reason)) => { - return Err(PreprocSourceMapError::UnmappedSource { - source: source_range.source, - reason: reason.clone(), - }); - } - None => { - return Err(PreprocSourceMapError::MissingSource { source: source_range.source }); - } - } - - let range_offset = self.range_offsets.get(&source_range.source).copied().unwrap_or(0); - let mapped_range = shift_text_range(source_range.range, range_offset).ok_or( - PreprocSourceMapError::RangeOutOfBounds { - source: source_range.source, - range: source_range.range, - mapped_range: source_range.range, - text_len: usize::MAX, - }, - )?; - let text_len = self - .text_lengths - .get(&source_range.source) - .copied() - .ok_or(PreprocSourceMapError::MissingSource { source: source_range.source })?; - if usize::from(mapped_range.end()) <= text_len { - return Ok(mapped_range); - } - - Err(PreprocSourceMapError::RangeOutOfBounds { - source: source_range.source, - range: source_range.range, - mapped_range, - text_len, - }) - } -} - -impl SourcePreprocContextIndex { - fn push_context(&mut self, file_id: FileId, model_file_id: FileId) { - let contexts = self.contexts_by_file.entry(file_id).or_default(); - if !contexts.contains(&model_file_id) { - contexts.push(model_file_id); - contexts.sort(); - } - } - - fn push_issue(&mut self, issue: SourcePreprocContextIndexIssue) { - self.issues.push(issue); - } - - pub fn relevant_contexts(&self, file_id: FileId) -> SourcePreprocRelevantContexts { - SourcePreprocRelevantContexts { - model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), - status: self.status(), - } - } - - pub fn status(&self) -> SourcePreprocContextStatus { - if self.issues.is_empty() { - SourcePreprocContextStatus::Complete - } else { - SourcePreprocContextStatus::Partial { skipped_models: self.issues.len() } - } - } - - pub fn issues(&self) -> &[SourcePreprocContextIndexIssue] { - &self.issues - } -} - -fn preproc_context_file_ids( - mapped: &MappedSourcePreprocModel, - model_file_id: FileId, -) -> Vec { - let mut file_ids = Vec::new(); - let mut seen = FxHashSet::default(); - push_unique_file_id(&mut file_ids, &mut seen, model_file_id); - - for definition in mapped.model.macro_definitions().iter() { - collect_context_source_range(mapped, definition.directive_range, &mut file_ids, &mut seen); - collect_context_source_range(mapped, definition.name_range, &mut file_ids, &mut seen); - if let Some(params) = &definition.params { - for param in params { - if let Some(range) = param.name_range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - if let Some(range) = param.range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - if let Some(default) = ¶m.default { - for token in default { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - } - } - } - } - for token in &definition.body_tokens { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - } - } - - for reference in mapped.model.macro_references().iter() { - collect_context_source_range(mapped, reference.directive_range, &mut file_ids, &mut seen); - collect_context_source_range(mapped, reference.name_range, &mut file_ids, &mut seen); - } - - for call in mapped.model.macro_calls().iter() { - collect_context_source_range(mapped, call.call_range, &mut file_ids, &mut seen); - for argument in &call.arguments { - if let Some(range) = argument.argument_range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - for token in &argument.tokens { - if let Some(range) = token.range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - } - } - } - - for include in mapped.model.include_graph().directives() { - collect_context_source_range(mapped, include.directive_range, &mut file_ids, &mut seen); - if let Some(range) = include.target_range { - collect_context_source_range(mapped, range, &mut file_ids, &mut seen); - } - if let Some(source) = include.resolved_source { - collect_context_source(mapped, source, &mut file_ids, &mut seen); - } - } - - for range in mapped.model.inactive_ranges() { - collect_context_source_range(mapped, *range, &mut file_ids, &mut seen); - } - - for provenance in mapped.model.token_provenance().iter() { - match provenance { - SourceTokenProvenance::Source { token_range } - | SourceTokenProvenance::MacroBody { body_token_range: token_range, .. } => { - collect_context_source_range(mapped, *token_range, &mut file_ids, &mut seen); - } - SourceTokenProvenance::MacroArgument { - body_token_range, argument_token_range, .. - } => { - collect_context_source_range(mapped, *body_token_range, &mut file_ids, &mut seen); - collect_context_source_range( - mapped, - *argument_token_range, - &mut file_ids, - &mut seen, - ); - } - SourceTokenProvenance::TokenPaste { .. } - | SourceTokenProvenance::Stringification { .. } - | SourceTokenProvenance::Builtin { .. } - | SourceTokenProvenance::Unavailable(_) => {} - SourceTokenProvenance::Predefine { source } => { - collect_context_source(mapped, *source, &mut file_ids, &mut seen); - } - } - } - - file_ids.sort(); - file_ids -} - -fn collect_context_source_range( - mapped: &MappedSourcePreprocModel, - range: SourceRange, - file_ids: &mut Vec, - seen: &mut FxHashSet, -) { - collect_context_source(mapped, range.source, file_ids, seen); -} - -fn collect_context_source( - mapped: &MappedSourcePreprocModel, - source: PreprocSourceId, - file_ids: &mut Vec, - seen: &mut FxHashSet, -) { - if let Ok(file_id) = mapped.source_map.file_id(source) { - push_unique_file_id(file_ids, seen, file_id); - } - if let Some(manifest_source) = mapped.source_map.predefine_manifest_source(source) { - push_unique_file_id(file_ids, seen, manifest_source.file_id); - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SourcePreprocQueryError { - UnsupportedFileKind(SourceFileKind), - TraceUnavailable, - Model(SourcePreprocError), - UnmappedSource { buffer_id: u32, path: String }, -} - fn source_file_identity(db: &dyn SourceDb, file_id: FileId) -> SourceFileIdentity { let path = db.file_path(file_id).map(|path| path.to_string()).unwrap_or_default(); let name = if path.is_empty() { "source".to_owned() } else { path.clone() }; @@ -754,491 +172,6 @@ fn insert_buffer_file_ids( } } -fn push_unique_file_id(file_ids: &mut Vec, seen: &mut FxHashSet, file_id: FileId) { - if seen.insert(file_id) { - file_ids.push(file_id); - } -} - -fn source_preproc_file_ids( - db: &dyn SourceRootDb, - file_id: FileId, - profile_id: Option, - trace: &PreprocessorTrace, - options: &SyntaxTreeOptions, - preprocess: &PreprocessConfig, -) -> Result { - let mut source_map = PreprocSourceMap::default(); - let path_file_ids = path_file_ids(db); - let root_source = PreprocSourceId::from(trace.root_buffer_id); - source_map.insert_real_file(root_source, file_id, db.file_text(file_id).len()); - let include_buffer_texts = include_buffer_texts_by_path(options); - let predefine_sources = trace - .source_buffers - .iter() - .filter(|source| source.origin == SourceBufferOrigin::Predefine) - .map(|source| PredefineSourceBuffer { - source: PreprocSourceId::from(source.buffer_id), - text: source.text.as_deref(), - }) - .collect::>(); - let predefine_map = - PredefineVirtualMapping::new(db, profile_id, &preprocess.predefines, predefine_sources); - - for source in &trace.source_buffers { - let source_id = PreprocSourceId::from(source.buffer_id); - if source_id == root_source { - source_map.insert_real_file(source_id, file_id, db.file_text(file_id).len()); - continue; - } - - match source.origin { - SourceBufferOrigin::Source => { - if let Some(mapped_file_id) = path_file_ids.get(&source.path) { - source_map.insert_real_file( - source_id, - mapped_file_id, - db.file_text(mapped_file_id).len(), - ); - continue; - } - - if let Some(text) = include_buffer_texts.get(&source.path) { - let path = - preproc_virtual_include_buffer_path(profile_id, source_id, &source.path); - let file_id = materialized_preproc_virtual_file_id(db, &path); - source_map.insert_virtual_file( - source_id, - file_id, - path, - PreprocVirtualOrigin::ExternalIncludeBuffer { source: source_id }, - text.len(), - ); - continue; - } - - source_map.insert_unmapped( - source_id, - SourcePreprocUnavailable::DetachedSource { source: source_id }, - ); - } - SourceBufferOrigin::Predefine => { - if let Some(entry) = predefine_map.entry(source_id) { - let manifest_source = match entry.manifest_source(db, &path_file_ids) { - Ok(manifest_source) => manifest_source, - Err(reason) => { - source_map.insert_unmapped(source_id, reason); - continue; - } - }; - source_map.insert_virtual_file_with_offset( - source_id, - entry.file_id, - entry.path.clone(), - PreprocVirtualOrigin::Predefines { profile: profile_id }, - entry.text_len, - entry.range_offset, - ); - if let Some(manifest_source) = manifest_source { - source_map.insert_predefine_manifest_source(source_id, manifest_source); - } - } else if let Some(reason) = predefine_map.unavailable_reason(source_id) { - source_map.insert_unmapped(source_id, reason.clone()); - } else { - source_map.insert_unmapped( - source_id, - SourcePreprocUnavailable::DetachedSource { source: source_id }, - ); - } - } - } - } - - Ok(source_map) -} - -pub fn preproc_virtual_predefines_path(profile_id: Option) -> VfsPath { - VfsPath::new_virtual_path(format!( - "/__vide/preproc/{}/predefines.sv", - profile_path_segment(profile_id) - )) -} - -pub fn preproc_virtual_builtin_path( - profile_id: Option, - name: &str, -) -> VfsPath { - VfsPath::new_virtual_path(format!( - "/__vide/preproc/{}/builtin/{}.sv", - profile_path_segment(profile_id), - sanitize_path_segment(name) - )) -} - -pub fn preproc_virtual_expansion_path( - profile_id: Option, - expansion: SourceMacroExpansionId, -) -> VfsPath { - VfsPath::new_virtual_path(format!( - "/__vide/preproc/{}/expansion/{}.sv", - profile_path_segment(profile_id), - expansion.raw() - )) -} - -pub fn preproc_virtual_speculative_path( - profile_id: Option, - universe: PreprocSpeculativeUniverseId, - root: &str, -) -> VfsPath { - VfsPath::new_virtual_path(format!( - "/__vide/preproc/{}/speculative/{}/{}.sv", - profile_path_segment(profile_id), - universe.0, - sanitize_path_segment(root) - )) -} - -fn preproc_virtual_include_buffer_path( - profile_id: Option, - source_id: PreprocSourceId, - source_path: &str, -) -> VfsPath { - VfsPath::new_virtual_path(format!( - "/__vide/preproc/{}/include-buffer/{}/{}.svh", - profile_path_segment(profile_id), - source_id.raw(), - source_basename(source_path) - )) -} - -fn profile_path_segment(profile_id: Option) -> String { - profile_id - .map(|profile_id| format!("profile-{}", profile_id.0)) - .unwrap_or_else(|| "default".to_owned()) -} - -fn source_basename(path: &str) -> String { - let name = path.rsplit(['/', '\\']).next().unwrap_or("buffer"); - let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem); - sanitize_path_segment(stem) -} - -fn sanitize_path_segment(input: &str) -> String { - let mut out = String::new(); - for ch in input.chars() { - match ch { - 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => out.push(ch), - _ => out.push('_'), - } - } - if out.is_empty() { "unnamed".to_owned() } else { out } -} - -fn include_buffer_texts_by_path(options: &SyntaxTreeOptions) -> FxHashMap { - options - .include_buffers - .iter() - .map(|buffer| (buffer.path.clone(), buffer.text.clone())) - .collect() -} - -fn materialized_predefine_text(predefine: &str) -> String { - let mut definition = predefine.to_owned(); - if let Some(index) = definition.find('=') { - definition.replace_range(index..index + 1, " "); - } else { - definition.push_str(" 1"); - } - format!("`define {definition}\n") -} - -struct PredefineVirtualMapping { - entries: FxHashMap, - unavailable: FxHashMap, -} - -struct PredefineVirtualEntry { - source: PreprocSourceId, - file_id: Option, - path: VfsPath, - text_len: usize, - range_offset: usize, - predefine: Predefine, -} - -struct PredefineSourceBuffer<'a> { - source: PreprocSourceId, - text: Option<&'a str>, -} - -struct PredefineConfigEntry { - text: String, - name: SmolStr, - range_offset: usize, - predefine: Predefine, -} - -impl PredefineVirtualMapping { - fn new( - db: &dyn SourceRootDb, - profile_id: Option, - predefines: &[Predefine], - sources: Vec>, - ) -> Self { - let texts = predefines - .iter() - .map(|predefine| materialized_predefine_text(predefine.as_str())) - .collect::>(); - let text_len = texts.iter().map(String::len).sum(); - let path = preproc_virtual_predefines_path(profile_id); - let file_id = materialized_preproc_virtual_file_id(db, &path); - let mut range_offset = 0usize; - let mut configs = Vec::new(); - for (index, predefine) in predefines.iter().enumerate() { - let text = &texts[index]; - if let Some(name) = materialized_predefine_name(text) { - configs.push(PredefineConfigEntry { - text: text.clone(), - name, - range_offset, - predefine: predefine.clone(), - }); - } - range_offset += text.len(); - } - - let mut configs_by_text = FxHashMap::>::default(); - for (index, config) in configs.iter().enumerate() { - let slot = configs_by_text.entry(config.text.clone()).or_insert(Some(index)); - if *slot != Some(index) { - *slot = None; - } - } - - let mut entries = FxHashMap::default(); - let mut unavailable = FxHashMap::default(); - for source in sources { - let Some(source_text) = source.text else { - unavailable.insert( - source.source, - SourcePreprocUnavailable::MissingPredefineSourceText { source: source.source }, - ); - continue; - }; - let Some(config_index) = configs_by_text.get(source_text).and_then(|index| *index) - else { - unavailable.insert( - source.source, - SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, - ); - continue; - }; - let config = &configs[config_index]; - if materialized_predefine_name(source_text).as_ref() != Some(&config.name) { - unavailable.insert( - source.source, - SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, - ); - continue; - } - entries.insert( - source.source, - PredefineVirtualEntry { - source: source.source, - file_id, - path: path.clone(), - text_len, - range_offset: config.range_offset, - predefine: config.predefine.clone(), - }, - ); - } - - Self { entries, unavailable } - } - - fn entry(&self, source: PreprocSourceId) -> Option<&PredefineVirtualEntry> { - self.entries.get(&source) - } - - fn unavailable_reason(&self, source: PreprocSourceId) -> Option<&SourcePreprocUnavailable> { - self.unavailable.get(&source) - } -} - -impl PredefineVirtualEntry { - fn manifest_source( - &self, - db: &dyn SourceRootDb, - path_file_ids: &PathIdentityIndex, - ) -> Result, SourcePreprocUnavailable> { - let Some(source) = self.predefine.source.as_ref() else { - return Ok(None); - }; - let Some(file_id) = path_file_ids.get_path(source.path.as_path()) else { - return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { - source: self.source, - }); - }; - if !manifest_predefine_source_matches( - db.file_text(file_id).as_ref(), - source.range, - &self.predefine, - ) { - return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { - source: self.source, - }); - } - Ok(Some(PreprocManifestSource { file_id, range: source.range })) - } -} - -fn materialized_predefine_name(text: &str) -> Option { - let rest = text.trim_start().strip_prefix("`define")?.trim_start(); - let name = - rest.split(|ch: char| ch.is_whitespace() || ch == '(').next().unwrap_or_default().trim(); - let name = name.strip_prefix('`').unwrap_or(name); - if name.is_empty() { None } else { Some(SmolStr::new(name)) } -} - -fn manifest_predefine_source_matches(text: &str, range: TextRange, predefine: &Predefine) -> bool { - let start = usize::from(range.start()); - let end = usize::from(range.end()); - let Some(raw_source) = text.get(start..end) else { - return false; - }; - let Some(source_definition) = unquote_manifest_predefine(raw_source) else { - return false; - }; - source_definition == predefine.as_str() - && predefine_definition_name(source_definition) - == predefine_definition_name(predefine.as_str()) -} - -fn unquote_manifest_predefine(text: &str) -> Option<&str> { - let text = text.trim(); - text.strip_prefix('"') - .and_then(|text| text.strip_suffix('"')) - .or_else(|| text.strip_prefix('\'').and_then(|text| text.strip_suffix('\''))) -} - -fn predefine_definition_name(predefine: &str) -> Option { - let name = predefine.split_once('=').map_or(predefine, |(name, _)| name); - let name = name.trim().strip_prefix('`').unwrap_or(name.trim()); - if name.is_empty() { None } else { Some(SmolStr::new(name)) } -} - -fn materialized_preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> Option { - file_id_for_vfs_path(db, path) -} - -fn file_id_for_vfs_path(db: &dyn SourceRootDb, path: &VfsPath) -> Option { - for file_id in db.files().iter().copied() { - let source_root_id = db.source_root_id(file_id); - let source_root = db.source_root(source_root_id); - if source_root.path_for_file(&file_id) == Some(path) { - return Some(file_id); - } - } - None -} - -fn shift_text_range(range: TextRange, offset: usize) -> Option { - let start = usize::from(range.start()).checked_add(offset)?; - let end = usize::from(range.end()).checked_add(offset)?; - Some(TextRange::new( - TextSize::from(u32::try_from(start).ok()?), - TextSize::from(u32::try_from(end).ok()?), - )) -} - -fn unshift_text_size(offset: TextSize, range_offset: usize) -> Option { - let offset = usize::from(offset).checked_sub(range_offset)?; - Some(TextSize::from(u32::try_from(offset).ok()?)) -} - -fn emitted_range_from_token_ranges( - token_ranges: &FxHashMap, - emitted_range: SourceEmittedTokenRange, -) -> Option { - if emitted_range.len == 0 { - return Some(TextRange::empty(TextSize::from(0))); - } - - let start = emitted_range.start; - let end = SourceEmittedTokenId::new(start.raw().checked_add(emitted_range.len - 1)?); - let start_range = token_ranges.get(&start)?; - let end_range = token_ranges.get(&end)?; - Some(TextRange::new(start_range.start(), end_range.end())) -} - -fn display_only_expansion_source_buffer_error( - entry: &PreprocExpansionMapping, -) -> PreprocSourceMapError { - PreprocSourceMapError::DisplayOnlyVirtualSource { - path: match &entry.source_buffer { - PreprocExpansionSourceBuffer::ParseStable { path, .. } - | PreprocExpansionSourceBuffer::DisplayOnly { path } => path.clone(), - }, - origin: entry.origin.clone(), - } -} - -fn record_expansion_display_texts( - profile_id: Option, - model: &SourcePreprocModel, - source_map: &mut PreprocSourceMap, -) { - for expansion in model.macro_expansions().iter() { - let Some((text, token_ranges)) = - expansion_display_text_and_ranges(model, expansion.emitted_token_range) - else { - continue; - }; - let path = preproc_virtual_expansion_path(profile_id, expansion.id); - source_map.insert_expansion_display_only( - expansion.id, - path, - text, - expansion.emitted_token_range, - token_ranges, - ); - } -} - -fn expansion_display_text_and_ranges( - model: &SourcePreprocModel, - emitted_range: SourceEmittedTokenRange, -) -> Option<(String, FxHashMap)> { - let mut text = String::new(); - let mut token_ranges = FxHashMap::default(); - - // This is intentionally a readable display form. It is not a - // parse-stable SystemVerilog source buffer or source-map authority. - for raw in - emitted_range.start.raw()..emitted_range.start.raw().checked_add(emitted_range.len)? - { - let token_id = SourceEmittedTokenId::new(raw); - let token = model.emitted_tokens().get(token_id)?; - if !text.is_empty() { - text.push(' '); - } - let start = text.len(); - text.push_str(token.text.as_str()); - let end = text.len(); - token_ranges.insert( - token_id, - TextRange::new( - TextSize::from(u32::try_from(start).ok()?), - TextSize::from(u32::try_from(end).ok()?), - ), - ); - } - - Some((text, token_ranges)) -} - fn syntax_tree_options_for_file( db: &dyn SourceRootDb, file_id: FileId, @@ -1478,115 +411,6 @@ fn include_buffers_for_profile( Arc::new(compilation_plan::include_buffers_for_plan(db, &plan)) } -pub(crate) fn workspace_preproc_model_file_ids( - db: &dyn SourceRootDb, - profile_id: Option, -) -> Vec { - let plan = db.compilation_plan_for_profile(profile_id); - let mut file_ids = FxHashSet::default(); - - for root in plan.roots.iter().copied() { - if matches!( - db.file_kind(root), - SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader - ) { - file_ids.insert(root); - } - } - file_ids.extend(plan.include_only.iter().copied()); - - for source_root_id in &plan.source_roots { - for candidate in db.source_root(*source_root_id).iter() { - if db.file_is_project_ignored(candidate) { - continue; - } - if matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { - file_ids.insert(candidate); - } - } - } - - for candidate in db.files().iter().copied() { - if db.file_is_project_ignored(candidate) { - continue; - } - if !matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { - continue; - } - let Some(path) = db.file_path(candidate) else { - continue; - }; - if plan.include_dirs.iter().any(|include_dir| path.starts_with(include_dir)) { - file_ids.insert(candidate); - } - } - - let mut file_ids = file_ids.into_iter().collect::>(); - file_ids.sort(); - file_ids -} - -fn source_preproc_model( - db: &dyn SourceRootDb, - file_id: FileId, -) -> Arc> { - let file_kind = db.file_kind(file_id); - if !matches!(file_kind, SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader) { - return Arc::new(Err(SourcePreprocQueryError::UnsupportedFileKind(file_kind))); - } - - let text = db.file_text(file_id); - let identity = source_file_identity(db, file_id); - let profile_id = db.file_compilation_profile(file_id); - let preprocess = db.file_preprocess_config(file_id); - let options = syntax_tree_options_for_file(db, file_id); - let Some(trace) = - SyntaxTree::preprocessor_trace(&text, &identity.name, &identity.path, &options) - else { - return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); - }; - - let mut source_map = - match source_preproc_file_ids(db, file_id, profile_id, &trace, &options, &preprocess) { - Ok(source_map) => source_map, - Err(err) => return Arc::new(Err(err)), - }; - let model = match SourcePreprocModel::from_trace(trace) { - Ok(model) => model, - Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), - }; - record_expansion_display_texts(profile_id, &model, &mut source_map); - - Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) -} - -fn source_preproc_context_index_for_profile( - db: &dyn SourceRootDb, - profile_id: Option, -) -> Arc { - let mut index = SourcePreprocContextIndex::default(); - - for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { - index.push_context(model_file_id, model_file_id); - let mapped = db.source_preproc_model(model_file_id); - match mapped.as_ref() { - Ok(mapped) => { - for file_id in preproc_context_file_ids(mapped, model_file_id) { - index.push_context(file_id, model_file_id); - } - } - Err(error) => { - index.push_issue(SourcePreprocContextIndexIssue { - model_file_id, - error: error.clone(), - }); - } - } - } - - Arc::new(index) -} - fn macro_reference_index_for_profile( db: &dyn SourceRootDb, profile_id: Option, @@ -1807,14 +631,20 @@ fn source_root_semantic_diagnostics( mod tests { use std::fmt; + use ::preproc::source::{ + PreprocSourceId, SourceMacroExpansionId, SourcePreprocUnavailable, SourceRange, + }; use rustc_hash::FxHashSet; - use syntax::{SourceBufferId, SourceBufferOrigin, SyntaxTreeOptions}; - use utils::paths::{AbsPathBuf, Utf8PathBuf}; + use syntax::{PreprocessorTrace, SourceBufferId, SourceBufferOrigin, SyntaxTreeOptions}; + use utils::{ + line_index::TextRange, + paths::{AbsPathBuf, Utf8PathBuf}, + }; use vfs::{FileSet, VfsPath}; use super::*; use crate::base_db::{ - project::{CompilationProfile, PredefineSource}, + project::{CompilationProfile, Predefine, PredefineSource}, salsa::{self, Durability}, }; diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs new file mode 100644 index 00000000..82133c6a --- /dev/null +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -0,0 +1,726 @@ +use ::preproc::source::{ + PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, + SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, + SourceTokenProvenance, +}; +use rustc_hash::{FxHashMap, FxHashSet}; +use smol_str::SmolStr; +use syntax::{PreprocessorTrace, SourceBufferOrigin, SyntaxTree, SyntaxTreeOptions}; +use triomphe::Arc; +use utils::{ + line_index::{TextRange, TextSize}, + path_identity::PathIdentityIndex, + uniq_vec::UniqVec, +}; +use vfs::{FileId, VfsPath}; + +use super::{ + SourceFileKind, SourceRootDb, path_file_ids, source_file_identity, syntax_tree_options_for_file, +}; +use crate::base_db::project::CompilationProfileId; + +mod source_mapping; + +#[cfg(not(test))] +use self::source_mapping::source_preproc_file_ids; +use self::source_mapping::{ + display_only_expansion_source_buffer_error, emitted_range_from_token_ranges, + record_expansion_display_texts, shift_text_range, unshift_text_size, +}; +#[cfg(test)] +pub(super) use self::source_mapping::{materialized_predefine_text, source_preproc_file_ids}; +pub use self::source_mapping::{ + preproc_virtual_builtin_path, preproc_virtual_expansion_path, preproc_virtual_predefines_path, + preproc_virtual_speculative_path, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MappedSourcePreprocModel { + pub model: SourcePreprocModel, + pub source_map: PreprocSourceMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PreprocSourceMap { + entries: FxHashMap, + expansion_entries: FxHashMap, + predefine_sources: FxHashMap, + text_lengths: FxHashMap, + range_offsets: FxHashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourcePreprocContextIndex { + contexts_by_file: FxHashMap>, + issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocContextIndexIssue { + pub model_file_id: FileId, + pub error: SourcePreprocQueryError, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocRelevantContexts { + pub model_file_ids: Vec, + pub status: SourcePreprocContextStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourcePreprocContextStatus { + Complete, + Partial { skipped_models: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocSourceMapping { + RealFile(FileId), + VirtualFile { file_id: FileId, path: VfsPath, origin: PreprocVirtualOrigin }, + VirtualDisplay { path: VfsPath, origin: PreprocVirtualOrigin }, + Unmapped(SourcePreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocExpansionMapping { + pub origin: PreprocVirtualOrigin, + pub emitted_range: SourceEmittedTokenRange, + pub display: PreprocExpansionDisplay, + pub source_buffer: PreprocExpansionSourceBuffer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocExpansionDisplay { + pub path: VfsPath, + pub text: String, + token_ranges: FxHashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocExpansionSourceBuffer { + ParseStable { + file_id: FileId, + path: VfsPath, + text: String, + token_ranges: FxHashMap, + }, + DisplayOnly { + path: VfsPath, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PreprocManifestSource { + pub file_id: FileId, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocVirtualOrigin { + Predefines { profile: Option }, + Builtin { name: SmolStr }, + ExternalIncludeBuffer { source: PreprocSourceId }, + Expansion { expansion: SourceMacroExpansionId }, + Speculative { universe: PreprocSpeculativeUniverseId }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PreprocSpeculativeUniverseId(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocSourceMapError { + MissingSource { + source: PreprocSourceId, + }, + UnmappedSource { + source: PreprocSourceId, + reason: SourcePreprocUnavailable, + }, + RangeOutOfBounds { + source: PreprocSourceId, + range: TextRange, + mapped_range: TextRange, + text_len: usize, + }, + MissingExpansionVirtualFile { + expansion: SourceMacroExpansionId, + }, + MissingEmittedToken { + token: SourceEmittedTokenId, + }, + MissingEmittedTokenRange { + range: SourceEmittedTokenRange, + }, + DisplayOnlyVirtualSource { + path: VfsPath, + origin: PreprocVirtualOrigin, + }, +} + +impl PreprocSourceMap { + pub fn insert_real_file(&mut self, source: PreprocSourceId, file_id: FileId, text_len: usize) { + self.entries.insert(source, PreprocSourceMapping::RealFile(file_id)); + self.predefine_sources.remove(&source); + self.text_lengths.insert(source, text_len); + self.range_offsets.insert(source, 0); + } + + pub fn insert_virtual_file( + &mut self, + source: PreprocSourceId, + file_id: Option, + path: VfsPath, + origin: PreprocVirtualOrigin, + text_len: usize, + ) { + self.insert_virtual_file_with_offset(source, file_id, path, origin, text_len, 0); + } + + fn insert_virtual_file_with_offset( + &mut self, + source: PreprocSourceId, + file_id: Option, + path: VfsPath, + origin: PreprocVirtualOrigin, + text_len: usize, + range_offset: usize, + ) { + let mapping = match file_id { + Some(file_id) => PreprocSourceMapping::VirtualFile { file_id, path, origin }, + None => PreprocSourceMapping::VirtualDisplay { path, origin }, + }; + self.entries.insert(source, mapping); + self.predefine_sources.remove(&source); + self.text_lengths.insert(source, text_len); + self.range_offsets.insert(source, range_offset); + } + + pub fn insert_unmapped(&mut self, source: PreprocSourceId, reason: SourcePreprocUnavailable) { + self.entries.insert(source, PreprocSourceMapping::Unmapped(reason)); + self.predefine_sources.remove(&source); + self.text_lengths.remove(&source); + self.range_offsets.remove(&source); + } + + fn insert_predefine_manifest_source( + &mut self, + source: PreprocSourceId, + manifest_source: PreprocManifestSource, + ) { + self.predefine_sources.insert(source, manifest_source); + } + + pub fn get(&self, source: PreprocSourceId) -> Option<&PreprocSourceMapping> { + self.entries.get(&source) + } + + pub fn predefine_manifest_source( + &self, + source: PreprocSourceId, + ) -> Option { + self.predefine_sources.get(&source).copied() + } + + pub fn insert_expansion_display_only( + &mut self, + expansion: SourceMacroExpansionId, + path: VfsPath, + display_text: String, + emitted_range: SourceEmittedTokenRange, + display_token_ranges: FxHashMap, + ) { + self.expansion_entries.insert( + expansion, + PreprocExpansionMapping { + origin: PreprocVirtualOrigin::Expansion { expansion }, + emitted_range, + display: PreprocExpansionDisplay { + path: path.clone(), + text: display_text, + token_ranges: display_token_ranges, + }, + source_buffer: PreprocExpansionSourceBuffer::DisplayOnly { path }, + }, + ); + } + + pub fn expansion(&self, expansion: SourceMacroExpansionId) -> Option<&PreprocExpansionMapping> { + self.expansion_entries.get(&expansion) + } + + pub fn expansion_display_source( + &self, + expansion: SourceMacroExpansionId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + Ok(PreprocSourceMapping::VirtualDisplay { + path: entry.display.path.clone(), + origin: entry.origin.clone(), + }) + } + + pub fn expansion_source_buffer( + &self, + expansion: SourceMacroExpansionId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + Ok(match &entry.source_buffer { + PreprocExpansionSourceBuffer::ParseStable { file_id, path, .. } => { + PreprocSourceMapping::VirtualFile { + file_id: *file_id, + path: path.clone(), + origin: entry.origin.clone(), + } + } + PreprocExpansionSourceBuffer::DisplayOnly { path } => { + PreprocSourceMapping::VirtualDisplay { + path: path.clone(), + origin: entry.origin.clone(), + } + } + }) + } + + pub fn emitted_display_range( + &self, + expansion: SourceMacroExpansionId, + emitted_range: SourceEmittedTokenRange, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + emitted_range_from_token_ranges(&entry.display.token_ranges, emitted_range) + .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) + } + + pub fn emitted_source_buffer_range( + &self, + expansion: SourceMacroExpansionId, + emitted_range: SourceEmittedTokenRange, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer + else { + return Err(display_only_expansion_source_buffer_error(entry)); + }; + emitted_range_from_token_ranges(token_ranges, emitted_range) + .ok_or(PreprocSourceMapError::MissingEmittedTokenRange { range: emitted_range }) + } + + pub fn emitted_token_display_range( + &self, + expansion: SourceMacroExpansionId, + token: SourceEmittedTokenId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + entry + .display + .token_ranges + .get(&token) + .copied() + .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) + } + + pub fn emitted_token_source_buffer_range( + &self, + expansion: SourceMacroExpansionId, + token: SourceEmittedTokenId, + ) -> Result { + let entry = self + .expansion(expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + let PreprocExpansionSourceBuffer::ParseStable { token_ranges, .. } = &entry.source_buffer + else { + return Err(display_only_expansion_source_buffer_error(entry)); + }; + token_ranges + .get(&token) + .copied() + .ok_or(PreprocSourceMapError::MissingEmittedToken { token }) + } + + pub fn insert_expansion_parse_stable_source_buffer( + &mut self, + expansion: SourceMacroExpansionId, + file_id: FileId, + path: VfsPath, + text: String, + token_ranges: FxHashMap, + ) -> Result<(), PreprocSourceMapError> { + let entry = self + .expansion_entries + .get_mut(&expansion) + .ok_or(PreprocSourceMapError::MissingExpansionVirtualFile { expansion })?; + entry.source_buffer = + PreprocExpansionSourceBuffer::ParseStable { file_id, path, text, token_ranges }; + Ok(()) + } + + pub fn expansion_display_text(&self, expansion: SourceMacroExpansionId) -> Option<&str> { + self.expansion(expansion).map(|entry| entry.display.text.as_str()) + } + + pub fn file_id(&self, source: PreprocSourceId) -> Result { + match self.get(source) { + Some(PreprocSourceMapping::RealFile(file_id)) => Ok(*file_id), + Some(PreprocSourceMapping::VirtualFile { file_id, .. }) => Ok(*file_id), + Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { + Err(PreprocSourceMapError::DisplayOnlyVirtualSource { + path: path.clone(), + origin: origin.clone(), + }) + } + Some(PreprocSourceMapping::Unmapped(reason)) => { + Err(PreprocSourceMapError::UnmappedSource { source, reason: reason.clone() }) + } + None => Err(PreprocSourceMapError::MissingSource { source }), + } + } + + pub fn source_positions_for_file_offset( + &self, + file_id: FileId, + offset: TextSize, + ) -> Vec { + let mut positions = self + .entries + .iter() + .filter_map(|(source, mapping)| { + let mapped_file_id = match mapping { + PreprocSourceMapping::RealFile(mapped_file_id) + | PreprocSourceMapping::VirtualFile { file_id: mapped_file_id, .. } => { + *mapped_file_id + } + PreprocSourceMapping::VirtualDisplay { .. } => return None, + PreprocSourceMapping::Unmapped(_) => return None, + }; + if mapped_file_id != file_id { + return None; + } + + let range_offset = self.range_offsets.get(source).copied().unwrap_or(0); + let source_offset = unshift_text_size(offset, range_offset)?; + let text_len = self.text_lengths.get(source).copied()?; + (usize::from(source_offset) <= text_len) + .then_some(SourcePosition { source: *source, offset: source_offset }) + }) + .collect::>(); + positions.sort_by_key(|position| position.source.raw()); + positions + } + + pub fn map_range(&self, source_range: SourceRange) -> Result { + match self.get(source_range.source) { + Some(PreprocSourceMapping::RealFile(_)) + | Some(PreprocSourceMapping::VirtualFile { .. }) + | Some(PreprocSourceMapping::VirtualDisplay { .. }) => {} + Some(PreprocSourceMapping::Unmapped(reason)) => { + return Err(PreprocSourceMapError::UnmappedSource { + source: source_range.source, + reason: reason.clone(), + }); + } + None => { + return Err(PreprocSourceMapError::MissingSource { source: source_range.source }); + } + } + + let range_offset = self.range_offsets.get(&source_range.source).copied().unwrap_or(0); + let mapped_range = shift_text_range(source_range.range, range_offset).ok_or( + PreprocSourceMapError::RangeOutOfBounds { + source: source_range.source, + range: source_range.range, + mapped_range: source_range.range, + text_len: usize::MAX, + }, + )?; + let text_len = self + .text_lengths + .get(&source_range.source) + .copied() + .ok_or(PreprocSourceMapError::MissingSource { source: source_range.source })?; + if usize::from(mapped_range.end()) <= text_len { + return Ok(mapped_range); + } + + Err(PreprocSourceMapError::RangeOutOfBounds { + source: source_range.source, + range: source_range.range, + mapped_range, + text_len, + }) + } +} + +impl SourcePreprocContextIndex { + fn push_context(&mut self, file_id: FileId, model_file_id: FileId) { + let contexts = self.contexts_by_file.entry(file_id).or_default(); + if !contexts.contains(&model_file_id) { + contexts.push(model_file_id); + contexts.sort(); + } + } + + fn push_issue(&mut self, issue: SourcePreprocContextIndexIssue) { + self.issues.push(issue); + } + + pub fn relevant_contexts(&self, file_id: FileId) -> SourcePreprocRelevantContexts { + SourcePreprocRelevantContexts { + model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), + status: self.status(), + } + } + + pub fn status(&self) -> SourcePreprocContextStatus { + if self.issues.is_empty() { + SourcePreprocContextStatus::Complete + } else { + SourcePreprocContextStatus::Partial { skipped_models: self.issues.len() } + } + } + + pub fn issues(&self) -> &[SourcePreprocContextIndexIssue] { + &self.issues + } +} + +fn preproc_context_file_ids( + mapped: &MappedSourcePreprocModel, + model_file_id: FileId, +) -> Vec { + let mut file_ids = UniqVec::::default(); + file_ids.push_unique(model_file_id); + + for definition in mapped.model.macro_definitions().iter() { + collect_context_source_range(mapped, definition.directive_range, &mut file_ids); + collect_context_source_range(mapped, definition.name_range, &mut file_ids); + if let Some(params) = &definition.params { + for param in params { + if let Some(range) = param.name_range { + collect_context_source_range(mapped, range, &mut file_ids); + } + if let Some(range) = param.range { + collect_context_source_range(mapped, range, &mut file_ids); + } + if let Some(default) = ¶m.default { + for token in default { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids); + } + } + } + } + } + for token in &definition.body_tokens { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids); + } + } + } + + for reference in mapped.model.macro_references().iter() { + collect_context_source_range(mapped, reference.directive_range, &mut file_ids); + collect_context_source_range(mapped, reference.name_range, &mut file_ids); + } + + for call in mapped.model.macro_calls().iter() { + collect_context_source_range(mapped, call.call_range, &mut file_ids); + for argument in &call.arguments { + if let Some(range) = argument.argument_range { + collect_context_source_range(mapped, range, &mut file_ids); + } + for token in &argument.tokens { + if let Some(range) = token.range { + collect_context_source_range(mapped, range, &mut file_ids); + } + } + } + } + + for include in mapped.model.include_graph().directives() { + collect_context_source_range(mapped, include.directive_range, &mut file_ids); + if let Some(range) = include.target_range { + collect_context_source_range(mapped, range, &mut file_ids); + } + if let Some(source) = include.resolved_source { + collect_context_source(mapped, source, &mut file_ids); + } + } + + for range in mapped.model.inactive_ranges() { + collect_context_source_range(mapped, *range, &mut file_ids); + } + + for provenance in mapped.model.token_provenance().iter() { + match provenance { + SourceTokenProvenance::Source { token_range } + | SourceTokenProvenance::MacroBody { body_token_range: token_range, .. } => { + collect_context_source_range(mapped, *token_range, &mut file_ids); + } + SourceTokenProvenance::MacroArgument { + body_token_range, argument_token_range, .. + } => { + collect_context_source_range(mapped, *body_token_range, &mut file_ids); + collect_context_source_range(mapped, *argument_token_range, &mut file_ids); + } + SourceTokenProvenance::TokenPaste { .. } + | SourceTokenProvenance::Stringification { .. } + | SourceTokenProvenance::Builtin { .. } + | SourceTokenProvenance::Unavailable(_) => {} + SourceTokenProvenance::Predefine { source } => { + collect_context_source(mapped, *source, &mut file_ids); + } + } + } + + let mut file_ids = file_ids.into_vec(); + file_ids.sort(); + file_ids +} + +fn collect_context_source_range( + mapped: &MappedSourcePreprocModel, + range: SourceRange, + file_ids: &mut UniqVec, +) { + collect_context_source(mapped, range.source, file_ids); +} + +fn collect_context_source( + mapped: &MappedSourcePreprocModel, + source: PreprocSourceId, + file_ids: &mut UniqVec, +) { + if let Ok(file_id) = mapped.source_map.file_id(source) { + file_ids.push_unique(file_id); + } + if let Some(manifest_source) = mapped.source_map.predefine_manifest_source(source) { + file_ids.push_unique(manifest_source.file_id); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourcePreprocQueryError { + UnsupportedFileKind(SourceFileKind), + TraceUnavailable, + Model(SourcePreprocError), + UnmappedSource { buffer_id: u32, path: String }, +} + +pub(crate) fn workspace_preproc_model_file_ids( + db: &dyn SourceRootDb, + profile_id: Option, +) -> Vec { + let plan = db.compilation_plan_for_profile(profile_id); + let mut file_ids = FxHashSet::default(); + + for root in plan.roots.iter().copied() { + if matches!( + db.file_kind(root), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + file_ids.insert(root); + } + } + file_ids.extend(plan.include_only.iter().copied()); + + for source_root_id in &plan.source_roots { + for candidate in db.source_root(*source_root_id).iter() { + if db.file_is_project_ignored(candidate) { + continue; + } + if matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { + file_ids.insert(candidate); + } + } + } + + for candidate in db.files().iter().copied() { + if db.file_is_project_ignored(candidate) { + continue; + } + if !matches!(db.file_kind(candidate), SourceFileKind::IncludeHeader) { + continue; + } + let Some(path) = db.file_path(candidate) else { + continue; + }; + if plan.include_dirs.iter().any(|include_dir| path.starts_with(include_dir)) { + file_ids.insert(candidate); + } + } + + let mut file_ids = file_ids.into_iter().collect::>(); + file_ids.sort(); + file_ids +} + +pub(super) fn source_preproc_model( + db: &dyn SourceRootDb, + file_id: FileId, +) -> Arc> { + let file_kind = db.file_kind(file_id); + if !matches!(file_kind, SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader) { + return Arc::new(Err(SourcePreprocQueryError::UnsupportedFileKind(file_kind))); + } + + let text = db.file_text(file_id); + let identity = source_file_identity(db, file_id); + let profile_id = db.file_compilation_profile(file_id); + let preprocess = db.file_preprocess_config(file_id); + let options = syntax_tree_options_for_file(db, file_id); + let Some(trace) = + SyntaxTree::preprocessor_trace(&text, &identity.name, &identity.path, &options) + else { + return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); + }; + + let mut source_map = + match source_preproc_file_ids(db, file_id, profile_id, &trace, &options, &preprocess) { + Ok(source_map) => source_map, + Err(err) => return Arc::new(Err(err)), + }; + let model = match SourcePreprocModel::from_trace(trace) { + Ok(model) => model, + Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), + }; + record_expansion_display_texts(profile_id, &model, &mut source_map); + + Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) +} + +pub(super) fn source_preproc_context_index_for_profile( + db: &dyn SourceRootDb, + profile_id: Option, +) -> Arc { + let mut index = SourcePreprocContextIndex::default(); + + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { + index.push_context(model_file_id, model_file_id); + let mapped = db.source_preproc_model(model_file_id); + match mapped.as_ref() { + Ok(mapped) => { + for file_id in preproc_context_file_ids(mapped, model_file_id) { + index.push_context(file_id, model_file_id); + } + } + Err(error) => { + index.push_issue(SourcePreprocContextIndexIssue { + model_file_id, + error: error.clone(), + }); + } + } + } + + Arc::new(index) +} diff --git a/crates/hir/src/base_db/source_db/preproc/source_mapping.rs b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs new file mode 100644 index 00000000..524c9289 --- /dev/null +++ b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs @@ -0,0 +1,487 @@ +use super::*; +use crate::base_db::project::{Predefine, PreprocessConfig}; + +pub(in crate::base_db::source_db) fn source_preproc_file_ids( + db: &dyn SourceRootDb, + file_id: FileId, + profile_id: Option, + trace: &PreprocessorTrace, + options: &SyntaxTreeOptions, + preprocess: &PreprocessConfig, +) -> Result { + let mut source_map = PreprocSourceMap::default(); + let path_file_ids = path_file_ids(db); + let root_source = PreprocSourceId::from(trace.root_buffer_id); + source_map.insert_real_file(root_source, file_id, db.file_text(file_id).len()); + let include_buffer_texts = include_buffer_texts_by_path(options); + let predefine_sources = trace + .source_buffers + .iter() + .filter(|source| source.origin == SourceBufferOrigin::Predefine) + .map(|source| PredefineSourceBuffer { + source: PreprocSourceId::from(source.buffer_id), + text: source.text.as_deref(), + }) + .collect::>(); + let predefine_map = + PredefineVirtualMapping::new(db, profile_id, &preprocess.predefines, predefine_sources); + + for source in &trace.source_buffers { + let source_id = PreprocSourceId::from(source.buffer_id); + if source_id == root_source { + source_map.insert_real_file(source_id, file_id, db.file_text(file_id).len()); + continue; + } + + match source.origin { + SourceBufferOrigin::Source => { + if let Some(mapped_file_id) = path_file_ids.get(&source.path) { + source_map.insert_real_file( + source_id, + mapped_file_id, + db.file_text(mapped_file_id).len(), + ); + continue; + } + + if let Some(text) = include_buffer_texts.get(&source.path) { + let path = + preproc_virtual_include_buffer_path(profile_id, source_id, &source.path); + let file_id = materialized_preproc_virtual_file_id(db, &path); + source_map.insert_virtual_file( + source_id, + file_id, + path, + PreprocVirtualOrigin::ExternalIncludeBuffer { source: source_id }, + text.len(), + ); + continue; + } + + source_map.insert_unmapped( + source_id, + SourcePreprocUnavailable::DetachedSource { source: source_id }, + ); + } + SourceBufferOrigin::Predefine => { + if let Some(entry) = predefine_map.entry(source_id) { + let manifest_source = match entry.manifest_source(db, &path_file_ids) { + Ok(manifest_source) => manifest_source, + Err(reason) => { + source_map.insert_unmapped(source_id, reason); + continue; + } + }; + source_map.insert_virtual_file_with_offset( + source_id, + entry.file_id, + entry.path.clone(), + PreprocVirtualOrigin::Predefines { profile: profile_id }, + entry.text_len, + entry.range_offset, + ); + if let Some(manifest_source) = manifest_source { + source_map.insert_predefine_manifest_source(source_id, manifest_source); + } + } else if let Some(reason) = predefine_map.unavailable_reason(source_id) { + source_map.insert_unmapped(source_id, reason.clone()); + } else { + source_map.insert_unmapped( + source_id, + SourcePreprocUnavailable::DetachedSource { source: source_id }, + ); + } + } + } + } + + Ok(source_map) +} + +pub fn preproc_virtual_predefines_path(profile_id: Option) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/predefines.sv", + profile_path_segment(profile_id) + )) +} + +pub fn preproc_virtual_builtin_path( + profile_id: Option, + name: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/builtin/{}.sv", + profile_path_segment(profile_id), + sanitize_path_segment(name) + )) +} + +pub fn preproc_virtual_expansion_path( + profile_id: Option, + expansion: SourceMacroExpansionId, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/expansion/{}.sv", + profile_path_segment(profile_id), + expansion.raw() + )) +} + +pub fn preproc_virtual_speculative_path( + profile_id: Option, + universe: PreprocSpeculativeUniverseId, + root: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/speculative/{}/{}.sv", + profile_path_segment(profile_id), + universe.0, + sanitize_path_segment(root) + )) +} + +fn preproc_virtual_include_buffer_path( + profile_id: Option, + source_id: PreprocSourceId, + source_path: &str, +) -> VfsPath { + VfsPath::new_virtual_path(format!( + "/__vide/preproc/{}/include-buffer/{}/{}.svh", + profile_path_segment(profile_id), + source_id.raw(), + source_basename(source_path) + )) +} + +fn profile_path_segment(profile_id: Option) -> String { + profile_id + .map(|profile_id| format!("profile-{}", profile_id.0)) + .unwrap_or_else(|| "default".to_owned()) +} + +fn source_basename(path: &str) -> String { + let name = path.rsplit(['/', '\\']).next().unwrap_or("buffer"); + let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem); + sanitize_path_segment(stem) +} + +fn sanitize_path_segment(input: &str) -> String { + let mut out = String::new(); + for ch in input.chars() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => out.push(ch), + _ => out.push('_'), + } + } + if out.is_empty() { "unnamed".to_owned() } else { out } +} + +fn include_buffer_texts_by_path(options: &SyntaxTreeOptions) -> FxHashMap { + options + .include_buffers + .iter() + .map(|buffer| (buffer.path.clone(), buffer.text.clone())) + .collect() +} + +pub(in crate::base_db::source_db) fn materialized_predefine_text(predefine: &str) -> String { + let mut definition = predefine.to_owned(); + if let Some(index) = definition.find('=') { + definition.replace_range(index..index + 1, " "); + } else { + definition.push_str(" 1"); + } + format!("`define {definition}\n") +} + +struct PredefineVirtualMapping { + entries: FxHashMap, + unavailable: FxHashMap, +} + +struct PredefineVirtualEntry { + source: PreprocSourceId, + file_id: Option, + path: VfsPath, + text_len: usize, + range_offset: usize, + predefine: Predefine, +} + +struct PredefineSourceBuffer<'a> { + source: PreprocSourceId, + text: Option<&'a str>, +} + +struct PredefineConfigEntry { + text: String, + name: SmolStr, + range_offset: usize, + predefine: Predefine, +} + +impl PredefineVirtualMapping { + fn new( + db: &dyn SourceRootDb, + profile_id: Option, + predefines: &[Predefine], + sources: Vec>, + ) -> Self { + let texts = predefines + .iter() + .map(|predefine| materialized_predefine_text(predefine.as_str())) + .collect::>(); + let text_len = texts.iter().map(String::len).sum(); + let path = preproc_virtual_predefines_path(profile_id); + let file_id = materialized_preproc_virtual_file_id(db, &path); + let mut range_offset = 0usize; + let mut configs = Vec::new(); + for (index, predefine) in predefines.iter().enumerate() { + let text = &texts[index]; + if let Some(name) = materialized_predefine_name(text) { + configs.push(PredefineConfigEntry { + text: text.clone(), + name, + range_offset, + predefine: predefine.clone(), + }); + } + range_offset += text.len(); + } + + let mut configs_by_text = FxHashMap::>::default(); + for (index, config) in configs.iter().enumerate() { + let slot = configs_by_text.entry(config.text.clone()).or_insert(Some(index)); + if *slot != Some(index) { + *slot = None; + } + } + + let mut entries = FxHashMap::default(); + let mut unavailable = FxHashMap::default(); + for source in sources { + let Some(source_text) = source.text else { + unavailable.insert( + source.source, + SourcePreprocUnavailable::MissingPredefineSourceText { source: source.source }, + ); + continue; + }; + let Some(config_index) = configs_by_text.get(source_text).and_then(|index| *index) + else { + unavailable.insert( + source.source, + SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, + ); + continue; + }; + let config = &configs[config_index]; + if materialized_predefine_name(source_text).as_ref() != Some(&config.name) { + unavailable.insert( + source.source, + SourcePreprocUnavailable::UnverifiedPredefineSource { source: source.source }, + ); + continue; + } + entries.insert( + source.source, + PredefineVirtualEntry { + source: source.source, + file_id, + path: path.clone(), + text_len, + range_offset: config.range_offset, + predefine: config.predefine.clone(), + }, + ); + } + + Self { entries, unavailable } + } + + fn entry(&self, source: PreprocSourceId) -> Option<&PredefineVirtualEntry> { + self.entries.get(&source) + } + + fn unavailable_reason(&self, source: PreprocSourceId) -> Option<&SourcePreprocUnavailable> { + self.unavailable.get(&source) + } +} + +impl PredefineVirtualEntry { + fn manifest_source( + &self, + db: &dyn SourceRootDb, + path_file_ids: &PathIdentityIndex, + ) -> Result, SourcePreprocUnavailable> { + let Some(source) = self.predefine.source.as_ref() else { + return Ok(None); + }; + let Some(file_id) = path_file_ids.get_path(source.path.as_path()) else { + return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { + source: self.source, + }); + }; + if !manifest_predefine_source_matches( + db.file_text(file_id).as_ref(), + source.range, + &self.predefine, + ) { + return Err(SourcePreprocUnavailable::UnverifiedPredefineSource { + source: self.source, + }); + } + Ok(Some(PreprocManifestSource { file_id, range: source.range })) + } +} + +fn materialized_predefine_name(text: &str) -> Option { + let rest = text.trim_start().strip_prefix("`define")?.trim_start(); + let name = + rest.split(|ch: char| ch.is_whitespace() || ch == '(').next().unwrap_or_default().trim(); + let name = name.strip_prefix('`').unwrap_or(name); + if name.is_empty() { None } else { Some(SmolStr::new(name)) } +} + +fn manifest_predefine_source_matches(text: &str, range: TextRange, predefine: &Predefine) -> bool { + let start = usize::from(range.start()); + let end = usize::from(range.end()); + let Some(raw_source) = text.get(start..end) else { + return false; + }; + let Some(source_definition) = unquote_manifest_predefine(raw_source) else { + return false; + }; + source_definition == predefine.as_str() + && predefine_definition_name(source_definition) + == predefine_definition_name(predefine.as_str()) +} + +fn unquote_manifest_predefine(text: &str) -> Option<&str> { + let text = text.trim(); + text.strip_prefix('"') + .and_then(|text| text.strip_suffix('"')) + .or_else(|| text.strip_prefix('\'').and_then(|text| text.strip_suffix('\''))) +} + +fn predefine_definition_name(predefine: &str) -> Option { + let name = predefine.split_once('=').map_or(predefine, |(name, _)| name); + let name = name.trim().strip_prefix('`').unwrap_or(name.trim()); + if name.is_empty() { None } else { Some(SmolStr::new(name)) } +} + +fn materialized_preproc_virtual_file_id(db: &dyn SourceRootDb, path: &VfsPath) -> Option { + file_id_for_vfs_path(db, path) +} + +fn file_id_for_vfs_path(db: &dyn SourceRootDb, path: &VfsPath) -> Option { + for file_id in db.files().iter().copied() { + let source_root_id = db.source_root_id(file_id); + let source_root = db.source_root(source_root_id); + if source_root.path_for_file(&file_id) == Some(path) { + return Some(file_id); + } + } + None +} + +pub(in crate::base_db::source_db::preproc) fn shift_text_range( + range: TextRange, + offset: usize, +) -> Option { + let start = usize::from(range.start()).checked_add(offset)?; + let end = usize::from(range.end()).checked_add(offset)?; + Some(TextRange::new( + TextSize::from(u32::try_from(start).ok()?), + TextSize::from(u32::try_from(end).ok()?), + )) +} + +pub(in crate::base_db::source_db::preproc) fn unshift_text_size( + offset: TextSize, + range_offset: usize, +) -> Option { + let offset = usize::from(offset).checked_sub(range_offset)?; + Some(TextSize::from(u32::try_from(offset).ok()?)) +} + +pub(in crate::base_db::source_db::preproc) fn emitted_range_from_token_ranges( + token_ranges: &FxHashMap, + emitted_range: SourceEmittedTokenRange, +) -> Option { + if emitted_range.len == 0 { + return Some(TextRange::empty(TextSize::from(0))); + } + + let start = emitted_range.start; + let end = SourceEmittedTokenId::new(start.raw().checked_add(emitted_range.len - 1)?); + let start_range = token_ranges.get(&start)?; + let end_range = token_ranges.get(&end)?; + Some(TextRange::new(start_range.start(), end_range.end())) +} + +pub(in crate::base_db::source_db::preproc) fn display_only_expansion_source_buffer_error( + entry: &PreprocExpansionMapping, +) -> PreprocSourceMapError { + PreprocSourceMapError::DisplayOnlyVirtualSource { + path: match &entry.source_buffer { + PreprocExpansionSourceBuffer::ParseStable { path, .. } + | PreprocExpansionSourceBuffer::DisplayOnly { path } => path.clone(), + }, + origin: entry.origin.clone(), + } +} + +pub(in crate::base_db::source_db::preproc) fn record_expansion_display_texts( + profile_id: Option, + model: &SourcePreprocModel, + source_map: &mut PreprocSourceMap, +) { + for expansion in model.macro_expansions().iter() { + let Some((text, token_ranges)) = + expansion_display_text_and_ranges(model, expansion.emitted_token_range) + else { + continue; + }; + let path = preproc_virtual_expansion_path(profile_id, expansion.id); + source_map.insert_expansion_display_only( + expansion.id, + path, + text, + expansion.emitted_token_range, + token_ranges, + ); + } +} + +fn expansion_display_text_and_ranges( + model: &SourcePreprocModel, + emitted_range: SourceEmittedTokenRange, +) -> Option<(String, FxHashMap)> { + let mut text = String::new(); + let mut token_ranges = FxHashMap::default(); + + // This is intentionally a readable display form. It is not a + // parse-stable SystemVerilog source buffer or source-map authority. + for raw in + emitted_range.start.raw()..emitted_range.start.raw().checked_add(emitted_range.len)? + { + let token_id = SourceEmittedTokenId::new(raw); + let token = model.emitted_tokens().get(token_id)?; + if !text.is_empty() { + text.push(' '); + } + let start = text.len(); + text.push_str(token.text.as_str()); + let end = text.len(); + token_ranges.insert( + token_id, + TextRange::new( + TextSize::from(u32::try_from(start).ok()?), + TextSize::from(u32::try_from(end).ok()?), + ), + ); + } + + Some((text, token_ranges)) +} diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index c09e7ece..b1774015 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,4104 +1,48 @@ -use std::{collections::BTreeMap, hash::Hash}; - use preproc::source::{ CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenId, - SourceEmittedTokenRange, SourceIncludeChainEntry, SourceIncludeDirectiveId, - SourceIncludeStatus, SourceMacroArgument as SourceMacroArgumentFact, - SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, - SourceMacroCallStatus as SourceMacroCallStatusFact, - SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroDefinitionId, + SourceEmittedTokenRange, SourceIncludeChainEntry, SourceIncludeStatus, + SourceMacroArgument as SourceMacroArgumentFact, SourceMacroCall as SourceMacroCallFact, + SourceMacroCallId, SourceMacroCallStatus as SourceMacroCallStatusFact, + SourceMacroDefinition as SourceMacroDefinitionFact, SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, SourceMacroExpansionQuery as SourceMacroExpansionQueryFact, SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, SourceMacroParam as SourceMacroParamFact, SourceMacroReference as SourceMacroReferenceFact, - SourceMacroReferenceId, SourceMacroReferenceSite, - SourceMacroResolution as SourceMacroResolutionFact, + SourceMacroReferenceSite, SourceMacroResolution as SourceMacroResolutionFact, SourceMacroResolutionReason as SourceMacroResolutionReasonFact, SourcePreprocError, SourcePreprocUnavailable, SourceRange, SourceTokenProvenance as SourceTokenProvenanceFact, }; -use rustc_hash::FxHashSet; use smol_str::SmolStr; use utils::{ line_index::{TextRange, TextSize}, uniq_vec::UniqVec, }; -use vfs::{FileId, VfsPath}; +use vfs::FileId; use crate::base_db::{ project::{CompilationProfileId, Predefine}, source_db::{ - MappedSourcePreprocModel, PreprocSourceMapError, PreprocSourceMapping, - PreprocVirtualOrigin, SourceFileKind, SourcePreprocContextStatus, SourcePreprocQueryError, - SourceRootDb, workspace_preproc_model_file_ids, + MappedSourcePreprocModel, PreprocSourceMapError, PreprocSourceMapping, SourceFileKind, + SourcePreprocContextStatus, SourcePreprocQueryError, SourceRootDb, + workspace_preproc_model_file_ids, }, }; -pub type PreprocResult = Result; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocError { - SourceQuery(SourcePreprocQueryError), - MissingRootSource, - UnmappedSource { - buffer_id: u32, - }, - MismatchedDefinitionRangeFiles { - event_id: u32, - directive_file_id: FileId, - name_file_id: FileId, - }, - MismatchedReferenceRangeFiles { - event_id: u32, - directive_file_id: FileId, - name_file_id: FileId, - }, - MissingDefinitionNameRange { - event_id: u32, - }, - SourceMap(PreprocSourceMapError), - Unavailable { - reason: PreprocUnavailable, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocUnavailable { - Source(SourcePreprocUnavailable), - AmbiguousMacroReferenceContexts { contexts: usize }, - AmbiguousMacroExpansionContexts { contexts: usize }, - AmbiguousMacroParamContexts { contexts: usize }, - AmbiguousMacroDefinitionContexts { contexts: usize }, - AmbiguousDiagnosticProvenance { targets: usize }, - AmbiguousIncludeTargets { targets: usize }, - PartialPreprocContextIndex { skipped_models: usize }, - DisplayOnlyVirtualExpansion { path: VfsPath, origin: PreprocVirtualOrigin }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PreprocAvailability { - Complete, - Partial, - Unavailable(PreprocUnavailable), -} - -macro_rules! mapped_preproc_id { - ($name:ident, $core:ty) => { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct $name($core); - - impl $name { - pub fn raw(self) -> usize { - self.0.raw() - } - } - - impl From<$core> for $name { - fn from(value: $core) -> Self { - Self(value) - } - } - }; -} - -mapped_preproc_id!(MacroReferenceId, SourceMacroReferenceId); -mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); -mapped_preproc_id!(MacroCallId, SourceMacroCallId); -mapped_preproc_id!(MacroExpansionId, SourceMacroExpansionId); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MacroDefinitionId { - Source(SourceMacroDefinitionId), - ConfiguredPredefine { file_id: FileId, range: TextRange }, -} - -impl From for MacroDefinitionId { - fn from(value: SourceMacroDefinitionId) -> Self { - Self::Source(value) - } -} - -const CONFIGURED_PREDEFINE_DEFINE_INDEX: usize = usize::MAX; -const CONFIGURED_PREDEFINE_EVENT_ID: u32 = u32::MAX; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MappedPreprocSource { - RealFile { file_id: FileId }, - VirtualFile { file_id: FileId, path: vfs::VfsPath, origin: PreprocVirtualOrigin }, - VirtualDisplay { path: vfs::VfsPath, origin: PreprocVirtualOrigin }, -} - -impl MappedPreprocSource { - pub fn file_id(&self) -> Option { - match self { - Self::RealFile { file_id } | Self::VirtualFile { file_id, .. } => Some(*file_id), - Self::VirtualDisplay { .. } => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacroResolution { - Resolved { - definition_id: MacroDefinitionId, - reason: MacroResolutionReason, - include_chain: Vec, - }, - Undefined, - Unavailable(PreprocUnavailable), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MacroResolutionReason { - VisibleDefinition, - IncludeGuardIfNDef, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDefinition { - pub id: MacroDefinitionId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub name: SmolStr, - pub params: Option>, - pub body_tokens: Vec, - pub define_index: usize, - pub event_id: u32, - pub directive_range: TextRange, - pub name_range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDefinitionParam { - pub param_index: usize, - pub name: Option, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroParamDefinition { - pub macro_definition: MacroDefinition, - pub param_index: usize, - pub name: SmolStr, - pub range: TextRange, - pub param_range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroParamReference { - pub macro_definition: MacroDefinition, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub param_index: usize, - pub token_index: usize, - pub name: SmolStr, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroParamReferenceDefinitions { - pub references: Vec, - pub range: TextRange, - pub definitions: Vec, - pub capability: PreprocAvailability, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroParamReferences { - pub references: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroUsage { - pub reference_id: MacroReferenceId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub name: SmolStr, - pub usage_index: usize, - pub directive_range: TextRange, - pub range: TextRange, - pub resolution: MacroResolution, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroUsageResolution { - pub usage: MacroUsage, - pub definition: MacroDefinition, - pub definition_provenance: MacroDefinitionProvenance, - pub include_chain: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDefinitionProvenance { - pub id: MacroDefinitionId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub event_id: u32, - pub file_id: FileId, - pub directive_range: TextRange, - pub name_range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IncludeChainEntry { - pub include_event_id: u32, - pub include_file_id: FileId, - pub include_range: TextRange, - pub included_file_id: FileId, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReference { - pub id: MacroReferenceId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub name: SmolStr, - pub directive_range: TextRange, - pub range: TextRange, - pub resolution: MacroResolution, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReferenceResolution { - pub reference: MacroReference, - pub definition: MacroDefinition, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReferenceDefinitions { - pub references: Vec, - pub range: TextRange, - pub definitions: Vec, - pub capability: PreprocAvailability, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroCall { - pub id: MacroCallId, - pub reference_id: MacroReferenceId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub arguments: Vec, - pub directive_range: TextRange, - pub range: TextRange, - pub callee: MacroResolution, - pub expansion: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroArgument { - pub argument_index: usize, - pub source: Option, - pub range: Option, - pub tokens: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroExpansion { - pub id: MacroExpansionId, - pub call: MacroCall, - pub definition_id: MacroDefinitionId, - pub definition: MacroDefinition, - pub emitted_token_range: SourceEmittedTokenRange, - pub display_source: MappedPreprocSource, - pub display_range: TextRange, - pub child_calls: Vec, - pub capability: PreprocAvailability, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroExpansionProvenance { - pub expansion: MacroExpansion, - pub tokens: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EmittedTokenProvenance { - pub token: SourceEmittedTokenId, - pub text: SmolStr, - pub display_range: TextRange, - pub provenance: TokenProvenance, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TokenProvenance { - SourceToken { - source: MappedPreprocSource, - range: TextRange, - }, - MacroBody { - call: MacroCall, - definition_id: MacroDefinitionId, - source: MappedPreprocSource, - range: TextRange, - }, - MacroArgument { - call: MacroCall, - argument_index: usize, - source: MappedPreprocSource, - range: TextRange, - }, - Predefine { - source: MappedPreprocSource, - }, - Builtin { - name: SmolStr, - }, - Unavailable(PreprocUnavailable), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DiagnosticProvenance { - SourceToken { - source: MappedPreprocSource, - range: TextRange, - }, - MacroBody { - call: MacroCall, - definition_id: MacroDefinitionId, - source: MappedPreprocSource, - range: TextRange, - }, - MacroArgument { - call: MacroCall, - argument_index: usize, - source: MappedPreprocSource, - range: TextRange, - }, - VirtualExpansion { - source: MappedPreprocSource, - range: TextRange, - }, - Unavailable(PreprocUnavailable), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacroExpansionQuery { - Available(Box), - Ambiguous(Vec), - Unavailable(Box), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroExpansionUnavailable { - pub call: MacroCall, - pub reason: PreprocUnavailable, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RecursiveMacroExpansion { - pub root_call: MacroCall, - pub expansions: Vec, - pub unavailable: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RecursiveMacroExpansionProvenance { - pub root_call: MacroCall, - pub expansions: Vec, - pub unavailable: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct MacroDefinitionKey { - file_id: FileId, - range_start: TextSize, - range_end: TextSize, - name: SmolStr, -} - -impl MacroDefinitionKey { - fn from_definition(definition: &MacroDefinition) -> Self { - Self { - file_id: definition.file_id, - range_start: definition.name_range.start(), - range_end: definition.name_range.end(), - name: definition.name.clone(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct MacroReferenceKey { - file_id: FileId, - range_start: TextSize, - range_end: TextSize, - name: SmolStr, -} - -impl MacroReferenceKey { - fn from_reference(reference: &MacroReference) -> Self { - Self { - file_id: reference.file_id, - range_start: reference.range.start(), - range_end: reference.range.end(), - name: reference.name.clone(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct MacroParamDefinitionKey { - macro_definition: MacroDefinitionKey, - param_index: usize, - range_start: TextSize, - range_end: TextSize, - name: SmolStr, -} - -impl MacroParamDefinitionKey { - fn from_definition(definition: &MacroParamDefinition) -> Self { - Self { - macro_definition: MacroDefinitionKey::from_definition(&definition.macro_definition), - param_index: definition.param_index, - range_start: definition.range.start(), - range_end: definition.range.end(), - name: definition.name.clone(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct MacroParamReferenceKey { - macro_definition: MacroDefinitionKey, - param_index: usize, - file_id: FileId, - range_start: TextSize, - range_end: TextSize, - name: SmolStr, -} - -impl MacroParamReferenceKey { - fn from_reference(reference: &MacroParamReference) -> Self { - Self { - macro_definition: MacroDefinitionKey::from_definition(&reference.macro_definition), - param_index: reference.param_index, - file_id: reference.file_id, - range_start: reference.range.start(), - range_end: reference.range.end(), - name: reference.name.clone(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct InactiveBranchKey { - file_id: FileId, - range_start: TextSize, - range_end: TextSize, -} - -impl InactiveBranchKey { - fn from_branch(branch: &InactiveBranch) -> Self { - Self { - file_id: branch.file_id, - range_start: branch.range.start(), - range_end: branch.range.end(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct MacroReferenceIndex { - references_by_definition: - BTreeMap>, - definitions_by_reference: - BTreeMap>, - issues: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReferences { - pub references: Vec, - pub status: MacroReferenceIndexStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacroReferenceIndexStatus { - Complete, - Partial { issues: Vec }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacroReferenceIndexIssue { - SkippedModel { - file_id: FileId, - error: PreprocError, - }, - UnavailableReference { - file_id: FileId, - reference_id: MacroReferenceId, - reason: PreprocUnavailable, - }, -} - -impl MacroReferenceIndex { - pub fn references_for(&self, definition: &MacroDefinition) -> Vec { - self.references_by_definition - .get(&MacroDefinitionKey::from_definition(definition)) - .map(|references| references.as_slice().to_vec()) - .unwrap_or_default() - } - - pub fn definitions_for_reference( - &self, - reference: &MacroReference, - ) -> Option<&[MacroDefinition]> { - self.definitions_by_reference - .get(&MacroReferenceKey::from_reference(reference)) - .map(UniqVec::as_slice) - } - - pub fn status(&self) -> MacroReferenceIndexStatus { - if self.issues.is_empty() { - MacroReferenceIndexStatus::Complete - } else { - MacroReferenceIndexStatus::Partial { issues: self.issues.clone() } - } - } - - fn push(&mut self, definition: MacroDefinition, reference: MacroReference) { - let definition_key = MacroDefinitionKey::from_definition(&definition); - let references = self.references_by_definition.entry(definition_key).or_default(); - push_unique_value( - references, - MacroReferenceKey::from_reference(&reference), - reference.clone(), - ); - - let reference_key = MacroReferenceKey::from_reference(&reference); - let definitions = self.definitions_by_reference.entry(reference_key).or_default(); - push_unique_value( - definitions, - MacroDefinitionKey::from_definition(&definition), - definition, - ); - } - - fn push_issue(&mut self, issue: MacroReferenceIndexIssue) { - if !self.issues.contains(&issue) { - self.issues.push(issue); - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IncludeDirective { - pub id: IncludeDirectiveId, - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub include_index: usize, - pub range: TextRange, - pub target: IncludeTarget, - pub status: IncludeDirectiveStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InactiveBranch { - pub source: MappedPreprocSource, - pub capability: PreprocAvailability, - pub file_id: FileId, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IncludeTarget { - Literal { path: SmolStr, resolved_file: Option }, - Token { raw: SmolStr }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IncludeDirectiveStatus { - Resolved { source: MappedPreprocSource }, - Unresolved, - Unavailable(PreprocUnavailable), -} - -impl From for PreprocError { - fn from(value: SourcePreprocQueryError) -> Self { - Self::SourceQuery(value) - } -} - -impl From for PreprocError { - fn from(value: SourcePreprocError) -> Self { - Self::SourceQuery(SourcePreprocQueryError::Model(value)) - } -} - -pub fn visible_macros_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = UniqVec::::default(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for position in mapped.source_map.source_positions_for_file_offset(file_id, offset) { - for definition in mapped.model.visible_macros_at(position) { - match map_macro_definition(mapped, definition) { - Ok(definition) => push_unique_macro_definition(&mut definitions, definition), - Err(error) => record_first_error(&mut first_error, error), - } - } - } - } - - if definitions.is_empty() - && let Err(error) = finish_empty_single_query(&contexts, first_error) - { - return Err(error); - } - - Ok(definitions.into_vec()) -} - -pub fn visible_macro_names_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut names = UniqVec::::default(); - for definition in visible_macros_at(db, file_id, offset)? { - names.push_unique(definition.name.clone()); - } - for name in configured_predefine_names(db, file_id) { - names.push_unique(name); - } - - Ok(names.into_vec()) -} - -fn configured_predefine_names(db: &dyn SourceRootDb, file_id: FileId) -> Vec { - let mut names = UniqVec::::default(); - - let profile_id = db.file_compilation_profile(file_id); - for predefine in &db.project_config().preprocess_for_profile(profile_id).predefines { - if let Some(name) = predefine_macro_name(predefine.as_str()) { - names.push_unique(name); - } - } - - for predefine in &db.file_preprocess_config(file_id).predefines { - if let Some(name) = predefine_macro_name(predefine.as_str()) { - names.push_unique(name); - } - } - - names.into_vec() -} - -fn predefine_macro_name(predefine: &str) -> Option { - let name = predefine.split_once('=').map_or(predefine, |(name, _)| name); - let name = name.trim().strip_prefix('`').unwrap_or(name.trim()); - if name.is_empty() { None } else { Some(SmolStr::new(name)) } -} - -fn configured_predefine_definitions_for_name( - db: &dyn SourceRootDb, - context_file_id: FileId, - name: &SmolStr, -) -> Vec { - let mut definitions = UniqVec::::default(); - let profile_id = db.file_compilation_profile(context_file_id); - let project_preprocess = db.project_config().preprocess_for_profile(profile_id); - for predefine in &project_preprocess.predefines { - if let Some(definition) = configured_predefine_definition(db, predefine, name) { - push_unique_macro_definition(&mut definitions, definition); - } - } - for predefine in &db.file_preprocess_config(context_file_id).predefines { - if let Some(definition) = configured_predefine_definition(db, predefine, name) { - push_unique_macro_definition(&mut definitions, definition); - } - } - definitions.into_vec() -} - -fn configured_predefine_definitions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = UniqVec::::default(); - let contexts = source_preproc_single_query_contexts(db, file_id); - for context_file_id in contexts.model_file_ids.iter().copied() { - let profile_id = db.file_compilation_profile(context_file_id); - let project_preprocess = db.project_config().preprocess_for_profile(profile_id); - for predefine in &project_preprocess.predefines { - if let Some(definition) = - configured_predefine_definition_at(db, predefine, file_id, offset) - { - push_unique_macro_definition(&mut definitions, definition); - } - } - for predefine in &db.file_preprocess_config(context_file_id).predefines { - if let Some(definition) = - configured_predefine_definition_at(db, predefine, file_id, offset) - { - push_unique_macro_definition(&mut definitions, definition); - } - } - } - if definitions.is_empty() { - finish_empty_single_query(&contexts, None)?; - } - Ok(definitions.into_vec()) -} - -fn configured_predefine_definition_at( - db: &dyn SourceRootDb, - predefine: &Predefine, - file_id: FileId, - offset: TextSize, -) -> Option { - let definition = - configured_predefine_definition(db, predefine, &predefine_macro_name(predefine.as_str())?)?; - (definition.file_id == file_id && definition.name_range.contains(offset)).then_some(definition) -} - -fn configured_predefine_definition( - db: &dyn SourceRootDb, - predefine: &Predefine, - name: &SmolStr, -) -> Option { - let predefine_name = predefine_macro_name(predefine.as_str())?; - if &predefine_name != name { - return None; - } - let source = predefine.source.as_ref()?; - let file_id = file_id_for_predefine_source_path(db, &source.path)?; - Some(MacroDefinition { - id: MacroDefinitionId::ConfiguredPredefine { file_id, range: source.range }, - source: MappedPreprocSource::RealFile { file_id }, - capability: PreprocAvailability::Complete, - file_id, - name: predefine_name, - params: None, - body_tokens: Vec::new(), - define_index: CONFIGURED_PREDEFINE_DEFINE_INDEX, - event_id: CONFIGURED_PREDEFINE_EVENT_ID, - directive_range: source.range, - name_range: source.range, - }) -} - -fn file_id_for_predefine_source_path( - db: &dyn SourceRootDb, - path: &utils::paths::AbsPathBuf, -) -> Option { - db.files().iter().copied().find(|file_id| db.file_path(*file_id).as_ref() == Some(path)) -} - -pub fn macro_definition_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for definition in mapped.model.macro_definitions().iter() { - let mapped_definition = map_macro_definition(mapped, definition)?; - if mapped_definition.file_id == file_id && mapped_definition.name_range.contains(offset) - { - return Ok(Some(mapped_definition)); - } - } - } - - let mut configured_definitions = configured_predefine_definitions_at(db, file_id, offset)?; - match configured_definitions.len() { - 0 => {} - 1 => return Ok(configured_definitions.pop()), - contexts => { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroDefinitionContexts { contexts }, - }); - } - } - - finish_empty_single_query(&contexts, first_error)?; - - Ok(None) -} - -pub fn macro_param_definition_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = macro_param_definitions_at(db, file_id, offset)?; - match definitions.len() { - 0 => Ok(None), - 1 => Ok(definitions.pop()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroParamContexts { contexts }, - }), - } -} - -pub fn macro_param_definitions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = UniqVec::::default(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for definition in mapped.model.macro_definitions().iter() { - let Some(params) = &definition.params else { - continue; - }; - for (param_index, param) in params.iter().enumerate() { - let Some(param_definition) = - map_macro_param_definition(mapped, definition, param_index, param)? - else { - continue; - }; - if param_definition.macro_definition.file_id == file_id - && param_definition.range.contains(offset) - { - push_unique_macro_param_definition(&mut definitions, param_definition); - } - } - } - } - - if definitions.is_empty() - && let Err(error) = finish_empty_single_query(&contexts, first_error) - { - return Err(error); - } - - Ok(definitions.into_vec()) -} - -pub fn macro_param_reference_definitions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = UniqVec::::default(); - let mut references = UniqVec::::default(); - let mut query_range = None; - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for definition in mapped.model.macro_definitions().iter() { - let Some(params) = &definition.params else { - continue; - }; - for (token_index, token) in definition.body_tokens.iter().enumerate() { - let Some(token_range) = token.range else { - continue; - }; - let (_, range) = - match mapped_source_range_at_offset(mapped, token_range, file_id, offset) { - Ok(Some(hit)) => hit, - Ok(None) => continue, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for (param_index, param) in params.iter().enumerate() { - if param.name.as_ref() != Some(&token.value) { - continue; - } - let Some(param_definition) = - map_macro_param_definition(mapped, definition, param_index, param)? - else { - continue; - }; - let reference = map_macro_param_reference( - mapped, - definition, - param_index, - token_index, - token_range, - )?; - query_range.get_or_insert(range); - push_unique_macro_param_definition(&mut definitions, param_definition); - push_unique_macro_param_reference(&mut references, reference); - } - } - } - } - - let Some(range) = query_range else { - finish_empty_single_query(&contexts, first_error)?; - return Ok(None); - }; - - let references = references.into_vec(); - let definitions = definitions.into_vec(); - Ok(Some(MacroParamReferenceDefinitions { - capability: macro_param_reference_context_capability(&references), - references, - range, - definitions, - })) -} - -pub fn macro_usage_resolution_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut resolutions = macro_usage_resolutions_at(db, file_id, offset)?; - match resolutions.len() { - 0 => Ok(None), - 1 => Ok(resolutions.pop()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts }, - }), - } -} - -pub fn macro_usage_resolutions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut resolutions = Vec::new(); - let mut first_error = None; - let mut unavailable_contexts = 0; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for reference in mapped.model.macro_references().iter() { - let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { - continue; - }; - match mapped_source_range_contains_provenance_offset( - mapped, - reference.name_range, - file_id, - offset, - ) { - Ok(true) => {} - Ok(false) => continue, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - } - - let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = - &reference.resolution - else { - if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { - unavailable_contexts += 1; - record_first_error(&mut first_error, unavailable_error(reason.clone())); - } - continue; - }; - let mapped_reference = map_macro_reference(mapped, reference)?; - let definition_fact = - mapped.model.macro_definitions().get(*definition).ok_or_else(|| { - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, - )) - })?; - let definition = map_macro_definition(mapped, definition_fact)?; - let definition_provenance = - map_definition_provenance_from_definition(mapped, definition_fact)?; - let include_chain = map_include_chain(mapped, include_chain)?; - - push_unique_macro_usage_resolution( - &mut resolutions, - MacroUsageResolution { - usage: MacroUsage { - reference_id: mapped_reference.id, - source: mapped_reference.source, - capability: mapped_reference.capability.clone(), - file_id: mapped_reference.file_id, - name: mapped_reference.name, - usage_index, - directive_range: mapped_reference.directive_range, - range: mapped_reference.range, - resolution: mapped_reference.resolution, - }, - definition, - definition_provenance, - include_chain, - }, - ); - } - } - - if !resolutions.is_empty() { - return Ok(resolutions); - } - if unavailable_contexts > 1 { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { - contexts: unavailable_contexts, - }, - }); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn macro_reference_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let Some(mut contexts) = macro_reference_definitions_at(db, file_id, offset)? else { - return Ok(None); - }; - if contexts.references.len() == 1 { - return Ok(contexts.references.pop()); - } - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { - contexts: contexts.references.len(), - }, - }) -} - -pub fn macro_reference_resolution_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let Some(mut resolution) = macro_reference_definitions_at(db, file_id, offset)? else { - return Ok(None); - }; - if resolution.references.len() != 1 { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { - contexts: resolution.references.len(), - }, - }); - } - let reference = resolution.references.pop().unwrap(); - match resolution.definitions.len() { - 0 => Ok(None), - 1 => { - let definition = resolution.definitions.pop().unwrap(); - Ok(Some(MacroReferenceResolution { reference, definition })) - } - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts }, - }), - } -} - -pub fn macro_reference_definitions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut definitions = UniqVec::::default(); - let mut references = Vec::new(); - let mut query_range = None; - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for reference in mapped.model.macro_references().iter() { - let (_, range) = match mapped_source_range_at_offset( - mapped, - reference.name_range, - file_id, - offset, - ) { - Ok(Some(hit)) => hit, - Ok(None) => continue, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - query_range.get_or_insert(range); - - let mapped_reference = match map_macro_reference(mapped, reference) { - Ok(reference) => reference, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - push_unique_macro_reference_context(&mut references, mapped_reference.clone()); - - match &reference.resolution { - SourceMacroResolutionFact::Resolved { definition, .. } => { - let Some(definition) = mapped.model.macro_definitions().get(*definition) else { - record_first_error( - &mut first_error, - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { - event_id: reference.event_id.raw(), - }, - )), - ); - continue; - }; - let definition = match map_macro_definition(mapped, definition) { - Ok(definition) => definition, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - push_unique_macro_definition(&mut definitions, definition); - } - SourceMacroResolutionFact::Undefined => { - for definition in configured_predefine_definitions_for_name( - db, - model_file_id, - &mapped_reference.name, - ) { - push_unique_macro_definition(&mut definitions, definition); - } - } - SourceMacroResolutionFact::Unavailable(_) => {} - } - } - } - - let Some(range) = query_range else { - finish_empty_single_query(&contexts, first_error)?; - return Ok(None); - }; - - Ok(Some(MacroReferenceDefinitions { - capability: macro_reference_context_capability(&references), - references, - range, - definitions: definitions.into_vec(), - })) -} - -pub fn immediate_macro_expansion_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut queries = macro_expansion_queries_at(db, file_id, offset)?; - match queries.len() { - 0 => Ok(None), - 1 => Ok(queries.pop()), - contexts => { - let available = queries - .iter() - .filter_map(|query| match query { - MacroExpansionQuery::Available(expansion) => Some(expansion.as_ref().clone()), - MacroExpansionQuery::Ambiguous(_) | MacroExpansionQuery::Unavailable(_) => None, - }) - .collect::>(); - if available.len() == contexts { - return Ok(Some(MacroExpansionQuery::Ambiguous(available))); - } - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }) - } - } -} - -pub fn macro_expansion_queries_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut queries = Vec::new(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let query = immediate_macro_expansion_for_call(mapped, call_fact)?; - push_unique_macro_expansion_query(&mut queries, query); - } - } - - if !queries.is_empty() { - return Ok(queries); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn recursive_macro_expansion_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let expansions = recursive_macro_expansions_at(db, file_id, offset)?; - match expansions.len() { - 0 => Ok(None), - 1 => Ok(expansions.into_iter().next()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }), - } -} - -pub fn recursive_macro_expansions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut expansions = Vec::new(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; - push_unique_recursive_macro_expansion(&mut expansions, recursive); - } - } - - if !expansions.is_empty() { - return Ok(expansions); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn recursive_macro_expansion_provenances_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut expansions = Vec::new(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; - push_unique_recursive_macro_expansion_provenance(&mut expansions, recursive); - } - } - - if !expansions.is_empty() { - return Ok(expansions); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn macro_expansion_provenance_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let provenances = macro_expansion_provenances_at(db, file_id, offset)?; - match provenances.len() { - 0 => Ok(None), - 1 => Ok(provenances.into_iter().next()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }), - } -} - -pub fn macro_expansion_provenances_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut provenances = Vec::new(); - let mut unavailable = Vec::new(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - for call_fact in source_macro_calls_at(mapped, file_id, offset) { - match macro_expansion_provenance_for_call(mapped, call_fact)? { - MacroExpansionProvenanceForCall::Available(provenance) => { - push_unique_macro_expansion_provenance(&mut provenances, *provenance); - } - MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), - } - } - } - - if !unavailable.is_empty() { - return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); - } - if !provenances.is_empty() { - return Ok(provenances); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn macro_expansion_provenance_for_range( - db: &dyn SourceRootDb, - file_id: FileId, - range: TextRange, -) -> PreprocResult> { - let provenances = macro_expansion_provenances_for_range(db, file_id, range)?; - match provenances.len() { - 0 => Ok(None), - 1 => Ok(provenances.into_iter().next()), - contexts => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }), - } -} - -pub fn macro_expansion_provenances_for_range( - db: &dyn SourceRootDb, - file_id: FileId, - range: TextRange, -) -> PreprocResult> { - let mut provenances = Vec::new(); - let mut unavailable = Vec::new(); - let mut ambiguous_contexts = 0; - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); - match call_facts.as_slice() { - [] => continue, - [call_fact] => match macro_expansion_provenance_for_call(mapped, call_fact)? { - MacroExpansionProvenanceForCall::Available(provenance) => { - push_unique_macro_expansion_provenance(&mut provenances, *provenance); - } - MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), - }, - call_facts => { - ambiguous_contexts += call_facts.len(); - } - } - } - - if ambiguous_contexts > 0 { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { - contexts: ambiguous_contexts + provenances.len() + unavailable.len(), - }, - }); - } - if !unavailable.is_empty() { - return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); - } - if !provenances.is_empty() { - return Ok(provenances); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -fn unavailable_or_ambiguous_macro_expansion_provenance( - available_contexts: usize, - mut unavailable: Vec, -) -> PreprocResult> { - let contexts = available_contexts + unavailable.len(); - if contexts == 1 { - return Err(PreprocError::Unavailable { reason: unavailable.pop().unwrap() }); - } - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, - }) -} - -pub fn diagnostic_provenance_for_range( - db: &dyn SourceRootDb, - file_id: FileId, - range: TextRange, -) -> PreprocResult> { - let mut provenances = Vec::new(); - let mut ambiguous_targets = 0; - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); - match call_facts.as_slice() { - [] => continue, - [call_fact] => { - let provenance = diagnostic_provenance_for_call(mapped, call_fact)?; - push_unique_diagnostic_provenance(&mut provenances, provenance); - } - call_facts => { - ambiguous_targets += call_facts.len(); - } - } - } - - let precise = provenances - .iter() - .filter(|provenance| !matches!(provenance, DiagnosticProvenance::Unavailable(_))) - .cloned() - .collect::>(); - if ambiguous_targets > 0 { - return Ok(Some(DiagnosticProvenance::Unavailable( - PreprocUnavailable::AmbiguousDiagnosticProvenance { - targets: ambiguous_targets + precise.len(), - }, - ))); - } - if precise.len() == 1 { - return Ok(Some(precise.into_iter().next().unwrap())); - } - if precise.len() > 1 { - return Ok(Some(DiagnosticProvenance::Unavailable( - PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: precise.len() }, - ))); - } - if provenances.len() == 1 { - return Ok(provenances.into_iter().next()); - } - if provenances.len() > 1 { - return Ok(Some(DiagnosticProvenance::Unavailable( - PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: provenances.len() }, - ))); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(None) -} - -pub fn macro_references( - db: &dyn SourceRootDb, - file_id: FileId, - definition: &MacroDefinition, -) -> PreprocResult { - let profile_id = db - .file_compilation_profile(file_id) - .or_else(|| db.file_compilation_profile(definition.file_id)); - let index = db.macro_reference_index_for_profile(profile_id); - Ok(MacroReferences { references: index.references_for(definition), status: index.status() }) -} - -pub fn macro_param_references( - db: &dyn SourceRootDb, - file_id: FileId, - definition: &MacroParamDefinition, -) -> PreprocResult { - let profile_id = db - .file_compilation_profile(file_id) - .or_else(|| db.file_compilation_profile(definition.macro_definition.file_id)); - let mut references = UniqVec::::default(); - let mut first_error = None; - - for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for source_definition in mapped.model.macro_definitions().iter() { - let mapped_definition = match map_macro_definition(mapped, source_definition) { - Ok(definition) => definition, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - if !same_macro_definition(&mapped_definition, &definition.macro_definition) { - continue; - } - let Some(params) = &source_definition.params else { - continue; - }; - let Some(param) = params.get(definition.param_index) else { - continue; - }; - if param.name.as_ref() != Some(&definition.name) { - continue; - } - - for (token_index, token) in source_definition.body_tokens.iter().enumerate() { - if param.name.as_ref() != Some(&token.value) { - continue; - } - let Some(token_range) = token.range else { - continue; - }; - match map_macro_param_reference( - mapped, - source_definition, - definition.param_index, - token_index, - token_range, - ) { - Ok(reference) => push_unique_macro_param_reference(&mut references, reference), - Err(error) => record_first_error(&mut first_error, error), - } - } - } - } - - if references.is_empty() - && let Some(error) = first_error - { - return Err(error); - } - - Ok(MacroParamReferences { references: references.into_vec() }) -} - -pub(crate) fn build_macro_reference_index( - db: &dyn SourceRootDb, - profile_id: Option, -) -> MacroReferenceIndex { - let mut index = MacroReferenceIndex::default(); - - for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped.as_ref() { - Ok(mapped) => mapped, - Err(error) => { - index.push_issue(MacroReferenceIndexIssue::SkippedModel { - file_id: model_file_id, - error: error.clone().into(), - }); - continue; - } - }; - collect_macro_references_in_model(db, mapped, model_file_id, &mut index); - } - - index -} - -fn collect_macro_references_in_model( - db: &dyn SourceRootDb, - mapped: &MappedSourcePreprocModel, - model_file_id: FileId, - index: &mut MacroReferenceIndex, -) { - for reference in mapped.model.macro_references().iter() { - let SourceMacroResolutionFact::Resolved { definition, .. } = reference.resolution else { - if reference.resolution == SourceMacroResolutionFact::Undefined { - collect_configured_predefine_reference(db, mapped, model_file_id, reference, index); - continue; - } - if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { - index.push_issue(MacroReferenceIndexIssue::UnavailableReference { - file_id: model_file_id, - reference_id: reference.id.into(), - reason: PreprocUnavailable::Source(reason.clone()), - }); - } - continue; - }; - - let Some(definition) = mapped.model.macro_definitions().get(definition) else { - index.push_issue(MacroReferenceIndexIssue::SkippedModel { - file_id: model_file_id, - error: PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, - )), - }); - continue; - }; - - let definition = match map_macro_definition(mapped, definition) { - Ok(definition) => definition, - Err(error) => { - index.push_issue(MacroReferenceIndexIssue::SkippedModel { - file_id: model_file_id, - error, - }); - continue; - } - }; - let reference = match map_macro_reference(mapped, reference) { - Ok(reference) => reference, - Err(error) => { - index.push_issue(MacroReferenceIndexIssue::SkippedModel { - file_id: model_file_id, - error, - }); - continue; - } - }; - index.push(definition, reference); - } -} - -fn collect_configured_predefine_reference( - db: &dyn SourceRootDb, - mapped: &MappedSourcePreprocModel, - model_file_id: FileId, - source_reference: &SourceMacroReferenceFact, - index: &mut MacroReferenceIndex, -) { - let reference = match map_macro_reference(mapped, source_reference) { - Ok(reference) => reference, - Err(error) => { - index.push_issue(MacroReferenceIndexIssue::SkippedModel { - file_id: model_file_id, - error, - }); - return; - } - }; - for definition in configured_predefine_definitions_for_name(db, model_file_id, &reference.name) - { - index.push(definition, reference.clone()); - } -} - -pub fn include_directive_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut directives = include_directives_at(db, file_id, offset)?; - match directives.len() { - 0 => Ok(None), - 1 => Ok(directives.pop()), - targets => Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousIncludeTargets { targets }, - }), - } -} - -pub fn include_directives_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let mut directives = Vec::new(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - for include in mapped.model.include_graph().directives() { - let Some(target_range) = include.target_range else { - continue; - }; - let (source, range) = - match mapped_source_range_at_offset(mapped, target_range, file_id, offset) { - Ok(Some(hit)) => hit, - Ok(None) => continue, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - let status = map_include_status(mapped, &include.status)?; - let resolved_file = match &status { - IncludeDirectiveStatus::Resolved { source } => source.file_id(), - IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, - }; - let target = match &include.target { - MacroIncludeTarget::Literal { path, .. } => { - IncludeTarget::Literal { path: path.clone(), resolved_file } - } - MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, - }; - let directive = IncludeDirective { - id: include.id.into(), - source, - capability: capability_status(&mapped.model.capabilities().include_edges), - file_id, - include_index: include.id.raw(), - range, - target, - status, - }; - push_unique_include_directive(&mut directives, directive); - } - } - - if !directives.is_empty() { - return Ok(directives); - } - finish_empty_single_query(&contexts, first_error)?; - - Ok(Vec::new()) -} - -pub fn inactive_branches( - db: &dyn SourceRootDb, - file_id: FileId, -) -> PreprocResult> { - let mut branches = UniqVec::::default(); - let mut first_error = None; - let contexts = source_preproc_single_query_contexts(db, file_id); - - for model_file_id in contexts.model_file_ids.iter().copied() { - let mapped = db.source_preproc_model(model_file_id); - let mapped = match mapped_result(mapped.as_ref()) { - Ok(mapped) => mapped, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - - for source_range in mapped.model.inactive_ranges() { - let (source, range) = match map_mapped_source_range(mapped, *source_range) { - Ok(mapped_range) => mapped_range, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - }; - let Some(branch_file_id) = source.file_id() else { - continue; - }; - if branch_file_id == file_id { - push_unique_inactive_branch( - &mut branches, - InactiveBranch { - source, - capability: capability_status(&mapped.model.capabilities().inactive_ranges), - file_id: branch_file_id, - range, - }, - ); - } - } - } - - if branches.is_empty() - && let Err(error) = finish_empty_single_query(&contexts, first_error) - { - return Err(error); - } - - Ok(branches.into_vec()) -} - -fn mapped_result( - result: &Result, -) -> PreprocResult<&MappedSourcePreprocModel> { - result.as_ref().map_err(|err| err.clone().into()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct SourcePreprocQueryContexts { - model_file_ids: Vec, - status: SourcePreprocContextStatus, -} - -impl SourcePreprocQueryContexts { - fn partial_error(&self) -> Option { - let SourcePreprocContextStatus::Partial { skipped_models } = self.status else { - return None; - }; - Some(PreprocError::Unavailable { - reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models }, - }) - } -} - -fn source_preproc_single_query_contexts( - db: &dyn SourceRootDb, - file_id: FileId, -) -> SourcePreprocQueryContexts { - let profile_id = db.file_compilation_profile(file_id); - let index = db.source_preproc_context_index_for_profile(profile_id); - let relevant = index.relevant_contexts(file_id); - let mut file_ids = Vec::new(); - let mut seen = FxHashSet::default(); - if matches!( - db.file_kind(file_id), - SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader - ) { - push_unique_file_id(&mut file_ids, &mut seen, file_id); - } - for model_file_id in relevant.model_file_ids { - push_unique_file_id(&mut file_ids, &mut seen, model_file_id); - } - SourcePreprocQueryContexts { model_file_ids: file_ids, status: relevant.status } -} - -fn finish_empty_single_query( - contexts: &SourcePreprocQueryContexts, - first_error: Option, -) -> PreprocResult<()> { - if let Some(error) = first_error { - return Err(error); - } - if let Some(error) = contexts.partial_error() { - return Err(error); - } - Ok(()) -} - -fn push_unique_file_id(file_ids: &mut Vec, seen: &mut FxHashSet, file_id: FileId) { - if seen.insert(file_id) { - file_ids.push(file_id); - } -} - -fn record_first_error(first_error: &mut Option, error: PreprocError) { - if first_error.is_none() { - *first_error = Some(error); - } -} - -fn require_file_backed_source(source: &MappedPreprocSource) -> PreprocResult { - source.file_id().ok_or_else(|| { - let MappedPreprocSource::VirtualDisplay { path, origin } = source else { - unreachable!("file-backed source should have a FileId"); - }; - PreprocError::SourceMap(PreprocSourceMapError::DisplayOnlyVirtualSource { - path: path.clone(), - origin: origin.clone(), - }) - }) -} - -fn map_source_range( - mapped: &MappedSourcePreprocModel, - source_range: SourceRange, -) -> PreprocResult<(FileId, TextRange)> { - let (source, range) = map_mapped_source_range(mapped, source_range)?; - Ok((require_file_backed_source(&source)?, range)) -} - -fn map_source_id( - mapped: &MappedSourcePreprocModel, - source: PreprocSourceId, -) -> PreprocResult { - mapped.source_map.file_id(source).map_err(PreprocError::SourceMap) -} - -fn map_mapped_source_range( - mapped: &MappedSourcePreprocModel, - source_range: SourceRange, -) -> PreprocResult<(MappedPreprocSource, TextRange)> { - let range = mapped.source_map.map_range(source_range).map_err(PreprocError::SourceMap)?; - let source = map_mapped_source_id(mapped, source_range.source)?; - Ok((source, range)) -} - -fn mapped_source_range_at_offset( - mapped: &MappedSourcePreprocModel, - source_range: SourceRange, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let (source, range) = map_mapped_source_range(mapped, source_range)?; - Ok((source.file_id() == Some(file_id) && range.contains(offset)).then_some((source, range))) -} - -fn mapped_source_range_contains_provenance_offset( - mapped: &MappedSourcePreprocModel, - source_range: SourceRange, - file_id: FileId, - offset: TextSize, -) -> PreprocResult { - Ok(mapped_source_range_at_offset(mapped, source_range, file_id, offset)?.is_some()) -} - -fn map_mapped_source_id( - mapped: &MappedSourcePreprocModel, - source: PreprocSourceId, -) -> PreprocResult { - match mapped.source_map.get(source) { - Some(PreprocSourceMapping::RealFile(file_id)) => { - Ok(MappedPreprocSource::RealFile { file_id: *file_id }) - } - Some(PreprocSourceMapping::VirtualFile { file_id, path, origin }) => { - Ok(MappedPreprocSource::VirtualFile { - file_id: *file_id, - path: path.clone(), - origin: origin.clone(), - }) - } - Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { - Ok(MappedPreprocSource::VirtualDisplay { path: path.clone(), origin: origin.clone() }) - } - Some(PreprocSourceMapping::Unmapped(reason)) => { - Err(PreprocError::SourceMap(PreprocSourceMapError::UnmappedSource { - source, - reason: reason.clone(), - })) - } - None => Err(PreprocError::SourceMap(PreprocSourceMapError::MissingSource { source })), - } -} - -fn map_macro_definition( - mapped: &MappedSourcePreprocModel, - definition: &SourceMacroDefinitionFact, -) -> PreprocResult { - let (mut source, mut directive_range, mut name_range) = map_definition_ranges( - mapped, - definition.event_id.raw(), - definition.directive_range, - definition.name_range, - )?; - if let Some(manifest_source) = - mapped.source_map.predefine_manifest_source(definition.name_range.source) - { - source = MappedPreprocSource::RealFile { file_id: manifest_source.file_id }; - directive_range = manifest_source.range; - name_range = manifest_source.range; - } - let params = definition - .params - .as_ref() - .map(|params| { - params - .iter() - .enumerate() - .map(|(param_index, param)| { - let range = param - .name_range - .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) - .transpose()?; - Ok(MacroDefinitionParam { param_index, name: param.name.clone(), range }) - }) - .collect::>>() - }) - .transpose()?; - let file_id = require_file_backed_source(&source)?; - Ok(MacroDefinition { - id: definition.id.into(), - file_id, - source, - capability: capability_status(&mapped.model.capabilities().definition_name_ranges), - name: definition.name.clone(), - params, - body_tokens: definition.body_tokens.iter().map(|token| token.raw.clone()).collect(), - define_index: define_index_for_definition(mapped, definition)?, - event_id: definition.event_id.raw(), - directive_range, - name_range, - }) -} - -fn map_macro_param_definition( - mapped: &MappedSourcePreprocModel, - definition: &SourceMacroDefinitionFact, - param_index: usize, - param: &SourceMacroParamFact, -) -> PreprocResult> { - let Some(name) = ¶m.name else { - return Ok(None); - }; - let Some(name_source_range) = param.name_range else { - return Ok(None); - }; - let macro_definition = map_macro_definition(mapped, definition)?; - let (source, range) = map_mapped_source_range(mapped, name_source_range)?; - let name_file_id = require_file_backed_source(&source)?; - if name_file_id != macro_definition.file_id { - return Err(PreprocError::MismatchedDefinitionRangeFiles { - event_id: definition.event_id.raw(), - directive_file_id: macro_definition.file_id, - name_file_id, - }); - } - let param_range = param - .range - .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) - .transpose()?; - - Ok(Some(MacroParamDefinition { - macro_definition, - param_index, - name: name.clone(), - range, - param_range, - })) -} - -fn map_macro_param_reference( - mapped: &MappedSourcePreprocModel, - definition: &SourceMacroDefinitionFact, - param_index: usize, - token_index: usize, - token_range: SourceRange, -) -> PreprocResult { - let macro_definition = map_macro_definition(mapped, definition)?; - let (source, range) = map_mapped_source_range(mapped, token_range)?; - let file_id = require_file_backed_source(&source)?; - let name = definition - .params - .as_ref() - .and_then(|params| params.get(param_index)) - .and_then(|param| param.name.clone()) - .ok_or_else(|| { - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, - )) - })?; - - Ok(MacroParamReference { - macro_definition, - source, - capability: PreprocAvailability::Complete, - file_id, - param_index, - token_index, - name, - range, - }) -} - -fn map_definition_provenance_from_definition( - mapped: &MappedSourcePreprocModel, - definition: &SourceMacroDefinitionFact, -) -> PreprocResult { - let definition = map_macro_definition(mapped, definition)?; - Ok(MacroDefinitionProvenance { - id: definition.id, - source: definition.source, - capability: definition.capability, - event_id: definition.event_id, - file_id: definition.file_id, - directive_range: definition.directive_range, - name_range: definition.name_range, - }) -} - -fn map_macro_reference( - mapped: &MappedSourcePreprocModel, - reference: &SourceMacroReferenceFact, -) -> PreprocResult { - let (source, directive_range, name_range) = map_reference_ranges(mapped, reference)?; - let file_id = require_file_backed_source(&source)?; - Ok(MacroReference { - id: reference.id.into(), - file_id, - source, - capability: capability_status(&mapped.model.capabilities().macro_reference_resolution), - name: reference.name.clone(), - directive_range, - range: name_range, - resolution: map_macro_resolution(mapped, &reference.resolution)?, - }) -} - -fn map_macro_call( - mapped: &MappedSourcePreprocModel, - call: &SourceMacroCallFact, -) -> PreprocResult { - let (source, range) = map_mapped_source_range(mapped, call.call_range)?; - let arguments = call - .arguments - .iter() - .map(|argument| map_macro_argument(mapped, argument)) - .collect::>>()?; - let file_id = require_file_backed_source(&source)?; - Ok(MacroCall { - id: call.id.into(), - reference_id: call.reference.into(), - file_id, - source, - capability: macro_call_availability(&call.status), - arguments, - directive_range: range, - range, - callee: map_macro_resolution(mapped, &call.callee)?, - expansion: call.expansion.map(Into::into), - }) -} - -fn map_macro_argument( - mapped: &MappedSourcePreprocModel, - argument: &SourceMacroArgumentFact, -) -> PreprocResult { - let (source, range) = argument - .argument_range - .map(|range| map_mapped_source_range(mapped, range)) - .transpose()? - .map_or((None, None), |(source, range)| (Some(source), Some(range))); - Ok(MacroArgument { - argument_index: argument.argument_index, - source, - range, - tokens: argument.tokens.iter().map(|token| token.raw.clone()).collect(), - }) -} - -fn map_macro_expansion( - mapped: &MappedSourcePreprocModel, - expansion: &SourceMacroExpansionFact, -) -> PreprocResult { - let Some(call) = mapped.model.macro_calls().get(expansion.call) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroCall { - call: expansion.call, - }), - }); - }; - let Some(definition) = mapped.model.macro_definitions().get(expansion.definition) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { - call: expansion.call, - }, - ), - }); - }; - Ok(MacroExpansion { - id: expansion.id.into(), - call: map_macro_call(mapped, call)?, - definition_id: expansion.definition.into(), - definition: map_macro_definition(mapped, definition)?, - emitted_token_range: expansion.emitted_token_range, - display_source: map_expansion_display_source(mapped, expansion.id)?, - display_range: mapped - .source_map - .emitted_display_range(expansion.id, expansion.emitted_token_range) - .map_err(PreprocError::SourceMap)?, - child_calls: expansion.child_calls.iter().copied().map(Into::into).collect(), - capability: macro_expansion_availability(&expansion.status), - }) -} - -fn map_expansion_display_source( - mapped: &MappedSourcePreprocModel, - expansion: SourceMacroExpansionId, -) -> PreprocResult { - match mapped.source_map.expansion_display_source(expansion).map_err(PreprocError::SourceMap)? { - PreprocSourceMapping::VirtualFile { file_id, path, origin } => { - Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) - } - PreprocSourceMapping::VirtualDisplay { path, origin } => { - Ok(MappedPreprocSource::VirtualDisplay { path, origin }) - } - PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), - PreprocSourceMapping::Unmapped(reason) => { - Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) - } - } -} - -fn map_expansion_source_buffer( - mapped: &MappedSourcePreprocModel, - expansion: SourceMacroExpansionId, -) -> PreprocResult { - match mapped.source_map.expansion_source_buffer(expansion).map_err(PreprocError::SourceMap)? { - PreprocSourceMapping::VirtualFile { file_id, path, origin } => { - Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) - } - PreprocSourceMapping::VirtualDisplay { path, origin } => { - Ok(MappedPreprocSource::VirtualDisplay { path, origin }) - } - PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), - PreprocSourceMapping::Unmapped(reason) => { - Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) - } - } -} - -fn display_only_virtual_expansion_unavailable(source: &MappedPreprocSource) -> PreprocUnavailable { - match source { - MappedPreprocSource::VirtualDisplay { path, origin } => { - PreprocUnavailable::DisplayOnlyVirtualExpansion { - path: path.clone(), - origin: origin.clone(), - } - } - MappedPreprocSource::RealFile { .. } | MappedPreprocSource::VirtualFile { .. } => { - PreprocUnavailable::Source(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) - } - } -} - -fn source_macro_calls_at( - mapped: &MappedSourcePreprocModel, - file_id: FileId, - offset: TextSize, -) -> Vec<&SourceMacroCallFact> { - mapped - .model - .macro_calls() - .iter() - .filter(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) && range.contains(offset) - }) - .collect() -} - -fn source_macro_calls_intersecting_range( - mapped: &MappedSourcePreprocModel, - file_id: FileId, - source_range: TextRange, -) -> Vec<&SourceMacroCallFact> { - mapped - .model - .macro_calls() - .iter() - .filter(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) - && range - .intersect(source_range) - .is_some_and(|intersection| !intersection.is_empty()) - }) - .collect() -} - -fn immediate_macro_expansion_for_call( - mapped: &MappedSourcePreprocModel, - call_fact: &SourceMacroCallFact, -) -> PreprocResult { - let call = map_macro_call(mapped, call_fact)?; - Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(expansion) => { - let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { - return Ok(MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { - call, - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, - ), - }))); - }; - MacroExpansionQuery::Available(Box::new(map_macro_expansion(mapped, expansion)?)) - } - SourceMacroExpansionQueryFact::Unavailable(reason) => { - MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { - call, - reason: PreprocUnavailable::Source(reason), - })) - } - }) -} - -fn recursive_macro_expansion_for_call( - mapped: &MappedSourcePreprocModel, - call_fact: &SourceMacroCallFact, -) -> PreprocResult { - let root_call = map_macro_call(mapped, call_fact)?; - let recursive = mapped.model.recursive_macro_expansion(call_fact.id); - let expansions = recursive - .expansions - .into_iter() - .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) - .map(|expansion| map_macro_expansion(mapped, expansion)) - .collect::>>()?; - let unavailable = recursive - .unavailable - .into_iter() - .map(|unavailable| { - let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, - ), - }); - }; - Ok(MacroExpansionUnavailable { - call: map_macro_call(mapped, call)?, - reason: PreprocUnavailable::Source(unavailable.reason), - }) - }) - .collect::>>()?; - - Ok(RecursiveMacroExpansion { root_call, expansions, unavailable }) -} - -fn recursive_macro_expansion_provenance_for_call( - mapped: &MappedSourcePreprocModel, - call_fact: &SourceMacroCallFact, -) -> PreprocResult { - let root_call = map_macro_call(mapped, call_fact)?; - let recursive = mapped.model.recursive_macro_expansion(call_fact.id); - let expansions = recursive - .expansions - .into_iter() - .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) - .map(|expansion| macro_expansion_provenance_for_expansion(mapped, expansion)) - .collect::>>()?; - let unavailable = recursive - .unavailable - .into_iter() - .map(|unavailable| { - let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, - ), - }); - }; - Ok(MacroExpansionUnavailable { - call: map_macro_call(mapped, call)?, - reason: PreprocUnavailable::Source(unavailable.reason), - }) - }) - .collect::>>()?; - - Ok(RecursiveMacroExpansionProvenance { root_call, expansions, unavailable }) -} - -fn diagnostic_provenance_for_call( - mapped: &MappedSourcePreprocModel, - call_fact: &SourceMacroCallFact, -) -> PreprocResult { - match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(expansion_id) => { - let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { - return Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, - ))); - }; - diagnostic_target_for_source_expansion(mapped, expansion) - } - SourceMacroExpansionQueryFact::Unavailable(reason) => { - Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason))) - } - } -} - -enum MacroExpansionProvenanceForCall { - Available(Box), - Unavailable(PreprocUnavailable), -} - -fn macro_expansion_provenance_for_call( - mapped: &MappedSourcePreprocModel, - call_fact: &SourceMacroCallFact, -) -> PreprocResult { - Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { - SourceMacroExpansionQueryFact::Available(expansion_id) => { - let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { - return Ok(MacroExpansionProvenanceForCall::Unavailable( - PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroExpansion { - call: call_fact.id, - }), - )); - }; - MacroExpansionProvenanceForCall::Available(Box::new( - macro_expansion_provenance_for_expansion(mapped, expansion)?, - )) - } - SourceMacroExpansionQueryFact::Unavailable(reason) => { - MacroExpansionProvenanceForCall::Unavailable(PreprocUnavailable::Source(reason)) - } - }) -} - -fn macro_expansion_provenance_for_expansion( - mapped: &MappedSourcePreprocModel, - expansion: &SourceMacroExpansionFact, -) -> PreprocResult { - let expansion_id = expansion.id; - let expansion = map_macro_expansion(mapped, expansion)?; - let mut tokens = Vec::new(); - for token_id in emitted_token_ids(expansion.emitted_token_range) { - let Some(token) = mapped.model.emitted_tokens().get(token_id) else { - return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { - token: token_id, - })); - }; - let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { - return Err(unavailable_error( - SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, - )); - }; - tokens.push(EmittedTokenProvenance { - token: token_id, - text: token.text.clone(), - display_range: mapped - .source_map - .emitted_token_display_range(expansion_id, token_id) - .map_err(PreprocError::SourceMap)?, - provenance: map_token_provenance(mapped, provenance)?, - }); - } - - Ok(MacroExpansionProvenance { expansion, tokens }) -} - -fn emitted_token_ids(range: SourceEmittedTokenRange) -> impl Iterator { - let start = range.start.raw(); - let end = start.saturating_add(range.len); - (start..end).map(SourceEmittedTokenId::new) -} - -fn map_token_provenance( - mapped: &MappedSourcePreprocModel, - provenance: &SourceTokenProvenanceFact, -) -> PreprocResult { - Ok(match provenance { - SourceTokenProvenanceFact::Source { token_range } => { - let (source, range) = map_mapped_source_range(mapped, *token_range)?; - TokenProvenance::SourceToken { source, range } - } - SourceTokenProvenanceFact::MacroBody { definition, body_token_range, call, .. } => { - let call = mapped_macro_call(mapped, *call)?; - let (source, range) = map_mapped_source_range(mapped, *body_token_range)?; - TokenProvenance::MacroBody { call, definition_id: (*definition).into(), source, range } - } - SourceTokenProvenanceFact::MacroArgument { - call, - argument_index, - argument_token_range, - .. - } => { - let call = mapped_macro_call(mapped, *call)?; - let Ok((source, range)) = map_mapped_source_range(mapped, *argument_token_range) else { - return Ok(TokenProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, - ))); - }; - TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } - } - SourceTokenProvenanceFact::TokenPaste { .. } - | SourceTokenProvenanceFact::Stringification { .. } => TokenProvenance::Unavailable( - PreprocUnavailable::Source(SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance), - ), - SourceTokenProvenanceFact::Predefine { source } => { - TokenProvenance::Predefine { source: map_mapped_source_id(mapped, *source)? } - } - SourceTokenProvenanceFact::Builtin { name } => { - TokenProvenance::Builtin { name: name.clone() } - } - SourceTokenProvenanceFact::Unavailable(reason) => { - TokenProvenance::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - }) -} - -fn mapped_macro_call( - mapped: &MappedSourcePreprocModel, - call: SourceMacroCallId, -) -> PreprocResult { - let Some(call) = mapped.model.macro_calls().get(call) else { - return Err(unavailable_error(SourcePreprocUnavailable::MissingMacroCall { call })); - }; - map_macro_call(mapped, call) -} - -fn diagnostic_target_for_source_expansion( - mapped: &MappedSourcePreprocModel, - expansion: &SourceMacroExpansionFact, -) -> PreprocResult { - let mut saw_unavailable = None; - for token_id in emitted_token_ids(expansion.emitted_token_range) { - let Some(token) = mapped.model.emitted_tokens().get(token_id) else { - return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { - token: token_id, - })); - }; - let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { - return Err(unavailable_error( - SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, - )); - }; - match map_token_provenance(mapped, provenance)? { - TokenProvenance::SourceToken { source, range } => { - return Ok(DiagnosticProvenance::SourceToken { source, range }); - } - TokenProvenance::MacroBody { call, definition_id, source, range } => { - return Ok(DiagnosticProvenance::MacroBody { call, definition_id, source, range }); - } - TokenProvenance::MacroArgument { call, argument_index, source, range } => { - return Ok(DiagnosticProvenance::MacroArgument { - call, - argument_index, - source, - range, - }); - } - TokenProvenance::Unavailable(reason) => { - saw_unavailable = Some(reason); - } - TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } => {} - } - } - - if let Some(reason) = saw_unavailable { - return Ok(DiagnosticProvenance::Unavailable(reason)); - } - - let source_buffer_source = map_expansion_source_buffer(mapped, expansion.id)?; - let MappedPreprocSource::VirtualFile { .. } = &source_buffer_source else { - return Ok(DiagnosticProvenance::Unavailable(display_only_virtual_expansion_unavailable( - &source_buffer_source, - ))); - }; - let source_buffer_range = mapped - .source_map - .emitted_source_buffer_range(expansion.id, expansion.emitted_token_range) - .map_err(PreprocError::SourceMap)?; - Ok(DiagnosticProvenance::VirtualExpansion { - source: source_buffer_source, - range: source_buffer_range, - }) -} - -fn map_macro_resolution( - mapped: &MappedSourcePreprocModel, - resolution: &SourceMacroResolutionFact, -) -> PreprocResult { - Ok(match resolution { - SourceMacroResolutionFact::Resolved { definition, reason, include_chain } => { - MacroResolution::Resolved { - definition_id: (*definition).into(), - reason: map_macro_resolution_reason(*reason), - include_chain: map_include_chain(mapped, include_chain)?, - } - } - SourceMacroResolutionFact::Undefined => MacroResolution::Undefined, - SourceMacroResolutionFact::Unavailable(reason) => { - MacroResolution::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - }) -} - -fn map_macro_resolution_reason(reason: SourceMacroResolutionReasonFact) -> MacroResolutionReason { - match reason { - SourceMacroResolutionReasonFact::VisibleDefinition => { - MacroResolutionReason::VisibleDefinition - } - SourceMacroResolutionReasonFact::IncludeGuardIfNDef => { - MacroResolutionReason::IncludeGuardIfNDef - } - } -} - -fn map_reference_ranges( - mapped: &MappedSourcePreprocModel, - reference: &SourceMacroReferenceFact, -) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { - let (directive_source, directive_range) = - map_mapped_source_range(mapped, reference.directive_range)?; - let (name_source, name_range) = map_mapped_source_range(mapped, reference.name_range)?; - if directive_source != name_source { - let directive_file_id = require_file_backed_source(&directive_source)?; - let name_file_id = require_file_backed_source(&name_source)?; - return Err(PreprocError::MismatchedReferenceRangeFiles { - event_id: reference.event_id.raw(), - directive_file_id, - name_file_id, - }); - } - Ok((directive_source, directive_range, name_range)) -} - -fn map_include_status( - mapped: &MappedSourcePreprocModel, - status: &SourceIncludeStatus, -) -> PreprocResult { - Ok(match status { - SourceIncludeStatus::Resolved { source } => { - IncludeDirectiveStatus::Resolved { source: map_mapped_source_id(mapped, *source)? } - } - SourceIncludeStatus::Unresolved => IncludeDirectiveStatus::Unresolved, - SourceIncludeStatus::Unavailable(reason) => { - IncludeDirectiveStatus::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - }) -} - -fn capability_status(status: &CapabilityStatus) -> PreprocAvailability { - match status { - CapabilityStatus::Complete => PreprocAvailability::Complete, - CapabilityStatus::Partial => PreprocAvailability::Partial, - CapabilityStatus::Unavailable(reason) => { - PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - } -} - -fn macro_call_availability(status: &SourceMacroCallStatusFact) -> PreprocAvailability { - match status { - SourceMacroCallStatusFact::ExpansionAvailable => PreprocAvailability::Complete, - SourceMacroCallStatusFact::ExpansionUnavailable(reason) => { - PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - } -} - -fn macro_expansion_availability(status: &SourceMacroExpansionStatusFact) -> PreprocAvailability { - match status { - SourceMacroExpansionStatusFact::Complete => PreprocAvailability::Complete, - SourceMacroExpansionStatusFact::Unavailable(reason) => { - PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) - } - } -} - -fn unavailable_error(reason: SourcePreprocUnavailable) -> PreprocError { - PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) } -} - -fn define_index_for_definition( - mapped: &MappedSourcePreprocModel, - definition: &SourceMacroDefinitionFact, -) -> PreprocResult { - mapped - .model - .defines() - .iter() - .position(|define| define.event_id == definition.event_id) - .ok_or_else(|| { - PreprocError::SourceQuery(SourcePreprocQueryError::Model( - SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, - )) - }) -} - -fn map_definition_ranges( - mapped: &MappedSourcePreprocModel, - event_id: u32, - directive_source_range: SourceRange, - name_source_range: SourceRange, -) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { - let (directive_source, directive_range) = - map_mapped_source_range(mapped, directive_source_range)?; - let (name_source, name_range) = map_mapped_source_range(mapped, name_source_range)?; - if directive_source != name_source { - let directive_file_id = require_file_backed_source(&directive_source)?; - let name_file_id = require_file_backed_source(&name_source)?; - return Err(PreprocError::MismatchedDefinitionRangeFiles { - event_id, - directive_file_id, - name_file_id, - }); - } - Ok((directive_source, directive_range, name_range)) -} - -fn map_include_chain( - mapped: &MappedSourcePreprocModel, - chain: &[SourceIncludeChainEntry], -) -> PreprocResult> { - chain - .iter() - .map(|entry| { - let (include_file_id, include_range) = map_source_range(mapped, entry.include_range)?; - let included_file_id = map_source_id(mapped, entry.included_source)?; - Ok(IncludeChainEntry { - include_event_id: entry.include_event_id.raw(), - include_file_id, - include_range, - included_file_id, - }) - }) - .collect() -} - -fn push_unique_macro_reference_context(refs: &mut Vec, reference: MacroReference) { - if refs.iter().any(|existing| existing == &reference) { - return; - } - refs.push(reference); -} - -fn push_unique_macro_usage_resolution( - resolutions: &mut Vec, - resolution: MacroUsageResolution, -) { - if resolutions.iter().any(|existing| existing == &resolution) { - return; - } - resolutions.push(resolution); -} - -fn push_unique_macro_expansion_query( - queries: &mut Vec, - query: MacroExpansionQuery, -) { - if queries.iter().any(|existing| existing == &query) { - return; - } - queries.push(query); -} - -fn push_unique_recursive_macro_expansion( - expansions: &mut Vec, - expansion: RecursiveMacroExpansion, -) { - if expansions.iter().any(|existing| existing == &expansion) { - return; - } - expansions.push(expansion); -} - -fn push_unique_recursive_macro_expansion_provenance( - expansions: &mut Vec, - expansion: RecursiveMacroExpansionProvenance, -) { - if expansions.iter().any(|existing| existing == &expansion) { - return; - } - expansions.push(expansion); -} - -fn push_unique_macro_expansion_provenance( - provenances: &mut Vec, - provenance: MacroExpansionProvenance, -) { - if provenances.iter().any(|existing| existing == &provenance) { - return; - } - provenances.push(provenance); -} - -fn push_unique_diagnostic_provenance( - provenances: &mut Vec, - provenance: DiagnosticProvenance, -) { - if provenances.iter().any(|existing| existing == &provenance) { - return; - } - provenances.push(provenance); -} - -fn push_unique_include_directive( - directives: &mut Vec, - directive: IncludeDirective, -) { - if directives.iter().any(|existing| { - existing.file_id == directive.file_id - && existing.range == directive.range - && existing.target == directive.target - && existing.status == directive.status - }) { - return; - } - directives.push(directive); -} - -fn push_unique_inactive_branch( - branches: &mut UniqVec, - branch: InactiveBranch, -) { - push_unique_value(branches, InactiveBranchKey::from_branch(&branch), branch); -} - -fn macro_reference_context_capability(references: &[MacroReference]) -> PreprocAvailability { - if references - .iter() - .all(|reference| matches!(reference.capability, PreprocAvailability::Complete)) - { - return PreprocAvailability::Complete; - } - if references - .iter() - .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) - { - return PreprocAvailability::Partial; - } - references - .iter() - .find_map(|reference| match &reference.capability { - PreprocAvailability::Unavailable(reason) => { - Some(PreprocAvailability::Unavailable(reason.clone())) - } - PreprocAvailability::Complete | PreprocAvailability::Partial => None, - }) - .unwrap_or(PreprocAvailability::Complete) -} - -fn push_unique_value(values: &mut UniqVec, key: K, value: T) { - values.push([key], value); -} - -fn push_unique_macro_definition( - definitions: &mut UniqVec, - definition: MacroDefinition, -) { - push_unique_value(definitions, MacroDefinitionKey::from_definition(&definition), definition); -} - -fn same_macro_definition(left: &MacroDefinition, right: &MacroDefinition) -> bool { - MacroDefinitionKey::from_definition(left) == MacroDefinitionKey::from_definition(right) -} - -fn push_unique_macro_param_definition( - definitions: &mut UniqVec, - definition: MacroParamDefinition, -) { - push_unique_value( - definitions, - MacroParamDefinitionKey::from_definition(&definition), - definition, - ); -} - -fn push_unique_macro_param_reference( - refs: &mut UniqVec, - reference: MacroParamReference, -) { - push_unique_value(refs, MacroParamReferenceKey::from_reference(&reference), reference); -} - -fn macro_param_reference_context_capability( - references: &[MacroParamReference], -) -> PreprocAvailability { - if references - .iter() - .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) - { - return PreprocAvailability::Partial; - } - references - .iter() - .find_map(|reference| match &reference.capability { - PreprocAvailability::Unavailable(reason) => { - Some(PreprocAvailability::Unavailable(reason.clone())) - } - PreprocAvailability::Complete | PreprocAvailability::Partial => None, - }) - .unwrap_or(PreprocAvailability::Complete) -} +mod conditionals; +mod definitions; +mod expansion; +mod helpers; +mod includes; +mod predefines; +mod reference_index; +mod reference_queries; +mod types; + +use self::helpers::*; +pub use self::{ + conditionals::*, definitions::*, expansion::*, includes::*, reference_index::*, + reference_queries::*, types::*, +}; #[cfg(test)] -mod tests { - use std::fmt; - - use rustc_hash::FxHashSet; - use triomphe::Arc; - use utils::{ - get::Get, - line_index::{TextRange, TextSize}, - paths::{AbsPathBuf, Utf8PathBuf}, - }; - use vfs::{FileId, FileSet, VfsPath, anchored_path::AnchoredPath}; - - use super::*; - use crate::{ - base_db::{ - diagnostics_config::DiagnosticsConfig, - project::{ - CompilationProfile, CompilationProfileId, Predefine, PredefineSource, - PreprocessConfig, ProjectConfig, - }, - salsa::{self, Durability}, - source_db::{ - FileLoader, PreprocExpansionSourceBuffer, PreprocVirtualOrigin, SourceDb, - SourceDbStorage, SourceFileKind, SourceRootDb, SourceRootDbStorage, - }, - source_root::{SourceRoot, SourceRootId}, - }, - container::InFile, - db::{HirDb, HirDbStorage, InternDbStorage}, - hir_def::module::ModuleId, - source_map::IsSrc, - }; - - const TOP: FileId = FileId(0); - const HEADER: FileId = FileId(1); - const LEAF: FileId = FileId(2); - const MANIFEST: FileId = FileId(3); - const ROOT: SourceRootId = SourceRootId(0); - const PROFILE: CompilationProfileId = CompilationProfileId(0); - - #[salsa::database(SourceDbStorage, SourceRootDbStorage, InternDbStorage, HirDbStorage)] - #[derive(Default)] - struct TestDb { - storage: salsa::Storage, - } - - impl salsa::Database for TestDb {} - - impl fmt::Debug for TestDb { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TestDb").finish() - } - } - - impl FileLoader for TestDb { - fn resolve_path(&self, path: AnchoredPath<'_>) -> Option { - let source_root_id = SourceRootDb::source_root_id(self, path.anchor_id); - SourceRootDb::source_root(self, source_root_id).resolve_path(path) - } - } - - fn db_with_files(root_text: &str, header_text: &str) -> TestDb { - db_with_entries(&[(TOP, "rtl/top.v", root_text), (HEADER, "include/defs.vh", header_text)]) - } - - fn db_with_nested_files(root_text: &str, header_text: &str, leaf_text: &str) -> TestDb { - db_with_entries(&[ - (TOP, "rtl/top.v", root_text), - (HEADER, "include/defs.vh", header_text), - (LEAF, "include/leaf.vh", leaf_text), - ]) - } - - fn db_with_entries(entries: &[(FileId, &str, &str)]) -> TestDb { - db_with_entries_and_predefines(entries, Vec::new()) - } - - fn db_with_entries_and_predefines( - entries: &[(FileId, &str, &str)], - predefines: Vec, - ) -> TestDb { - db_with_entries_and_predefine_entries( - entries, - predefines.into_iter().map(Predefine::new).collect(), - ) - } - - fn db_with_entries_and_predefine_entries( - entries: &[(FileId, &str, &str)], - predefines: Vec, - ) -> TestDb { - let include_dir = abs_path("include"); - - let mut file_set = FileSet::default(); - for (file_id, path, _) in entries { - file_set.insert(*file_id, VfsPath::from(abs_path(path))); - } - let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); - - let preprocess = PreprocessConfig { predefines, include_dirs: vec![include_dir.clone()] }; - let project_config = ProjectConfig::new( - vec![Some(PROFILE)], - vec![CompilationProfile { - source_roots: vec![ROOT], - top_modules: Vec::new(), - preprocess: preprocess.clone(), - }], - ); - - let mut files = FxHashSet::default(); - for (file_id, _, _) in entries { - files.insert(*file_id); - } - - let mut db = TestDb::default(); - db.set_files_with_durability(Box::new(files), Durability::HIGH); - db.set_project_config_with_durability(Arc::new(project_config), Durability::HIGH); - db.set_diagnostics_config_with_durability( - Arc::new(DiagnosticsConfig::default()), - Durability::HIGH, - ); - db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); - - for (file_id, path, text) in entries { - let path = abs_path(path); - let vfs_path = VfsPath::from(path.clone()); - db.set_source_root_id_with_durability(*file_id, ROOT, Durability::LOW); - db.set_file_path_with_durability(*file_id, Some(path), Durability::LOW); - db.set_file_kind_with_durability( - *file_id, - SourceFileKind::from_path(&vfs_path), - Durability::LOW, - ); - db.set_file_text_with_durability(*file_id, Arc::from(*text), Durability::LOW); - db.set_file_preprocess_config_with_durability( - *file_id, - Arc::new(preprocess.clone()), - 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 offset(text: &str, needle: &str) -> TextSize { - TextSize::from(u32::try_from(text.find(needle).unwrap()).unwrap()) - } - - fn offset_after(text: &str, needle: &str) -> TextSize { - TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) - } - - fn offset_after_n(text: &str, needle: &str, occurrence: usize) -> TextSize { - let mut cursor = 0; - for index in 0..=occurrence { - let relative = text[cursor..].find(needle).unwrap_or_else(|| { - panic!("missing occurrence {occurrence} of {needle:?} in fixture") - }); - let absolute = cursor + relative; - if index == occurrence { - return TextSize::from(u32::try_from(absolute + needle.len()).unwrap()); - } - cursor = absolute + needle.len(); - } - unreachable!() - } - - fn text_at_range(text: &str, range: TextRange) -> &str { - &text[usize::from(range.start())..usize::from(range.end())] - } - - fn assert_expansion_is_display_only_source_buffer( - mapped: &MappedSourcePreprocModel, - expansion: &MacroExpansion, - ) { - let expansion_id = SourceMacroExpansionId::new(expansion.id.raw()); - let entry = mapped - .source_map - .expansion(expansion_id) - .expect("expansion should have a display entry"); - assert!(matches!(&entry.source_buffer, PreprocExpansionSourceBuffer::DisplayOnly { .. })); - assert!(matches!( - mapped - .source_map - .emitted_source_buffer_range(expansion_id, expansion.emitted_token_range), - Err(PreprocSourceMapError::DisplayOnlyVirtualSource { .. }) - )); - } - - #[test] - fn preproc_include_usage_resolves_to_header_define() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `HEADER_WIDTH; -endmodule -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let db = db_with_files(root_text, header_text); - - let resolution = macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")) - .unwrap() - .unwrap(); - assert_eq!(resolution.usage.file_id, TOP); - assert_eq!(resolution.definition.file_id, HEADER); - assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); - assert_eq!(text_at_range(header_text, resolution.definition.name_range), "HEADER_WIDTH"); - - let include = - include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); - assert_eq!(text_at_range(root_text, include.range), "\"defs.vh\""); - assert!(include_directive_at(&db, TOP, offset(root_text, "`include")).unwrap().is_none()); - assert!(include_directive_at(&db, TOP, include.range.end()).unwrap().is_none()); - let IncludeTarget::Literal { resolved_file, .. } = include.target else { - panic!("literal include expected"); - }; - assert_eq!(resolved_file, Some(HEADER)); - } - - #[test] - fn preproc_macro_expansion_queries_map_call_ranges() { - let root_text = r#"`define OBJ 8 -`define LEAF 3 -`define WRAP `LEAF -module top; -localparam int A = `OBJ; -localparam int B = `WRAP; -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let immediate = - immediate_macro_expansion_at(&db, TOP, offset(root_text, "`OBJ")).unwrap().unwrap(); - let MacroExpansionQuery::Available(immediate) = immediate else { - panic!("object-like macro expansion should be available"); - }; - assert_eq!(immediate.call.file_id, TOP); - assert_eq!(text_at_range(root_text, immediate.call.range), "`OBJ"); - assert_eq!(immediate.emitted_token_range.len, 1); - assert!(matches!(immediate.capability, PreprocAvailability::Complete)); - - let recursive = - recursive_macro_expansion_at(&db, TOP, offset(root_text, "`WRAP")).unwrap().unwrap(); - assert_eq!(recursive.root_call.file_id, TOP); - assert_eq!(text_at_range(root_text, recursive.root_call.range), "`WRAP"); - assert!(recursive.unavailable.is_empty()); - assert_eq!(recursive.expansions.len(), 2); - let wrap_expansion = recursive - .expansions - .iter() - .find(|expansion| expansion.definition.name.as_str() == "WRAP") - .expect("outer expansion should be mapped"); - let leaf_expansion = recursive - .expansions - .iter() - .find(|expansion| expansion.definition.name.as_str() == "LEAF") - .expect("nested expansion should be mapped"); - assert_eq!(text_at_range(root_text, wrap_expansion.call.range), "`WRAP"); - assert_eq!(text_at_range(root_text, leaf_expansion.call.range), "`LEAF"); - assert_eq!(wrap_expansion.child_calls, vec![leaf_expansion.call.id]); - } - - #[test] - fn preproc_macro_expansion_exposes_display_virtual_source_and_token_provenance() { - let root_text = r#"`define MAKE_DECL(name) logic name; -module top; -`MAKE_DECL(generated) -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let provenance = macro_expansion_provenance_at(&db, TOP, offset(root_text, "`MAKE_DECL")) - .unwrap() - .unwrap(); - let MappedPreprocSource::VirtualDisplay { path, origin } = - &provenance.expansion.display_source - else { - panic!("macro expansion should expose a display-only virtual expansion source"); - }; - assert_eq!( - path, - &VfsPath::new_virtual_path("/__vide/preproc/profile-0/expansion/0.sv".to_owned()) - ); - assert_eq!( - origin, - &PreprocVirtualOrigin::Expansion { expansion: SourceMacroExpansionId::new(0) } - ); - - let mapped = db.source_preproc_model(TOP); - let mapped = mapped.as_ref().as_ref().unwrap(); - let expansion_display = - mapped.source_map.expansion_display_text(SourceMacroExpansionId::new(0)).unwrap(); - assert_eq!(expansion_display, "logic generated ;"); - assert_eq!(provenance.expansion.display_range, TextRange::new(0.into(), 17.into())); - - let logic = provenance - .tokens - .iter() - .find(|token| token.text.as_str() == "logic") - .expect("macro body token should be present"); - let TokenProvenance::MacroBody { source, range, .. } = &logic.provenance else { - panic!("logic should come from the macro body: {logic:?}"); - }; - assert_eq!(source.file_id(), Some(TOP)); - assert_eq!(text_at_range(root_text, *range), "logic"); - assert_eq!(logic.display_range, TextRange::new(0.into(), 5.into())); - - let generated = provenance - .tokens - .iter() - .find(|token| token.text.as_str() == "generated") - .expect("macro argument token should be present"); - let TokenProvenance::MacroArgument { source, range, argument_index, .. } = - &generated.provenance - else { - panic!("generated should come from the macro argument: {generated:?}"); - }; - assert_eq!(*argument_index, 0); - assert_eq!(source.file_id(), Some(TOP)); - assert_eq!(text_at_range(root_text, *range), "generated"); - assert_eq!(generated.display_range, TextRange::new(6.into(), 15.into())); - } - - #[test] - fn preproc_maps_nested_actual_argument_macro_usage_without_dropping_expansion() { - let root_text = r#"`define PAYL payload_i -`define NEXT(x) ((x) + 12'd1) -module top(input logic [3:0] payload_i, output logic [3:0] y); -assign y = `NEXT(`PAYL); -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let payl = macro_reference_definitions_at(&db, TOP, offset_after(root_text, "`NEXT(")) - .unwrap() - .expect("nested actual-argument macro reference should be mapped"); - assert_eq!(text_at_range(root_text, payl.range), "`PAYL"); - assert!( - payl.definitions.iter().any(|definition| { - definition.file_id == TOP && definition.name.as_str() == "PAYL" - }) - ); - - let provenance = - macro_expansion_provenance_at(&db, TOP, offset(root_text, "`NEXT")).unwrap().unwrap(); - let argument = provenance - .expansion - .call - .arguments - .iter() - .find(|argument| argument.argument_index == 0) - .expect("NEXT call should expose its written actual argument"); - assert_eq!(argument.source.as_ref().and_then(MappedPreprocSource::file_id), Some(TOP)); - assert_eq!(text_at_range(root_text, argument.range.unwrap()), "`PAYL"); - assert_eq!(argument.tokens, vec![SmolStr::new("`PAYL")]); - - let payload = provenance - .tokens - .iter() - .find(|token| token.text.as_str() == "payload_i") - .expect("expanded payload token should stay in NEXT expansion provenance"); - assert!(matches!( - payload.provenance, - TokenProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - )) - )); - - let payl_offset = offset(root_text, "`PAYL"); - let queries = macro_expansion_queries_at(&db, TOP, payl_offset).unwrap(); - assert!(queries.iter().any(|query| matches!( - query, - MacroExpansionQuery::Available(expansion) - if expansion.definition.name.as_str() == "NEXT" - ))); - assert!(queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); - assert!(matches!( - immediate_macro_expansion_at(&db, TOP, payl_offset), - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } - }) - )); - assert!(matches!( - macro_expansion_provenance_at(&db, TOP, payl_offset), - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } - }) - )); - } - - #[test] - fn preproc_numeric_literal_expansion_display_is_not_source_buffer() { - let root_text = r#"`define ONE 12'd1 -module top; -localparam int W = `ONE; -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let provenance = - macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ONE")).unwrap().unwrap(); - let mapped = db.source_preproc_model(TOP); - let mapped = mapped.as_ref().as_ref().unwrap(); - assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); - - let display_text = mapped - .source_map - .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) - .unwrap(); - assert!(display_text.contains("12")); - assert!(display_text.contains("'d")); - assert!(display_text.contains("1")); - } - - #[test] - fn preproc_escaped_identifier_expansion_display_is_not_source_buffer() { - let root_text = concat!( - "`define ESCAPED \\escaped.name \n", - "module top;\n", - "wire `ESCAPED;\n", - "endmodule\n", - ); - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let provenance = macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ESCAPED")) - .unwrap() - .unwrap(); - let mapped = db.source_preproc_model(TOP); - let mapped = mapped.as_ref().as_ref().unwrap(); - assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); - - let display_text = mapped - .source_map - .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) - .unwrap(); - assert!(display_text.contains("\\escaped.name")); - } - - #[test] - fn macro_generated_declaration_hir_range_resolves_to_expanded_token_provenance() { - let root_text = r#"`define MAKE_DECL(name) logic name; -module top; -`MAKE_DECL(generated) -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); - let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); - let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); - let (module, module_src_map) = db.module_with_source_map(module_id); - let (declaration_id, _) = - module.declarations.iter().next().expect("generated declaration should lower to HIR"); - let declaration_src = module_src_map - .get(declaration_id) - .expect("generated declaration should keep a source-map range"); - - let provenance = macro_expansion_provenance_for_range(&db, TOP, declaration_src.range()) - .unwrap() - .unwrap(); - - assert_eq!(provenance.expansion.emitted_token_range.len, 3); - assert!( - provenance - .tokens - .iter() - .any(|token| matches!(token.provenance, TokenProvenance::MacroBody { .. })) - ); - assert!( - provenance - .tokens - .iter() - .any(|token| matches!(token.provenance, TokenProvenance::MacroArgument { .. })) - ); - } - - #[test] - fn diagnostic_provenance_for_range_spanning_two_macro_calls_is_ambiguous() { - let root_text = r#"`define A 1 -`define B 2 -module top; -localparam int W = `A + `B; -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - let range = TextRange::new(offset(root_text, "`A"), offset_after(root_text, "`B")); - - let provenance = diagnostic_provenance_for_range(&db, TOP, range).unwrap().unwrap(); - - assert!(matches!( - provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::AmbiguousDiagnosticProvenance { - targets: 2 - }) - )); - let expansion_error = macro_expansion_provenances_for_range(&db, TOP, range).unwrap_err(); - assert!(matches!( - expansion_error, - PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } - } - )); - } - - #[test] - fn diagnostic_provenance_for_adjacent_macro_calls_only_hits_intersecting_call() { - let root_text = r#"`define ID(x) x -module top; -localparam int W = `ID(1)`ID(2); -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - let two_range = - TextRange::new(offset(root_text, "`ID(2)"), offset_after(root_text, "`ID(2)")); - - let provenance = diagnostic_provenance_for_range(&db, TOP, two_range).unwrap().unwrap(); - - let DiagnosticProvenance::MacroArgument { call, argument_index, source, range } = - provenance - else { - panic!("adjacent single-call range should resolve precisely: {provenance:?}"); - }; - assert_eq!(text_at_range(root_text, call.range), "`ID(2)"); - assert_eq!(argument_index, 0); - assert_eq!(source.file_id(), Some(TOP)); - assert_eq!(text_at_range(root_text, range), "2"); - } - - #[test] - fn diagnostic_provenance_for_nested_macro_call_range_is_precise() { - let root_text = r#"`define LEAF 3 -`define WRAP `LEAF -module top; -localparam int W = `WRAP; -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - let leaf_range = - TextRange::new(offset(root_text, "`LEAF"), offset_after(root_text, "`LEAF")); - - let provenance = diagnostic_provenance_for_range(&db, TOP, leaf_range).unwrap().unwrap(); - - let DiagnosticProvenance::MacroBody { call, source, range, .. } = provenance else { - panic!("nested macro call range should resolve precisely"); - }; - assert_eq!(text_at_range(root_text, call.range), "`LEAF"); - assert_eq!(source.file_id(), Some(TOP)); - assert_eq!(text_at_range(root_text, range), "3"); - } - - #[test] - fn diagnostic_provenance_returns_unavailable_for_unsupported_expansion_mapping() { - let root_text = r#"`define JOIN(a,b) a``b -`define STR(x) `"x`" -module top; -wire `JOIN(foo,bar); -string s = `STR(foo); -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - let call_range = - TextRange::new(offset(root_text, "`JOIN"), offset_after(root_text, "`JOIN(foo,bar)")); - - let provenance = diagnostic_provenance_for_range(&db, TOP, call_range).unwrap().unwrap(); - assert!(matches!( - provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(_)) - )); - - let stringification_range = - TextRange::new(offset(root_text, "`STR"), offset_after(root_text, "`STR(foo)")); - let provenance = - diagnostic_provenance_for_range(&db, TOP, stringification_range).unwrap().unwrap(); - assert!( - matches!( - &provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - | SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - | SourcePreprocUnavailable::ExpansionAuthorityUnavailable - )) - ), - "stringification should be unsupported or unavailable, got {provenance:?}" - ); - } - - #[test] - fn diagnostic_provenance_for_unbacked_predefine_expansion_is_structured_unavailable() { - let root_text = r#"module top; -`MAKE_CHILD -endmodule -"#; - let db = db_with_entries_and_predefines( - &[(TOP, "rtl/top.v", root_text)], - vec!["MAKE_CHILD=child u();".to_owned()], - ); - let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); - let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); - let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); - let (module, module_src_map) = db.module_with_source_map(module_id); - let (instantiation_id, _) = module - .instantiations - .iter() - .next() - .expect("predefine expansion should lower to a module instantiation"); - let instantiation_src = module_src_map - .get(instantiation_id) - .expect("generated instantiation should keep a source-map range"); - - let provenance = - diagnostic_provenance_for_range(&db, TOP, instantiation_src.range()).unwrap().unwrap(); - - assert!(matches!( - provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - | SourcePreprocUnavailable::ExpansionAuthorityUnavailable - )) - )); - } - - #[test] - fn preproc_nested_include_chain_maps_to_file_ids() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `LEAF_WIDTH; -endmodule -"#; - let header_text = "`include \"leaf.vh\"\n"; - let leaf_text = "`define LEAF_WIDTH 4\n"; - let db = db_with_nested_files(root_text, header_text, leaf_text); - - let resolution = - macro_usage_resolution_at(&db, TOP, offset(root_text, "LEAF_WIDTH")).unwrap().unwrap(); - - assert_eq!(resolution.definition.file_id, LEAF); - assert_eq!(resolution.definition_provenance.file_id, LEAF); - assert_eq!(resolution.include_chain.len(), 2); - assert_eq!(resolution.include_chain[0].include_file_id, TOP); - assert_eq!(resolution.include_chain[0].included_file_id, HEADER); - assert!( - text_at_range(root_text, resolution.include_chain[0].include_range).contains("defs.vh") - ); - assert_eq!(resolution.include_chain[1].include_file_id, HEADER); - assert_eq!(resolution.include_chain[1].included_file_id, LEAF); - assert!( - text_at_range(header_text, resolution.include_chain[1].include_range) - .contains("leaf.vh") - ); - } - - #[test] - fn preproc_unsaved_include_buffer_updates_query_result() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `HEADER_WIDTH; -endmodule -"#; - let mut db = db_with_files(root_text, "`define OTHER_WIDTH 8\n"); - - assert!( - macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")) - .unwrap() - .is_none() - ); - - db.set_file_text_with_durability( - HEADER, - Arc::from("`define HEADER_WIDTH 16\n"), - Durability::LOW, - ); - - let resolution = macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")) - .unwrap() - .unwrap(); - assert_eq!(resolution.definition.file_id, HEADER); - assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); - } - - #[test] - fn preproc_visible_macro_names_include_predefines_without_file_mapping() { - let root_text = r#"`define A005_LOCAL 1 -module top; -localparam int W = `A005_; -endmodule -"#; - let db = db_with_entries_and_predefines( - &[(TOP, "rtl/top.v", root_text)], - vec!["A005_MAGIC=42".to_owned()], - ); - - let names = visible_macro_names_at(&db, TOP, offset_after(root_text, "`A005_")).unwrap(); - - assert!(names.iter().any(|name| name == "A005_LOCAL"), "{names:?}"); - assert!(names.iter().any(|name| name == "A005_MAGIC"), "{names:?}"); - } - - #[test] - fn preproc_single_offset_contexts_exclude_unrelated_profile_models() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `HEADER_WIDTH; -endmodule -"#; - let header_text = "`define HEADER_WIDTH 8\n"; - let unrelated_header_text = "`define UNUSED_WIDTH 16\n"; - let db = db_with_nested_files(root_text, header_text, unrelated_header_text); - - let contexts = source_preproc_single_query_contexts(&db, HEADER); - - assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); - assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); - assert!( - !contexts.model_file_ids.contains(&LEAF), - "single-offset query contexts should not include unrelated profile model: {contexts:?}" - ); - } - - #[test] - fn preproc_partial_context_index_is_structured_unavailable() { - let contexts = SourcePreprocQueryContexts { - model_file_ids: Vec::new(), - status: SourcePreprocContextStatus::Partial { skipped_models: 2 }, - }; - - let error = finish_empty_single_query(&contexts, None).unwrap_err(); - - assert!(matches!( - error, - PreprocError::Unavailable { - reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models: 2 } - } - )); - } - - #[test] - fn preproc_manifest_predefine_definition_uses_manifest_provenance() { - let root_text = r#"`ifdef Z_FROM_MANIFEST -module top; -localparam int W = `Z_FROM_MANIFEST; -endmodule -`endif -"#; - let manifest_text = "defines = [\"A_OTHER=2\", \"Z_FROM_MANIFEST=1\"]\n"; - let manifest_range = TextRange::new( - offset(manifest_text, "\"Z_FROM_MANIFEST=1\""), - offset_after(manifest_text, "\"Z_FROM_MANIFEST=1\""), - ); - let other_range = TextRange::new( - offset(manifest_text, "\"A_OTHER=2\""), - offset_after(manifest_text, "\"A_OTHER=2\""), - ); - let predefine = Predefine::with_source( - "Z_FROM_MANIFEST=1", - PredefineSource { path: abs_path("vide.toml"), range: manifest_range }, - ); - let other_predefine = Predefine::with_source( - "A_OTHER=2", - PredefineSource { path: abs_path("vide.toml"), range: other_range }, - ); - let db = db_with_entries_and_predefine_entries( - &[(TOP, "rtl/top.v", root_text), (MANIFEST, "vide.toml", manifest_text)], - vec![other_predefine, predefine], - ); - - let resolution = - macro_reference_definitions_at(&db, TOP, offset(root_text, "Z_FROM_MANIFEST;")) - .unwrap() - .unwrap(); - assert!( - resolution.definitions.iter().any(|definition| { - definition.file_id == MANIFEST && definition.name_range == manifest_range - }), - "predefine reference should target the manifest source range: {resolution:?}" - ); - - let definition = - macro_definition_at(&db, MANIFEST, manifest_range.start()).unwrap().unwrap(); - assert_eq!(definition.file_id, MANIFEST); - assert_eq!(definition.name.as_str(), "Z_FROM_MANIFEST"); - assert_eq!(definition.name_range, manifest_range); - assert_eq!(text_at_range(manifest_text, definition.name_range), "\"Z_FROM_MANIFEST=1\""); - - let references = macro_references(&db, MANIFEST, &definition).unwrap(); - assert!( - references.references.iter().any(|reference| { - reference.file_id == TOP - && text_at_range(root_text, reference.range) == "Z_FROM_MANIFEST" - }), - "manifest predefine definition should find source references: {references:?}" - ); - } - - #[test] - fn preproc_visible_macro_names_follow_define_undef_boundaries() { - let root_text = r#"`define A005_LOCAL 1 -`undef A005_LOCAL -`define A005_NEXT 2 -module top; -localparam int W = `A005_; -endmodule -"#; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - let names_after_define = - visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_LOCAL 1\n")) - .unwrap(); - let names_after_undef = - visible_macro_names_at(&db, TOP, offset_after(root_text, "`undef A005_LOCAL\n")) - .unwrap(); - let names_after_next = - visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_NEXT 2\n")) - .unwrap(); - - assert!(names_after_define.iter().any(|name| name == "A005_LOCAL")); - assert!(!names_after_undef.iter().any(|name| name == "A005_LOCAL")); - assert!(names_after_next.iter().any(|name| name == "A005_NEXT")); - } - - #[test] - fn preproc_inactive_branch_uses_header_define() { - let root_text = r#"`include "defs.vh" -`ifndef HEADER_FLAG -wire disabled_by_header; -`endif -wire active; -"#; - let header_text = "`define HEADER_FLAG\n"; - let db = db_with_files(root_text, header_text); - - let branches = inactive_branches(&db, TOP).unwrap(); - assert_eq!(branches.len(), 1); - assert_eq!(branches[0].file_id, TOP); - assert!(text_at_range(root_text, branches[0].range).contains("disabled_by_header")); - } - - #[test] - fn preproc_included_define_references_include_root_conditionals() { - let root_text = r#"`include "defs.vh" -`ifdef HEADER_FLAG -localparam int ENABLED = `HEADER_FLAG; -`endif -"#; - let header_text = "`define HEADER_FLAG 1\n"; - let db = db_with_files(root_text, header_text); - let definition = macro_definition_at(&db, HEADER, offset_after(header_text, "`define ")) - .unwrap() - .unwrap(); - - assert_eq!(definition.source.file_id(), Some(HEADER)); - assert!(matches!(definition.capability, PreprocAvailability::Complete)); - - let refs = macro_references(&db, HEADER, &definition).unwrap().references; - - assert!(refs.iter().any(|reference| { - reference.file_id == TOP && text_at_range(root_text, reference.range) == "HEADER_FLAG" - })); - assert!(refs.iter().any(|reference| { - reference.file_id == TOP - && matches!( - reference.resolution, - MacroResolution::Resolved { - reason: MacroResolutionReason::VisibleDefinition, - .. - } - ) - && text_at_range(root_text, reference.range) == "HEADER_FLAG" - })); - - let definitions = - macro_reference_definitions_at(&db, TOP, offset_after(root_text, "ENABLED = `")) - .unwrap() - .unwrap(); - assert_eq!(text_at_range(root_text, definitions.range), "`HEADER_FLAG"); - assert!( - macro_reference_definitions_at(&db, TOP, definitions.range.end()).unwrap().is_none() - ); - assert!(macro_usage_resolution_at(&db, TOP, definitions.range.end()).unwrap().is_none()); - assert!(matches!(definitions.capability, PreprocAvailability::Complete)); - assert!(definitions.definitions.iter().any(|indexed| { - indexed.file_id == HEADER - && indexed.name_range == definition.name_range - && indexed.name == definition.name - })); - } - - #[test] - fn preproc_header_ifdef_reference_uses_including_root_context() { - let root_text = r#"`include "defs.vh" -`include "leaf.vh" -"#; - let header_text = "`define FEATURE_B 1\n"; - let leaf_text = r#"`ifdef FEATURE_B -wire enabled; -`endif -"#; - let db = db_with_nested_files(root_text, header_text, leaf_text); - - let definitions = macro_reference_definitions_at(&db, LEAF, offset(leaf_text, "FEATURE_B")) - .unwrap() - .unwrap(); - - assert_eq!(text_at_range(leaf_text, definitions.range), "FEATURE_B"); - assert!(definitions.definitions.iter().any(|definition| { - definition.file_id == HEADER - && text_at_range(header_text, definition.name_range) == "FEATURE_B" - })); - } - - #[test] - fn preproc_header_macro_body_references_use_expansion_context() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `DEMO_WIDTH; -localparam int N = `DEMO_NEXT(1); -localparam int R = `DEMO_RESET; -endmodule -"#; - let header_text = r#"`ifndef SHARED_DEFS_SVH -`define SHARED_DEFS_SVH -`include "leaf.vh" -`define DEMO_WIDTH `MATH_WIDTH -`define DEMO_RESET {`DEMO_WIDTH{1'b0}} -`define DEMO_NEXT(value) ((value) + `MATH_ONE) -`endif -"#; - let leaf_text = r#"`define MATH_WIDTH 12 -`define MATH_ONE 12'd1 -"#; - let db = db_with_nested_files(root_text, header_text, leaf_text); - - let math_width = - macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_WIDTH")) - .unwrap() - .unwrap(); - assert!(math_width.definitions.iter().any(|definition| { - definition.file_id == LEAF - && text_at_range(leaf_text, definition.name_range) == "MATH_WIDTH" - })); - - let math_one = macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_ONE")) - .unwrap() - .unwrap(); - assert!(math_one.definitions.iter().any(|definition| { - definition.file_id == LEAF - && text_at_range(leaf_text, definition.name_range) == "MATH_ONE" - })); - - let demo_width = macro_reference_definitions_at( - &db, - HEADER, - offset_after(header_text, "`define DEMO_RESET {`"), - ) - .unwrap() - .unwrap(); - assert!(demo_width.definitions.iter().any(|definition| { - definition.file_id == HEADER - && text_at_range(header_text, definition.name_range) == "DEMO_WIDTH" - })); - } - - #[test] - fn preproc_macro_param_references_resolve_to_formals() { - let root_text = r#"`include "defs.vh" -module top; -localparam int W = `SHIFT(4, 1); -endmodule -"#; - let header_text = "`define SHIFT(value, amount) ((value) << amount)\n"; - let db = db_with_files(root_text, header_text); - - let value_definition = - macro_param_definition_at(&db, HEADER, offset_after(header_text, "SHIFT(")) - .unwrap() - .unwrap(); - assert_eq!(value_definition.name.as_str(), "value"); - assert_eq!(text_at_range(header_text, value_definition.range), "value"); - assert!( - macro_param_definition_at(&db, HEADER, value_definition.range.end()).unwrap().is_none() - ); - - let value_reference = macro_param_reference_definitions_at( - &db, - HEADER, - offset_after(header_text, "SHIFT(value, amount) (("), - ) - .unwrap() - .unwrap(); - assert_eq!(text_at_range(header_text, value_reference.range), "value"); - assert!( - macro_param_reference_definitions_at(&db, HEADER, value_reference.range.end()) - .unwrap() - .is_none() - ); - assert!(value_reference.definitions.iter().any(|definition| { - definition.param_index == value_definition.param_index - && text_at_range(header_text, definition.range) == "value" - })); - - let refs = macro_param_references(&db, HEADER, &value_definition).unwrap().references; - assert!(refs.iter().any(|reference| { - reference.file_id == HEADER && text_at_range(header_text, reference.range) == "value" - })); - assert!( - !refs.iter().any(|reference| text_at_range(header_text, reference.range) == "amount") - ); - } - - #[test] - fn preproc_header_reference_reports_all_including_context_definitions() { - let root_text = r#"`define WIDTH 8 -`include "defs.vh" -`undef WIDTH -`define WIDTH 16 -`include "defs.vh" -"#; - let header_text = "localparam int W = `WIDTH;\n"; - let db = db_with_files(root_text, header_text); - - let definitions = macro_reference_definitions_at(&db, HEADER, offset(header_text, "WIDTH")) - .unwrap() - .unwrap(); - - assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); - assert_eq!(definitions.definitions.len(), 2); - assert!(definitions.definitions.iter().any(|definition| { - definition.file_id == TOP - && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) - })); - assert!(definitions.definitions.iter().any(|definition| { - definition.file_id == TOP - && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) - })); - } - - #[test] - fn preproc_header_macro_body_reference_reports_all_expansion_context_definitions() { - let root_text = r#"`define WIDTH 8 -`include "defs.vh" -localparam int A = `USE_WIDTH; -`undef WIDTH -`define WIDTH 16 -`include "defs.vh" -localparam int B = `USE_WIDTH; -"#; - let header_text = "`define USE_WIDTH `WIDTH\n"; - let db = db_with_files(root_text, header_text); - - let definitions = - macro_reference_definitions_at(&db, HEADER, offset_after(header_text, "USE_WIDTH `")) - .unwrap() - .unwrap(); - - assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); - assert_eq!(definitions.definitions.len(), 2); - assert!(definitions.definitions.iter().any(|definition| { - definition.file_id == TOP - && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) - })); - assert!(definitions.definitions.iter().any(|definition| { - definition.file_id == TOP - && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) - })); - } - - #[test] - fn preproc_macro_definition_at_only_hits_name_range() { - let root_text = "`define HEADER_FLAG 1\n"; - let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); - - assert!(macro_definition_at(&db, TOP, offset(root_text, "`define")).unwrap().is_none()); - - let definition = - macro_definition_at(&db, TOP, offset(root_text, "HEADER_FLAG")).unwrap().unwrap(); - assert_eq!(text_at_range(root_text, definition.name_range), "HEADER_FLAG"); - assert!(macro_definition_at(&db, TOP, definition.name_range.end()).unwrap().is_none()); - assert_ne!(definition.directive_range, definition.name_range); - } - - #[test] - fn preproc_ifndef_guard_reference_resolves_to_following_define() { - let root_text = "`include \"defs.vh\"\n"; - let header_text = r#"`ifndef HEADER_FLAG -`define HEADER_FLAG -`endif -"#; - let db = db_with_files(root_text, header_text); - let resolution = - macro_reference_definitions_at(&db, HEADER, offset(header_text, "HEADER_FLAG")) - .unwrap() - .unwrap(); - - assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); - let definition = - resolution.definitions.iter().find(|definition| definition.file_id == HEADER).unwrap(); - assert_eq!(text_at_range(header_text, definition.name_range), "HEADER_FLAG"); - - let refs = macro_references(&db, HEADER, definition).unwrap().references; - assert!(refs.iter().any(|reference| { - reference.file_id == HEADER - && reference.range.start() == offset(header_text, "HEADER_FLAG") - && text_at_range(header_text, reference.range) == "HEADER_FLAG" - })); - } - - #[test] - fn preproc_project_header_guard_reference_is_indexed_without_include() { - let root_text = "module top; endmodule\n"; - let header_text = r#"`ifndef HEADER_FLAG -`define HEADER_FLAG -`endif -"#; - let db = db_with_files(root_text, header_text); - let resolution = - macro_reference_definitions_at(&db, HEADER, offset(header_text, "HEADER_FLAG")) - .unwrap() - .unwrap(); - - assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); - assert!(resolution.definitions.iter().any(|definition| { - definition.file_id == HEADER - && text_at_range(header_text, definition.name_range) == "HEADER_FLAG" - })); - } -} +mod tests; diff --git a/crates/hir/src/preproc/conditionals.rs b/crates/hir/src/preproc/conditionals.rs new file mode 100644 index 00000000..dc7426d4 --- /dev/null +++ b/crates/hir/src/preproc/conditionals.rs @@ -0,0 +1,51 @@ +use super::*; + +pub fn inactive_branches( + db: &dyn SourceRootDb, + file_id: FileId, +) -> PreprocResult> { + let mut branches = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for source_range in mapped.model.inactive_ranges() { + let (source, range) = match map_mapped_source_range(mapped, *source_range) { + Ok(mapped_range) => mapped_range, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let Some(branch_file_id) = source.file_id() else { + continue; + }; + if branch_file_id == file_id { + let branch = InactiveBranch { + source, + capability: capability_status(&mapped.model.capabilities().inactive_ranges), + file_id: branch_file_id, + range, + }; + branches.push_keyed(branch, InactiveBranchKey::from_branch); + } + } + } + + if branches.is_empty() + && let Err(error) = finish_empty_single_query(&contexts, first_error) + { + return Err(error); + } + + Ok(branches.into_vec()) +} diff --git a/crates/hir/src/preproc/definitions.rs b/crates/hir/src/preproc/definitions.rs new file mode 100644 index 00000000..1bf5a6e5 --- /dev/null +++ b/crates/hir/src/preproc/definitions.rs @@ -0,0 +1,235 @@ +use super::{ + predefines::{configured_predefine_definitions_at, configured_predefine_names}, + *, +}; + +pub fn visible_macros_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for position in mapped.source_map.source_positions_for_file_offset(file_id, offset) { + for definition in mapped.model.visible_macros_at(position) { + match map_macro_definition(mapped, definition) { + Ok(definition) => { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + Err(error) => record_first_error(&mut first_error, error), + } + } + } + } + + if definitions.is_empty() + && let Err(error) = finish_empty_single_query(&contexts, first_error) + { + return Err(error); + } + + Ok(definitions.into_vec()) +} + +pub fn visible_macro_names_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut names = UniqVec::::default(); + for definition in visible_macros_at(db, file_id, offset)? { + names.push_unique(definition.name.clone()); + } + for name in configured_predefine_names(db, file_id) { + names.push_unique(name); + } + + Ok(names.into_vec()) +} + +pub fn macro_definition_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for definition in mapped.model.macro_definitions().iter() { + let mapped_definition = map_macro_definition(mapped, definition)?; + if mapped_definition.file_id == file_id && mapped_definition.name_range.contains(offset) + { + return Ok(Some(mapped_definition)); + } + } + } + + if let Some(definition) = configured_predefine_definitions_at(db, file_id, offset)? + .into_single_or_none(|contexts| PreprocUnavailable::AmbiguousMacroDefinitionContexts { + contexts, + })? + { + return Ok(Some(definition)); + } + + finish_empty_single_query(&contexts, first_error)?; + + Ok(None) +} + +pub fn macro_param_definition_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + macro_param_definitions_at(db, file_id, offset)?.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroParamContexts { contexts } + }) +} + +pub fn macro_param_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for definition in mapped.model.macro_definitions().iter() { + let Some(params) = &definition.params else { + continue; + }; + for (param_index, param) in params.iter().enumerate() { + let Some(param_definition) = + map_macro_param_definition(mapped, definition, param_index, param)? + else { + continue; + }; + if param_definition.macro_definition.file_id == file_id + && param_definition.range.contains(offset) + { + definitions + .push_keyed(param_definition, MacroParamDefinitionKey::from_definition); + } + } + } + } + + if definitions.is_empty() + && let Err(error) = finish_empty_single_query(&contexts, first_error) + { + return Err(error); + } + + Ok(definitions.into_vec()) +} + +pub fn macro_param_reference_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = UniqVec::::default(); + let mut references = UniqVec::::default(); + let mut query_range = None; + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for definition in mapped.model.macro_definitions().iter() { + let Some(params) = &definition.params else { + continue; + }; + for (token_index, token) in definition.body_tokens.iter().enumerate() { + let Some(token_range) = token.range else { + continue; + }; + let (_, range) = + match mapped_source_range_at_offset(mapped, token_range, file_id, offset) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for (param_index, param) in params.iter().enumerate() { + if param.name.as_ref() != Some(&token.value) { + continue; + } + let Some(param_definition) = + map_macro_param_definition(mapped, definition, param_index, param)? + else { + continue; + }; + let reference = map_macro_param_reference( + mapped, + definition, + param_index, + token_index, + token_range, + )?; + query_range.get_or_insert(range); + definitions + .push_keyed(param_definition, MacroParamDefinitionKey::from_definition); + references.push_keyed(reference, MacroParamReferenceKey::from_reference); + } + } + } + } + + let Some(range) = query_range else { + finish_empty_single_query(&contexts, first_error)?; + return Ok(None); + }; + + let references = references.into_vec(); + let definitions = definitions.into_vec(); + Ok(Some(MacroParamReferenceDefinitions { + capability: macro_param_reference_context_capability(&references), + references, + range, + definitions, + })) +} diff --git a/crates/hir/src/preproc/expansion.rs b/crates/hir/src/preproc/expansion.rs new file mode 100644 index 00000000..935020ff --- /dev/null +++ b/crates/hir/src/preproc/expansion.rs @@ -0,0 +1,324 @@ +use super::*; + +pub fn immediate_macro_expansion_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut queries = macro_expansion_queries_at(db, file_id, offset)?; + match queries.len() { + 0 => Ok(None), + 1 => Ok(queries.pop()), + contexts => { + let available = queries + .iter() + .filter_map(|query| match query { + MacroExpansionQuery::Available(expansion) => Some(expansion.as_ref().clone()), + MacroExpansionQuery::Ambiguous(_) | MacroExpansionQuery::Unavailable(_) => None, + }) + .collect::>(); + if available.len() == contexts { + return Ok(Some(MacroExpansionQuery::Ambiguous(available))); + } + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }) + } + } +} + +pub fn macro_expansion_queries_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut queries = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let query = immediate_macro_expansion_for_call(mapped, call_fact)?; + queries.push_unique_eq(query); + } + } + + if !queries.is_empty() { + return Ok(queries.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +pub fn recursive_macro_expansion_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + recursive_macro_expansions_at(db, file_id, offset)?.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts } + }) +} + +pub fn recursive_macro_expansions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut expansions = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; + expansions.push_unique_eq(recursive); + } + } + + if !expansions.is_empty() { + return Ok(expansions.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +pub fn recursive_macro_expansion_provenances_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut expansions = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; + expansions.push_unique_eq(recursive); + } + } + + if !expansions.is_empty() { + return Ok(expansions.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +pub fn macro_expansion_provenance_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + macro_expansion_provenances_at(db, file_id, offset)?.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts } + }) +} + +pub fn macro_expansion_provenances_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut provenances = UniqVec::::default(); + let mut unavailable = Vec::new(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + for call_fact in source_macro_calls_at(mapped, file_id, offset) { + match macro_expansion_provenance_for_call(mapped, call_fact)? { + MacroExpansionProvenanceForCall::Available(provenance) => { + provenances.push_unique_eq(*provenance); + } + MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), + } + } + } + + if !unavailable.is_empty() { + return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); + } + if !provenances.is_empty() { + return Ok(provenances.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +pub fn macro_expansion_provenance_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + macro_expansion_provenances_for_range(db, file_id, range)?.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts } + }) +} + +pub fn macro_expansion_provenances_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mut provenances = UniqVec::::default(); + let mut unavailable = Vec::new(); + let mut ambiguous_contexts = 0; + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); + match call_facts.as_slice() { + [] => continue, + [call_fact] => match macro_expansion_provenance_for_call(mapped, call_fact)? { + MacroExpansionProvenanceForCall::Available(provenance) => { + provenances.push_unique_eq(*provenance); + } + MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), + }, + call_facts => { + ambiguous_contexts += call_facts.len(); + } + } + } + + if ambiguous_contexts > 0 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { + contexts: ambiguous_contexts + provenances.len() + unavailable.len(), + }, + }); + } + if !unavailable.is_empty() { + return unavailable_or_ambiguous_macro_expansion_provenance(provenances.len(), unavailable); + } + if !provenances.is_empty() { + return Ok(provenances.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +fn unavailable_or_ambiguous_macro_expansion_provenance( + available_contexts: usize, + mut unavailable: Vec, +) -> PreprocResult> { + let contexts = available_contexts + unavailable.len(); + if contexts == 1 { + return Err(PreprocError::Unavailable { reason: unavailable.pop().unwrap() }); + } + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts }, + }) +} + +pub fn diagnostic_provenance_for_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mut provenances = UniqVec::::default(); + let mut ambiguous_targets = 0; + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let call_facts = source_macro_calls_intersecting_range(mapped, file_id, range); + match call_facts.as_slice() { + [] => continue, + [call_fact] => { + let provenance = diagnostic_provenance_for_call(mapped, call_fact)?; + provenances.push_unique_eq(provenance); + } + call_facts => { + ambiguous_targets += call_facts.len(); + } + } + } + + let precise = provenances + .as_slice() + .iter() + .filter(|provenance| !matches!(provenance, DiagnosticProvenance::Unavailable(_))) + .cloned() + .collect::>(); + if ambiguous_targets > 0 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { + targets: ambiguous_targets + precise.len(), + }, + ))); + } + if precise.len() == 1 { + return Ok(Some(precise.into_iter().next().unwrap())); + } + if precise.len() > 1 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: precise.len() }, + ))); + } + if provenances.len() == 1 { + return Ok(provenances.into_vec().into_iter().next()); + } + if provenances.len() > 1 { + return Ok(Some(DiagnosticProvenance::Unavailable( + PreprocUnavailable::AmbiguousDiagnosticProvenance { targets: provenances.len() }, + ))); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(None) +} diff --git a/crates/hir/src/preproc/helpers.rs b/crates/hir/src/preproc/helpers.rs new file mode 100644 index 00000000..22eced76 --- /dev/null +++ b/crates/hir/src/preproc/helpers.rs @@ -0,0 +1,8 @@ +use super::*; + +mod context; +mod expansion; +mod facts; +mod source; + +pub(in crate::preproc) use self::{context::*, expansion::*, facts::*, source::*}; diff --git a/crates/hir/src/preproc/helpers/context.rs b/crates/hir/src/preproc/helpers/context.rs new file mode 100644 index 00000000..14ecc82e --- /dev/null +++ b/crates/hir/src/preproc/helpers/context.rs @@ -0,0 +1,99 @@ +use super::*; + +pub(in crate::preproc) fn mapped_result( + result: &Result, +) -> PreprocResult<&MappedSourcePreprocModel> { + result.as_ref().map_err(|err| err.clone().into()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(in crate::preproc) struct SourcePreprocQueryContexts { + pub(in crate::preproc) model_file_ids: Vec, + pub(in crate::preproc) status: SourcePreprocContextStatus, +} + +impl SourcePreprocQueryContexts { + fn partial_error(&self) -> Option { + let SourcePreprocContextStatus::Partial { skipped_models } = self.status else { + return None; + }; + Some(PreprocError::Unavailable { + reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models }, + }) + } +} + +pub(in crate::preproc) fn source_preproc_single_query_contexts( + db: &dyn SourceRootDb, + file_id: FileId, +) -> SourcePreprocQueryContexts { + let profile_id = db.file_compilation_profile(file_id); + let index = db.source_preproc_context_index_for_profile(profile_id); + let relevant = index.relevant_contexts(file_id); + let mut file_ids = UniqVec::::default(); + if matches!( + db.file_kind(file_id), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + file_ids.push_unique(file_id); + } + for model_file_id in relevant.model_file_ids { + file_ids.push_unique(model_file_id); + } + SourcePreprocQueryContexts { model_file_ids: file_ids.into_vec(), status: relevant.status } +} + +pub(in crate::preproc) fn finish_empty_single_query( + contexts: &SourcePreprocQueryContexts, + first_error: Option, +) -> PreprocResult<()> { + if let Some(error) = first_error { + return Err(error); + } + if let Some(error) = contexts.partial_error() { + return Err(error); + } + Ok(()) +} + +pub(in crate::preproc) fn record_first_error( + first_error: &mut Option, + error: PreprocError, +) { + if first_error.is_none() { + *first_error = Some(error); + } +} + +pub(in crate::preproc) trait PreprocSingleExt { + fn into_single_or_none(self, ambiguous: F) -> PreprocResult> + where + F: FnOnce(usize) -> PreprocUnavailable; + + fn into_exactly_one(self, ambiguous: F) -> PreprocResult + where + F: FnOnce(usize) -> PreprocUnavailable; +} + +impl PreprocSingleExt for Vec { + fn into_single_or_none(mut self, ambiguous: F) -> PreprocResult> + where + F: FnOnce(usize) -> PreprocUnavailable, + { + match self.len() { + 0 => Ok(None), + 1 => Ok(self.pop()), + contexts => Err(PreprocError::Unavailable { reason: ambiguous(contexts) }), + } + } + + fn into_exactly_one(mut self, ambiguous: F) -> PreprocResult + where + F: FnOnce(usize) -> PreprocUnavailable, + { + match self.len() { + 1 => Ok(self.pop().unwrap()), + contexts => Err(PreprocError::Unavailable { reason: ambiguous(contexts) }), + } + } +} diff --git a/crates/hir/src/preproc/helpers/expansion.rs b/crates/hir/src/preproc/helpers/expansion.rs new file mode 100644 index 00000000..7dde8b71 --- /dev/null +++ b/crates/hir/src/preproc/helpers/expansion.rs @@ -0,0 +1,419 @@ +use super::*; + +pub(in crate::preproc) fn map_macro_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let Some(call) = mapped.model.macro_calls().get(expansion.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroCall { + call: expansion.call, + }), + }); + }; + let Some(definition) = mapped.model.macro_definitions().get(expansion.definition) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { + call: expansion.call, + }, + ), + }); + }; + Ok(MacroExpansion { + id: expansion.id.into(), + call: map_macro_call(mapped, call)?, + definition_id: expansion.definition.into(), + definition: map_macro_definition(mapped, definition)?, + emitted_token_range: expansion.emitted_token_range, + display_source: map_expansion_display_source(mapped, expansion.id)?, + display_range: mapped + .source_map + .emitted_display_range(expansion.id, expansion.emitted_token_range) + .map_err(PreprocError::SourceMap)?, + child_calls: expansion.child_calls.iter().copied().map(Into::into).collect(), + capability: macro_expansion_availability(&expansion.status), + }) +} + +pub(in crate::preproc) fn map_expansion_display_source( + mapped: &MappedSourcePreprocModel, + expansion: SourceMacroExpansionId, +) -> PreprocResult { + match mapped.source_map.expansion_display_source(expansion).map_err(PreprocError::SourceMap)? { + PreprocSourceMapping::VirtualFile { file_id, path, origin } => { + Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) + } + PreprocSourceMapping::VirtualDisplay { path, origin } => { + Ok(MappedPreprocSource::VirtualDisplay { path, origin }) + } + PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), + PreprocSourceMapping::Unmapped(reason) => { + Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) + } + } +} + +pub(in crate::preproc) fn map_expansion_source_buffer( + mapped: &MappedSourcePreprocModel, + expansion: SourceMacroExpansionId, +) -> PreprocResult { + match mapped.source_map.expansion_source_buffer(expansion).map_err(PreprocError::SourceMap)? { + PreprocSourceMapping::VirtualFile { file_id, path, origin } => { + Ok(MappedPreprocSource::VirtualFile { file_id, path, origin }) + } + PreprocSourceMapping::VirtualDisplay { path, origin } => { + Ok(MappedPreprocSource::VirtualDisplay { path, origin }) + } + PreprocSourceMapping::RealFile(file_id) => Ok(MappedPreprocSource::RealFile { file_id }), + PreprocSourceMapping::Unmapped(reason) => { + Err(PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) }) + } + } +} + +pub(in crate::preproc) fn display_only_virtual_expansion_unavailable( + source: &MappedPreprocSource, +) -> PreprocUnavailable { + match source { + MappedPreprocSource::VirtualDisplay { path, origin } => { + PreprocUnavailable::DisplayOnlyVirtualExpansion { + path: path.clone(), + origin: origin.clone(), + } + } + MappedPreprocSource::RealFile { .. } | MappedPreprocSource::VirtualFile { .. } => { + PreprocUnavailable::Source(SourcePreprocUnavailable::ExpansionAuthorityUnavailable) + } + } +} + +pub(in crate::preproc) fn source_macro_calls_at( + mapped: &MappedSourcePreprocModel, + file_id: FileId, + offset: TextSize, +) -> Vec<&SourceMacroCallFact> { + mapped + .model + .macro_calls() + .iter() + .filter(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == Some(file_id) && range.contains(offset) + }) + .collect() +} + +pub(in crate::preproc) fn source_macro_calls_intersecting_range( + mapped: &MappedSourcePreprocModel, + file_id: FileId, + source_range: TextRange, +) -> Vec<&SourceMacroCallFact> { + mapped + .model + .macro_calls() + .iter() + .filter(|call| { + let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { + return false; + }; + source.file_id() == Some(file_id) + && range + .intersect(source_range) + .is_some_and(|intersection| !intersection.is_empty()) + }) + .collect() +} + +pub(in crate::preproc) fn immediate_macro_expansion_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let call = map_macro_call(mapped, call_fact)?; + Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion) else { + return Ok(MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + ), + }))); + }; + MacroExpansionQuery::Available(Box::new(map_macro_expansion(mapped, expansion)?)) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + MacroExpansionQuery::Unavailable(Box::new(MacroExpansionUnavailable { + call, + reason: PreprocUnavailable::Source(reason), + })) + } + }) +} + +pub(in crate::preproc) fn recursive_macro_expansion_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let root_call = map_macro_call(mapped, call_fact)?; + let recursive = mapped.model.recursive_macro_expansion(call_fact.id); + let expansions = recursive + .expansions + .into_iter() + .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) + .map(|expansion| map_macro_expansion(mapped, expansion)) + .collect::>>()?; + let unavailable = recursive + .unavailable + .into_iter() + .map(|unavailable| { + let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, + ), + }); + }; + Ok(MacroExpansionUnavailable { + call: map_macro_call(mapped, call)?, + reason: PreprocUnavailable::Source(unavailable.reason), + }) + }) + .collect::>>()?; + + Ok(RecursiveMacroExpansion { root_call, expansions, unavailable }) +} + +pub(in crate::preproc) fn recursive_macro_expansion_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + let root_call = map_macro_call(mapped, call_fact)?; + let recursive = mapped.model.recursive_macro_expansion(call_fact.id); + let expansions = recursive + .expansions + .into_iter() + .filter_map(|expansion| mapped.model.macro_expansions().get(expansion)) + .map(|expansion| macro_expansion_provenance_for_expansion(mapped, expansion)) + .collect::>>()?; + let unavailable = recursive + .unavailable + .into_iter() + .map(|unavailable| { + let Some(call) = mapped.model.macro_calls().get(unavailable.call) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroCall { call: unavailable.call }, + ), + }); + }; + Ok(MacroExpansionUnavailable { + call: map_macro_call(mapped, call)?, + reason: PreprocUnavailable::Source(unavailable.reason), + }) + }) + .collect::>>()?; + + Ok(RecursiveMacroExpansionProvenance { root_call, expansions, unavailable }) +} + +pub(in crate::preproc) fn diagnostic_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion_id) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { + return Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingMacroExpansion { call: call_fact.id }, + ))); + }; + diagnostic_target_for_source_expansion(mapped, expansion) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + Ok(DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(reason))) + } + } +} + +pub(in crate::preproc) enum MacroExpansionProvenanceForCall { + Available(Box), + Unavailable(PreprocUnavailable), +} + +pub(in crate::preproc) fn macro_expansion_provenance_for_call( + mapped: &MappedSourcePreprocModel, + call_fact: &SourceMacroCallFact, +) -> PreprocResult { + Ok(match mapped.model.immediate_macro_expansion(call_fact.id) { + SourceMacroExpansionQueryFact::Available(expansion_id) => { + let Some(expansion) = mapped.model.macro_expansions().get(expansion_id) else { + return Ok(MacroExpansionProvenanceForCall::Unavailable( + PreprocUnavailable::Source(SourcePreprocUnavailable::MissingMacroExpansion { + call: call_fact.id, + }), + )); + }; + MacroExpansionProvenanceForCall::Available(Box::new( + macro_expansion_provenance_for_expansion(mapped, expansion)?, + )) + } + SourceMacroExpansionQueryFact::Unavailable(reason) => { + MacroExpansionProvenanceForCall::Unavailable(PreprocUnavailable::Source(reason)) + } + }) +} + +pub(in crate::preproc) fn macro_expansion_provenance_for_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let expansion_id = expansion.id; + let expansion = map_macro_expansion(mapped, expansion)?; + let mut tokens = Vec::new(); + for token_id in emitted_token_ids(expansion.emitted_token_range) { + let Some(token) = mapped.model.emitted_tokens().get(token_id) else { + return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { + token: token_id, + })); + }; + let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { + return Err(unavailable_error( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + )); + }; + tokens.push(EmittedTokenProvenance { + token: token_id, + text: token.text.clone(), + display_range: mapped + .source_map + .emitted_token_display_range(expansion_id, token_id) + .map_err(PreprocError::SourceMap)?, + provenance: map_token_provenance(mapped, provenance)?, + }); + } + + Ok(MacroExpansionProvenance { expansion, tokens }) +} + +pub(in crate::preproc) fn emitted_token_ids( + range: SourceEmittedTokenRange, +) -> impl Iterator { + let start = range.start.raw(); + let end = start.saturating_add(range.len); + (start..end).map(SourceEmittedTokenId::new) +} + +pub(in crate::preproc) fn map_token_provenance( + mapped: &MappedSourcePreprocModel, + provenance: &SourceTokenProvenanceFact, +) -> PreprocResult { + Ok(match provenance { + SourceTokenProvenanceFact::Source { token_range } => { + let (source, range) = map_mapped_source_range(mapped, *token_range)?; + TokenProvenance::SourceToken { source, range } + } + SourceTokenProvenanceFact::MacroBody { definition, body_token_range, call, .. } => { + let call = mapped_macro_call(mapped, *call)?; + let (source, range) = map_mapped_source_range(mapped, *body_token_range)?; + TokenProvenance::MacroBody { call, definition_id: (*definition).into(), source, range } + } + SourceTokenProvenanceFact::MacroArgument { + call, + argument_index, + argument_token_range, + .. + } => { + let call = mapped_macro_call(mapped, *call)?; + let Ok((source, range)) = map_mapped_source_range(mapped, *argument_token_range) else { + return Ok(TokenProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + ))); + }; + TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } + } + SourceTokenProvenanceFact::TokenPaste { .. } + | SourceTokenProvenanceFact::Stringification { .. } => TokenProvenance::Unavailable( + PreprocUnavailable::Source(SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance), + ), + SourceTokenProvenanceFact::Predefine { source } => { + TokenProvenance::Predefine { source: map_mapped_source_id(mapped, *source)? } + } + SourceTokenProvenanceFact::Builtin { name } => { + TokenProvenance::Builtin { name: name.clone() } + } + SourceTokenProvenanceFact::Unavailable(reason) => { + TokenProvenance::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +pub(in crate::preproc) fn mapped_macro_call( + mapped: &MappedSourcePreprocModel, + call: SourceMacroCallId, +) -> PreprocResult { + let Some(call) = mapped.model.macro_calls().get(call) else { + return Err(unavailable_error(SourcePreprocUnavailable::MissingMacroCall { call })); + }; + map_macro_call(mapped, call) +} + +pub(in crate::preproc) fn diagnostic_target_for_source_expansion( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult { + let mut saw_unavailable = None; + for token_id in emitted_token_ids(expansion.emitted_token_range) { + let Some(token) = mapped.model.emitted_tokens().get(token_id) else { + return Err(PreprocError::SourceMap(PreprocSourceMapError::MissingEmittedToken { + token: token_id, + })); + }; + let Some(provenance) = mapped.model.token_provenance().get(token.provenance) else { + return Err(unavailable_error( + SourcePreprocUnavailable::TokenProvenanceAuthorityUnavailable, + )); + }; + match map_token_provenance(mapped, provenance)? { + TokenProvenance::SourceToken { source, range } => { + return Ok(DiagnosticProvenance::SourceToken { source, range }); + } + TokenProvenance::MacroBody { call, definition_id, source, range } => { + return Ok(DiagnosticProvenance::MacroBody { call, definition_id, source, range }); + } + TokenProvenance::MacroArgument { call, argument_index, source, range } => { + return Ok(DiagnosticProvenance::MacroArgument { + call, + argument_index, + source, + range, + }); + } + TokenProvenance::Unavailable(reason) => { + saw_unavailable = Some(reason); + } + TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } => {} + } + } + + if let Some(reason) = saw_unavailable { + return Ok(DiagnosticProvenance::Unavailable(reason)); + } + + let source_buffer_source = map_expansion_source_buffer(mapped, expansion.id)?; + let MappedPreprocSource::VirtualFile { .. } = &source_buffer_source else { + return Ok(DiagnosticProvenance::Unavailable(display_only_virtual_expansion_unavailable( + &source_buffer_source, + ))); + }; + let source_buffer_range = mapped + .source_map + .emitted_source_buffer_range(expansion.id, expansion.emitted_token_range) + .map_err(PreprocError::SourceMap)?; + Ok(DiagnosticProvenance::VirtualExpansion { + source: source_buffer_source, + range: source_buffer_range, + }) +} diff --git a/crates/hir/src/preproc/helpers/facts.rs b/crates/hir/src/preproc/helpers/facts.rs new file mode 100644 index 00000000..1689392c --- /dev/null +++ b/crates/hir/src/preproc/helpers/facts.rs @@ -0,0 +1,407 @@ +use super::*; + +pub(in crate::preproc) fn map_macro_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, +) -> PreprocResult { + let (mut source, mut directive_range, mut name_range) = map_definition_ranges( + mapped, + definition.event_id.raw(), + definition.directive_range, + definition.name_range, + )?; + if let Some(manifest_source) = + mapped.source_map.predefine_manifest_source(definition.name_range.source) + { + source = MappedPreprocSource::RealFile { file_id: manifest_source.file_id }; + directive_range = manifest_source.range; + name_range = manifest_source.range; + } + let params = definition + .params + .as_ref() + .map(|params| { + params + .iter() + .enumerate() + .map(|(param_index, param)| { + let range = param + .name_range + .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) + .transpose()?; + Ok(MacroDefinitionParam { param_index, name: param.name.clone(), range }) + }) + .collect::>>() + }) + .transpose()?; + let file_id = require_file_backed_source(&source)?; + Ok(MacroDefinition { + id: definition.id.into(), + file_id, + source, + capability: capability_status(&mapped.model.capabilities().definition_name_ranges), + name: definition.name.clone(), + params, + body_tokens: definition.body_tokens.iter().map(|token| token.raw.clone()).collect(), + define_index: define_index_for_definition(mapped, definition)?, + event_id: definition.event_id.raw(), + directive_range, + name_range, + }) +} + +pub(in crate::preproc) fn map_macro_param_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, + param_index: usize, + param: &SourceMacroParamFact, +) -> PreprocResult> { + let Some(name) = ¶m.name else { + return Ok(None); + }; + let Some(name_source_range) = param.name_range else { + return Ok(None); + }; + let macro_definition = map_macro_definition(mapped, definition)?; + let (source, range) = map_mapped_source_range(mapped, name_source_range)?; + let name_file_id = require_file_backed_source(&source)?; + if name_file_id != macro_definition.file_id { + return Err(PreprocError::MismatchedDefinitionRangeFiles { + event_id: definition.event_id.raw(), + directive_file_id: macro_definition.file_id, + name_file_id, + }); + } + let param_range = param + .range + .map(|range| map_mapped_source_range(mapped, range).map(|(_, range)| range)) + .transpose()?; + + Ok(Some(MacroParamDefinition { + macro_definition, + param_index, + name: name.clone(), + range, + param_range, + })) +} + +pub(in crate::preproc) fn map_macro_param_reference( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, + param_index: usize, + token_index: usize, + token_range: SourceRange, +) -> PreprocResult { + let macro_definition = map_macro_definition(mapped, definition)?; + let (source, range) = map_mapped_source_range(mapped, token_range)?; + let file_id = require_file_backed_source(&source)?; + let name = definition + .params + .as_ref() + .and_then(|params| params.get(param_index)) + .and_then(|param| param.name.clone()) + .ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, + )) + })?; + + Ok(MacroParamReference { + macro_definition, + source, + capability: PreprocAvailability::Complete, + file_id, + param_index, + token_index, + name, + range, + }) +} + +pub(in crate::preproc) fn map_definition_provenance_from_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, +) -> PreprocResult { + let definition = map_macro_definition(mapped, definition)?; + Ok(MacroDefinitionProvenance { + id: definition.id, + source: definition.source, + capability: definition.capability, + event_id: definition.event_id, + file_id: definition.file_id, + directive_range: definition.directive_range, + name_range: definition.name_range, + }) +} + +pub(in crate::preproc) fn map_macro_reference( + mapped: &MappedSourcePreprocModel, + reference: &SourceMacroReferenceFact, +) -> PreprocResult { + let (source, directive_range, name_range) = map_reference_ranges(mapped, reference)?; + let file_id = require_file_backed_source(&source)?; + Ok(MacroReference { + id: reference.id.into(), + file_id, + source, + capability: capability_status(&mapped.model.capabilities().macro_reference_resolution), + name: reference.name.clone(), + directive_range, + range: name_range, + resolution: map_macro_resolution(mapped, &reference.resolution)?, + }) +} + +pub(in crate::preproc) fn map_macro_call( + mapped: &MappedSourcePreprocModel, + call: &SourceMacroCallFact, +) -> PreprocResult { + let (source, range) = map_mapped_source_range(mapped, call.call_range)?; + let arguments = call + .arguments + .iter() + .map(|argument| map_macro_argument(mapped, argument)) + .collect::>>()?; + let file_id = require_file_backed_source(&source)?; + Ok(MacroCall { + id: call.id.into(), + reference_id: call.reference.into(), + file_id, + source, + capability: macro_call_availability(&call.status), + arguments, + directive_range: range, + range, + callee: map_macro_resolution(mapped, &call.callee)?, + expansion: call.expansion.map(Into::into), + }) +} + +pub(in crate::preproc) fn map_macro_argument( + mapped: &MappedSourcePreprocModel, + argument: &SourceMacroArgumentFact, +) -> PreprocResult { + let (source, range) = argument + .argument_range + .map(|range| map_mapped_source_range(mapped, range)) + .transpose()? + .map_or((None, None), |(source, range)| (Some(source), Some(range))); + Ok(MacroArgument { + argument_index: argument.argument_index, + source, + range, + tokens: argument.tokens.iter().map(|token| token.raw.clone()).collect(), + }) +} + +pub(in crate::preproc) fn map_macro_resolution( + mapped: &MappedSourcePreprocModel, + resolution: &SourceMacroResolutionFact, +) -> PreprocResult { + Ok(match resolution { + SourceMacroResolutionFact::Resolved { definition, reason, include_chain } => { + MacroResolution::Resolved { + definition_id: (*definition).into(), + reason: map_macro_resolution_reason(*reason), + include_chain: map_include_chain(mapped, include_chain)?, + } + } + SourceMacroResolutionFact::Undefined => MacroResolution::Undefined, + SourceMacroResolutionFact::Unavailable(reason) => { + MacroResolution::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +pub(in crate::preproc) fn map_macro_resolution_reason( + reason: SourceMacroResolutionReasonFact, +) -> MacroResolutionReason { + match reason { + SourceMacroResolutionReasonFact::VisibleDefinition => { + MacroResolutionReason::VisibleDefinition + } + SourceMacroResolutionReasonFact::IncludeGuardIfNDef => { + MacroResolutionReason::IncludeGuardIfNDef + } + } +} + +pub(in crate::preproc) fn map_reference_ranges( + mapped: &MappedSourcePreprocModel, + reference: &SourceMacroReferenceFact, +) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { + let (directive_source, directive_range) = + map_mapped_source_range(mapped, reference.directive_range)?; + let (name_source, name_range) = map_mapped_source_range(mapped, reference.name_range)?; + if directive_source != name_source { + let directive_file_id = require_file_backed_source(&directive_source)?; + let name_file_id = require_file_backed_source(&name_source)?; + return Err(PreprocError::MismatchedReferenceRangeFiles { + event_id: reference.event_id.raw(), + directive_file_id, + name_file_id, + }); + } + Ok((directive_source, directive_range, name_range)) +} + +pub(in crate::preproc) fn map_include_status( + mapped: &MappedSourcePreprocModel, + status: &SourceIncludeStatus, +) -> PreprocResult { + Ok(match status { + SourceIncludeStatus::Resolved { source } => { + IncludeDirectiveStatus::Resolved { source: map_mapped_source_id(mapped, *source)? } + } + SourceIncludeStatus::Unresolved => IncludeDirectiveStatus::Unresolved, + SourceIncludeStatus::Unavailable(reason) => { + IncludeDirectiveStatus::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + }) +} + +pub(in crate::preproc) fn capability_status(status: &CapabilityStatus) -> PreprocAvailability { + match status { + CapabilityStatus::Complete => PreprocAvailability::Complete, + CapabilityStatus::Partial => PreprocAvailability::Partial, + CapabilityStatus::Unavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + +pub(in crate::preproc) fn macro_call_availability( + status: &SourceMacroCallStatusFact, +) -> PreprocAvailability { + match status { + SourceMacroCallStatusFact::ExpansionAvailable => PreprocAvailability::Complete, + SourceMacroCallStatusFact::ExpansionUnavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + +pub(in crate::preproc) fn macro_expansion_availability( + status: &SourceMacroExpansionStatusFact, +) -> PreprocAvailability { + match status { + SourceMacroExpansionStatusFact::Complete => PreprocAvailability::Complete, + SourceMacroExpansionStatusFact::Unavailable(reason) => { + PreprocAvailability::Unavailable(PreprocUnavailable::Source(reason.clone())) + } + } +} + +pub(in crate::preproc) fn unavailable_error(reason: SourcePreprocUnavailable) -> PreprocError { + PreprocError::Unavailable { reason: PreprocUnavailable::Source(reason) } +} + +pub(in crate::preproc) fn define_index_for_definition( + mapped: &MappedSourcePreprocModel, + definition: &SourceMacroDefinitionFact, +) -> PreprocResult { + mapped + .model + .defines() + .iter() + .position(|define| define.event_id == definition.event_id) + .ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: definition.event_id.raw() }, + )) + }) +} + +pub(in crate::preproc) fn map_definition_ranges( + mapped: &MappedSourcePreprocModel, + event_id: u32, + directive_source_range: SourceRange, + name_source_range: SourceRange, +) -> PreprocResult<(MappedPreprocSource, TextRange, TextRange)> { + let (directive_source, directive_range) = + map_mapped_source_range(mapped, directive_source_range)?; + let (name_source, name_range) = map_mapped_source_range(mapped, name_source_range)?; + if directive_source != name_source { + let directive_file_id = require_file_backed_source(&directive_source)?; + let name_file_id = require_file_backed_source(&name_source)?; + return Err(PreprocError::MismatchedDefinitionRangeFiles { + event_id, + directive_file_id, + name_file_id, + }); + } + Ok((directive_source, directive_range, name_range)) +} + +pub(in crate::preproc) fn map_include_chain( + mapped: &MappedSourcePreprocModel, + chain: &[SourceIncludeChainEntry], +) -> PreprocResult> { + chain + .iter() + .map(|entry| { + let (include_file_id, include_range) = map_source_range(mapped, entry.include_range)?; + let included_file_id = map_source_id(mapped, entry.included_source)?; + Ok(IncludeChainEntry { + include_event_id: entry.include_event_id.raw(), + include_file_id, + include_range, + included_file_id, + }) + }) + .collect() +} + +pub(in crate::preproc) fn macro_reference_context_capability( + references: &[MacroReference], +) -> PreprocAvailability { + if references + .iter() + .all(|reference| matches!(reference.capability, PreprocAvailability::Complete)) + { + return PreprocAvailability::Complete; + } + if references + .iter() + .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) + { + return PreprocAvailability::Partial; + } + references + .iter() + .find_map(|reference| match &reference.capability { + PreprocAvailability::Unavailable(reason) => { + Some(PreprocAvailability::Unavailable(reason.clone())) + } + PreprocAvailability::Complete | PreprocAvailability::Partial => None, + }) + .unwrap_or(PreprocAvailability::Complete) +} + +pub(in crate::preproc) fn same_macro_definition( + left: &MacroDefinition, + right: &MacroDefinition, +) -> bool { + MacroDefinitionKey::from_definition(left) == MacroDefinitionKey::from_definition(right) +} + +pub(in crate::preproc) fn macro_param_reference_context_capability( + references: &[MacroParamReference], +) -> PreprocAvailability { + if references + .iter() + .any(|reference| matches!(reference.capability, PreprocAvailability::Partial)) + { + return PreprocAvailability::Partial; + } + references + .iter() + .find_map(|reference| match &reference.capability { + PreprocAvailability::Unavailable(reason) => { + Some(PreprocAvailability::Unavailable(reason.clone())) + } + PreprocAvailability::Complete | PreprocAvailability::Partial => None, + }) + .unwrap_or(PreprocAvailability::Complete) +} diff --git a/crates/hir/src/preproc/helpers/source.rs b/crates/hir/src/preproc/helpers/source.rs new file mode 100644 index 00000000..8693144f --- /dev/null +++ b/crates/hir/src/preproc/helpers/source.rs @@ -0,0 +1,86 @@ +use super::*; + +pub(in crate::preproc) fn require_file_backed_source( + source: &MappedPreprocSource, +) -> PreprocResult { + source.file_id().ok_or_else(|| { + let MappedPreprocSource::VirtualDisplay { path, origin } = source else { + unreachable!("file-backed source should have a FileId"); + }; + PreprocError::SourceMap(PreprocSourceMapError::DisplayOnlyVirtualSource { + path: path.clone(), + origin: origin.clone(), + }) + }) +} + +pub(in crate::preproc) fn map_source_range( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, +) -> PreprocResult<(FileId, TextRange)> { + let (source, range) = map_mapped_source_range(mapped, source_range)?; + Ok((require_file_backed_source(&source)?, range)) +} + +pub(in crate::preproc) fn map_source_id( + mapped: &MappedSourcePreprocModel, + source: PreprocSourceId, +) -> PreprocResult { + mapped.source_map.file_id(source).map_err(PreprocError::SourceMap) +} + +pub(in crate::preproc) fn map_mapped_source_range( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, +) -> PreprocResult<(MappedPreprocSource, TextRange)> { + let range = mapped.source_map.map_range(source_range).map_err(PreprocError::SourceMap)?; + let source = map_mapped_source_id(mapped, source_range.source)?; + Ok((source, range)) +} + +pub(in crate::preproc) fn mapped_source_range_at_offset( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let (source, range) = map_mapped_source_range(mapped, source_range)?; + Ok((source.file_id() == Some(file_id) && range.contains(offset)).then_some((source, range))) +} + +pub(in crate::preproc) fn mapped_source_range_contains_provenance_offset( + mapped: &MappedSourcePreprocModel, + source_range: SourceRange, + file_id: FileId, + offset: TextSize, +) -> PreprocResult { + Ok(mapped_source_range_at_offset(mapped, source_range, file_id, offset)?.is_some()) +} + +pub(in crate::preproc) fn map_mapped_source_id( + mapped: &MappedSourcePreprocModel, + source: PreprocSourceId, +) -> PreprocResult { + match mapped.source_map.get(source) { + Some(PreprocSourceMapping::RealFile(file_id)) => { + Ok(MappedPreprocSource::RealFile { file_id: *file_id }) + } + Some(PreprocSourceMapping::VirtualFile { file_id, path, origin }) => { + Ok(MappedPreprocSource::VirtualFile { + file_id: *file_id, + path: path.clone(), + origin: origin.clone(), + }) + } + Some(PreprocSourceMapping::VirtualDisplay { path, origin }) => { + Ok(MappedPreprocSource::VirtualDisplay { path: path.clone(), origin: origin.clone() }) + } + Some(PreprocSourceMapping::Unmapped(reason)) => { + Err(PreprocError::SourceMap(PreprocSourceMapError::UnmappedSource { + source, + reason: reason.clone(), + })) + } + None => Err(PreprocError::SourceMap(PreprocSourceMapError::MissingSource { source })), + } +} diff --git a/crates/hir/src/preproc/includes.rs b/crates/hir/src/preproc/includes.rs new file mode 100644 index 00000000..bfa463b5 --- /dev/null +++ b/crates/hir/src/preproc/includes.rs @@ -0,0 +1,78 @@ +use super::*; + +pub fn include_directive_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + include_directives_at(db, file_id, offset)? + .into_single_or_none(|targets| PreprocUnavailable::AmbiguousIncludeTargets { targets }) +} + +pub fn include_directives_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut directives = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + for include in mapped.model.include_graph().directives() { + let Some(target_range) = include.target_range else { + continue; + }; + let (source, range) = + match mapped_source_range_at_offset(mapped, target_range, file_id, offset) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let status = map_include_status(mapped, &include.status)?; + let resolved_file = match &status { + IncludeDirectiveStatus::Resolved { source } => source.file_id(), + IncludeDirectiveStatus::Unresolved | IncludeDirectiveStatus::Unavailable(_) => None, + }; + let target = match &include.target { + MacroIncludeTarget::Literal { path, .. } => { + IncludeTarget::Literal { path: path.clone(), resolved_file } + } + MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, + }; + let directive = IncludeDirective { + id: include.id.into(), + source, + capability: capability_status(&mapped.model.capabilities().include_edges), + file_id, + include_index: include.id.raw(), + range, + target, + status, + }; + directives.push_unique_by(directive, |existing, directive| { + existing.file_id == directive.file_id + && existing.range == directive.range + && existing.target == directive.target + && existing.status == directive.status + }); + } + } + + if !directives.is_empty() { + return Ok(directives.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} diff --git a/crates/hir/src/preproc/predefines.rs b/crates/hir/src/preproc/predefines.rs new file mode 100644 index 00000000..0f1fe432 --- /dev/null +++ b/crates/hir/src/preproc/predefines.rs @@ -0,0 +1,122 @@ +use super::*; + +pub(super) fn configured_predefine_names(db: &dyn SourceRootDb, file_id: FileId) -> Vec { + let mut names = UniqVec::::default(); + + let profile_id = db.file_compilation_profile(file_id); + for predefine in &db.project_config().preprocess_for_profile(profile_id).predefines { + if let Some(name) = predefine_macro_name(predefine.as_str()) { + names.push_unique(name); + } + } + + for predefine in &db.file_preprocess_config(file_id).predefines { + if let Some(name) = predefine_macro_name(predefine.as_str()) { + names.push_unique(name); + } + } + + names.into_vec() +} + +fn predefine_macro_name(predefine: &str) -> Option { + let name = predefine.split_once('=').map_or(predefine, |(name, _)| name); + let name = name.trim().strip_prefix('`').unwrap_or(name.trim()); + if name.is_empty() { None } else { Some(SmolStr::new(name)) } +} + +pub(super) fn configured_predefine_definitions_for_name( + db: &dyn SourceRootDb, + context_file_id: FileId, + name: &SmolStr, +) -> Vec { + let mut definitions = UniqVec::::default(); + let profile_id = db.file_compilation_profile(context_file_id); + let project_preprocess = db.project_config().preprocess_for_profile(profile_id); + for predefine in &project_preprocess.predefines { + if let Some(definition) = configured_predefine_definition(db, predefine, name) { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + } + for predefine in &db.file_preprocess_config(context_file_id).predefines { + if let Some(definition) = configured_predefine_definition(db, predefine, name) { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + } + definitions.into_vec() +} + +pub(super) fn configured_predefine_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = UniqVec::::default(); + let contexts = source_preproc_single_query_contexts(db, file_id); + for context_file_id in contexts.model_file_ids.iter().copied() { + let profile_id = db.file_compilation_profile(context_file_id); + let project_preprocess = db.project_config().preprocess_for_profile(profile_id); + for predefine in &project_preprocess.predefines { + if let Some(definition) = + configured_predefine_definition_at(db, predefine, file_id, offset) + { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + } + for predefine in &db.file_preprocess_config(context_file_id).predefines { + if let Some(definition) = + configured_predefine_definition_at(db, predefine, file_id, offset) + { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + } + } + if definitions.is_empty() { + finish_empty_single_query(&contexts, None)?; + } + Ok(definitions.into_vec()) +} + +fn configured_predefine_definition_at( + db: &dyn SourceRootDb, + predefine: &Predefine, + file_id: FileId, + offset: TextSize, +) -> Option { + let definition = + configured_predefine_definition(db, predefine, &predefine_macro_name(predefine.as_str())?)?; + (definition.file_id == file_id && definition.name_range.contains(offset)).then_some(definition) +} + +fn configured_predefine_definition( + db: &dyn SourceRootDb, + predefine: &Predefine, + name: &SmolStr, +) -> Option { + let predefine_name = predefine_macro_name(predefine.as_str())?; + if &predefine_name != name { + return None; + } + let source = predefine.source.as_ref()?; + let file_id = file_id_for_predefine_source_path(db, &source.path)?; + Some(MacroDefinition { + id: MacroDefinitionId::ConfiguredPredefine { file_id, range: source.range }, + source: MappedPreprocSource::RealFile { file_id }, + capability: PreprocAvailability::Complete, + file_id, + name: predefine_name, + params: None, + body_tokens: Vec::new(), + define_index: CONFIGURED_PREDEFINE_DEFINE_INDEX, + event_id: CONFIGURED_PREDEFINE_EVENT_ID, + directive_range: source.range, + name_range: source.range, + }) +} + +fn file_id_for_predefine_source_path( + db: &dyn SourceRootDb, + path: &utils::paths::AbsPathBuf, +) -> Option { + db.files().iter().copied().find(|file_id| db.file_path(*file_id).as_ref() == Some(path)) +} diff --git a/crates/hir/src/preproc/reference_index.rs b/crates/hir/src/preproc/reference_index.rs new file mode 100644 index 00000000..89cd2f93 --- /dev/null +++ b/crates/hir/src/preproc/reference_index.rs @@ -0,0 +1,190 @@ +use super::{predefines::configured_predefine_definitions_for_name, *}; + +pub fn macro_references( + db: &dyn SourceRootDb, + file_id: FileId, + definition: &MacroDefinition, +) -> PreprocResult { + let profile_id = db + .file_compilation_profile(file_id) + .or_else(|| db.file_compilation_profile(definition.file_id)); + let index = db.macro_reference_index_for_profile(profile_id); + Ok(MacroReferences { references: index.references_for(definition), status: index.status() }) +} + +pub fn macro_param_references( + db: &dyn SourceRootDb, + file_id: FileId, + definition: &MacroParamDefinition, +) -> PreprocResult { + let profile_id = db + .file_compilation_profile(file_id) + .or_else(|| db.file_compilation_profile(definition.macro_definition.file_id)); + let mut references = UniqVec::::default(); + let mut first_error = None; + + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for source_definition in mapped.model.macro_definitions().iter() { + let mapped_definition = match map_macro_definition(mapped, source_definition) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + if !same_macro_definition(&mapped_definition, &definition.macro_definition) { + continue; + } + let Some(params) = &source_definition.params else { + continue; + }; + let Some(param) = params.get(definition.param_index) else { + continue; + }; + if param.name.as_ref() != Some(&definition.name) { + continue; + } + + for (token_index, token) in source_definition.body_tokens.iter().enumerate() { + if param.name.as_ref() != Some(&token.value) { + continue; + } + let Some(token_range) = token.range else { + continue; + }; + match map_macro_param_reference( + mapped, + source_definition, + definition.param_index, + token_index, + token_range, + ) { + Ok(reference) => { + references.push_keyed(reference, MacroParamReferenceKey::from_reference); + } + Err(error) => record_first_error(&mut first_error, error), + } + } + } + } + + if references.is_empty() + && let Some(error) = first_error + { + return Err(error); + } + + Ok(MacroParamReferences { references: references.into_vec() }) +} + +pub(crate) fn build_macro_reference_index( + db: &dyn SourceRootDb, + profile_id: Option, +) -> MacroReferenceIndex { + let mut index = MacroReferenceIndex::default(); + + for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped.as_ref() { + Ok(mapped) => mapped, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error: error.clone().into(), + }); + continue; + } + }; + collect_macro_references_in_model(db, mapped, model_file_id, &mut index); + } + + index +} + +fn collect_macro_references_in_model( + db: &dyn SourceRootDb, + mapped: &MappedSourcePreprocModel, + model_file_id: FileId, + index: &mut MacroReferenceIndex, +) { + for reference in mapped.model.macro_references().iter() { + let SourceMacroResolutionFact::Resolved { definition, .. } = reference.resolution else { + if reference.resolution == SourceMacroResolutionFact::Undefined { + collect_configured_predefine_reference(db, mapped, model_file_id, reference, index); + continue; + } + if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { + index.push_issue(MacroReferenceIndexIssue::UnavailableReference { + file_id: model_file_id, + reference_id: reference.id.into(), + reason: PreprocUnavailable::Source(reason.clone()), + }); + } + continue; + }; + + let Some(definition) = mapped.model.macro_definitions().get(definition) else { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error: PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )), + }); + continue; + }; + + let definition = match map_macro_definition(mapped, definition) { + Ok(definition) => definition, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + continue; + } + }; + let reference = match map_macro_reference(mapped, reference) { + Ok(reference) => reference, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + continue; + } + }; + index.push(definition, reference); + } +} + +fn collect_configured_predefine_reference( + db: &dyn SourceRootDb, + mapped: &MappedSourcePreprocModel, + model_file_id: FileId, + source_reference: &SourceMacroReferenceFact, + index: &mut MacroReferenceIndex, +) { + let reference = match map_macro_reference(mapped, source_reference) { + Ok(reference) => reference, + Err(error) => { + index.push_issue(MacroReferenceIndexIssue::SkippedModel { + file_id: model_file_id, + error, + }); + return; + } + }; + for definition in configured_predefine_definitions_for_name(db, model_file_id, &reference.name) + { + index.push(definition, reference.clone()); + } +} diff --git a/crates/hir/src/preproc/reference_queries.rs b/crates/hir/src/preproc/reference_queries.rs new file mode 100644 index 00000000..89561254 --- /dev/null +++ b/crates/hir/src/preproc/reference_queries.rs @@ -0,0 +1,235 @@ +use super::{predefines::configured_predefine_definitions_for_name, *}; + +pub fn macro_usage_resolution_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + macro_usage_resolutions_at(db, file_id, offset)?.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts } + }) +} + +pub fn macro_usage_resolutions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut resolutions = UniqVec::::default(); + let mut first_error = None; + let mut unavailable_contexts = 0; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for reference in mapped.model.macro_references().iter() { + let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { + continue; + }; + match mapped_source_range_contains_provenance_offset( + mapped, + reference.name_range, + file_id, + offset, + ) { + Ok(true) => {} + Ok(false) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + } + + let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = + &reference.resolution + else { + if let SourceMacroResolutionFact::Unavailable(reason) = &reference.resolution { + unavailable_contexts += 1; + record_first_error(&mut first_error, unavailable_error(reason.clone())); + } + continue; + }; + let mapped_reference = map_macro_reference(mapped, reference)?; + let definition_fact = + mapped.model.macro_definitions().get(*definition).ok_or_else(|| { + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id: reference.event_id.raw() }, + )) + })?; + let definition = map_macro_definition(mapped, definition_fact)?; + let definition_provenance = + map_definition_provenance_from_definition(mapped, definition_fact)?; + let include_chain = map_include_chain(mapped, include_chain)?; + + resolutions.push_unique_eq(MacroUsageResolution { + usage: MacroUsage { + reference_id: mapped_reference.id, + source: mapped_reference.source, + capability: mapped_reference.capability.clone(), + file_id: mapped_reference.file_id, + name: mapped_reference.name, + usage_index, + directive_range: mapped_reference.directive_range, + range: mapped_reference.range, + resolution: mapped_reference.resolution, + }, + definition, + definition_provenance, + include_chain, + }); + } + } + + if !resolutions.is_empty() { + return Ok(resolutions.into_vec()); + } + if unavailable_contexts > 1 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { + contexts: unavailable_contexts, + }, + }); + } + finish_empty_single_query(&contexts, first_error)?; + + Ok(Vec::new()) +} + +pub fn macro_reference_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let Some(contexts) = macro_reference_definitions_at(db, file_id, offset)? else { + return Ok(None); + }; + Ok(Some(contexts.references.into_exactly_one(|contexts| { + PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts } + })?)) +} + +pub fn macro_reference_resolution_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let Some(mut resolution) = macro_reference_definitions_at(db, file_id, offset)? else { + return Ok(None); + }; + if resolution.references.len() != 1 { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroReferenceContexts { + contexts: resolution.references.len(), + }, + }); + } + let reference = resolution.references.pop().unwrap(); + let definition = resolution.definitions.into_single_or_none(|contexts| { + PreprocUnavailable::AmbiguousMacroReferenceContexts { contexts } + })?; + Ok(definition.map(|definition| MacroReferenceResolution { reference, definition })) +} + +pub fn macro_reference_definitions_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> PreprocResult> { + let mut definitions = UniqVec::::default(); + let mut references = UniqVec::::default(); + let mut query_range = None; + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for reference in mapped.model.macro_references().iter() { + let (_, range) = match mapped_source_range_at_offset( + mapped, + reference.name_range, + file_id, + offset, + ) { + Ok(Some(hit)) => hit, + Ok(None) => continue, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + query_range.get_or_insert(range); + + let mapped_reference = match map_macro_reference(mapped, reference) { + Ok(reference) => reference, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + references.push_unique_eq(mapped_reference.clone()); + + match &reference.resolution { + SourceMacroResolutionFact::Resolved { definition, .. } => { + let Some(definition) = mapped.model.macro_definitions().get(*definition) else { + record_first_error( + &mut first_error, + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { + event_id: reference.event_id.raw(), + }, + )), + ); + continue; + }; + let definition = match map_macro_definition(mapped, definition) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + SourceMacroResolutionFact::Undefined => { + for definition in configured_predefine_definitions_for_name( + db, + model_file_id, + &mapped_reference.name, + ) { + definitions.push_keyed(definition, MacroDefinitionKey::from_definition); + } + } + SourceMacroResolutionFact::Unavailable(_) => {} + } + } + } + + let Some(range) = query_range else { + finish_empty_single_query(&contexts, first_error)?; + return Ok(None); + }; + + Ok(Some(MacroReferenceDefinitions { + capability: macro_reference_context_capability(references.as_slice()), + references: references.into_vec(), + range, + definitions: definitions.into_vec(), + })) +} diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs new file mode 100644 index 00000000..7c07395a --- /dev/null +++ b/crates/hir/src/preproc/tests.rs @@ -0,0 +1,1091 @@ +use std::fmt; + +use rustc_hash::FxHashSet; +use triomphe::Arc; +use utils::{ + get::Get, + line_index::{TextRange, TextSize}, + paths::{AbsPathBuf, Utf8PathBuf}, +}; +use vfs::{FileId, FileSet, VfsPath, anchored_path::AnchoredPath}; + +use super::*; +use crate::{ + base_db::{ + diagnostics_config::DiagnosticsConfig, + project::{ + CompilationProfile, CompilationProfileId, Predefine, PredefineSource, PreprocessConfig, + ProjectConfig, + }, + salsa::{self, Durability}, + source_db::{ + FileLoader, PreprocExpansionSourceBuffer, PreprocVirtualOrigin, SourceDb, + SourceDbStorage, SourceFileKind, SourceRootDb, SourceRootDbStorage, + }, + source_root::{SourceRoot, SourceRootId}, + }, + container::InFile, + db::{HirDb, HirDbStorage, InternDbStorage}, + hir_def::module::ModuleId, + source_map::IsSrc, +}; + +const TOP: FileId = FileId(0); +const HEADER: FileId = FileId(1); +const LEAF: FileId = FileId(2); +const MANIFEST: FileId = FileId(3); +const ROOT: SourceRootId = SourceRootId(0); +const PROFILE: CompilationProfileId = CompilationProfileId(0); + +#[salsa::database(SourceDbStorage, SourceRootDbStorage, InternDbStorage, HirDbStorage)] +#[derive(Default)] +struct TestDb { + storage: salsa::Storage, +} + +impl salsa::Database for TestDb {} + +impl fmt::Debug for TestDb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestDb").finish() + } +} + +impl FileLoader for TestDb { + fn resolve_path(&self, path: AnchoredPath<'_>) -> Option { + let source_root_id = SourceRootDb::source_root_id(self, path.anchor_id); + SourceRootDb::source_root(self, source_root_id).resolve_path(path) + } +} + +fn db_with_files(root_text: &str, header_text: &str) -> TestDb { + db_with_entries(&[(TOP, "rtl/top.v", root_text), (HEADER, "include/defs.vh", header_text)]) +} + +fn db_with_nested_files(root_text: &str, header_text: &str, leaf_text: &str) -> TestDb { + db_with_entries(&[ + (TOP, "rtl/top.v", root_text), + (HEADER, "include/defs.vh", header_text), + (LEAF, "include/leaf.vh", leaf_text), + ]) +} + +fn db_with_entries(entries: &[(FileId, &str, &str)]) -> TestDb { + db_with_entries_and_predefines(entries, Vec::new()) +} + +fn db_with_entries_and_predefines( + entries: &[(FileId, &str, &str)], + predefines: Vec, +) -> TestDb { + db_with_entries_and_predefine_entries( + entries, + predefines.into_iter().map(Predefine::new).collect(), + ) +} + +fn db_with_entries_and_predefine_entries( + entries: &[(FileId, &str, &str)], + predefines: Vec, +) -> TestDb { + let include_dir = abs_path("include"); + + let mut file_set = FileSet::default(); + for (file_id, path, _) in entries { + file_set.insert(*file_id, VfsPath::from(abs_path(path))); + } + let root = SourceRoot::new_local_with_source_files(file_set, vec![TOP]); + + let preprocess = PreprocessConfig { predefines, include_dirs: vec![include_dir.clone()] }; + let project_config = ProjectConfig::new( + vec![Some(PROFILE)], + vec![CompilationProfile { + source_roots: vec![ROOT], + top_modules: Vec::new(), + preprocess: preprocess.clone(), + }], + ); + + let mut files = FxHashSet::default(); + for (file_id, _, _) in entries { + files.insert(*file_id); + } + + let mut db = TestDb::default(); + db.set_files_with_durability(Box::new(files), Durability::HIGH); + db.set_project_config_with_durability(Arc::new(project_config), Durability::HIGH); + db.set_diagnostics_config_with_durability( + Arc::new(DiagnosticsConfig::default()), + Durability::HIGH, + ); + db.set_source_root_with_durability(ROOT, Arc::new(root), Durability::LOW); + + for (file_id, path, text) in entries { + let path = abs_path(path); + let vfs_path = VfsPath::from(path.clone()); + db.set_source_root_id_with_durability(*file_id, ROOT, Durability::LOW); + db.set_file_path_with_durability(*file_id, Some(path), Durability::LOW); + db.set_file_kind_with_durability( + *file_id, + SourceFileKind::from_path(&vfs_path), + Durability::LOW, + ); + db.set_file_text_with_durability(*file_id, Arc::from(*text), Durability::LOW); + db.set_file_preprocess_config_with_durability( + *file_id, + Arc::new(preprocess.clone()), + 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 offset(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).unwrap()).unwrap()) +} + +fn offset_after(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) +} + +fn offset_after_n(text: &str, needle: &str, occurrence: usize) -> TextSize { + let mut cursor = 0; + for index in 0..=occurrence { + let relative = text[cursor..] + .find(needle) + .unwrap_or_else(|| panic!("missing occurrence {occurrence} of {needle:?} in fixture")); + let absolute = cursor + relative; + if index == occurrence { + return TextSize::from(u32::try_from(absolute + needle.len()).unwrap()); + } + cursor = absolute + needle.len(); + } + unreachable!() +} + +fn text_at_range(text: &str, range: TextRange) -> &str { + &text[usize::from(range.start())..usize::from(range.end())] +} + +fn assert_expansion_is_display_only_source_buffer( + mapped: &MappedSourcePreprocModel, + expansion: &MacroExpansion, +) { + let expansion_id = SourceMacroExpansionId::new(expansion.id.raw()); + let entry = + mapped.source_map.expansion(expansion_id).expect("expansion should have a display entry"); + assert!(matches!(&entry.source_buffer, PreprocExpansionSourceBuffer::DisplayOnly { .. })); + assert!(matches!( + mapped.source_map.emitted_source_buffer_range(expansion_id, expansion.emitted_token_range), + Err(PreprocSourceMapError::DisplayOnlyVirtualSource { .. }) + )); +} + +#[test] +fn preproc_include_usage_resolves_to_header_define() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `HEADER_WIDTH; +endmodule +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let db = db_with_files(root_text, header_text); + + let resolution = + macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")).unwrap().unwrap(); + assert_eq!(resolution.usage.file_id, TOP); + assert_eq!(resolution.definition.file_id, HEADER); + assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); + assert_eq!(text_at_range(header_text, resolution.definition.name_range), "HEADER_WIDTH"); + + let include = include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); + assert_eq!(text_at_range(root_text, include.range), "\"defs.vh\""); + assert!(include_directive_at(&db, TOP, offset(root_text, "`include")).unwrap().is_none()); + assert!(include_directive_at(&db, TOP, include.range.end()).unwrap().is_none()); + let IncludeTarget::Literal { resolved_file, .. } = include.target else { + panic!("literal include expected"); + }; + assert_eq!(resolved_file, Some(HEADER)); +} + +#[test] +fn preproc_macro_expansion_queries_map_call_ranges() { + let root_text = r#"`define OBJ 8 +`define LEAF 3 +`define WRAP `LEAF +module top; +localparam int A = `OBJ; +localparam int B = `WRAP; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let immediate = + immediate_macro_expansion_at(&db, TOP, offset(root_text, "`OBJ")).unwrap().unwrap(); + let MacroExpansionQuery::Available(immediate) = immediate else { + panic!("object-like macro expansion should be available"); + }; + assert_eq!(immediate.call.file_id, TOP); + assert_eq!(text_at_range(root_text, immediate.call.range), "`OBJ"); + assert_eq!(immediate.emitted_token_range.len, 1); + assert!(matches!(immediate.capability, PreprocAvailability::Complete)); + + let recursive = + recursive_macro_expansion_at(&db, TOP, offset(root_text, "`WRAP")).unwrap().unwrap(); + assert_eq!(recursive.root_call.file_id, TOP); + assert_eq!(text_at_range(root_text, recursive.root_call.range), "`WRAP"); + assert!(recursive.unavailable.is_empty()); + assert_eq!(recursive.expansions.len(), 2); + let wrap_expansion = recursive + .expansions + .iter() + .find(|expansion| expansion.definition.name.as_str() == "WRAP") + .expect("outer expansion should be mapped"); + let leaf_expansion = recursive + .expansions + .iter() + .find(|expansion| expansion.definition.name.as_str() == "LEAF") + .expect("nested expansion should be mapped"); + assert_eq!(text_at_range(root_text, wrap_expansion.call.range), "`WRAP"); + assert_eq!(text_at_range(root_text, leaf_expansion.call.range), "`LEAF"); + assert_eq!(wrap_expansion.child_calls, vec![leaf_expansion.call.id]); +} + +#[test] +fn preproc_macro_expansion_exposes_display_virtual_source_and_token_provenance() { + let root_text = r#"`define MAKE_DECL(name) logic name; +module top; +`MAKE_DECL(generated) +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`MAKE_DECL")).unwrap().unwrap(); + let MappedPreprocSource::VirtualDisplay { path, origin } = &provenance.expansion.display_source + else { + panic!("macro expansion should expose a display-only virtual expansion source"); + }; + assert_eq!( + path, + &VfsPath::new_virtual_path("/__vide/preproc/profile-0/expansion/0.sv".to_owned()) + ); + assert_eq!( + origin, + &PreprocVirtualOrigin::Expansion { expansion: SourceMacroExpansionId::new(0) } + ); + + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + let expansion_display = + mapped.source_map.expansion_display_text(SourceMacroExpansionId::new(0)).unwrap(); + assert_eq!(expansion_display, "logic generated ;"); + assert_eq!(provenance.expansion.display_range, TextRange::new(0.into(), 17.into())); + + let logic = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "logic") + .expect("macro body token should be present"); + let TokenProvenance::MacroBody { source, range, .. } = &logic.provenance else { + panic!("logic should come from the macro body: {logic:?}"); + }; + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, *range), "logic"); + assert_eq!(logic.display_range, TextRange::new(0.into(), 5.into())); + + let generated = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "generated") + .expect("macro argument token should be present"); + let TokenProvenance::MacroArgument { source, range, argument_index, .. } = + &generated.provenance + else { + panic!("generated should come from the macro argument: {generated:?}"); + }; + assert_eq!(*argument_index, 0); + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, *range), "generated"); + assert_eq!(generated.display_range, TextRange::new(6.into(), 15.into())); +} + +#[test] +fn preproc_maps_nested_actual_argument_macro_usage_without_dropping_expansion() { + let root_text = r#"`define PAYL payload_i +`define NEXT(x) ((x) + 12'd1) +module top(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`PAYL); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let payl = macro_reference_definitions_at(&db, TOP, offset_after(root_text, "`NEXT(")) + .unwrap() + .expect("nested actual-argument macro reference should be mapped"); + assert_eq!(text_at_range(root_text, payl.range), "`PAYL"); + assert!( + payl.definitions + .iter() + .any(|definition| { definition.file_id == TOP && definition.name.as_str() == "PAYL" }) + ); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`NEXT")).unwrap().unwrap(); + let argument = provenance + .expansion + .call + .arguments + .iter() + .find(|argument| argument.argument_index == 0) + .expect("NEXT call should expose its written actual argument"); + assert_eq!(argument.source.as_ref().and_then(MappedPreprocSource::file_id), Some(TOP)); + assert_eq!(text_at_range(root_text, argument.range.unwrap()), "`PAYL"); + assert_eq!(argument.tokens, vec![SmolStr::new("`PAYL")]); + + let payload = provenance + .tokens + .iter() + .find(|token| token.text.as_str() == "payload_i") + .expect("expanded payload token should stay in NEXT expansion provenance"); + assert!(matches!( + payload.provenance, + TokenProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + )) + )); + + let payl_offset = offset(root_text, "`PAYL"); + let queries = macro_expansion_queries_at(&db, TOP, payl_offset).unwrap(); + assert!(queries.iter().any(|query| matches!( + query, + MacroExpansionQuery::Available(expansion) + if expansion.definition.name.as_str() == "NEXT" + ))); + assert!(queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); + assert!(matches!( + immediate_macro_expansion_at(&db, TOP, payl_offset), + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + }) + )); + assert!(matches!( + macro_expansion_provenance_at(&db, TOP, payl_offset), + Err(PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + }) + )); +} + +#[test] +fn preproc_numeric_literal_expansion_display_is_not_source_buffer() { + let root_text = r#"`define ONE 12'd1 +module top; +localparam int W = `ONE; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ONE")).unwrap().unwrap(); + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); + + let display_text = mapped + .source_map + .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) + .unwrap(); + assert!(display_text.contains("12")); + assert!(display_text.contains("'d")); + assert!(display_text.contains("1")); +} + +#[test] +fn preproc_escaped_identifier_expansion_display_is_not_source_buffer() { + let root_text = concat!( + "`define ESCAPED \\escaped.name \n", + "module top;\n", + "wire `ESCAPED;\n", + "endmodule\n", + ); + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, "`ESCAPED")).unwrap().unwrap(); + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + assert_expansion_is_display_only_source_buffer(mapped, &provenance.expansion); + + let display_text = mapped + .source_map + .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) + .unwrap(); + assert!(display_text.contains("\\escaped.name")); +} + +#[test] +fn macro_generated_declaration_hir_range_resolves_to_expanded_token_provenance() { + let root_text = r#"`define MAKE_DECL(name) logic name; +module top; +`MAKE_DECL(generated) +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); + let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); + let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); + let (declaration_id, _) = + module.declarations.iter().next().expect("generated declaration should lower to HIR"); + let declaration_src = module_src_map + .get(declaration_id) + .expect("generated declaration should keep a source-map range"); + + let provenance = + macro_expansion_provenance_for_range(&db, TOP, declaration_src.range()).unwrap().unwrap(); + + assert_eq!(provenance.expansion.emitted_token_range.len, 3); + assert!( + provenance + .tokens + .iter() + .any(|token| matches!(token.provenance, TokenProvenance::MacroBody { .. })) + ); + assert!( + provenance + .tokens + .iter() + .any(|token| matches!(token.provenance, TokenProvenance::MacroArgument { .. })) + ); +} + +#[test] +fn diagnostic_provenance_for_range_spanning_two_macro_calls_is_ambiguous() { + let root_text = r#"`define A 1 +`define B 2 +module top; +localparam int W = `A + `B; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let range = TextRange::new(offset(root_text, "`A"), offset_after(root_text, "`B")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, range).unwrap().unwrap(); + + assert!(matches!( + provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::AmbiguousDiagnosticProvenance { + targets: 2 + }) + )); + let expansion_error = macro_expansion_provenances_for_range(&db, TOP, range).unwrap_err(); + assert!(matches!( + expansion_error, + PreprocError::Unavailable { + reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } + } + )); +} + +#[test] +fn diagnostic_provenance_for_adjacent_macro_calls_only_hits_intersecting_call() { + let root_text = r#"`define ID(x) x +module top; +localparam int W = `ID(1)`ID(2); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let two_range = TextRange::new(offset(root_text, "`ID(2)"), offset_after(root_text, "`ID(2)")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, two_range).unwrap().unwrap(); + + let DiagnosticProvenance::MacroArgument { call, argument_index, source, range } = provenance + else { + panic!("adjacent single-call range should resolve precisely: {provenance:?}"); + }; + assert_eq!(text_at_range(root_text, call.range), "`ID(2)"); + assert_eq!(argument_index, 0); + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, range), "2"); +} + +#[test] +fn diagnostic_provenance_for_nested_macro_call_range_is_precise() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module top; +localparam int W = `WRAP; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let leaf_range = TextRange::new(offset(root_text, "`LEAF"), offset_after(root_text, "`LEAF")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, leaf_range).unwrap().unwrap(); + + let DiagnosticProvenance::MacroBody { call, source, range, .. } = provenance else { + panic!("nested macro call range should resolve precisely"); + }; + assert_eq!(text_at_range(root_text, call.range), "`LEAF"); + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, range), "3"); +} + +#[test] +fn diagnostic_provenance_returns_unavailable_for_unsupported_expansion_mapping() { + let root_text = r#"`define JOIN(a,b) a``b +`define STR(x) `"x`" +module top; +wire `JOIN(foo,bar); +string s = `STR(foo); +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let call_range = + TextRange::new(offset(root_text, "`JOIN"), offset_after(root_text, "`JOIN(foo,bar)")); + + let provenance = diagnostic_provenance_for_range(&db, TOP, call_range).unwrap().unwrap(); + assert!(matches!(provenance, DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(_)))); + + let stringification_range = + TextRange::new(offset(root_text, "`STR"), offset_after(root_text, "`STR(foo)")); + let provenance = + diagnostic_provenance_for_range(&db, TOP, stringification_range).unwrap().unwrap(); + assert!( + matches!( + &provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance + | SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + | SourcePreprocUnavailable::ExpansionAuthorityUnavailable + )) + ), + "stringification should be unsupported or unavailable, got {provenance:?}" + ); +} + +#[test] +fn diagnostic_provenance_for_unbacked_predefine_expansion_is_structured_unavailable() { + let root_text = r#"module top; +`MAKE_CHILD +endmodule +"#; + let db = db_with_entries_and_predefines( + &[(TOP, "rtl/top.v", root_text)], + vec!["MAKE_CHILD=child u();".to_owned()], + ); + let (hir_file, _) = db.hir_file_with_source_map(TOP.into()); + let (local_module_id, _) = hir_file.modules.iter().next().unwrap(); + let module_id: ModuleId = InFile::new(TOP.into(), local_module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); + let (instantiation_id, _) = module + .instantiations + .iter() + .next() + .expect("predefine expansion should lower to a module instantiation"); + let instantiation_src = module_src_map + .get(instantiation_id) + .expect("generated instantiation should keep a source-map range"); + + let provenance = + diagnostic_provenance_for_range(&db, TOP, instantiation_src.range()).unwrap().unwrap(); + + assert!(matches!( + provenance, + DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } + | SourcePreprocUnavailable::ExpansionAuthorityUnavailable + )) + )); +} + +#[test] +fn preproc_nested_include_chain_maps_to_file_ids() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `LEAF_WIDTH; +endmodule +"#; + let header_text = "`include \"leaf.vh\"\n"; + let leaf_text = "`define LEAF_WIDTH 4\n"; + let db = db_with_nested_files(root_text, header_text, leaf_text); + + let resolution = + macro_usage_resolution_at(&db, TOP, offset(root_text, "LEAF_WIDTH")).unwrap().unwrap(); + + assert_eq!(resolution.definition.file_id, LEAF); + assert_eq!(resolution.definition_provenance.file_id, LEAF); + assert_eq!(resolution.include_chain.len(), 2); + assert_eq!(resolution.include_chain[0].include_file_id, TOP); + assert_eq!(resolution.include_chain[0].included_file_id, HEADER); + assert!( + text_at_range(root_text, resolution.include_chain[0].include_range).contains("defs.vh") + ); + assert_eq!(resolution.include_chain[1].include_file_id, HEADER); + assert_eq!(resolution.include_chain[1].included_file_id, LEAF); + assert!( + text_at_range(header_text, resolution.include_chain[1].include_range).contains("leaf.vh") + ); +} + +#[test] +fn preproc_unsaved_include_buffer_updates_query_result() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `HEADER_WIDTH; +endmodule +"#; + let mut db = db_with_files(root_text, "`define OTHER_WIDTH 8\n"); + + assert!( + macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")).unwrap().is_none() + ); + + db.set_file_text_with_durability( + HEADER, + Arc::from("`define HEADER_WIDTH 16\n"), + Durability::LOW, + ); + + let resolution = + macro_usage_resolution_at(&db, TOP, offset(root_text, "HEADER_WIDTH")).unwrap().unwrap(); + assert_eq!(resolution.definition.file_id, HEADER); + assert_eq!(resolution.definition.name.as_str(), "HEADER_WIDTH"); +} + +#[test] +fn preproc_visible_macro_names_include_predefines_without_file_mapping() { + let root_text = r#"`define A005_LOCAL 1 +module top; +localparam int W = `A005_; +endmodule +"#; + let db = db_with_entries_and_predefines( + &[(TOP, "rtl/top.v", root_text)], + vec!["A005_MAGIC=42".to_owned()], + ); + + let names = visible_macro_names_at(&db, TOP, offset_after(root_text, "`A005_")).unwrap(); + + assert!(names.iter().any(|name| name == "A005_LOCAL"), "{names:?}"); + assert!(names.iter().any(|name| name == "A005_MAGIC"), "{names:?}"); +} + +#[test] +fn preproc_single_offset_contexts_exclude_unrelated_profile_models() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `HEADER_WIDTH; +endmodule +"#; + let header_text = "`define HEADER_WIDTH 8\n"; + let unrelated_header_text = "`define UNUSED_WIDTH 16\n"; + let db = db_with_nested_files(root_text, header_text, unrelated_header_text); + + let contexts = source_preproc_single_query_contexts(&db, HEADER); + + assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); + assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); + assert!( + !contexts.model_file_ids.contains(&LEAF), + "single-offset query contexts should not include unrelated profile model: {contexts:?}" + ); +} + +#[test] +fn preproc_partial_context_index_is_structured_unavailable() { + let contexts = SourcePreprocQueryContexts { + model_file_ids: Vec::new(), + status: SourcePreprocContextStatus::Partial { skipped_models: 2 }, + }; + + let error = finish_empty_single_query(&contexts, None).unwrap_err(); + + assert!(matches!( + error, + PreprocError::Unavailable { + reason: PreprocUnavailable::PartialPreprocContextIndex { skipped_models: 2 } + } + )); +} + +#[test] +fn preproc_manifest_predefine_definition_uses_manifest_provenance() { + let root_text = r#"`ifdef Z_FROM_MANIFEST +module top; +localparam int W = `Z_FROM_MANIFEST; +endmodule +`endif +"#; + let manifest_text = "defines = [\"A_OTHER=2\", \"Z_FROM_MANIFEST=1\"]\n"; + let manifest_range = TextRange::new( + offset(manifest_text, "\"Z_FROM_MANIFEST=1\""), + offset_after(manifest_text, "\"Z_FROM_MANIFEST=1\""), + ); + let other_range = TextRange::new( + offset(manifest_text, "\"A_OTHER=2\""), + offset_after(manifest_text, "\"A_OTHER=2\""), + ); + let predefine = Predefine::with_source( + "Z_FROM_MANIFEST=1", + PredefineSource { path: abs_path("vide.toml"), range: manifest_range }, + ); + let other_predefine = Predefine::with_source( + "A_OTHER=2", + PredefineSource { path: abs_path("vide.toml"), range: other_range }, + ); + let db = db_with_entries_and_predefine_entries( + &[(TOP, "rtl/top.v", root_text), (MANIFEST, "vide.toml", manifest_text)], + vec![other_predefine, predefine], + ); + + let resolution = + macro_reference_definitions_at(&db, TOP, offset(root_text, "Z_FROM_MANIFEST;")) + .unwrap() + .unwrap(); + assert!( + resolution.definitions.iter().any(|definition| { + definition.file_id == MANIFEST && definition.name_range == manifest_range + }), + "predefine reference should target the manifest source range: {resolution:?}" + ); + + let definition = macro_definition_at(&db, MANIFEST, manifest_range.start()).unwrap().unwrap(); + assert_eq!(definition.file_id, MANIFEST); + assert_eq!(definition.name.as_str(), "Z_FROM_MANIFEST"); + assert_eq!(definition.name_range, manifest_range); + assert_eq!(text_at_range(manifest_text, definition.name_range), "\"Z_FROM_MANIFEST=1\""); + + let references = macro_references(&db, MANIFEST, &definition).unwrap(); + assert!( + references.references.iter().any(|reference| { + reference.file_id == TOP + && text_at_range(root_text, reference.range) == "Z_FROM_MANIFEST" + }), + "manifest predefine definition should find source references: {references:?}" + ); +} + +#[test] +fn preproc_visible_macro_names_follow_define_undef_boundaries() { + let root_text = r#"`define A005_LOCAL 1 +`undef A005_LOCAL +`define A005_NEXT 2 +module top; +localparam int W = `A005_; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let names_after_define = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_LOCAL 1\n")) + .unwrap(); + let names_after_undef = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`undef A005_LOCAL\n")).unwrap(); + let names_after_next = + visible_macro_names_at(&db, TOP, offset_after(root_text, "`define A005_NEXT 2\n")).unwrap(); + + assert!(names_after_define.iter().any(|name| name == "A005_LOCAL")); + assert!(!names_after_undef.iter().any(|name| name == "A005_LOCAL")); + assert!(names_after_next.iter().any(|name| name == "A005_NEXT")); +} + +#[test] +fn preproc_inactive_branch_uses_header_define() { + let root_text = r#"`include "defs.vh" +`ifndef HEADER_FLAG +wire disabled_by_header; +`endif +wire active; +"#; + let header_text = "`define HEADER_FLAG\n"; + let db = db_with_files(root_text, header_text); + + let branches = inactive_branches(&db, TOP).unwrap(); + assert_eq!(branches.len(), 1); + assert_eq!(branches[0].file_id, TOP); + assert!(text_at_range(root_text, branches[0].range).contains("disabled_by_header")); +} + +#[test] +fn preproc_included_define_references_include_root_conditionals() { + let root_text = r#"`include "defs.vh" +`ifdef HEADER_FLAG +localparam int ENABLED = `HEADER_FLAG; +`endif +"#; + let header_text = "`define HEADER_FLAG 1\n"; + let db = db_with_files(root_text, header_text); + let definition = + macro_definition_at(&db, HEADER, offset_after(header_text, "`define ")).unwrap().unwrap(); + + assert_eq!(definition.source.file_id(), Some(HEADER)); + assert!(matches!(definition.capability, PreprocAvailability::Complete)); + + let refs = macro_references(&db, HEADER, &definition).unwrap().references; + + assert!(refs.iter().any(|reference| { + reference.file_id == TOP && text_at_range(root_text, reference.range) == "HEADER_FLAG" + })); + assert!(refs.iter().any(|reference| { + reference.file_id == TOP + && matches!( + reference.resolution, + MacroResolution::Resolved { reason: MacroResolutionReason::VisibleDefinition, .. } + ) + && text_at_range(root_text, reference.range) == "HEADER_FLAG" + })); + + let definitions = + macro_reference_definitions_at(&db, TOP, offset_after(root_text, "ENABLED = `")) + .unwrap() + .unwrap(); + assert_eq!(text_at_range(root_text, definitions.range), "`HEADER_FLAG"); + assert!(macro_reference_definitions_at(&db, TOP, definitions.range.end()).unwrap().is_none()); + assert!(macro_usage_resolution_at(&db, TOP, definitions.range.end()).unwrap().is_none()); + assert!(matches!(definitions.capability, PreprocAvailability::Complete)); + assert!(definitions.definitions.iter().any(|indexed| { + indexed.file_id == HEADER + && indexed.name_range == definition.name_range + && indexed.name == definition.name + })); +} + +#[test] +fn preproc_header_ifdef_reference_uses_including_root_context() { + let root_text = r#"`include "defs.vh" +`include "leaf.vh" +"#; + let header_text = "`define FEATURE_B 1\n"; + let leaf_text = r#"`ifdef FEATURE_B +wire enabled; +`endif +"#; + let db = db_with_nested_files(root_text, header_text, leaf_text); + + let definitions = + macro_reference_definitions_at(&db, LEAF, offset(leaf_text, "FEATURE_B")).unwrap().unwrap(); + + assert_eq!(text_at_range(leaf_text, definitions.range), "FEATURE_B"); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == HEADER + && text_at_range(header_text, definition.name_range) == "FEATURE_B" + })); +} + +#[test] +fn preproc_header_macro_body_references_use_expansion_context() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `DEMO_WIDTH; +localparam int N = `DEMO_NEXT(1); +localparam int R = `DEMO_RESET; +endmodule +"#; + let header_text = r#"`ifndef SHARED_DEFS_SVH +`define SHARED_DEFS_SVH +`include "leaf.vh" +`define DEMO_WIDTH `MATH_WIDTH +`define DEMO_RESET {`DEMO_WIDTH{1'b0}} +`define DEMO_NEXT(value) ((value) + `MATH_ONE) +`endif +"#; + let leaf_text = r#"`define MATH_WIDTH 12 +`define MATH_ONE 12'd1 +"#; + let db = db_with_nested_files(root_text, header_text, leaf_text); + + let math_width = macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_WIDTH")) + .unwrap() + .unwrap(); + assert!(math_width.definitions.iter().any(|definition| { + definition.file_id == LEAF + && text_at_range(leaf_text, definition.name_range) == "MATH_WIDTH" + })); + + let math_one = macro_reference_definitions_at(&db, HEADER, offset(header_text, "MATH_ONE")) + .unwrap() + .unwrap(); + assert!(math_one.definitions.iter().any(|definition| { + definition.file_id == LEAF && text_at_range(leaf_text, definition.name_range) == "MATH_ONE" + })); + + let demo_width = macro_reference_definitions_at( + &db, + HEADER, + offset_after(header_text, "`define DEMO_RESET {`"), + ) + .unwrap() + .unwrap(); + assert!(demo_width.definitions.iter().any(|definition| { + definition.file_id == HEADER + && text_at_range(header_text, definition.name_range) == "DEMO_WIDTH" + })); +} + +#[test] +fn preproc_macro_param_references_resolve_to_formals() { + let root_text = r#"`include "defs.vh" +module top; +localparam int W = `SHIFT(4, 1); +endmodule +"#; + let header_text = "`define SHIFT(value, amount) ((value) << amount)\n"; + let db = db_with_files(root_text, header_text); + + let value_definition = + macro_param_definition_at(&db, HEADER, offset_after(header_text, "SHIFT(")) + .unwrap() + .unwrap(); + assert_eq!(value_definition.name.as_str(), "value"); + assert_eq!(text_at_range(header_text, value_definition.range), "value"); + assert!( + macro_param_definition_at(&db, HEADER, value_definition.range.end()).unwrap().is_none() + ); + + let value_reference = macro_param_reference_definitions_at( + &db, + HEADER, + offset_after(header_text, "SHIFT(value, amount) (("), + ) + .unwrap() + .unwrap(); + assert_eq!(text_at_range(header_text, value_reference.range), "value"); + assert!( + macro_param_reference_definitions_at(&db, HEADER, value_reference.range.end()) + .unwrap() + .is_none() + ); + assert!(value_reference.definitions.iter().any(|definition| { + definition.param_index == value_definition.param_index + && text_at_range(header_text, definition.range) == "value" + })); + + let refs = macro_param_references(&db, HEADER, &value_definition).unwrap().references; + assert!(refs.iter().any(|reference| { + reference.file_id == HEADER && text_at_range(header_text, reference.range) == "value" + })); + assert!(!refs.iter().any(|reference| text_at_range(header_text, reference.range) == "amount")); +} + +#[test] +fn preproc_header_reference_reports_all_including_context_definitions() { + let root_text = r#"`define WIDTH 8 +`include "defs.vh" +`undef WIDTH +`define WIDTH 16 +`include "defs.vh" +"#; + let header_text = "localparam int W = `WIDTH;\n"; + let db = db_with_files(root_text, header_text); + + let definitions = + macro_reference_definitions_at(&db, HEADER, offset(header_text, "WIDTH")).unwrap().unwrap(); + + assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); + assert_eq!(definitions.definitions.len(), 2); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) + })); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) + })); +} + +#[test] +fn preproc_header_macro_body_reference_reports_all_expansion_context_definitions() { + let root_text = r#"`define WIDTH 8 +`include "defs.vh" +localparam int A = `USE_WIDTH; +`undef WIDTH +`define WIDTH 16 +`include "defs.vh" +localparam int B = `USE_WIDTH; +"#; + let header_text = "`define USE_WIDTH `WIDTH\n"; + let db = db_with_files(root_text, header_text); + + let definitions = + macro_reference_definitions_at(&db, HEADER, offset_after(header_text, "USE_WIDTH `")) + .unwrap() + .unwrap(); + + assert_eq!(text_at_range(header_text, definitions.range), "`WIDTH"); + assert_eq!(definitions.definitions.len(), 2); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 0) + })); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP + && definition.name_range.start() == offset_after_n(root_text, "`define ", 1) + })); +} + +#[test] +fn preproc_macro_definition_at_only_hits_name_range() { + let root_text = "`define HEADER_FLAG 1\n"; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + assert!(macro_definition_at(&db, TOP, offset(root_text, "`define")).unwrap().is_none()); + + let definition = + macro_definition_at(&db, TOP, offset(root_text, "HEADER_FLAG")).unwrap().unwrap(); + assert_eq!(text_at_range(root_text, definition.name_range), "HEADER_FLAG"); + assert!(macro_definition_at(&db, TOP, definition.name_range.end()).unwrap().is_none()); + assert_ne!(definition.directive_range, definition.name_range); +} + +#[test] +fn preproc_ifndef_guard_reference_resolves_to_following_define() { + let root_text = "`include \"defs.vh\"\n"; + let header_text = r#"`ifndef HEADER_FLAG +`define HEADER_FLAG +`endif +"#; + let db = db_with_files(root_text, header_text); + let resolution = + macro_reference_definitions_at(&db, HEADER, offset(header_text, "HEADER_FLAG")) + .unwrap() + .unwrap(); + + assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); + let definition = + resolution.definitions.iter().find(|definition| definition.file_id == HEADER).unwrap(); + assert_eq!(text_at_range(header_text, definition.name_range), "HEADER_FLAG"); + + let refs = macro_references(&db, HEADER, definition).unwrap().references; + assert!(refs.iter().any(|reference| { + reference.file_id == HEADER + && reference.range.start() == offset(header_text, "HEADER_FLAG") + && text_at_range(header_text, reference.range) == "HEADER_FLAG" + })); +} + +#[test] +fn preproc_project_header_guard_reference_is_indexed_without_include() { + let root_text = "module top; endmodule\n"; + let header_text = r#"`ifndef HEADER_FLAG +`define HEADER_FLAG +`endif +"#; + let db = db_with_files(root_text, header_text); + let resolution = + macro_reference_definitions_at(&db, HEADER, offset(header_text, "HEADER_FLAG")) + .unwrap() + .unwrap(); + + assert!(resolution.references.iter().any(|reference| reference.file_id == HEADER)); + assert!(resolution.definitions.iter().any(|definition| { + definition.file_id == HEADER + && text_at_range(header_text, definition.name_range) == "HEADER_FLAG" + })); +} diff --git a/crates/hir/src/preproc/types.rs b/crates/hir/src/preproc/types.rs new file mode 100644 index 00000000..3a446463 --- /dev/null +++ b/crates/hir/src/preproc/types.rs @@ -0,0 +1,607 @@ +use std::collections::BTreeMap; + +use preproc::source::{ + SourceEmittedTokenId, SourceEmittedTokenRange, SourceIncludeDirectiveId, SourceMacroCallId, + SourceMacroDefinitionId, SourceMacroExpansionId, SourceMacroReferenceId, SourcePreprocError, + SourcePreprocUnavailable, +}; +use smol_str::SmolStr; +use utils::{ + line_index::{TextRange, TextSize}, + uniq_vec::UniqVec, +}; +use vfs::{FileId, VfsPath}; + +use crate::base_db::source_db::{ + PreprocSourceMapError, PreprocVirtualOrigin, SourcePreprocQueryError, +}; + +pub type PreprocResult = Result; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocError { + SourceQuery(SourcePreprocQueryError), + MissingRootSource, + UnmappedSource { + buffer_id: u32, + }, + MismatchedDefinitionRangeFiles { + event_id: u32, + directive_file_id: FileId, + name_file_id: FileId, + }, + MismatchedReferenceRangeFiles { + event_id: u32, + directive_file_id: FileId, + name_file_id: FileId, + }, + MissingDefinitionNameRange { + event_id: u32, + }, + SourceMap(PreprocSourceMapError), + Unavailable { + reason: PreprocUnavailable, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocUnavailable { + Source(SourcePreprocUnavailable), + AmbiguousMacroReferenceContexts { contexts: usize }, + AmbiguousMacroExpansionContexts { contexts: usize }, + AmbiguousMacroParamContexts { contexts: usize }, + AmbiguousMacroDefinitionContexts { contexts: usize }, + AmbiguousDiagnosticProvenance { targets: usize }, + AmbiguousIncludeTargets { targets: usize }, + PartialPreprocContextIndex { skipped_models: usize }, + DisplayOnlyVirtualExpansion { path: VfsPath, origin: PreprocVirtualOrigin }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreprocAvailability { + Complete, + Partial, + Unavailable(PreprocUnavailable), +} + +macro_rules! mapped_preproc_id { + ($name:ident, $core:ty) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct $name($core); + + impl $name { + pub fn raw(self) -> usize { + self.0.raw() + } + } + + impl From<$core> for $name { + fn from(value: $core) -> Self { + Self(value) + } + } + }; +} + +mapped_preproc_id!(MacroReferenceId, SourceMacroReferenceId); +mapped_preproc_id!(IncludeDirectiveId, SourceIncludeDirectiveId); +mapped_preproc_id!(MacroCallId, SourceMacroCallId); +mapped_preproc_id!(MacroExpansionId, SourceMacroExpansionId); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MacroDefinitionId { + Source(SourceMacroDefinitionId), + ConfiguredPredefine { file_id: FileId, range: TextRange }, +} + +impl From for MacroDefinitionId { + fn from(value: SourceMacroDefinitionId) -> Self { + Self::Source(value) + } +} + +pub(crate) const CONFIGURED_PREDEFINE_DEFINE_INDEX: usize = usize::MAX; +pub(crate) const CONFIGURED_PREDEFINE_EVENT_ID: u32 = u32::MAX; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MappedPreprocSource { + RealFile { file_id: FileId }, + VirtualFile { file_id: FileId, path: vfs::VfsPath, origin: PreprocVirtualOrigin }, + VirtualDisplay { path: vfs::VfsPath, origin: PreprocVirtualOrigin }, +} + +impl MappedPreprocSource { + pub fn file_id(&self) -> Option { + match self { + Self::RealFile { file_id } | Self::VirtualFile { file_id, .. } => Some(*file_id), + Self::VirtualDisplay { .. } => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroResolution { + Resolved { + definition_id: MacroDefinitionId, + reason: MacroResolutionReason, + include_chain: Vec, + }, + Undefined, + Unavailable(PreprocUnavailable), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MacroResolutionReason { + VisibleDefinition, + IncludeGuardIfNDef, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefinition { + pub id: MacroDefinitionId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub name: SmolStr, + pub params: Option>, + pub body_tokens: Vec, + pub define_index: usize, + pub event_id: u32, + pub directive_range: TextRange, + pub name_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefinitionParam { + pub param_index: usize, + pub name: Option, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamDefinition { + pub macro_definition: MacroDefinition, + pub param_index: usize, + pub name: SmolStr, + pub range: TextRange, + pub param_range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReference { + pub macro_definition: MacroDefinition, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub param_index: usize, + pub token_index: usize, + pub name: SmolStr, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReferenceDefinitions { + pub references: Vec, + pub range: TextRange, + pub definitions: Vec, + pub capability: PreprocAvailability, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParamReferences { + pub references: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUsage { + pub reference_id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub name: SmolStr, + pub usage_index: usize, + pub directive_range: TextRange, + pub range: TextRange, + pub resolution: MacroResolution, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUsageResolution { + pub usage: MacroUsage, + pub definition: MacroDefinition, + pub definition_provenance: MacroDefinitionProvenance, + pub include_chain: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefinitionProvenance { + pub id: MacroDefinitionId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub event_id: u32, + pub file_id: FileId, + pub directive_range: TextRange, + pub name_range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeChainEntry { + pub include_event_id: u32, + pub include_file_id: FileId, + pub include_range: TextRange, + pub included_file_id: FileId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReference { + pub id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub name: SmolStr, + pub directive_range: TextRange, + pub range: TextRange, + pub resolution: MacroResolution, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReferenceResolution { + pub reference: MacroReference, + pub definition: MacroDefinition, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReferenceDefinitions { + pub references: Vec, + pub range: TextRange, + pub definitions: Vec, + pub capability: PreprocAvailability, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroCall { + pub id: MacroCallId, + pub reference_id: MacroReferenceId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub arguments: Vec, + pub directive_range: TextRange, + pub range: TextRange, + pub callee: MacroResolution, + pub expansion: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroArgument { + pub argument_index: usize, + pub source: Option, + pub range: Option, + pub tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansion { + pub id: MacroExpansionId, + pub call: MacroCall, + pub definition_id: MacroDefinitionId, + pub definition: MacroDefinition, + pub emitted_token_range: SourceEmittedTokenRange, + pub display_source: MappedPreprocSource, + pub display_range: TextRange, + pub child_calls: Vec, + pub capability: PreprocAvailability, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansionProvenance { + pub expansion: MacroExpansion, + pub tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmittedTokenProvenance { + pub token: SourceEmittedTokenId, + pub text: SmolStr, + pub display_range: TextRange, + pub provenance: TokenProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenProvenance { + SourceToken { + source: MappedPreprocSource, + range: TextRange, + }, + MacroBody { + call: MacroCall, + definition_id: MacroDefinitionId, + source: MappedPreprocSource, + range: TextRange, + }, + MacroArgument { + call: MacroCall, + argument_index: usize, + source: MappedPreprocSource, + range: TextRange, + }, + Predefine { + source: MappedPreprocSource, + }, + Builtin { + name: SmolStr, + }, + Unavailable(PreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiagnosticProvenance { + SourceToken { + source: MappedPreprocSource, + range: TextRange, + }, + MacroBody { + call: MacroCall, + definition_id: MacroDefinitionId, + source: MappedPreprocSource, + range: TextRange, + }, + MacroArgument { + call: MacroCall, + argument_index: usize, + source: MappedPreprocSource, + range: TextRange, + }, + VirtualExpansion { + source: MappedPreprocSource, + range: TextRange, + }, + Unavailable(PreprocUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroExpansionQuery { + Available(Box), + Ambiguous(Vec), + Unavailable(Box), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroExpansionUnavailable { + pub call: MacroCall, + pub reason: PreprocUnavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecursiveMacroExpansion { + pub root_call: MacroCall, + pub expansions: Vec, + pub unavailable: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecursiveMacroExpansionProvenance { + pub root_call: MacroCall, + pub expansions: Vec, + pub unavailable: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct MacroDefinitionKey { + file_id: FileId, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroDefinitionKey { + pub(crate) fn from_definition(definition: &MacroDefinition) -> Self { + Self { + file_id: definition.file_id, + range_start: definition.name_range.start(), + range_end: definition.name_range.end(), + name: definition.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct MacroReferenceKey { + file_id: FileId, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroReferenceKey { + pub(crate) fn from_reference(reference: &MacroReference) -> Self { + Self { + file_id: reference.file_id, + range_start: reference.range.start(), + range_end: reference.range.end(), + name: reference.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct MacroParamDefinitionKey { + macro_definition: MacroDefinitionKey, + param_index: usize, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroParamDefinitionKey { + pub(crate) fn from_definition(definition: &MacroParamDefinition) -> Self { + Self { + macro_definition: MacroDefinitionKey::from_definition(&definition.macro_definition), + param_index: definition.param_index, + range_start: definition.range.start(), + range_end: definition.range.end(), + name: definition.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct MacroParamReferenceKey { + macro_definition: MacroDefinitionKey, + param_index: usize, + file_id: FileId, + range_start: TextSize, + range_end: TextSize, + name: SmolStr, +} + +impl MacroParamReferenceKey { + pub(crate) fn from_reference(reference: &MacroParamReference) -> Self { + Self { + macro_definition: MacroDefinitionKey::from_definition(&reference.macro_definition), + param_index: reference.param_index, + file_id: reference.file_id, + range_start: reference.range.start(), + range_end: reference.range.end(), + name: reference.name.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct InactiveBranchKey { + file_id: FileId, + range_start: TextSize, + range_end: TextSize, +} + +impl InactiveBranchKey { + pub(crate) fn from_branch(branch: &InactiveBranch) -> Self { + Self { + file_id: branch.file_id, + range_start: branch.range.start(), + range_end: branch.range.end(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MacroReferenceIndex { + references_by_definition: + BTreeMap>, + definitions_by_reference: + BTreeMap>, + issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReferences { + pub references: Vec, + pub status: MacroReferenceIndexStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroReferenceIndexStatus { + Complete, + Partial { issues: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroReferenceIndexIssue { + SkippedModel { + file_id: FileId, + error: PreprocError, + }, + UnavailableReference { + file_id: FileId, + reference_id: MacroReferenceId, + reason: PreprocUnavailable, + }, +} + +impl MacroReferenceIndex { + pub fn references_for(&self, definition: &MacroDefinition) -> Vec { + self.references_by_definition + .get(&MacroDefinitionKey::from_definition(definition)) + .map(|references| references.as_slice().to_vec()) + .unwrap_or_default() + } + + pub fn definitions_for_reference( + &self, + reference: &MacroReference, + ) -> Option<&[MacroDefinition]> { + self.definitions_by_reference + .get(&MacroReferenceKey::from_reference(reference)) + .map(UniqVec::as_slice) + } + + pub fn status(&self) -> MacroReferenceIndexStatus { + if self.issues.is_empty() { + MacroReferenceIndexStatus::Complete + } else { + MacroReferenceIndexStatus::Partial { issues: self.issues.clone() } + } + } + + pub(super) fn push(&mut self, definition: MacroDefinition, reference: MacroReference) { + let definition_key = MacroDefinitionKey::from_definition(&definition); + let references = self.references_by_definition.entry(definition_key).or_default(); + references.push([MacroReferenceKey::from_reference(&reference)], reference.clone()); + + let reference_key = MacroReferenceKey::from_reference(&reference); + let definitions = self.definitions_by_reference.entry(reference_key).or_default(); + definitions.push([MacroDefinitionKey::from_definition(&definition)], definition); + } + + pub(super) fn push_issue(&mut self, issue: MacroReferenceIndexIssue) { + if !self.issues.contains(&issue) { + self.issues.push(issue); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeDirective { + pub id: IncludeDirectiveId, + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub include_index: usize, + pub range: TextRange, + pub target: IncludeTarget, + pub status: IncludeDirectiveStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InactiveBranch { + pub source: MappedPreprocSource, + pub capability: PreprocAvailability, + pub file_id: FileId, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncludeTarget { + Literal { path: SmolStr, resolved_file: Option }, + Token { raw: SmolStr }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncludeDirectiveStatus { + Resolved { source: MappedPreprocSource }, + Unresolved, + Unavailable(PreprocUnavailable), +} + +impl From for PreprocError { + fn from(value: SourcePreprocQueryError) -> Self { + Self::SourceQuery(value) + } +} + +impl From for PreprocError { + fn from(value: SourcePreprocError) -> Self { + Self::SourceQuery(SourcePreprocQueryError::Model(value)) + } +} diff --git a/crates/utils/src/uniq_vec.rs b/crates/utils/src/uniq_vec.rs index b3b4504e..5343a1bf 100644 --- a/crates/utils/src/uniq_vec.rs +++ b/crates/utils/src/uniq_vec.rs @@ -34,6 +34,14 @@ impl UniqVec { true } + pub fn push_keyed(&mut self, value: T, key: F) -> bool + where + F: FnOnce(&T) -> K, + { + let key = key(&value); + self.push([key], value) + } + pub fn contains(&self, key: &K) -> bool { self.seen.contains(key) } @@ -64,3 +72,20 @@ impl UniqVec { self.push([value.clone()], value) } } + +impl UniqVec { + pub fn push_unique_by(&mut self, value: T, same: F) -> bool + where + F: Fn(&T, &T) -> bool, + { + if self.items.iter().any(|existing| same(existing, &value)) { + return false; + } + self.items.push(value); + true + } + + pub fn push_unique_eq(&mut self, value: T) -> bool { + self.push_unique_by(value, |existing, value| existing == value) + } +} From eb4f0c61d91f8e8ba5fc1099ddf1aa79c35f2e24 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sun, 7 Jun 2026 23:53:06 +0800 Subject: [PATCH 39/80] feat(hover): make macro hover more compact --- crates/ide/src/hover.rs | 12 ++++---- crates/ide/src/markup.rs | 6 ++++ crates/ide/src/verilog_2005.rs | 51 +++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index aba578c2..b646b182 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -230,18 +230,15 @@ fn render_recursive_expansion( if !markup.is_empty() { markup.newline(); } - markup.push_with_code_fence(¯o_definition_line(&root.expansion.definition)); + render_macro_expansion_header(markup, &root.expansion.definition); render_macro_expansion_separator(markup); - render_macro_expansion_label(markup); markup.push_with_code_fence(&expanded_text_from_tokens(&root.tokens)); render_macro_expansion_separator(markup); render_macro_source_link(db, markup, &root.expansion.definition, root.expansion.call.file_id); } -fn render_macro_expansion_label(markup: &mut Markup) { - markup.newline(); - markup.print("Expands to"); - markup.newline(); +fn render_macro_expansion_header(markup: &mut Markup, definition: &MacroDefinition) { + markup.push_with_code_fence(¯o_signature(definition)); } fn render_macro_expansion_separator(markup: &mut Markup) { @@ -493,7 +490,8 @@ fn render_macro_source_link( let Some(source) = macro_definition_source_link(db, definition, anchor_file_id) else { return; }; - markup.print("From ["); + markup.print_with_strong("Macro"); + markup.print(" from ["); markup.print(&markdown_link_label(&source.label)); markup.print("](<"); markup.print(&markdown_link_destination(&source.target)); diff --git a/crates/ide/src/markup.rs b/crates/ide/src/markup.rs index 6072db97..1f7e7b64 100644 --- a/crates/ide/src/markup.rs +++ b/crates/ide/src/markup.rs @@ -36,6 +36,12 @@ impl Markup { self.text.push_str(contents); } + pub fn print_with_strong(&mut self, contents: &str) { + self.text.push_str("**"); + self.text.push_str(contents); + self.text.push_str("**"); + } + pub fn println(&mut self, contents: &str) { self.text.push_str(contents); } diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 145d6937..f5ed6baf 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1490,11 +1490,13 @@ endmodule let info = hover.info.as_str(); assert!( info.contains("```systemverilog") - && info.contains("`define `MAKE_DECL(name) logic name ;") - && info.contains("Expands to") + && info.contains("Macro") + && info.contains("`MAKE_DECL(name)") + && !info.contains("`define `MAKE_DECL(name)") + && !info.contains("Expands to") && info.contains("--------------------") && info.contains("logic generated ;") - && info.contains("From [feature.v]") + && info.contains("from [feature.v]") && !info.contains("Context ") && !info.contains("Signature") && !info.contains("Arguments") @@ -1536,15 +1538,17 @@ endmodule let info = hover.info.as_str(); assert!( info.contains("```systemverilog") - && info.contains("`define `DEMO_NEXT(value)") - && info.contains("`MATH_ONE") - && info.contains("Expands to") + && info.contains("Macro") + && info.contains("`DEMO_NEXT(value)") + && !info.contains("`define `DEMO_NEXT(value)") + && !info.contains("`MATH_ONE") + && !info.contains("Expands to") && info.contains("--------------------") && info.contains("( ( payload_i ) + 12 'd 1 )") && info.contains("payload_i") && info.contains("12") && info.contains("'d") - && info.contains("From [feature.v]") + && info.contains("from [feature.v]") && !info.contains("Context ") && !info.contains("Expansion steps"), "nested macro hover should show compact signature, result, and source: {info}" @@ -1571,14 +1575,16 @@ endmodule let call_info = call_hover.info.as_str(); assert!( call_info.contains("```systemverilog") - && call_info.contains("`define `DEMO_NEXT(value)") - && call_info.contains("`MATH_ONE") - && call_info.contains("Expands to") + && call_info.contains("Macro") + && call_info.contains("`DEMO_NEXT(value)") + && !call_info.contains("`define `DEMO_NEXT(value)") + && !call_info.contains("`MATH_ONE") + && !call_info.contains("Expands to") && call_info.contains("--------------------") && call_info.contains("( ( payload_i ) + 12 'd 1 )") && call_info.contains("payload_i") && call_info.contains("12") - && call_info.contains("From [feature.v]") + && call_info.contains("from [feature.v]") && !call_info.contains("Context ") && !call_info.contains("Expansion steps"), "outer macro hover should keep compact expansion facts: {call_info}" @@ -1604,10 +1610,15 @@ endmodule let payl_info = payl_hover.info.as_str(); assert!( payl_info.contains("```systemverilog") - && payl_info.contains("`define `PAYL payload_i") - && payl_info.contains("From [feature.v]") + && payl_info.contains("Macro") + && payl_info.contains("`PAYL") + && !payl_info.contains("`define `PAYL payload_i") + && !payl_info.contains("Expands to") + && payl_info.contains("--------------------") + && payl_info.contains("payload_i") + && payl_info.contains("from [feature.v]") && !payl_info.contains("unavailable"), - "PAYL hover should show the macro definition display without unavailable text: {payl_info}" + "PAYL hover should show the nested macro expansion without unavailable text: {payl_info}" ); } @@ -1630,7 +1641,7 @@ endmodule assert!( info.contains("```systemverilog") && info.contains("`define `JOIN(a, b) a `` b") - && info.contains("From [feature.v]") + && info.contains("from [feature.v]") && !info.contains("unavailable"), "unsupported expansion hover should show the macro definition display: {info}" ); @@ -1672,7 +1683,7 @@ endmodule hover.info.as_str().contains("`define `LOCAL_WIDTH 8"), "hover should show macro definition" ); - assert!(hover.info.as_str().contains("From [feature.v]"), "hover should show macro source"); + assert!(hover.info.as_str().contains("from [feature.v]"), "hover should show macro source"); let conditional_nav = analysis .goto_definition(position(file_id, &markers, "conditional")) @@ -1797,12 +1808,14 @@ endmodule .expect("included macro hover expected"); assert!(hover.info.as_str().contains("HEADER_WIDTH"), "hover should mention macro name"); assert!( - hover.info.as_str().contains("`define `HEADER_WIDTH 8"), - "hover should show macro definition" + hover.info.as_str().contains("macro") + && hover.info.as_str().contains("`HEADER_WIDTH") + && !hover.info.as_str().contains("`define `HEADER_WIDTH"), + "hover should show macro expansion header" ); assert!(hover.info.as_str().contains("8"), "hover should show macro expansion"); assert!( - hover.info.as_str().contains("From [include/defs.vh]"), + hover.info.as_str().contains("from [include/defs.vh]"), "hover should show project-relative macro source path: {}", hover.info.as_str() ); From 179cb0e307e4ed580b483d331f8ebc6ec1761f33 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 00:19:27 +0800 Subject: [PATCH 40/80] fix(preproc): preserve nested argument macro provenance --- crates/hir/src/preproc/tests.rs | 26 +++-- crates/preproc/src/source/model/tests.rs | 107 +++++++++++++++++- crates/slang/bindings/rust/ffi/wrapper.cc | 70 ++++++++++++ crates/slang/bindings/rust/tests.rs | 99 ++++++++++++++++ .../include/slang/parsing/Preprocessor.h | 3 +- .../source/parsing/Preprocessor_macros.cpp | 7 +- 6 files changed, 298 insertions(+), 14 deletions(-) diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 7c07395a..a87e6222 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -354,12 +354,12 @@ endmodule .iter() .find(|token| token.text.as_str() == "payload_i") .expect("expanded payload token should stay in NEXT expansion provenance"); - assert!(matches!( - payload.provenance, - TokenProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - )) - )); + let TokenProvenance::MacroBody { call, source, range, .. } = &payload.provenance else { + panic!("nested PAYL expansion should keep direct macro body provenance: {payload:?}"); + }; + assert_eq!(source.file_id(), Some(TOP)); + assert_eq!(text_at_range(root_text, *range), "payload_i"); + assert_eq!(text_at_range(root_text, call.range), "`PAYL"); let payl_offset = offset(root_text, "`PAYL"); let queries = macro_expansion_queries_at(&db, TOP, payl_offset).unwrap(); @@ -368,12 +368,18 @@ endmodule MacroExpansionQuery::Available(expansion) if expansion.definition.name.as_str() == "NEXT" ))); - assert!(queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); + assert!(queries.iter().any(|query| matches!( + query, + MacroExpansionQuery::Available(expansion) + if expansion.definition.name.as_str() == "PAYL" + ))); + assert!(!queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); assert!(matches!( immediate_macro_expansion_at(&db, TOP, payl_offset), - Err(PreprocError::Unavailable { - reason: PreprocUnavailable::AmbiguousMacroExpansionContexts { contexts: 2 } - }) + Ok(Some(MacroExpansionQuery::Ambiguous(expansions))) + if expansions.len() == 2 + && expansions.iter().any(|expansion| expansion.definition.name.as_str() == "NEXT") + && expansions.iter().any(|expansion| expansion.definition.name.as_str() == "PAYL") )); assert!(matches!( macro_expansion_provenance_at(&db, TOP, payl_offset), diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index 8777c195..b9dfbe08 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -534,7 +534,112 @@ endmodule panic!("PAYL usage should resolve through its runtime definition identity"); }; assert_eq!(model.macro_definitions().get(*definition).unwrap().name.as_str(), "PAYL"); - assert!(model.macro_calls().iter().any(|call| call.reference == payl_reference.id)); + let payl_call = model + .macro_calls() + .iter() + .find(|call| call.reference == payl_reference.id) + .expect("nested PAYL usage should create a call"); + assert_eq!(payl_call.parent_expansion_identity, next_call.expansion_identity); + + let SourceMacroExpansionQuery::Available(payl_expansion_id) = + model.immediate_macro_expansion(payl_call.id) + else { + panic!("nested PAYL usage should have its own immediate expansion"); + }; + let payl_expansion = model.macro_expansions().get(payl_expansion_id).unwrap(); + assert_eq!(payl_expansion.call, payl_call.id); + + let (payload, payload_identity, payload_body_range) = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroBody { identity, call, body_token_range, .. } = + model.token_provenance().get(token.provenance)? + else { + return None; + }; + (*call == payl_call.id).then_some((token, *identity, *body_token_range)) + }) + .expect("PAYL emitted token should keep direct macro body provenance"); + assert_eq!(payload.text.as_str(), "payload_i"); + assert_eq!(text_at_range(root_text, payload_body_range.range), "payload_i"); + assert_eq!(Some(payload_identity.call), payl_call.identity); + assert_eq!(Some(payload_identity.expansion), payl_call.expansion_identity); + assert_eq!(payload_identity.parent_expansion, next_call.expansion_identity); + assert_eq!(payl_expansion.emitted_token_range.start, payload.id); + assert_eq!(payl_expansion.emitted_token_range.len, 1); + + let recursive = model.recursive_macro_expansion(next_call.id); + assert!(recursive.expansions.contains(&payl_expansion_id)); + assert!(recursive.unavailable.is_empty()); +} + +#[test] +fn source_model_preserves_nested_actual_argument_macro_parent_chain() { + let root_text = r#"`define LEAF payload_i +`define WRAP `LEAF +`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`WRAP); +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let call_by_name = |name: &str| { + model + .macro_calls() + .iter() + .find(|call| { + let reference = model.macro_references().get(call.reference).unwrap(); + reference.name.as_str() == name + && matches!(reference.site, SourceMacroReferenceSite::Usage { .. }) + }) + .unwrap_or_else(|| panic!("{name} usage should create a call")) + }; + + let next_call = call_by_name("NEXT"); + let wrap_call = call_by_name("WRAP"); + let leaf_call = call_by_name("LEAF"); + assert_eq!(wrap_call.parent_expansion_identity, next_call.expansion_identity); + assert_eq!(leaf_call.parent_expansion_identity, wrap_call.expansion_identity); + + let SourceMacroExpansionQuery::Available(next_expansion_id) = + model.immediate_macro_expansion(next_call.id) + else { + panic!("NEXT should have an immediate expansion"); + }; + let SourceMacroExpansionQuery::Available(wrap_expansion_id) = + model.immediate_macro_expansion(wrap_call.id) + else { + panic!("WRAP should have an immediate expansion"); + }; + let SourceMacroExpansionQuery::Available(leaf_expansion_id) = + model.immediate_macro_expansion(leaf_call.id) + else { + panic!("LEAF should have an immediate expansion"); + }; + + let next_recursive = model.recursive_macro_expansion(next_call.id); + assert!(next_recursive.expansions.contains(&next_expansion_id)); + assert!(next_recursive.expansions.contains(&wrap_expansion_id)); + assert!(next_recursive.expansions.contains(&leaf_expansion_id)); + assert!(next_recursive.unavailable.is_empty()); + + let (payload, identity, body_token_range) = model + .emitted_tokens() + .iter() + .find_map(|token| { + let SourceTokenProvenance::MacroBody { call, identity, body_token_range, .. } = + model.token_provenance().get(token.provenance)? + else { + return None; + }; + (*call == leaf_call.id).then_some((token, *identity, *body_token_range)) + }) + .expect("final payload token should keep LEAF body provenance"); + assert_eq!(payload.text.as_str(), "payload_i"); + assert_eq!(identity.parent_expansion, wrap_call.expansion_identity); + assert_eq!(text_at_range(root_text, body_token_range.range), "payload_i"); } #[test] diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index a1a9077f..22c8a466 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -324,6 +324,72 @@ void apply_direct_macro_token_provenance( provenance.argumentTokenIndex != slang::SourceManager::MacroTokenProvenance::InvalidIndex; } +bool apply_original_macro_loc_provenance_for_nested_argument( + ::RawPreprocessorTraceEmittedToken& result, + slang::parsing::Token token, + const slang::SourceManager& sourceManager, + slang::SourceLocation location) { + if (!sourceManager.isMacroArgLoc(location)) + return false; + + auto originalLocation = sourceManager.getOriginalLoc(location); + if (!originalLocation.valid() || !sourceManager.isMacroLoc(originalLocation)) + return false; + + switch (sourceManager.getMacroExpansionKind(originalLocation)) { + case slang::SourceManager::MacroExpansionKind::TokenPaste: + case slang::SourceManager::MacroExpansionKind::Stringification: + return false; + case slang::SourceManager::MacroExpansionKind::Body: + case slang::SourceManager::MacroExpansionKind::Argument: + break; + } + + auto originalProvenance = sourceManager.getMacroTokenProvenance(originalLocation); + if (!has_direct_macro_token_provenance(originalProvenance)) + return false; + + auto originalTokenRange = + slang::SourceRange(originalLocation, originalLocation + token.rawText().length()); + result.macro_name = rust::String(std::string(sourceManager.getMacroName(originalLocation))); + + if (sourceManager.isMacroArgLoc(originalLocation)) { + result.argument_token_range = + to_rust_original_macro_loc_range(sourceManager, originalTokenRange); + + auto formalRange = sourceManager.getExpansionRange(originalLocation); + result.body_token_range = to_rust_original_macro_loc_range(sourceManager, formalRange); + result.call_range = to_rust_macro_argument_callsite_range(sourceManager, formalRange); + + if (originalProvenance->bodyTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + originalProvenance->argumentIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + originalProvenance->argumentTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + result.call_range.has_range && result.body_token_range.has_range && + result.argument_token_range.has_range) { + apply_direct_macro_token_provenance(result, *originalProvenance); + result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT; + return true; + } + return false; + } + + result.call_range = + to_rust_macro_callsite_range_from_macro_loc(sourceManager, originalLocation); + result.body_token_range = to_rust_original_macro_loc_range(sourceManager, originalTokenRange); + if (originalProvenance->bodyTokenIndex != + slang::SourceManager::MacroTokenProvenance::InvalidIndex && + result.call_range.has_range && result.body_token_range.has_range) { + apply_direct_macro_token_provenance(result, *originalProvenance); + result.provenance_kind = TRACE_TOKEN_PROVENANCE_MACRO_BODY; + return true; + } + + return false; +} + ::RawPreprocessorTraceToken empty_preprocessor_trace_token() { ::RawPreprocessorTraceToken token; token.raw_text = rust::String(); @@ -437,6 +503,10 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( result.macro_name = rust::String(macroName); auto directProvenance = sourceManager.getMacroTokenProvenance(location); + if (apply_original_macro_loc_provenance_for_nested_argument( + result, token, sourceManager, location)) + return result; + if (sourceManager.isMacroArgLoc(location)) { auto tokenRange = token.range(); result.argument_token_range = to_rust_original_macro_loc_range(sourceManager, tokenRange); diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index f12fcee4..3c1a8ed5 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1356,6 +1356,105 @@ endmodule assert!(payl.macro_call_id.is_some()); assert!(payl.macro_expansion_id.is_some()); assert_eq!(&source[payl.range.as_ref().unwrap().range.clone()], "`PAYL"); + + let payload = trace + .emitted_tokens + .iter() + .find(|token| { + matches!( + &token.provenance, + PreprocessorTraceTokenProvenance::MacroBody { macro_name, .. } + if macro_name == "PAYL" + ) + }) + .expect("nested PAYL expansion should attribute payload_i to PAYL"); + assert_eq!(payload.raw_text, "payload_i"); + let PreprocessorTraceTokenProvenance::MacroBody { + macro_name, + identity, + call_range, + body_token_range, + } = &payload.provenance + else { + panic!("expected PAYL macro body provenance for nested argument token: {payload:?}"); + }; + assert_eq!(macro_name, "PAYL"); + assert_eq!(identity.call_id, payl.macro_call_id.unwrap()); + assert_eq!(identity.definition_id, payl.macro_definition_id.unwrap()); + assert_eq!(identity.expansion_id, payl.macro_expansion_id.unwrap()); + assert_eq!(identity.parent_expansion_id, next.macro_expansion_id); + assert_eq!(identity.body_token_index, 0); + assert_eq!(&source[call_range.range.clone()], "`PAYL"); + assert_eq!(&source[body_token_range.range.clone()], "payload_i"); +} + +#[test] +fn preprocessor_trace_preserves_parent_chain_for_nested_actual_argument_macros() { + let source = r#"`define LEAF payload_i +`define WRAP `LEAF +`define NEXT(x) ((x) + 12'd1) +module m(input logic [3:0] payload_i, output logic [3:0] y); +assign y = `NEXT(`WRAP); +endmodule +"#; + let trace = SyntaxTree::preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions::default(), + ) + .expect("trace should include macro usage events"); + + let next = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`NEXT") + }) + .expect("outer NEXT usage should be traced"); + let wrap = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`WRAP") + }) + .expect("actual-argument WRAP usage should be traced"); + let leaf = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`LEAF") + }) + .expect("nested LEAF usage should be traced"); + + assert_eq!(next.parent_macro_expansion_id, None); + assert_eq!(wrap.parent_macro_expansion_id, next.macro_expansion_id); + assert_eq!(leaf.parent_macro_expansion_id, wrap.macro_expansion_id); + + let payload = trace + .emitted_tokens + .iter() + .find(|token| { + token.raw_text == "payload_i" + && matches!( + &token.provenance, + PreprocessorTraceTokenProvenance::MacroBody { macro_name, .. } + if macro_name == "LEAF" + ) + }) + .expect("final payload token should keep LEAF provenance"); + let PreprocessorTraceTokenProvenance::MacroBody { identity, call_range, .. } = + &payload.provenance + else { + panic!("expected LEAF macro body provenance for payload token: {payload:?}"); + }; + assert_eq!(identity.call_id, leaf.macro_call_id.unwrap()); + assert_eq!(identity.expansion_id, leaf.macro_expansion_id.unwrap()); + assert_eq!(identity.parent_expansion_id, wrap.macro_expansion_id); + assert_eq!(&source[call_range.range.clone()], "`LEAF"); } #[test] diff --git a/crates/slang/include/slang/parsing/Preprocessor.h b/crates/slang/include/slang/parsing/Preprocessor.h index 73e0cf36..eb366080 100644 --- a/crates/slang/include/slang/parsing/Preprocessor.h +++ b/crates/slang/include/slang/parsing/Preprocessor.h @@ -335,7 +335,8 @@ class SLANG_EXPORT Preprocessor { syntax::MacroActualArgumentListSyntax* actualArgs); bool expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& expansion); bool expandReplacementList(std::span& tokens, - SmallSet& alreadyExpanded); + SmallSet& alreadyExpanded, + uint32_t parentExpansionId = 0); bool applyMacroOps(std::span tokens, SmallVectorBase& dest); void recordMacroUsageTrace(Token directive, syntax::MacroActualArgumentListSyntax* actualArgs, MacroDef macro, diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index 4bfc9bcb..baaae174 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -525,7 +525,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, if (!it->second.isExpanded) { std::span argTokens = it->second; SmallSet alreadyExpanded; - if (!expandReplacementList(argTokens, alreadyExpanded)) + if (!expandReplacementList(argTokens, alreadyExpanded, expansion.getExpansionId())) return false; it->second = argTokens; @@ -729,7 +729,8 @@ void Preprocessor::MacroExpansion::append(Token token, SourceLocation location, } bool Preprocessor::expandReplacementList( - std::span& tokens, SmallSet& alreadyExpanded) { + std::span& tokens, SmallSet& alreadyExpanded, + uint32_t parentExpansionId) { SmallVector outBuffer; SmallVector expansionBuffer; @@ -774,6 +775,8 @@ bool Preprocessor::expandReplacementList( metadata.definitionId = macro.definitionId; if (sourceManager.isMacroLoc(token.location())) metadata.parentExpansionId = token.location().buffer().getId(); + else + metadata.parentExpansionId = parentExpansionId; MacroExpansion expansion{sourceManager, alloc, expansionBuffer, token, false, metadata}; if (!expandMacro(macro, expansion, actualArgs)) From e44b40f3d124f92c0bcd4a7e03da5fa8b81a4a0f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 00:35:31 +0800 Subject: [PATCH 41/80] feat(ide): highlight preproc macro references --- crates/hir/src/preproc/reference_queries.rs | 51 +++++++++++++++ crates/hir/src/preproc/tests.rs | 29 +++++++++ crates/ide/src/semantic_tokens.rs | 71 +++++++++++++++++++++ src/lsp_ext/to_proto.rs | 1 + 4 files changed, 152 insertions(+) diff --git a/crates/hir/src/preproc/reference_queries.rs b/crates/hir/src/preproc/reference_queries.rs index 89561254..0408b9e9 100644 --- a/crates/hir/src/preproc/reference_queries.rs +++ b/crates/hir/src/preproc/reference_queries.rs @@ -116,6 +116,57 @@ pub fn macro_reference_at( })?)) } +pub fn macro_references_in_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mut references = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for reference in mapped.model.macro_references().iter() { + let Ok((source, reference_range)) = + map_mapped_source_range(mapped, reference.name_range) + else { + continue; + }; + if source.file_id() != Some(file_id) + || reference_range + .intersect(range) + .is_none_or(|intersection| intersection.is_empty()) + { + continue; + } + + match map_macro_reference(mapped, reference) { + Ok(reference) => { + references.push_unique_eq(reference); + } + Err(error) => record_first_error(&mut first_error, error), + } + } + } + + if references.is_empty() + && let Err(error) = finish_empty_single_query(&contexts, first_error) + { + return Err(error); + } + + Ok(references.into_vec()) +} + pub fn macro_reference_resolution_at( db: &dyn SourceRootDb, file_id: FileId, diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index a87e6222..490fac51 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -1076,6 +1076,35 @@ fn preproc_ifndef_guard_reference_resolves_to_following_define() { })); } +#[test] +fn preproc_macro_references_in_range_includes_undefined_conditionals() { + let root_text = r#"`define KNOWN 1 +`ifdef UNKNOWN +`endif +`ifndef KNOWN +`endif +module top; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let references = + macro_references_in_range(&db, TOP, TextRange::up_to(TextSize::of(root_text))).unwrap(); + + let unknown = references + .iter() + .find(|reference| reference.name.as_str() == "UNKNOWN") + .expect("undefined conditional macro reference should be present"); + assert_eq!(text_at_range(root_text, unknown.range), "UNKNOWN"); + assert!(matches!(unknown.resolution, MacroResolution::Undefined)); + + let known = references + .iter() + .find(|reference| reference.name.as_str() == "KNOWN") + .expect("resolved conditional macro reference should be present"); + assert_eq!(text_at_range(root_text, known.range), "KNOWN"); + assert!(matches!(known.resolution, MacroResolution::Resolved { .. })); +} + #[test] fn preproc_project_header_guard_reference_is_indexed_without_include() { let root_text = "module top; endmodule\n"; diff --git a/crates/ide/src/semantic_tokens.rs b/crates/ide/src/semantic_tokens.rs index b3e27564..6abb2cab 100644 --- a/crates/ide/src/semantic_tokens.rs +++ b/crates/ide/src/semantic_tokens.rs @@ -14,6 +14,7 @@ use hir::{ }, stmt::StmtKind, }, + preproc::macro_references_in_range, scope::NonAnsiPortEntry, semantics::{Semantics, pathres::PathResolution}, source_map::{IsNamedSrc, IsSrc, ToAstNode}, @@ -62,6 +63,7 @@ pub struct SemaToken { pub enum SemaTokenTag { Port(SemaTokenPort), Instance, + Macro, Type, None, } @@ -147,10 +149,33 @@ pub(crate) fn semantic_tokens( let mut collector = SemaTokenCollector::new(config, range); collect_file(&sema, file_id, &mut collector); + collect_preproc_macro_references(db, file_id.file_id(), range, &mut collector); collector.finish() } +fn collect_preproc_macro_references( + db: &RootDb, + file_id: FileId, + range: TextRange, + collector: &mut SemaTokenCollector, +) { + let Ok(references) = macro_references_in_range(db, file_id, range) else { + return; + }; + + for reference in references { + if reference.range.intersect(collector.range).is_none() { + continue; + } + collector.tokens.add(SemaToken { + range: reference.range, + tag: SemaTokenTag::Macro, + mods: SemaTokenModifier::REF, + }); + } +} + fn collect_file( sema: &Semantics<'_, RootDb>, file_id: HirFileId, @@ -556,6 +581,52 @@ mod tests { } } + #[test] + fn conditional_macro_references_use_macro_semantic_tokens_when_undefined() { + let text = r#" +`define KNOWN 1 +`ifdef UNKNOWN +`endif +`ifndef KNOWN +`endif +module top; +endmodule +"#; + let (host, file_id) = setup(text); + let tokens = host + .make_analysis() + .semantic_tokens( + file_id, + SemaTokenConfig { port: SemaTokenPortConfig { clk_rst: false, io: false } }, + Some(TextRange::up_to(TextSize::of(text))), + ) + .unwrap(); + + let token_at = |start: usize, len: usize| { + let range = + TextRange::new(TextSize::from(start as u32), TextSize::from((start + len) as u32)); + tokens + .iter() + .find(|token| !token.is_empty() && token.range == range) + .copied() + .unwrap_or_else(|| panic!("expected semantic token at {range:?}: {tokens:?}")) + }; + let unknown_start = text.find("UNKNOWN").expect("UNKNOWN conditional should exist"); + let known_start = text.rfind("KNOWN").expect("KNOWN conditional should exist"); + + assert_eq!( + ( + token_at(unknown_start, "UNKNOWN".len()).tag, + token_at(unknown_start, "UNKNOWN".len()).mods + ), + (SemaTokenTag::Macro, SemaTokenModifier::REF) + ); + assert_eq!( + (token_at(known_start, "KNOWN".len()).tag, token_at(known_start, "KNOWN".len()).mods), + (SemaTokenTag::Macro, SemaTokenModifier::REF) + ); + } + #[test] fn named_port_connection_labels_use_target_module_ports() { let text = r#" diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 1b5e53ee..76171c09 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -746,6 +746,7 @@ pub(crate) fn semantic_tokens( SemaTokenTag::Port(SemaTokenPort::Rst) => sema_token_types::RST_PORT, SemaTokenTag::Port(SemaTokenPort::Others) => sema_token_types::OTHERS_PORT, SemaTokenTag::Instance => sema_token_types::INSTANCE, + SemaTokenTag::Macro => sema_token_types::MACRO, SemaTokenTag::Type => sema_token_types::TYPE_ALIAS, SemaTokenTag::None => sema_token_types::GENERIC, }; From a7e163ba16168df97309b08ae12213cceb370654 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 01:53:07 +0800 Subject: [PATCH 42/80] refactor(slang): add parse-path preprocessor trace recorder --- crates/slang/bindings/rust/ffi/wrapper.cc | 2 +- .../include/slang/parsing/Preprocessor.h | 18 ++--- .../include/slang/parsing/PreprocessorTrace.h | 78 +++++++++++++++++++ crates/slang/source/CMakeLists.txt | 1 + crates/slang/source/parsing/Preprocessor.cpp | 51 +++++++++++- .../source/parsing/PreprocessorTrace.cpp | 45 +++++++++++ 6 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 crates/slang/include/slang/parsing/PreprocessorTrace.h create mode 100644 crates/slang/source/parsing/PreprocessorTrace.cpp diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index 22c8a466..0a53e891 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -777,7 +777,7 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( } ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_macro_usage_record( - const slang::parsing::Preprocessor::MacroUsageTraceRecord& record, + const slang::parsing::MacroUsageTraceRecord& record, uint32_t eventId, const slang::SourceManager& sourceManager) { ::RawPreprocessorTraceEvent directive; diff --git a/crates/slang/include/slang/parsing/Preprocessor.h b/crates/slang/include/slang/parsing/Preprocessor.h index eb366080..a54e381c 100644 --- a/crates/slang/include/slang/parsing/Preprocessor.h +++ b/crates/slang/include/slang/parsing/Preprocessor.h @@ -12,6 +12,7 @@ #include "slang/parsing/Lexer.h" #include "slang/parsing/NumberParser.h" #include "slang/parsing/Token.h" +#include "slang/parsing/PreprocessorTrace.h" #include "slang/syntax/SyntaxNode.h" #include "slang/text/SourceManager.h" #include "slang/text/SourceLocation.h" @@ -73,7 +74,8 @@ class SLANG_EXPORT Preprocessor { public: Preprocessor(SourceManager& sourceManager, BumpAllocator& alloc, Diagnostics& diagnostics, const Bag& options = {}, - std::span inheritedMacros = {}); + std::span inheritedMacros = {}, + PreprocessorTraceRecorder* traceRecorder = nullptr); /// Gets the next token in the stream, after applying preprocessor rules. Token next(); @@ -155,17 +157,6 @@ class SLANG_EXPORT Preprocessor { /// Gets the frontend identity assigned to a macro definition syntax node. uint32_t getMacroDefinitionId(const syntax::DefineDirectiveSyntax& syntax) const; - /// A macro usage observed by the preprocessor while expanding source tokens. - struct MacroUsageTraceRecord { - Token directive; - syntax::MacroActualArgumentListSyntax* actualArgs = nullptr; - SourceRange range; - uint32_t callId = 0; - uint32_t definitionId = 0; - uint32_t expansionId = 0; - uint32_t parentExpansionId = 0; - }; - /// Gets all macro usages observed while preprocessing, including usages expanded from /// macro replacement lists that do not become directive trivia in the parsed token stream. std::span getMacroUsageTraceRecords() const { @@ -350,6 +341,8 @@ class SLANG_EXPORT Preprocessor { const syntax::DefineDirectiveSyntax& right); uint32_t allocateMacroDefinitionId(const syntax::DefineDirectiveSyntax* syntax); uint32_t allocateMacroCallId(); + void recordTracePredefines(); + void recordTraceToken(Token token); // functions to advance the underlying token stream Token peek(); @@ -438,6 +431,7 @@ class SLANG_EXPORT Preprocessor { std::vector macroUsageTraceRecords; uint32_t nextMacroDefinitionId = 1; uint32_t nextMacroCallId = 1; + PreprocessorTraceRecorder* traceRecorder = nullptr; // list of expanded macro tokens to drain before continuing with active lexer SmallVector expandedTokens; diff --git a/crates/slang/include/slang/parsing/PreprocessorTrace.h b/crates/slang/include/slang/parsing/PreprocessorTrace.h new file mode 100644 index 00000000..36677779 --- /dev/null +++ b/crates/slang/include/slang/parsing/PreprocessorTrace.h @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +// PreprocessorTrace.h +// Shared preprocessor trace facts +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include +#include +#include +#include + +#include "slang/parsing/Token.h" + +namespace slang { + +struct SourceBuffer; + +namespace syntax { +class SyntaxNode; +struct DefineDirectiveSyntax; +struct MacroActualArgumentListSyntax; +} // namespace syntax + +namespace parsing { + +/// A macro usage observed by the preprocessor while expanding source tokens. +struct MacroUsageTraceRecord { + Token directive; + syntax::MacroActualArgumentListSyntax* actualArgs = nullptr; + SourceRange range; + uint32_t callId = 0; + uint32_t definitionId = 0; + uint32_t expansionId = 0; + uint32_t parentExpansionId = 0; +}; + +struct PreprocessorTraceDirectiveEvent { + const syntax::SyntaxNode* syntax = nullptr; + uint32_t macroDefinitionId = 0; +}; + +struct PreprocessorTraceEvent { + enum class Kind : uint8_t { Directive, MacroUsage }; + + uint32_t eventId = 0; + Kind kind = Kind::Directive; + PreprocessorTraceDirectiveEvent directive; + MacroUsageTraceRecord macroUsage; +}; + +struct PreprocessorTraceSnapshot { + std::optional rootBufferId; + std::vector events; + std::vector emittedTokens; +}; + +class PreprocessorTraceRecorder { +public: + void setRootBuffer(SourceBuffer buffer); + + void recordDirective(const syntax::SyntaxNode& syntax, uint32_t macroDefinitionId = 0); + void recordEmittedToken(Token token); + void flushMacroUsageRecords(std::span records); + + PreprocessorTraceSnapshot snapshot() const { return snapshot_; } + +private: + PreprocessorTraceEvent& pushEvent(PreprocessorTraceEvent::Kind kind); + + PreprocessorTraceSnapshot snapshot_; + size_t flushedMacroUsageRecordCount_ = 0; +}; + +} // namespace parsing +} // namespace slang diff --git a/crates/slang/source/CMakeLists.txt b/crates/slang/source/CMakeLists.txt index 90d40c04..50f7a2ee 100644 --- a/crates/slang/source/CMakeLists.txt +++ b/crates/slang/source/CMakeLists.txt @@ -80,6 +80,7 @@ add_library( parsing/Preprocessor.cpp parsing/Preprocessor_macros.cpp parsing/Preprocessor_pragmas.cpp + parsing/PreprocessorTrace.cpp parsing/Token.cpp syntax/SyntaxFacts.cpp syntax/SyntaxNode.cpp diff --git a/crates/slang/source/parsing/Preprocessor.cpp b/crates/slang/source/parsing/Preprocessor.cpp index c0b1e95f..9d066424 100644 --- a/crates/slang/source/parsing/Preprocessor.cpp +++ b/crates/slang/source/parsing/Preprocessor.cpp @@ -24,15 +24,17 @@ using LF = LexerFacts; Preprocessor::Preprocessor(SourceManager& sourceManager, BumpAllocator& alloc, Diagnostics& diagnostics, const Bag& options_, - std::span inheritedMacros) : + std::span inheritedMacros, + PreprocessorTraceRecorder* traceRecorder) : sourceManager(sourceManager), alloc(alloc), diagnostics(diagnostics), options(options_.getOrDefault()), lexerOptions(options_.getOrDefault()), - numberParser(diagnostics, alloc, options.languageVersion) { + numberParser(diagnostics, alloc, options.languageVersion), traceRecorder(traceRecorder) { keywordVersionStack.push_back(LF::getDefaultKeywordVersion(options.languageVersion)); resetAllDirectives(); undefineAll(); + recordTracePredefines(); // Add in any inherited macros that aren't already set in our map. for (auto define : inheritedMacros) { @@ -238,7 +240,50 @@ uint32_t Preprocessor::allocateMacroCallId() { } Token Preprocessor::next() { - return consume(); + auto token = consume(); + recordTraceToken(token); + return token; +} + +void Preprocessor::recordTracePredefines() { + if (!traceRecorder) + return; + + std::vector defines; + for (auto& [name, def] : macros) { + if (def.commandLine && def.syntax) + defines.push_back(def); + } + + std::ranges::sort(defines, [](const MacroDef& left, const MacroDef& right) { + return left.syntax->name.valueText() < right.syntax->name.valueText(); + }); + + for (const auto& def : defines) + traceRecorder->recordDirective(*def.syntax, def.definitionId); +} + +void Preprocessor::recordTraceToken(Token token) { + if (!traceRecorder) + return; + + for (auto trivia : token.trivia()) { + if (trivia.kind != TriviaKind::Directive) + continue; + + auto* syntax = trivia.syntax(); + if (!syntax || syntax->kind == SyntaxKind::MacroUsage) + continue; + + uint32_t definitionId = 0; + if (auto* define = syntax->as_if()) + definitionId = getMacroDefinitionId(*define); + traceRecorder->recordDirective(*syntax, definitionId); + } + + traceRecorder->flushMacroUsageRecords(getMacroUsageTraceRecords()); + if (token.kind != TokenKind::EndOfFile) + traceRecorder->recordEmittedToken(token); } Token Preprocessor::nextProcessed() { diff --git a/crates/slang/source/parsing/PreprocessorTrace.cpp b/crates/slang/source/parsing/PreprocessorTrace.cpp new file mode 100644 index 00000000..75dc86dc --- /dev/null +++ b/crates/slang/source/parsing/PreprocessorTrace.cpp @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +// PreprocessorTrace.cpp +// Shared preprocessor trace facts +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#include "slang/parsing/PreprocessorTrace.h" + +#include "slang/text/SourceManager.h" + +namespace slang::parsing { + +void PreprocessorTraceRecorder::setRootBuffer(SourceBuffer buffer) { + if (buffer) + snapshot_.rootBufferId = buffer.id.getId(); +} + +void PreprocessorTraceRecorder::recordDirective(const syntax::SyntaxNode& syntax, + uint32_t macroDefinitionId) { + auto& event = pushEvent(PreprocessorTraceEvent::Kind::Directive); + event.directive.syntax = &syntax; + event.directive.macroDefinitionId = macroDefinitionId; +} + +void PreprocessorTraceRecorder::recordEmittedToken(Token token) { + snapshot_.emittedTokens.push_back(token); +} + +void PreprocessorTraceRecorder::flushMacroUsageRecords( + std::span records) { + for (; flushedMacroUsageRecordCount_ < records.size(); flushedMacroUsageRecordCount_++) { + auto& event = pushEvent(PreprocessorTraceEvent::Kind::MacroUsage); + event.macroUsage = records[flushedMacroUsageRecordCount_]; + } +} + +PreprocessorTraceEvent& PreprocessorTraceRecorder::pushEvent(PreprocessorTraceEvent::Kind kind) { + auto& event = snapshot_.events.emplace_back(); + event.eventId = uint32_t(snapshot_.events.size() - 1); + event.kind = kind; + return event; +} + +} // namespace slang::parsing From ef72f6a7f81804417583427d530caeda864167f9 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:03:15 +0800 Subject: [PATCH 43/80] feat(slang): expose parse-derived preprocessor trace --- crates/slang/bindings/rust/ffi.rs | 16 ++ crates/slang/bindings/rust/ffi/wrapper.cc | 165 ++++++++++++++++-- crates/slang/bindings/rust/ffi/wrapper.h | 14 +- crates/slang/bindings/rust/lib.rs | 64 ++++--- crates/slang/bindings/rust/tests.rs | 35 ++++ .../include/slang/parsing/PreprocessorTrace.h | 4 +- .../slang/include/slang/syntax/SyntaxTree.h | 28 ++- crates/slang/source/parsing/Preprocessor.cpp | 2 +- .../source/parsing/PreprocessorTrace.cpp | 3 +- crates/slang/source/syntax/SyntaxTree.cpp | 42 +++-- 10 files changed, 312 insertions(+), 61 deletions(-) diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index 86b2a510..35576e59 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -465,6 +465,17 @@ mod slang_ffi { expand_includes: bool, ) -> SharedPtr; + #[namespace = "wrapper::syntax"] + fn SyntaxTree_fromTextWithOptionsAndTrace( + text: CxxSV, + name: CxxSV, + path: CxxSV, + predefines: Vec, + include_paths: Vec, + include_buffers: Vec, + expand_includes: bool, + ) -> SharedPtr; + #[namespace = "wrapper::syntax"] fn SyntaxTree_fromLibraryMapText( text: CxxSV, @@ -531,6 +542,9 @@ mod slang_ffi { expand_includes: bool, ) -> RawPreprocessorTrace; + #[namespace = "wrapper::syntax"] + fn SyntaxTree_preprocessorTraceFromParsed(tree: &SyntaxTree) -> RawPreprocessorTrace; + #[namespace = "wrapper::syntax"] fn SyntaxTree_buffer_id(tree: &SyntaxTree) -> u32; } @@ -652,6 +666,7 @@ impl_functions! { impl SyntaxTree { fn fromText(text: CxxSV, name: CxxSV, path: CxxSV) -> SharedPtr |> SyntaxTree_fromText; fn fromTextWithOptions(text: CxxSV, name: CxxSV, path: CxxSV, predefines: Vec, include_paths: Vec, include_buffers: Vec, expand_includes: bool) -> SharedPtr |> SyntaxTree_fromTextWithOptions; + fn fromTextWithOptionsAndTrace(text: CxxSV, name: CxxSV, path: CxxSV, predefines: Vec, include_paths: Vec, include_buffers: Vec, expand_includes: bool) -> SharedPtr |> SyntaxTree_fromTextWithOptionsAndTrace; fn fromLibraryMapText(text: CxxSV, name: CxxSV, path: CxxSV) -> SharedPtr |> SyntaxTree_fromLibraryMapText; fn root(&self) -> *const SyntaxNode |> SyntaxTree_root; fn diagnostics(&self) -> Vec |> SyntaxTree_diagnostics; @@ -661,6 +676,7 @@ impl_functions! { fn directiveAtOffset(text: CxxSV, name: CxxSV, path: CxxSV, offset: usize) -> RawLexedTokenAtOffset |> SyntaxTree_directiveAtOffset; fn tokenWordAtOffset(text: CxxSV, name: CxxSV, path: CxxSV, offset: usize) -> RawLexedTokenAtOffset |> SyntaxTree_tokenWordAtOffset; fn preprocessorTrace(text: CxxSV, name: CxxSV, path: CxxSV, predefines: Vec, include_paths: Vec, include_buffers: Vec, expand_includes: bool) -> RawPreprocessorTrace |> SyntaxTree_preprocessorTrace; + fn preprocessorTraceFromParsed(&self) -> RawPreprocessorTrace |> SyntaxTree_preprocessorTraceFromParsed; fn buffer_id(&self) -> u32 |> SyntaxTree_buffer_id; } } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index 0a53e891..a6b8b93a 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -2,6 +2,7 @@ #include "slang/parsing/ExpectedSyntax.h" #include "slang/parsing/ParserMetadata.h" +#include "slang/parsing/PreprocessorTrace.h" #include "slang/syntax/AllSyntax.h" #include @@ -706,7 +707,7 @@ rust::Vec<::RawPreprocessorTraceActualArgument> to_rust_trace_actual_arguments( ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( const slang::syntax::SyntaxNode& syntax, uint32_t eventId, - const slang::parsing::Preprocessor& preprocessor) { + uint32_t macroDefinitionId) { ::RawPreprocessorTraceEvent directive; directive.event_id = eventId; directive.kind = static_cast(syntax.kind); @@ -734,9 +735,8 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( switch (syntax.kind) { case slang::syntax::SyntaxKind::DefineDirective: { const auto& define = syntax.as(); - auto definitionId = preprocessor.getMacroDefinitionId(define); - directive.macro_definition_id = definitionId; - directive.has_macro_definition_id = definitionId != 0; + directive.macro_definition_id = macroDefinitionId; + directive.has_macro_definition_id = macroDefinitionId != 0; directive.name = to_rust_preprocessor_trace_token(define.name); if (define.formalArguments) { for (auto* param : define.formalArguments->args) @@ -803,6 +803,108 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_macro_usage_record( return directive; } +rust::Vec<::RawSourceBufferId> collectSourceBufferIds( + const slang::SourceManager& sourceManager, + const std::unordered_set& predefineBufferIds); + +::RawPreprocessorTrace empty_preprocessor_trace() { + ::RawPreprocessorTrace result; + result.root_buffer_id = 0; + result.has_root_buffer_id = false; + result.source_buffers = rust::Vec<::RawSourceBufferId>(); + result.events = rust::Vec<::RawPreprocessorTraceEvent>(); + result.include_edges = rust::Vec<::RawPreprocessorTraceIncludeEdge>(); + result.emitted_tokens = rust::Vec<::RawPreprocessorTraceEmittedToken>(); + return result; +} + +uint32_t trace_macro_definition_id( + const slang::syntax::SyntaxNode& syntax, + const slang::parsing::Preprocessor& preprocessor) { + if (auto* define = syntax.as_if()) + return preprocessor.getMacroDefinitionId(*define); + return 0; +} + +std::unordered_set predefine_buffer_ids( + const slang::parsing::PreprocessorTraceSnapshot& trace) { + std::unordered_set bufferIds; + for (const auto& event : trace.events) { + if (event.kind != slang::parsing::PreprocessorTraceEvent::Kind::Directive || + !event.directive.isPredefine || !event.directive.syntax) { + continue; + } + + auto* directive = event.directive.syntax->as_if(); + if (!directive) + continue; + auto location = directive->directive.location(); + if (location.valid()) + bufferIds.insert(location.buffer().getId()); + } + return bufferIds; +} + +::RawPreprocessorTrace to_rust_preprocessor_trace_snapshot( + const slang::parsing::PreprocessorTraceSnapshot& trace, + const slang::SourceManager& sourceManager) { + auto result = empty_preprocessor_trace(); + if (!trace.rootBufferId) + return result; + + result.root_buffer_id = *trace.rootBufferId; + result.has_root_buffer_id = true; + + std::unordered_map + includeEventIdsByLocation; + for (const auto& event : trace.events) { + switch (event.kind) { + case slang::parsing::PreprocessorTraceEvent::Kind::Directive: { + if (!event.directive.syntax) + continue; + + if (event.directive.syntax->kind == slang::syntax::SyntaxKind::IncludeDirective) { + const auto& include = + event.directive.syntax->as(); + if (auto key = trace_source_location_key(include.directive.location())) + includeEventIdsByLocation.emplace(*key, event.eventId); + } + + result.events.emplace_back(to_rust_preprocessor_trace_event( + *event.directive.syntax, event.eventId, event.directive.macroDefinitionId)); + break; + } + case slang::parsing::PreprocessorTraceEvent::Kind::MacroUsage: + result.events.emplace_back(to_rust_preprocessor_trace_macro_usage_record( + event.macroUsage, event.eventId, sourceManager)); + break; + } + } + + for (auto token : trace.emittedTokens) + result.emitted_tokens.emplace_back( + to_rust_preprocessor_trace_emitted_token(token, sourceManager)); + + for (auto buffer : sourceManager.getAllBuffers()) { + auto includedFrom = sourceManager.getIncludedFrom(buffer); + auto key = trace_source_location_key(includedFrom); + if (!key) + continue; + + auto includeIt = includeEventIdsByLocation.find(*key); + if (includeIt == includeEventIdsByLocation.end()) + continue; + + ::RawPreprocessorTraceIncludeEdge edge; + edge.include_event_id = includeIt->second; + edge.included_buffer_id = buffer.getId(); + result.include_edges.emplace_back(edge); + } + + result.source_buffers = collectSourceBufferIds(sourceManager, predefine_buffer_ids(trace)); + return result; +} + std::optional mapSourceRangeToContext( const slang::DiagnosticEngine& engine, slang::SourceLocation context, @@ -1027,7 +1129,8 @@ std::shared_ptr SourceSession::parseText( rust::Vec include_paths, rust::Vec<::RawSourceBuffer> include_buffers, std::optional expectedSyntaxCursor, - bool expandIncludes) { + bool expandIncludes, + bool collectPreprocessorTrace) { slang::Bag options; auto& ppOptions = options.insertOrGet(); for (const auto& predefine : predefines) @@ -1046,15 +1149,20 @@ std::shared_ptr SourceSession::parseText( assignSourceBuffer(std::string(buffer.path), std::string(buffer.text)); } + auto traceMode = collectPreprocessorTrace + ? slang::syntax::PreprocessorTraceMode::Enabled + : slang::syntax::PreprocessorTraceMode::Disabled; std::shared_ptr<::slang::syntax::SyntaxTree> tree; if (path.empty()) { - tree = ::slang::syntax::SyntaxTree::fromText(text, *sourceManager, name, path, options); + tree = ::slang::syntax::SyntaxTree::fromText( + text, *sourceManager, name, path, options, nullptr, traceMode); } else { auto buffer = assignSourceBuffer(path, text); if (!name.empty()) sourceManager->addLineDirective(slang::SourceLocation(buffer.id, 0), 2, name, 0); - tree = ::slang::syntax::SyntaxTree::fromBuffer(buffer, *sourceManager, options); + tree = ::slang::syntax::SyntaxTree::fromBuffer(buffer, *sourceManager, options, {}, + traceMode); } return std::make_shared(std::move(tree), shared_from_this()); @@ -1114,6 +1222,27 @@ std::shared_ptr SyntaxTree_fromTextWithOptions( expandIncludes); } +std::shared_ptr SyntaxTree_fromTextWithOptionsAndTrace( + std::string_view text, + std::string_view name, + std::string_view path, + rust::Vec predefines, + rust::Vec include_paths, + rust::Vec<::RawSourceBuffer> include_buffers, + bool expandIncludes) { + auto session = std::make_shared(); + return session->parseText( + text, + name, + path, + std::move(predefines), + std::move(include_paths), + std::move(include_buffers), + std::nullopt, + expandIncludes, + true); +} + std::shared_ptr SyntaxTree_fromLibraryMapText( std::string_view text, std::string_view name, @@ -1238,13 +1367,7 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( rust::Vec includePaths, rust::Vec<::RawSourceBuffer> includeBuffers, bool expandIncludes) { - ::RawPreprocessorTrace result; - result.root_buffer_id = 0; - result.has_root_buffer_id = false; - result.source_buffers = rust::Vec<::RawSourceBufferId>(); - result.events = rust::Vec<::RawPreprocessorTraceEvent>(); - result.include_edges = rust::Vec<::RawPreprocessorTraceIncludeEdge>(); - result.emitted_tokens = rust::Vec<::RawPreprocessorTraceEmittedToken>(); + auto result = empty_preprocessor_trace(); slang::SourceManager sourceManager; std::unordered_map assignedBuffers; @@ -1306,7 +1429,8 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( continue; auto eventId = static_cast(result.events.size()); - result.events.emplace_back(to_rust_preprocessor_trace_event(*define, eventId, preprocessor)); + result.events.emplace_back(to_rust_preprocessor_trace_event( + *define, eventId, preprocessor.getMacroDefinitionId(*define))); } preprocessor.pushSource(rootBuffer); @@ -1338,7 +1462,8 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( if (auto key = trace_source_location_key(include.directive.location())) includeEventIdsByLocation.emplace(*key, eventId); } - auto event = to_rust_preprocessor_trace_event(*syntax, eventId, preprocessor); + auto event = to_rust_preprocessor_trace_event( + *syntax, eventId, trace_macro_definition_id(*syntax, preprocessor)); result.events.emplace_back(std::move(event)); } } @@ -1371,6 +1496,14 @@ ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( return result; } +::RawPreprocessorTrace SyntaxTree_preprocessorTraceFromParsed(const SyntaxTree& tree) { + auto* trace = tree.inner().getPreprocessorTrace(); + if (!trace) + return empty_preprocessor_trace(); + + return to_rust_preprocessor_trace_snapshot(*trace, tree.inner().sourceManager()); +} + std::unique_ptr SyntaxNode_range(const SyntaxNode& node) { return mapRawSourceRangeWithContext(node.sourceRange(), node); } diff --git a/crates/slang/bindings/rust/ffi/wrapper.h b/crates/slang/bindings/rust/ffi/wrapper.h index 2048e956..7d99f77e 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.h +++ b/crates/slang/bindings/rust/ffi/wrapper.h @@ -93,7 +93,8 @@ namespace wrapper { rust::Vec includePaths, rust::Vec<::RawSourceBuffer> includeBuffers, std::optional expectedSyntaxCursor = std::nullopt, - bool expandIncludes = true); + bool expandIncludes = true, + bool collectPreprocessorTrace = false); std::shared_ptr parseLibraryMapText( std::string_view text, @@ -461,6 +462,15 @@ namespace wrapper { rust::Vec<::RawSourceBuffer> include_buffers, bool expandIncludes); + std::shared_ptr SyntaxTree_fromTextWithOptionsAndTrace( + std::string_view text, + std::string_view name, + std::string_view path, + rust::Vec predefines, + rust::Vec include_paths, + rust::Vec<::RawSourceBuffer> include_buffers, + bool expandIncludes); + std::shared_ptr SyntaxTree_fromLibraryMapText( std::string_view text, std::string_view name, @@ -567,6 +577,8 @@ namespace wrapper { rust::Vec includePaths, rust::Vec<::RawSourceBuffer> includeBuffers, bool expandIncludes); + + ::RawPreprocessorTrace SyntaxTree_preprocessorTraceFromParsed(const SyntaxTree& tree); } namespace ast { diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 7a1a7448..321dab87 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -63,6 +63,12 @@ pub struct SyntaxTree { _ptr: SharedPtr, } +#[derive(Debug, Clone)] +pub struct SyntaxTreeWithPreprocessorTrace { + pub tree: SyntaxTree, + pub preprocessor_trace: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SyntaxTreeOptions { pub predefines: Vec, @@ -1515,6 +1521,14 @@ impl PreprocessorTraceActualArgument { } } +fn raw_include_buffers(options: &SyntaxTreeOptions) -> Vec { + options + .include_buffers + .iter() + .map(|buffer| ffi::RawSourceBuffer { path: buffer.path.clone(), text: buffer.text.clone() }) + .collect() +} + impl SyntaxTree { #[inline] pub fn from_text(text: &str, name: &str, path: &str) -> SyntaxTree { @@ -1537,19 +1551,35 @@ impl SyntaxTree { CxxSV::new(path), options.predefines.clone(), options.include_paths.clone(), - options - .include_buffers - .iter() - .map(|buffer| ffi::RawSourceBuffer { - path: buffer.path.clone(), - text: buffer.text.clone(), - }) - .collect(), + raw_include_buffers(options), options.expand_includes, ), } } + #[inline] + pub fn from_text_with_options_and_trace( + text: &str, + name: &str, + path: &str, + options: &SyntaxTreeOptions, + ) -> SyntaxTreeWithPreprocessorTrace { + let tree = SyntaxTree { + _ptr: ffi::SyntaxTree::fromTextWithOptionsAndTrace( + CxxSV::new(text), + CxxSV::new(name), + CxxSV::new(path), + options.predefines.clone(), + options.include_paths.clone(), + raw_include_buffers(options), + options.expand_includes, + ), + }; + let preprocessor_trace = + PreprocessorTrace::from_raw(tree._ptr.preprocessorTraceFromParsed()); + SyntaxTreeWithPreprocessorTrace { tree, preprocessor_trace } + } + #[inline] pub fn from_library_map_text(text: &str, name: &str, path: &str) -> SyntaxTree { SyntaxTree { @@ -1607,14 +1637,7 @@ impl SyntaxTree { offset, options.predefines.clone(), options.include_paths.clone(), - options - .include_buffers - .iter() - .map(|buffer| ffi::RawSourceBuffer { - path: buffer.path.clone(), - text: buffer.text.clone(), - }) - .collect(), + raw_include_buffers(options), options.expand_includes, ) .into_iter() @@ -1679,14 +1702,7 @@ impl SyntaxTree { CxxSV::new(path), options.predefines.clone(), options.include_paths.clone(), - options - .include_buffers - .iter() - .map(|buffer| ffi::RawSourceBuffer { - path: buffer.path.clone(), - text: buffer.text.clone(), - }) - .collect(), + raw_include_buffers(options), options.expand_includes, )) } diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index 3c1a8ed5..6af0beda 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1061,6 +1061,41 @@ wire disabled_by_header; })); } +#[test] +fn preprocessor_trace_from_parsed_tree_matches_static_trace() { + let dir = TestDir::new("slang-parse-preprocessor-trace"); + let rtl_dir = dir.create_dir_all("rtl"); + let include_dir = dir.create_dir_all("include"); + let header_path = dir.write("include/defs.vh", ""); + let source_path = rtl_dir.join("top.v").to_string(); + let source = r#"`include "defs.vh" +`define ID(x) x +module m; +localparam int A = `FROM_API; +localparam int B = `ID(`HEADER_VALUE); +endmodule +"#; + let options = SyntaxTreeOptions { + predefines: vec!["FROM_API=11".to_owned()], + include_paths: vec![include_dir.to_string()], + include_buffers: vec![SyntaxTreeBuffer { + path: header_path.to_string(), + text: "`define HEADER_VALUE 7\n".to_owned(), + }], + expand_includes: true, + }; + + let parsed = + SyntaxTree::from_text_with_options_and_trace(source, "source", &source_path, &options); + assert_eq!(parsed.tree.diagnostics(), Vec::new()); + let parsed_trace = + parsed.preprocessor_trace.expect("parse-derived trace should be present when requested"); + let static_trace = SyntaxTree::preprocessor_trace(source, "source", &source_path, &options) + .expect("static trace should be present for comparison"); + + assert_eq!(parsed_trace, static_trace); +} + #[test] fn preprocessor_trace_reports_emitted_macro_body_and_argument_provenance() { let source = r#"`define OBJ 8 diff --git a/crates/slang/include/slang/parsing/PreprocessorTrace.h b/crates/slang/include/slang/parsing/PreprocessorTrace.h index 36677779..38d6452b 100644 --- a/crates/slang/include/slang/parsing/PreprocessorTrace.h +++ b/crates/slang/include/slang/parsing/PreprocessorTrace.h @@ -40,6 +40,7 @@ struct MacroUsageTraceRecord { struct PreprocessorTraceDirectiveEvent { const syntax::SyntaxNode* syntax = nullptr; uint32_t macroDefinitionId = 0; + bool isPredefine = false; }; struct PreprocessorTraceEvent { @@ -61,7 +62,8 @@ class PreprocessorTraceRecorder { public: void setRootBuffer(SourceBuffer buffer); - void recordDirective(const syntax::SyntaxNode& syntax, uint32_t macroDefinitionId = 0); + void recordDirective(const syntax::SyntaxNode& syntax, uint32_t macroDefinitionId = 0, + bool isPredefine = false); void recordEmittedToken(Token token); void flushMacroUsageRecords(std::span records); diff --git a/crates/slang/include/slang/syntax/SyntaxTree.h b/crates/slang/include/slang/syntax/SyntaxTree.h index 93d66709..6479aeb1 100644 --- a/crates/slang/include/slang/syntax/SyntaxTree.h +++ b/crates/slang/include/slang/syntax/SyntaxTree.h @@ -24,6 +24,7 @@ struct SourceBuffer; namespace slang::parsing { struct ParserMetadata; +struct PreprocessorTraceSnapshot; } namespace slang::syntax { @@ -31,6 +32,8 @@ namespace slang::syntax { class SyntaxNode; struct DefineDirectiveSyntax; +enum class PreprocessorTraceMode { Disabled, Enabled }; + /// The SyntaxTree is the easiest way to interface with the lexer / preprocessor / /// parser stack. Give it some source text and it produces a parse tree. /// @@ -111,7 +114,9 @@ class SLANG_EXPORT SyntaxTree { static std::shared_ptr fromText(std::string_view text, SourceManager& sourceManager, std::string_view name = "source"sv, std::string_view path = "", const Bag& options = {}, - const SourceLibrary* library = nullptr); + const SourceLibrary* library = nullptr, + PreprocessorTraceMode traceMode = + PreprocessorTraceMode::Disabled); /// Creates a syntax tree from a full compilation unit already in memory. /// @a text is the actual source code text. @@ -135,7 +140,9 @@ class SLANG_EXPORT SyntaxTree { static std::shared_ptr fromBuffer(const SourceBuffer& buffer, SourceManager& sourceManager, const Bag& options = {}, - MacroList inheritedMacros = {}); + MacroList inheritedMacros = {}, + PreprocessorTraceMode traceMode = + PreprocessorTraceMode::Disabled); /// Creates a syntax tree by concatenating several loaded source buffers. /// @a buffers is the list of buffers that should be concatenated to form @@ -147,7 +154,9 @@ class SLANG_EXPORT SyntaxTree { static std::shared_ptr fromBuffers(std::span buffers, SourceManager& sourceManager, const Bag& options = {}, - MacroList inheritedMacros = {}); + MacroList inheritedMacros = {}, + PreprocessorTraceMode traceMode = + PreprocessorTraceMode::Disabled); /// Creates a syntax tree from a library map file. /// @a path is the path to the source file on disk. @@ -207,6 +216,11 @@ class SLANG_EXPORT SyntaxTree { /// Gets various bits of metadata collected during parsing. const parsing::ParserMetadata& getMetadata() const { return *metadata; } + /// Gets the preprocessor trace recorded while parsing, if requested. + const parsing::PreprocessorTraceSnapshot* getPreprocessorTrace() const { + return preprocessorTrace.get(); + } + /// Gets the list of macros that were defined at the end of the loaded source file. MacroList getDefinedMacros() const { return macros; } @@ -219,12 +233,15 @@ class SLANG_EXPORT SyntaxTree { private: SyntaxTree(SyntaxNode* root, const SourceLibrary* library, SourceManager& sourceManager, BumpAllocator&& alloc, Diagnostics&& diagnostics, parsing::ParserMetadata&& metadata, - std::vector&& macros, Bag options); + std::vector&& macros, Bag options, + std::unique_ptr&& preprocessorTrace = nullptr); static std::shared_ptr create(SourceManager& sourceManager, std::span source, const Bag& options, MacroList inheritedMacros, - bool guess); + bool guess, + PreprocessorTraceMode traceMode = + PreprocessorTraceMode::Disabled); SyntaxNode* rootNode; const SourceLibrary* library; @@ -234,6 +251,7 @@ class SLANG_EXPORT SyntaxTree { Bag options_; std::unique_ptr metadata; std::vector macros; + std::unique_ptr preprocessorTrace; }; } // namespace slang::syntax diff --git a/crates/slang/source/parsing/Preprocessor.cpp b/crates/slang/source/parsing/Preprocessor.cpp index 9d066424..0b4501bf 100644 --- a/crates/slang/source/parsing/Preprocessor.cpp +++ b/crates/slang/source/parsing/Preprocessor.cpp @@ -260,7 +260,7 @@ void Preprocessor::recordTracePredefines() { }); for (const auto& def : defines) - traceRecorder->recordDirective(*def.syntax, def.definitionId); + traceRecorder->recordDirective(*def.syntax, def.definitionId, true); } void Preprocessor::recordTraceToken(Token token) { diff --git a/crates/slang/source/parsing/PreprocessorTrace.cpp b/crates/slang/source/parsing/PreprocessorTrace.cpp index 75dc86dc..e73f386e 100644 --- a/crates/slang/source/parsing/PreprocessorTrace.cpp +++ b/crates/slang/source/parsing/PreprocessorTrace.cpp @@ -17,10 +17,11 @@ void PreprocessorTraceRecorder::setRootBuffer(SourceBuffer buffer) { } void PreprocessorTraceRecorder::recordDirective(const syntax::SyntaxNode& syntax, - uint32_t macroDefinitionId) { + uint32_t macroDefinitionId, bool isPredefine) { auto& event = pushEvent(PreprocessorTraceEvent::Kind::Directive); event.directive.syntax = &syntax; event.directive.macroDefinitionId = macroDefinitionId; + event.directive.isPredefine = isPredefine; } void PreprocessorTraceRecorder::recordEmittedToken(Token token) { diff --git a/crates/slang/source/syntax/SyntaxTree.cpp b/crates/slang/source/syntax/SyntaxTree.cpp index 78c2d2ed..199d8f5f 100644 --- a/crates/slang/source/syntax/SyntaxTree.cpp +++ b/crates/slang/source/syntax/SyntaxTree.cpp @@ -10,6 +10,7 @@ #include "slang/parsing/Parser.h" #include "slang/parsing/ParserMetadata.h" #include "slang/parsing/Preprocessor.h" +#include "slang/parsing/PreprocessorTrace.h" #include "slang/text/SourceManager.h" #include "slang/util/TimeTrace.h" @@ -78,7 +79,8 @@ std::shared_ptr SyntaxTree::fromText(std::string_view text, const Ba std::shared_ptr SyntaxTree::fromText(std::string_view text, SourceManager& sourceManager, std::string_view name, std::string_view path, - const Bag& options, const SourceLibrary* library) { + const Bag& options, const SourceLibrary* library, + PreprocessorTraceMode traceMode) { SourceBuffer buffer = sourceManager.assignText(path, text, {}, library); if (!buffer) return nullptr; @@ -86,7 +88,7 @@ std::shared_ptr SyntaxTree::fromText(std::string_view text, if (!name.empty()) sourceManager.addLineDirective(SourceLocation(buffer.id, 0), 2, name, 0); - return create(sourceManager, std::span(&buffer, 1), options, {}, false); + return create(sourceManager, std::span(&buffer, 1), options, {}, false, traceMode); } std::shared_ptr SyntaxTree::fromFileInMemory(std::string_view text, @@ -106,14 +108,17 @@ std::shared_ptr SyntaxTree::fromFileInMemory(std::string_view text, std::shared_ptr SyntaxTree::fromBuffer(const SourceBuffer& buffer, SourceManager& sourceManager, const Bag& options, - MacroList inheritedMacros) { - return create(sourceManager, std::span(&buffer, 1), options, inheritedMacros, false); + MacroList inheritedMacros, + PreprocessorTraceMode traceMode) { + return create(sourceManager, std::span(&buffer, 1), options, inheritedMacros, false, + traceMode); } std::shared_ptr SyntaxTree::fromBuffers(std::span buffers, SourceManager& sourceManager, - const Bag& options, MacroList inheritedMacros) { - return create(sourceManager, buffers, options, inheritedMacros, false); + const Bag& options, MacroList inheritedMacros, + PreprocessorTraceMode traceMode) { + return create(sourceManager, buffers, options, inheritedMacros, false, traceMode); } SourceManager& SyntaxTree::getDefaultSourceManager() { @@ -123,16 +128,18 @@ SourceManager& SyntaxTree::getDefaultSourceManager() { SyntaxTree::SyntaxTree(SyntaxNode* root, const SourceLibrary* library, SourceManager& sourceManager, BumpAllocator&& alloc, Diagnostics&& diagnostics, ParserMetadata&& metadata, - std::vector&& macros, Bag options) : + std::vector&& macros, Bag options, + std::unique_ptr&& preprocessorTrace) : rootNode(root), library(library), sourceMan(sourceManager), alloc(std::move(alloc)), diagnosticsBuffer(std::move(diagnostics)), options_(std::move(options)), - metadata(std::make_unique(std::move(metadata))), macros(std::move(macros)) { + metadata(std::make_unique(std::move(metadata))), macros(std::move(macros)), + preprocessorTrace(std::move(preprocessorTrace)) { } std::shared_ptr SyntaxTree::create(SourceManager& sourceManager, std::span sources, const Bag& options, MacroList inheritedMacros, - bool guess) { + bool guess, PreprocessorTraceMode traceMode) { if (sources.empty()) SLANG_THROW(std::invalid_argument("sources cannot be empty")); @@ -145,7 +152,13 @@ std::shared_ptr SyntaxTree::create(SourceManager& sourceManager, BumpAllocator alloc; Diagnostics diagnostics; - Preprocessor preprocessor(sourceManager, alloc, diagnostics, options, inheritedMacros); + std::optional traceRecorder; + if (traceMode == PreprocessorTraceMode::Enabled) { + traceRecorder.emplace(); + traceRecorder->setRootBuffer(sources.front()); + } + Preprocessor preprocessor(sourceManager, alloc, diagnostics, options, inheritedMacros, + traceRecorder ? &*traceRecorder : nullptr); const SourceLibrary* library = nullptr; for (auto it = sources.rbegin(); it != sources.rend(); it++) { @@ -167,12 +180,17 @@ std::shared_ptr SyntaxTree::create(SourceManager& sourceManager, else { root = &parser.parseGuess(); if (!parser.isDone()) - return create(sourceManager, sources, options, inheritedMacros, false); + return create(sourceManager, sources, options, inheritedMacros, false, traceMode); } + std::unique_ptr trace; + if (traceRecorder) + trace = std::make_unique(traceRecorder->snapshot()); + return std::shared_ptr( new SyntaxTree(root, library, sourceManager, std::move(alloc), std::move(diagnostics), - parser.getMetadata(), preprocessor.getDefinedMacros(), options)); + parser.getMetadata(), preprocessor.getDefinedMacros(), options, + std::move(trace))); } std::shared_ptr SyntaxTree::fromLibraryMapFile(std::string_view path, From b18893904d7d46b3d6d14adf77077224a7423a88 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:10:55 +0800 Subject: [PATCH 44/80] refactor(hir): share parse-derived preprocessor trace --- crates/hir/src/base_db/compilation_plan.rs | 64 +++++++++++++++------ crates/hir/src/base_db/source_db.rs | 40 ++++++++++--- crates/hir/src/base_db/source_db/preproc.rs | 12 +--- 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/crates/hir/src/base_db/compilation_plan.rs b/crates/hir/src/base_db/compilation_plan.rs index 29094cdb..c0398385 100644 --- a/crates/hir/src/base_db/compilation_plan.rs +++ b/crates/hir/src/base_db/compilation_plan.rs @@ -1,4 +1,4 @@ -use preproc::source::{MacroIncludeTarget, SourcePreprocModel}; +use preproc::source::{MacroIncludeTarget, SourcePreprocError, SourcePreprocModel}; use rustc_hash::FxHashSet; use syntax::{SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions}; use utils::{ @@ -24,6 +24,19 @@ pub struct CompilationPlan { pub include_dirs: Vec, pub top_modules: Vec, pub predefines: Vec, + pub include_scan_issues: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeScanIssue { + pub file_id: FileId, + pub reason: IncludeScanIssueReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncludeScanIssueReason { + TraceUnavailable, + Model(SourcePreprocError), } impl CompilationPlan { @@ -59,10 +72,18 @@ impl CompilationPlan { include_dirs: Vec, predefines: Vec, ) -> Self { - let include_only = + let (include_only, include_scan_issues) = include_targets_for_source_roots(db, &source_roots, &include_dirs, &predefines); let roots = compile_roots_for_source_roots(db, &source_roots, &include_only); - CompilationPlan { source_roots, roots, include_only, include_dirs, top_modules, predefines } + CompilationPlan { + source_roots, + roots, + include_only, + include_dirs, + top_modules, + predefines, + include_scan_issues, + } } } @@ -212,9 +233,10 @@ fn include_targets_for_source_roots( roots: &[SourceRootId], include_dirs: &[AbsPathBuf], predefines: &[String], -) -> FxHashSet { +) -> (FxHashSet, Vec) { let path_file_ids = path_file_ids(db); let mut included = FxHashSet::default(); + let mut issues = Vec::new(); let mut scanned = FxHashSet::default(); let mut pending = Vec::new(); for root_id in roots { @@ -239,7 +261,14 @@ fn include_targets_for_source_roots( continue; }; - for include in literal_include_targets(db, file_id, predefines) { + let include_targets = match literal_include_targets(db, file_id, predefines) { + Ok(targets) => targets, + Err(issue) => { + issues.push(issue); + continue; + } + }; + for include in include_targets { let MacroIncludeTarget::Literal { path, .. } = &include.target else { continue; }; @@ -252,19 +281,19 @@ fn include_targets_for_source_roots( } } - included + (included, issues) } fn literal_include_targets( db: &dyn SourceRootDb, file_id: FileId, predefines: &[String], -) -> Vec { +) -> Result, IncludeScanIssue> { if !matches!( db.file_kind(file_id), SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader ) { - return Vec::new(); + return Ok(Vec::new()); } let path = db.file_path(file_id).map(|path| path.to_string()).unwrap_or_default(); @@ -273,15 +302,18 @@ fn literal_include_targets( predefines: predefines.to_vec(), ..SyntaxTreeOptions::without_include_expansion() }; - let Some(trace) = - SyntaxTree::preprocessor_trace(&db.file_text(file_id), &name, &path, &options) - else { - return Vec::new(); - }; - let Ok(model) = SourcePreprocModel::from_trace(trace) else { - return Vec::new(); + let parsed = SyntaxTree::from_text_with_options_and_trace( + &db.file_text(file_id), + &name, + &path, + &options, + ); + let Some(trace) = parsed.preprocessor_trace else { + return Err(IncludeScanIssue { file_id, reason: IncludeScanIssueReason::TraceUnavailable }); }; - model.includes().to_vec() + let model = SourcePreprocModel::from_trace(trace) + .map_err(|err| IncludeScanIssue { file_id, reason: IncludeScanIssueReason::Model(err) })?; + Ok(model.includes().to_vec()) } fn resolve_include_target( diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 3ca5f095..8fac3da4 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,7 +1,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{ - Compilation, ParserExpectedSyntax, SyntaxDiagnostic, SyntaxTree, SyntaxTreeBuffer, - SyntaxTreeBufferIds, + Compilation, ParserExpectedSyntax, PreprocessorTrace, SyntaxDiagnostic, SyntaxTree, + SyntaxTreeBuffer, SyntaxTreeBufferIds, }; use triomphe::Arc; use utils::{line_index::TextSize, path_identity::PathIdentityIndex}; @@ -139,6 +139,12 @@ pub struct CompilationDiagnostic { pub diagnostic: SyntaxDiagnostic, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedCompilationUnit { + pub syntax_tree: SyntaxTree, + pub preprocessor_trace: Option, +} + fn source_file_identity(db: &dyn SourceDb, file_id: FileId) -> SourceFileIdentity { let path = db.file_path(file_id).map(|path| path.to_string()).unwrap_or_default(); let name = if path.is_empty() { "source".to_owned() } else { path.clone() }; @@ -203,7 +209,7 @@ fn syntax_tree_options_for_profile( } } -fn parse_src_for_compilation(db: &dyn SourceRootDb, file_id: FileId) -> SyntaxTree { +fn parsed_compilation_unit(db: &dyn SourceRootDb, file_id: FileId) -> ParsedCompilationUnit { let _span = tracing::info_span!("slang.parse_for_compilation", ?file_id).entered(); let text = { let _span = @@ -223,15 +229,32 @@ fn parse_src_for_compilation(db: &dyn SourceRootDb, file_id: FileId) -> SyntaxTr include_buffer_count ) .entered(); - SyntaxTree::from_text_with_options(&text, &identity.name, &identity.path, &options) - } - SourceFileKind::LibraryMap => { - SyntaxTree::from_library_map_text(&text, &identity.name, &identity.path) + let parsed = SyntaxTree::from_text_with_options_and_trace( + &text, + &identity.name, + &identity.path, + &options, + ); + ParsedCompilationUnit { + syntax_tree: parsed.tree, + preprocessor_trace: parsed.preprocessor_trace, + } } - SourceFileKind::ProjectManifest => SyntaxTree::from_text("", "", ""), + SourceFileKind::LibraryMap => ParsedCompilationUnit { + syntax_tree: SyntaxTree::from_library_map_text(&text, &identity.name, &identity.path), + preprocessor_trace: None, + }, + SourceFileKind::ProjectManifest => ParsedCompilationUnit { + syntax_tree: SyntaxTree::from_text("", "", ""), + preprocessor_trace: None, + }, } } +fn parse_src_for_compilation(db: &dyn SourceRootDb, file_id: FileId) -> SyntaxTree { + db.parsed_compilation_unit(file_id).syntax_tree.clone() +} + fn parser_expected_syntax( db: &dyn SourceRootDb, file_id: FileId, @@ -349,6 +372,7 @@ pub trait SourceRootDb: SourceDb { &self, profile_id: Option, ) -> Arc; + fn parsed_compilation_unit(&self, file_id: FileId) -> ParsedCompilationUnit; fn parse_src_for_compilation(&self, file_id: FileId) -> SyntaxTree; fn parser_expected_syntax( &self, diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs index 82133c6a..4f5b5562 100644 --- a/crates/hir/src/base_db/source_db/preproc.rs +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -5,7 +5,7 @@ use ::preproc::source::{ }; use rustc_hash::{FxHashMap, FxHashSet}; use smol_str::SmolStr; -use syntax::{PreprocessorTrace, SourceBufferOrigin, SyntaxTree, SyntaxTreeOptions}; +use syntax::{PreprocessorTrace, SourceBufferOrigin, SyntaxTreeOptions}; use triomphe::Arc; use utils::{ line_index::{TextRange, TextSize}, @@ -14,9 +14,7 @@ use utils::{ }; use vfs::{FileId, VfsPath}; -use super::{ - SourceFileKind, SourceRootDb, path_file_ids, source_file_identity, syntax_tree_options_for_file, -}; +use super::{SourceFileKind, SourceRootDb, path_file_ids, syntax_tree_options_for_file}; use crate::base_db::project::CompilationProfileId; mod source_mapping; @@ -673,14 +671,10 @@ pub(super) fn source_preproc_model( return Arc::new(Err(SourcePreprocQueryError::UnsupportedFileKind(file_kind))); } - let text = db.file_text(file_id); - let identity = source_file_identity(db, file_id); let profile_id = db.file_compilation_profile(file_id); let preprocess = db.file_preprocess_config(file_id); let options = syntax_tree_options_for_file(db, file_id); - let Some(trace) = - SyntaxTree::preprocessor_trace(&text, &identity.name, &identity.path, &options) - else { + let Some(trace) = db.parsed_compilation_unit(file_id).preprocessor_trace.clone() else { return Arc::new(Err(SourcePreprocQueryError::TraceUnavailable)); }; From fe4dd7d5ad3e86a0146bb507ae730d9a68329737 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:18:16 +0800 Subject: [PATCH 45/80] refactor(slang): remove static preprocessor trace path --- crates/preproc/src/source/model/tests.rs | 28 +++-- crates/slang/bindings/rust/ffi.rs | 12 -- crates/slang/bindings/rust/ffi/wrapper.cc | 145 ---------------------- crates/slang/bindings/rust/ffi/wrapper.h | 9 -- crates/slang/bindings/rust/lib.rs | 17 --- crates/slang/bindings/rust/tests.rs | 107 +++++++--------- 6 files changed, 58 insertions(+), 260 deletions(-) diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index b9dfbe08..a6b0fef4 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -14,6 +14,17 @@ const ROOT_PATH: &str = "sample/rtl/top.sv"; const HEADER_PATH: &str = "sample/include/defs.vh"; const INCLUDE_DIR: &str = "sample/include"; +fn preprocessor_trace( + root_text: &str, + name: &str, + path: &str, + options: &SyntaxTreeOptions, +) -> PreprocessorTrace { + SyntaxTree::from_text_with_options_and_trace(root_text, name, path, options) + .preprocessor_trace + .expect("parse-derived trace should be present when requested") +} + fn source_model( root_text: &str, header_text: &str, @@ -27,8 +38,7 @@ fn source_model( expand_includes: true, ..SyntaxTreeOptions::default() }; - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); + let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); let header_source = first_non_root_source(&trace, root_source); let model = SourcePreprocModel::from_trace(trace).unwrap(); @@ -64,8 +74,7 @@ fn source_model_from_root( root_text: &str, options: SyntaxTreeOptions, ) -> (SourcePreprocModel, PreprocSourceId) { - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); + let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); let model = SourcePreprocModel::from_trace(trace).unwrap(); (model, root_source) @@ -255,13 +264,7 @@ fn visible_macro_query_reads_timeline_without_event_records() { `undef A `define B 2 "#; - let trace = SyntaxTree::preprocessor_trace( - root_text, - "source", - ROOT_PATH, - &SyntaxTreeOptions::default(), - ) - .expect("trace should include root source"); + let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &SyntaxTreeOptions::default()); let root_source = PreprocSourceId::from(trace.root_buffer_id); let mut model = SourcePreprocModel::from_trace(trace).unwrap(); @@ -1282,8 +1285,7 @@ logic [`LEAF_WIDTH-1:0] data; expand_includes: true, ..SyntaxTreeOptions::default() }; - let trace = SyntaxTree::preprocessor_trace(root_text, "source", ROOT_PATH, &options) - .expect("trace should include root source"); + let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); let model = SourcePreprocModel::from_trace(trace).unwrap(); let header_source = source_by_path_suffix(&model, "include/defs.vh"); diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index 35576e59..4ec5186d 100644 --- a/crates/slang/bindings/rust/ffi.rs +++ b/crates/slang/bindings/rust/ffi.rs @@ -531,17 +531,6 @@ mod slang_ffi { offset: usize, ) -> RawLexedTokenAtOffset; - #[namespace = "wrapper::syntax"] - fn SyntaxTree_preprocessorTrace( - text: CxxSV, - name: CxxSV, - path: CxxSV, - predefines: Vec, - include_paths: Vec, - include_buffers: Vec, - expand_includes: bool, - ) -> RawPreprocessorTrace; - #[namespace = "wrapper::syntax"] fn SyntaxTree_preprocessorTraceFromParsed(tree: &SyntaxTree) -> RawPreprocessorTrace; @@ -675,7 +664,6 @@ impl_functions! { fn libraryMapExpectedSyntaxAtOffset(text: CxxSV, name: CxxSV, path: CxxSV, offset: usize) -> Vec |> SyntaxTree_libraryMapExpectedSyntaxAtOffset; fn directiveAtOffset(text: CxxSV, name: CxxSV, path: CxxSV, offset: usize) -> RawLexedTokenAtOffset |> SyntaxTree_directiveAtOffset; fn tokenWordAtOffset(text: CxxSV, name: CxxSV, path: CxxSV, offset: usize) -> RawLexedTokenAtOffset |> SyntaxTree_tokenWordAtOffset; - fn preprocessorTrace(text: CxxSV, name: CxxSV, path: CxxSV, predefines: Vec, include_paths: Vec, include_buffers: Vec, expand_includes: bool) -> RawPreprocessorTrace |> SyntaxTree_preprocessorTrace; fn preprocessorTraceFromParsed(&self) -> RawPreprocessorTrace |> SyntaxTree_preprocessorTraceFromParsed; fn buffer_id(&self) -> u32 |> SyntaxTree_buffer_id; } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index a6b8b93a..1565cd1e 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -818,14 +818,6 @@ ::RawPreprocessorTrace empty_preprocessor_trace() { return result; } -uint32_t trace_macro_definition_id( - const slang::syntax::SyntaxNode& syntax, - const slang::parsing::Preprocessor& preprocessor) { - if (auto* define = syntax.as_if()) - return preprocessor.getMacroDefinitionId(*define); - return 0; -} - std::unordered_set predefine_buffer_ids( const slang::parsing::PreprocessorTraceSnapshot& trace) { std::unordered_set bufferIds; @@ -1359,143 +1351,6 @@ ::RawLexedTokenAtOffset SyntaxTree_tokenWordAtOffset( return result; } -::RawPreprocessorTrace SyntaxTree_preprocessorTrace( - std::string_view text, - std::string_view name, - std::string_view path, - rust::Vec predefines, - rust::Vec includePaths, - rust::Vec<::RawSourceBuffer> includeBuffers, - bool expandIncludes) { - auto result = empty_preprocessor_trace(); - - slang::SourceManager sourceManager; - std::unordered_map assignedBuffers; - std::unordered_set sourceBufferIds; - auto assignSourceBuffer = [&](std::string_view bufferPath, - std::string_view bufferText) -> slang::SourceBuffer { - if (bufferPath.empty()) - return {}; - - auto key = source_manager_path_key(bufferPath); - auto it = assignedBuffers.find(key); - if (it != assignedBuffers.end()) - return it->second; - - std::string ownedText(bufferText); - auto buffer = sourceManager.assignText(key, ownedText); - assignedBuffers.emplace(std::move(key), buffer); - sourceBufferIds.insert(buffer.id.getId()); - return buffer; - }; - - for (const auto& includeBuffer : includeBuffers) - assignSourceBuffer(std::string(includeBuffer.path), std::string(includeBuffer.text)); - - auto bufferPath = path.empty() ? (name.empty() ? std::string_view("source") : name) : path; - auto rootBuffer = assignSourceBuffer(bufferPath, text); - if (!rootBuffer) - return result; - - if (!path.empty() && !name.empty()) - sourceManager.addLineDirective(slang::SourceLocation(rootBuffer.id, 0), 2, name, 0); - - result.root_buffer_id = rootBuffer.id.getId(); - result.has_root_buffer_id = true; - - slang::Bag options; - auto& ppOptions = options.insertOrGet(); - for (const auto& predefine : predefines) - ppOptions.predefines.emplace_back(std::string(predefine)); - for (const auto& includePath : includePaths) - ppOptions.additionalIncludePaths.emplace_back(std::string(includePath)); - ppOptions.expandIncludes = expandIncludes; - - slang::BumpAllocator alloc; - slang::Diagnostics diagnostics; - slang::parsing::Preprocessor preprocessor(sourceManager, alloc, diagnostics, options); - std::unordered_set predefineBufferIds; - for (auto buffer : sourceManager.getAllBuffers()) { - auto bufferId = buffer.getId(); - if (!sourceBufferIds.contains(bufferId)) - predefineBufferIds.insert(bufferId); - } - - for (auto* define : preprocessor.getDefinedMacros()) { - if (!define) - continue; - auto location = define->directive.location(); - if (!location.valid() || !predefineBufferIds.contains(location.buffer().getId())) - continue; - - auto eventId = static_cast(result.events.size()); - result.events.emplace_back(to_rust_preprocessor_trace_event( - *define, eventId, preprocessor.getMacroDefinitionId(*define))); - } - - preprocessor.pushSource(rootBuffer); - std::unordered_map - includeEventIdsByLocation; - size_t macroUsageTraceRecordCount = 0; - auto flushMacroUsageTraceRecords = [&]() { - auto records = preprocessor.getMacroUsageTraceRecords(); - for (; macroUsageTraceRecordCount < records.size(); macroUsageTraceRecordCount++) { - auto eventId = static_cast(result.events.size()); - result.events.emplace_back(to_rust_preprocessor_trace_macro_usage_record( - records[macroUsageTraceRecordCount], eventId, sourceManager)); - } - }; - - while (true) { - auto token = preprocessor.next(); - for (auto trivia : token.trivia()) { - if (trivia.kind != slang::parsing::TriviaKind::Directive) - continue; - - if (auto* syntax = trivia.syntax()) { - if (syntax->kind == slang::syntax::SyntaxKind::MacroUsage) - continue; - - auto eventId = static_cast(result.events.size()); - if (syntax->kind == slang::syntax::SyntaxKind::IncludeDirective) { - const auto& include = syntax->as(); - if (auto key = trace_source_location_key(include.directive.location())) - includeEventIdsByLocation.emplace(*key, eventId); - } - auto event = to_rust_preprocessor_trace_event( - *syntax, eventId, trace_macro_definition_id(*syntax, preprocessor)); - result.events.emplace_back(std::move(event)); - } - } - flushMacroUsageTraceRecords(); - - if (token.kind == slang::parsing::TokenKind::EndOfFile) - break; - - result.emitted_tokens.emplace_back( - to_rust_preprocessor_trace_emitted_token(token, sourceManager)); - } - - for (auto buffer : sourceManager.getAllBuffers()) { - auto includedFrom = sourceManager.getIncludedFrom(buffer); - auto key = trace_source_location_key(includedFrom); - if (!key) - continue; - - auto includeIt = includeEventIdsByLocation.find(*key); - if (includeIt == includeEventIdsByLocation.end()) - continue; - - ::RawPreprocessorTraceIncludeEdge edge; - edge.include_event_id = includeIt->second; - edge.included_buffer_id = buffer.getId(); - result.include_edges.emplace_back(edge); - } - - result.source_buffers = collectSourceBufferIds(sourceManager, predefineBufferIds); - return result; -} - ::RawPreprocessorTrace SyntaxTree_preprocessorTraceFromParsed(const SyntaxTree& tree) { auto* trace = tree.inner().getPreprocessorTrace(); if (!trace) diff --git a/crates/slang/bindings/rust/ffi/wrapper.h b/crates/slang/bindings/rust/ffi/wrapper.h index 7d99f77e..5828fdbf 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.h +++ b/crates/slang/bindings/rust/ffi/wrapper.h @@ -569,15 +569,6 @@ namespace wrapper { std::string_view name, std::string_view path, size_t offset); - ::RawPreprocessorTrace SyntaxTree_preprocessorTrace( - std::string_view text, - std::string_view name, - std::string_view path, - rust::Vec predefines, - rust::Vec includePaths, - rust::Vec<::RawSourceBuffer> includeBuffers, - bool expandIncludes); - ::RawPreprocessorTrace SyntaxTree_preprocessorTraceFromParsed(const SyntaxTree& tree); } diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 321dab87..037bddd2 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -1690,23 +1690,6 @@ impl SyntaxTree { )) } - pub fn preprocessor_trace( - text: &str, - name: &str, - path: &str, - options: &SyntaxTreeOptions, - ) -> Option { - PreprocessorTrace::from_raw(ffi::SyntaxTree::preprocessorTrace( - CxxSV::new(text), - CxxSV::new(name), - CxxSV::new(path), - options.predefines.clone(), - options.include_paths.clone(), - raw_include_buffers(options), - options.expand_includes, - )) - } - pub fn buffer_id(&self) -> u32 { self._ptr.buffer_id() } diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index 6af0beda..c38f4334 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -19,6 +19,17 @@ fn get_multi_module_tree() -> SyntaxTree { SyntaxTree::from_text("module A; endmodule; module B; endmodule;", "source", "") } +fn preprocessor_trace( + source: &str, + name: &str, + path: &str, + options: &SyntaxTreeOptions, +) -> PreprocessorTrace { + SyntaxTree::from_text_with_options_and_trace(source, name, path, options) + .preprocessor_trace + .expect("parse-derived trace should be present when requested") +} + fn get_tree_with_trivia() -> SyntaxTree { SyntaxTree::from_text( r#" @@ -918,8 +929,7 @@ wire disabled_by_header; expand_includes: true, }; - let trace = SyntaxTree::preprocessor_trace(source, "source", &source_path, &options) - .expect("root source buffer should be available"); + let trace = preprocessor_trace(source, "source", &source_path, &options); let normalized_path_for_buffer_id = |buffer_id: u32| { trace .source_buffers @@ -1048,13 +1058,12 @@ wire disabled_by_header; root_header_branch.disabled_ranges.iter().any(|range| range.buffer_id == root_buffer_id) ); - let unexpanded_trace = SyntaxTree::preprocessor_trace( + let unexpanded_trace = preprocessor_trace( source, "source", &source_path, &SyntaxTreeOptions { expand_includes: false, ..options.clone() }, - ) - .expect("root source buffer should be available"); + ); assert!(unexpanded_trace.events.iter().all(|event| { event.kind != SyntaxKind::DEFINE_DIRECTIVE || event.name.as_ref().map(|name| name.value_text.as_str()) != Some("HEADER_FLAG") @@ -1062,7 +1071,7 @@ wire disabled_by_header; } #[test] -fn preprocessor_trace_from_parsed_tree_matches_static_trace() { +fn preprocessor_trace_from_parsed_tree_reports_macro_include_facts() { let dir = TestDir::new("slang-parse-preprocessor-trace"); let rtl_dir = dir.create_dir_all("rtl"); let include_dir = dir.create_dir_all("include"); @@ -1090,10 +1099,17 @@ endmodule assert_eq!(parsed.tree.diagnostics(), Vec::new()); let parsed_trace = parsed.preprocessor_trace.expect("parse-derived trace should be present when requested"); - let static_trace = SyntaxTree::preprocessor_trace(source, "source", &source_path, &options) - .expect("static trace should be present for comparison"); - - assert_eq!(parsed_trace, static_trace); + assert!(parsed_trace.events.iter().any(|event| event.kind == SyntaxKind::INCLUDE_DIRECTIVE)); + assert!(parsed_trace.events.iter().any(|event| { + event.kind == SyntaxKind::DEFINE_DIRECTIVE + && event.name.as_ref().is_some_and(|name| name.value_text == "FROM_API") + })); + assert!(parsed_trace.events.iter().any(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.raw_text == "`ID") + })); + assert!(parsed_trace.emitted_tokens.iter().any(|token| token.raw_text == "11")); + assert!(parsed_trace.emitted_tokens.iter().any(|token| token.raw_text == "7")); } #[test] @@ -1105,13 +1121,8 @@ localparam int A = `OBJ; localparam int B = `ID(7); endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include emitted tokens"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); assert!( trace.emitted_tokens.iter().any(|token| { @@ -1217,13 +1228,8 @@ module m; localparam int W = `WRAP; endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include emitted tokens"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let leaf = trace .emitted_tokens @@ -1257,13 +1263,8 @@ module m(input logic [3:0] payload_i, output logic [3:0] y); assign y = `NEXT(payload_i[3:0]); endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include emitted tokens"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let payload = trace .emitted_tokens @@ -1351,13 +1352,8 @@ module m(input logic [3:0] payload_i, output logic [3:0] y); assign y = `NEXT(`PAYL); endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include macro usage events"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let next = trace .events @@ -1432,13 +1428,8 @@ module m(input logic [3:0] payload_i, output logic [3:0] y); assign y = `NEXT(`WRAP); endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include macro usage events"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let next = trace .events @@ -1501,13 +1492,8 @@ fn preprocessor_trace_reports_escaped_identifier_macro_body_identity() { "wire `ESC;\n", "endmodule\n" ); - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include emitted tokens"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let escaped = trace .emitted_tokens @@ -1541,13 +1527,8 @@ wire `JOIN(foo,bar); string s = `STR(foo); endmodule "#; - let trace = SyntaxTree::preprocessor_trace( - source, - "source", - "sample/rtl/top.sv", - &SyntaxTreeOptions::default(), - ) - .expect("trace should include emitted tokens"); + let trace = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); let pasted = trace .emitted_tokens @@ -1571,7 +1552,7 @@ localparam int P = `FROM_API; localparam int L = `__LINE__; endmodule "#; - let trace = SyntaxTree::preprocessor_trace( + let trace = preprocessor_trace( source, "source", "sample/rtl/top.sv", @@ -1579,8 +1560,7 @@ endmodule predefines: vec!["FROM_API=11".to_owned()], ..SyntaxTreeOptions::default() }, - ) - .expect("trace should include emitted tokens"); + ); let predefine_source = trace .source_buffers @@ -1661,8 +1641,7 @@ fn preprocessor_trace_records_nested_include_edges() { ..SyntaxTreeOptions::default() }; - let trace = SyntaxTree::preprocessor_trace(source, "source", &source_path, &options) - .expect("root source buffer should be available"); + let trace = preprocessor_trace(source, "source", &source_path, &options); assert!( trace .events From 3f0d50aa878303c8eb83ad2ff2646e5ba84ec148 Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 02:22:33 +0800 Subject: [PATCH 46/80] Add struct document symbols --- crates/hir/src/hir_def/aggregate.rs | 29 +++++++++++++- crates/ide/src/document_symbols.rs | 60 ++++++++++++++++++++++------- crates/ide/src/lib.rs | 1 + crates/ide/src/verilog_2005.rs | 36 +++++++++++++++++ src/lsp_ext/to_proto.rs | 1 + 5 files changed, 111 insertions(+), 16 deletions(-) diff --git a/crates/hir/src/hir_def/aggregate.rs b/crates/hir/src/hir_def/aggregate.rs index 8b4b9250..f12acfec 100644 --- a/crates/hir/src/hir_def/aggregate.rs +++ b/crates/hir/src/hir_def/aggregate.rs @@ -75,6 +75,7 @@ pub(crate) fn lower_struct_def( #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct StructSrc { pub node: SyntaxNodePtr, + pub name: Option, } impl IsSrc for StructSrc { @@ -89,6 +90,18 @@ impl IsSrc for StructSrc { } } +impl IsNamedSrc for StructSrc { + #[inline] + fn name_kind(&self) -> Option { + self.name.map(|name| name.kind()) + } + + #[inline] + fn name_range(&self) -> Option { + self.name.map(|name| name.range()) + } +} + impl<'a> ToAstNode<'a, ast::StructUnionType<'a>> for StructSrc { fn to_node(&self, tree: &'a syntax::SyntaxTree) -> Option> { let mut node = self.node.to_node(tree)?; @@ -101,16 +114,28 @@ impl<'a> ToAstNode<'a, ast::StructUnionType<'a>> for StructSrc { impl From> for StructSrc { fn from(node: ast::StructUnionType<'_>) -> Self { - StructSrc { node: AstNodeExt::to_ptr(&node) } + let syntax = node.syntax(); + let name = struct_name_token(node).map(|name| SyntaxTokenPtr::from_token_in(syntax, name)); + StructSrc { node: AstNodeExt::to_ptr(&node), name } } } impl<'a> FromSourceAst<'a, ast::StructUnionType<'a>> for StructSrc { fn from_source_ast(node: SourceAst>) -> Self { - StructSrc { node: AstNodeExt::to_ptr(&node.into_inner()) } + let node = node.into_inner(); + let syntax = node.syntax(); + let name = struct_name_token(node) + .and_then(|name| root_token_in(syntax, name).map(SyntaxTokenPtr::from_token)); + StructSrc { node: AstNodeExt::to_ptr(&node), name } } } +fn struct_name_token(node: ast::StructUnionType<'_>) -> Option> { + let data_type = ast::DataType::StructUnionType(node); + let typedef = data_type.syntax().parent().and_then(ast::TypedefDeclaration::cast)?; + typedef.name() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClassMemberKind { Property, diff --git a/crates/ide/src/document_symbols.rs b/crates/ide/src/document_symbols.rs index 3a251c54..92dfb220 100644 --- a/crates/ide/src/document_symbols.rs +++ b/crates/ide/src/document_symbols.rs @@ -7,6 +7,7 @@ use hir::{ file::HirFileId, hir_def::{ DEFAULT_NAME, + aggregate::{StructDef, StructId, StructKind, StructSrc}, block::{BlockId, BlockInfo, BlockItem, BlockSrc, LocalBlockId}, declaration::{Declaration, DeclarationId, DeclarationSrc}, expr::declarator::{DeclId, Declarator, DeclaratorSrc, DeclsRange}, @@ -224,9 +225,7 @@ pub(crate) fn document_symbols(db: &dyn HirDb, file_id: FileId) -> Vec { build_subroutine(&mut collector, subroutine_id, file, src_map) } - FileItem::StructId(_) => { - // TODO: implement document symbols for these items - } + FileItem::StructId(struct_id) => build_struct(&mut collector, struct_id, file, src_map), FileItem::ConfigDeclId(config_id) => { build_config_decl(&mut collector, config_id, file, src_map) } @@ -328,9 +327,7 @@ fn collect_module_items( ModuleItem::SubroutineId(subroutine_id) => { build_subroutine(collector, subroutine_id, module, src_map) } - ModuleItem::StructId(_) => { - // TODO: implement document symbols for these items - } + ModuleItem::StructId(struct_id) => build_struct(collector, struct_id, module, src_map), } } collector.pop(); @@ -365,9 +362,7 @@ fn collect_block_items( BlockItem::TypedefId(typedef_id) => { build_typedef(collector, typedef_id, block, src_map) } - BlockItem::StructId(_) => { - // TODO: implement document symbols for these items - } + BlockItem::StructId(struct_id) => build_struct(collector, struct_id, block, src_map), } } collector.pop(); @@ -489,6 +484,7 @@ fn build_generate_region( + GetRef + GetRef + GetRef + + GetRef + GetRef, SrcMap: Get> + Get> @@ -497,6 +493,7 @@ fn build_generate_region( + Get> + Get> + Get> + + Get> + Get>, { let hir = arena.get(generate_region_id); @@ -527,7 +524,9 @@ fn build_generate_region( let proc = arena.get(proc_id); build_stmt(db, collector, proc.stmt, arena, src_map); } - GenerateItem::StructId(_) => {} + GenerateItem::StructId(struct_id) => { + build_struct(collector, struct_id, arena, src_map); + } GenerateItem::SubroutineId(subroutine_id) => { build_subroutine(collector, subroutine_id, arena, src_map); } @@ -578,14 +577,43 @@ fn build_generate_block( } } } - GenerateBlockItem::ContAssignId(_) - | GenerateBlockItem::DefParamId(_) - | GenerateBlockItem::StructId(_) => {} + GenerateBlockItem::ContAssignId(_) | GenerateBlockItem::DefParamId(_) => {} + GenerateBlockItem::StructId(struct_id) => { + build_struct(collector, struct_id, generate_block, src_map); + } } } collector.pop(); } +#[inline] +fn build_struct( + collector: &mut SymbolCollecter, + struct_id: Idx, + arena: &Arn, + src_map: &SrcMap, +) where + Arn: GetRef, + SrcMap: Get>, +{ + let hir = arena.get(struct_id); + let Some(src) = src_map.get(struct_id) else { + return; + }; + + let name = hir.name.clone().or_else(|| Some(struct_kind_name(hir.kind))); + collector.push_symbol_with_kind(&name, src, SymbolKind::Struct); + collector.pop(); +} + +#[inline] +fn struct_kind_name(kind: StructKind) -> SmolStr { + match kind { + StructKind::Struct => SmolStr::new_static("struct"), + StructKind::Union => SmolStr::new_static("union"), + } +} + #[inline] fn build_specify_block( collector: &mut SymbolCollecter, @@ -666,7 +694,11 @@ fn build_typedef( let Some(src) = src_map.get(typedef_id) else { return; }; - collector.push_symbol_with_kind(&hir.name, src, SymbolKind::Typedef); + let kind = match hir.ty { + Some(hir::hir_def::expr::data_ty::DataTy::Struct(_)) => SymbolKind::Struct, + _ => SymbolKind::Typedef, + }; + collector.push_symbol_with_kind(&hir.name, src, kind); collector.pop(); } diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index cdac0865..a2f12299 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -58,6 +58,7 @@ pub enum SymbolKind { Genvar, Specparam, Typedef, + Struct, Instance, Block, Stmt, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 0ad63130..07b58e8a 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -2091,3 +2091,39 @@ fn verilog_2005_lsp_snapshots() { assert_snapshot!("verilog_2005_lsp_snapshots", report); } + +#[test] +fn document_symbols_include_typedef_structs_and_nested_generate_structs() { + let text = r#" +module top; + typedef struct packed { + logic ready; + } packet_t; + + generate + if (1) begin : g + typedef union packed { + logic [7:0] raw; + logic flag; + } state_t; + end + endgenerate +endmodule +"#; + let (host, file_id) = setup(text); + let analysis = host.make_analysis(); + + let symbols = analysis.document_symbol(file_id).unwrap(); + let mut lines = Vec::new(); + collect_symbol_lines(&symbols, 0, &mut lines); + let dump = lines.join("\n"); + + assert!( + dump.contains("packet_t Struct"), + "typedef struct should surface as a struct symbol: {dump}" + ); + assert!( + dump.contains("state_t Struct"), + "nested generate typedef union should surface as a struct symbol: {dump}" + ); +} diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 1b5e53ee..4209f6cf 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -266,6 +266,7 @@ fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { SymbolKind::Genvar => LspSymbolKind::VARIABLE, SymbolKind::Specparam => LspSymbolKind::TYPE_PARAMETER, SymbolKind::Typedef => LspSymbolKind::TYPE_PARAMETER, + SymbolKind::Struct => LspSymbolKind::STRUCT, SymbolKind::Instance => LspSymbolKind::OBJECT, SymbolKind::Block => LspSymbolKind::NAMESPACE, SymbolKind::Stmt => LspSymbolKind::NAMESPACE, From 6896bf0270481ce58871f6bf2a665e5893627a0a Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 02:23:25 +0800 Subject: [PATCH 47/80] Add SystemVerilog refactor code actions --- crates/hir/src/display.rs | 33 +- crates/hir/src/hir_def/expr/data_ty.rs | 3 +- crates/hir/src/hir_def/subroutine.rs | 6 +- crates/ide/src/code_action/context.rs | 1 + crates/ide/src/code_action/handlers.rs | 17 + .../handlers/convert_always_block.rs | 141 +++++ .../convert_named_port_connections.rs | 119 ++++ .../handlers/convert_port_declarations.rs | 397 +++++++++++++ .../code_action/handlers/extract_variable.rs | 217 +++++++ .../code_action/handlers/merge_nested_if.rs | 130 +++++ .../handlers/pull_assignment_up.rs | 129 +++++ .../handlers/reformat_number_literal.rs | 96 +++ .../handlers/remove_parentheses.rs | 148 +++++ crates/ide/src/code_action/tests.rs | 548 +++++++++++++++++- crates/ide/src/rename.rs | 10 +- crates/slang/bindings/rust/ast.rs | 18 + src/i18n.rs | 24 + src/i18n/en.toml | 14 + src/i18n/zh-CN.toml | 18 +- src/lsp_ext/to_proto.rs | 20 + src/tests.rs | 123 ++++ 21 files changed, 2188 insertions(+), 24 deletions(-) create mode 100644 crates/ide/src/code_action/handlers/convert_always_block.rs create mode 100644 crates/ide/src/code_action/handlers/convert_named_port_connections.rs create mode 100644 crates/ide/src/code_action/handlers/convert_port_declarations.rs create mode 100644 crates/ide/src/code_action/handlers/extract_variable.rs create mode 100644 crates/ide/src/code_action/handlers/merge_nested_if.rs create mode 100644 crates/ide/src/code_action/handlers/pull_assignment_up.rs create mode 100644 crates/ide/src/code_action/handlers/reformat_number_literal.rs create mode 100644 crates/ide/src/code_action/handlers/remove_parentheses.rs mode change 100644 => 100755 crates/slang/bindings/rust/ast.rs diff --git a/crates/hir/src/display.rs b/crates/hir/src/display.rs index df0d0c43..34b5a52a 100644 --- a/crates/hir/src/display.rs +++ b/crates/hir/src/display.rs @@ -106,9 +106,6 @@ impl HirDisplay for InContainer { match self.value { DataTy::Builtin(ty_id) => match ty_id.lookup(f.db) { BuiltinDataTy::Int { kind, signing } => { - if signing { - f.write_str("signed ")?; - } match kind { IntKind::Byte => f.write_str("byte"), IntKind::ShortInt => f.write_str("shortint"), @@ -116,27 +113,45 @@ impl HirDisplay for InContainer { IntKind::LongInt => f.write_str("longint"), IntKind::Integer => f.write_str("integer"), IntKind::Time => f.write_str("time"), + }?; + if signing { + f.write_str(" signed")?; } + Ok(()) } BuiltinDataTy::Vector { kind, signing, dimensions } => { - if signing { - f.write_str("signed ")?; - } + let mut wrote_head = false; match kind { VecKind::Bit => { if !f.simplified_ty { - f.write_str("bit")? + f.write_str("bit")?; + wrote_head = true; } } VecKind::Logic => { if !f.simplified_ty { - f.write_str("logic")? + f.write_str("logic")?; + wrote_head = true; } } - VecKind::Reg => f.write_str("reg")?, + VecKind::Reg => { + f.write_str("reg")?; + wrote_head = true; + } + } + if signing { + if wrote_head { + f.write_str(" ")?; + } + f.write_str("signed")?; + wrote_head = true; } for dim in dimensions.iter().flatten() { + if wrote_head { + f.write_str(" ")?; + } self.with_value(*dim).hir_fmt(f)?; + wrote_head = true; } Ok(()) } diff --git a/crates/hir/src/hir_def/expr/data_ty.rs b/crates/hir/src/hir_def/expr/data_ty.rs index e4ce7250..9d420b67 100644 --- a/crates/hir/src/hir_def/expr/data_ty.rs +++ b/crates/hir/src/hir_def/expr/data_ty.rs @@ -140,8 +140,7 @@ impl LowerExprCtx<'_> { LogicType(_) => Either::Right(VecKind::Logic), }; - let signing = Self::lower_signing(ty.signing()) - .unwrap_or(matches!(kind, Either::Left(IntKind::Time) | Either::Right(_))); + let signing = Self::lower_signing(ty.signing()).unwrap_or(matches!(kind, Either::Left(_))); let dimensions = ty.dimensions().children().map(|dim| self.lower_dimension(dim)).collect(); match kind { diff --git a/crates/hir/src/hir_def/subroutine.rs b/crates/hir/src/hir_def/subroutine.rs index e833b25c..6c1c2e7a 100644 --- a/crates/hir/src/hir_def/subroutine.rs +++ b/crates/hir/src/hir_def/subroutine.rs @@ -24,7 +24,7 @@ use super::{ impl_lower_expr, timing_control::{EventExpr, EventExprSrc}, }, - lower_ident, lower_ident_opt, + lower_ident_opt, module::{ModuleId, generate::GenerateBlockId}, stmt::{LowerStmt, Stmt, StmtId, StmtSrc, impl_lower_stmt}, typedef::{Typedef, TypedefId, TypedefSrc, lower_typedef_data_ty}, @@ -236,10 +236,10 @@ where fn lower_name(name: ast::Name) -> Option { if let Some(id) = name.as_identifier_name().and_then(|n| n.identifier()) { - return lower_ident(Some(id)); + return lower_ident_opt(Some(id)); } if let Some(select) = name.as_identifier_select_name() { - return select.identifier().and_then(|tok| lower_ident(Some(tok))); + return select.identifier().and_then(|tok| lower_ident_opt(Some(tok))); } if let Some(scoped) = name.as_scoped_name() { return lower_name(scoped.right()); diff --git a/crates/ide/src/code_action/context.rs b/crates/ide/src/code_action/context.rs index 24ac41e6..e72ebe37 100644 --- a/crates/ide/src/code_action/context.rs +++ b/crates/ide/src/code_action/context.rs @@ -26,6 +26,7 @@ impl<'a> CodeActionCtx<'a> { ) -> Option { let parsed_file = sema.parse_file(file_id); parsed_file.compilation_unit()?; + Some(Self { sema, file_id, range, diagnostics, parsed_file }) } diff --git a/crates/ide/src/code_action/handlers.rs b/crates/ide/src/code_action/handlers.rs index 3289147b..33390b07 100644 --- a/crates/ide/src/code_action/handlers.rs +++ b/crates/ide/src/code_action/handlers.rs @@ -8,13 +8,21 @@ mod add_instance_parens; mod add_missing_connections; mod add_missing_parameters; mod apply_de_morgan; +mod convert_always_block; mod convert_literal_base; +mod convert_named_port_connections; mod convert_ordered_connections; +mod convert_port_declarations; mod expand_compound_assignment; mod expand_postfix_inc_dec; +mod extract_variable; mod insert_expected_token; mod invert_if_else; +mod merge_nested_if; +mod pull_assignment_up; +mod reformat_number_literal; mod remove_empty_port_connections; +mod remove_parentheses; mod sort_named_instantiation_items; mod split_declaration_declarators; mod wrap_statement_in_begin_end; @@ -22,22 +30,31 @@ mod wrap_statement_in_begin_end; pub(crate) fn all() -> &'static [Handler] { &[ convert_literal_base::convert_literal_base, + reformat_number_literal::reformat_number_literal, add_missing_connections::add_missing_connections, add_missing_parameters::add_missing_parameters, convert_ordered_connections::convert_ordered_ports, convert_ordered_connections::convert_ordered_params, + convert_named_port_connections::convert_named_port_connection_shorthand, remove_empty_port_connections::remove_empty_port_connections, add_implicit_named_port_parens::add_implicit_named_port_parens, add_instance_parens::add_instance_parens, + convert_always_block::convert_always_block, + convert_port_declarations::convert_port_declarations, split_declaration_declarators::split_declaration_declarators, sort_named_instantiation_items::sort_named_parameter_assignments, sort_named_instantiation_items::sort_named_port_connections, add_default_case_item::add_default_case_item, invert_if_else::invert_if_else, + merge_nested_if::merge_nested_if, wrap_statement_in_begin_end::unwrap_single_statement_block, wrap_statement_in_begin_end::wrap_statement_in_begin_end, + remove_parentheses::remove_parentheses, expand_postfix_inc_dec::expand_postfix_inc_dec, expand_compound_assignment::expand_compound_assignment, + extract_variable::extract_variable, + pull_assignment_up::pull_assignment_up, + pull_assignment_up::pull_assignment_down, apply_de_morgan::apply_de_morgan, insert_expected_token::insert_expected_token, ] diff --git a/crates/ide/src/code_action/handlers/convert_always_block.rs b/crates/ide/src/code_action/handlers/convert_always_block.rs new file mode 100644 index 00000000..169cad5b --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_always_block.rs @@ -0,0 +1,141 @@ +use std::ops::Range; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ALWAYS_TO_COMB_ID: CodeActionId = CodeActionId { + name: "convert_always_to_always_comb", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_TO_COMB_LABEL: &str = "Convert to always_comb"; + +const ALWAYS_TO_FF_ID: CodeActionId = CodeActionId { + name: "convert_always_to_always_ff", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_TO_FF_LABEL: &str = "Convert to always_ff"; + +const ALWAYS_COMB_TO_ALWAYS_ID: CodeActionId = CodeActionId { + name: "convert_always_comb_to_always", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_COMB_TO_ALWAYS_LABEL: &str = "Convert to always @(*)"; + +const ALWAYS_FF_TO_ALWAYS_ID: CodeActionId = CodeActionId { + name: "convert_always_ff_to_always", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_FF_TO_ALWAYS_LABEL: &str = "Convert to always @(...)"; + +pub(super) fn convert_always_block( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let proc = ctx.find_node_at_offset::()?; + let keyword = proc.keyword()?.text_range_in(proc.syntax())?; + let target = proc.syntax().text_range()?; + let mut allowed_ranges = vec![keyword]; + + match proc { + ast::ProceduralBlock::AlwaysBlock(_) => { + let timing_stmt = proc.statement().as_timing_control_statement()?; + let timing = timing_stmt.timing_control(); + allowed_ranges.push(timing.syntax().text_range()?); + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + if timing.as_implicit_event_control().is_some() { + let stmt_range = timing_stmt.statement().syntax().text_range()?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let stmt_text = text.get(Range::from(stmt_range))?; + collector.add(ALWAYS_TO_COMB_ID, ALWAYS_TO_COMB_LABEL, target, |builder| { + builder.replace(keyword, "always_comb"); + builder + .replace(timing_stmt.syntax().text_range().unwrap(), stmt_text.to_owned()); + }); + } + + if edge_sensitive_timing_control(timing) { + collector.add(ALWAYS_TO_FF_ID, ALWAYS_TO_FF_LABEL, target, |builder| { + builder.replace(keyword, "always_ff"); + }); + } + + Some(()) + } + ast::ProceduralBlock::AlwaysCombBlock(_) => { + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + collector.add( + ALWAYS_COMB_TO_ALWAYS_ID, + ALWAYS_COMB_TO_ALWAYS_LABEL, + target, + |builder| { + builder.replace(keyword, "always"); + builder.insert(keyword.end(), " @(*)"); + }, + ) + } + ast::ProceduralBlock::AlwaysFFBlock(_) => { + let timing_stmt = proc.statement().as_timing_control_statement()?; + allowed_ranges.push(timing_stmt.timing_control().syntax().text_range()?); + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + if !edge_sensitive_timing_control(timing_stmt.timing_control()) { + return None; + } + + collector.add(ALWAYS_FF_TO_ALWAYS_ID, ALWAYS_FF_TO_ALWAYS_LABEL, target, |builder| { + builder.replace(keyword, "always"); + }) + } + _ => None, + } +} + +fn range_intersects_any( + range: utils::text_edit::TextRange, + allowed_ranges: &[utils::text_edit::TextRange], +) -> bool { + allowed_ranges.iter().any(|allowed| range_intersects(range, *allowed)) +} + +fn range_intersects(lhs: utils::text_edit::TextRange, rhs: utils::text_edit::TextRange) -> bool { + if lhs.is_empty() { + rhs.contains(lhs.start()) + } else { + lhs.start() < rhs.end() && rhs.start() < lhs.end() + } +} + +fn edge_sensitive_timing_control(timing: ast::TimingControl<'_>) -> bool { + timing + .as_event_control_with_expression() + .is_some_and(|control| edge_sensitive_event_expr(control.expr())) +} + +fn edge_sensitive_event_expr(expr: ast::EventExpression<'_>) -> bool { + match expr { + ast::EventExpression::ParenthesizedEventExpression(expr) => { + edge_sensitive_event_expr(expr.expr()) + } + ast::EventExpression::BinaryEventExpression(expr) => { + edge_sensitive_event_expr(expr.left()) && edge_sensitive_event_expr(expr.right()) + } + ast::EventExpression::SignalEventExpression(expr) => expr.edge().is_some(), + } +} diff --git a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs new file mode 100644 index 00000000..81a42328 --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs @@ -0,0 +1,119 @@ +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const EXPAND_ID: CodeActionId = CodeActionId { + name: "expand_named_port_connection_shorthand", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const EXPAND_LABEL: &str = "Expand named port shorthand"; + +const COLLAPSE_ID: CodeActionId = CodeActionId { + name: "collapse_named_port_connection_shorthand", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const COLLAPSE_LABEL: &str = "Collapse named port to shorthand"; + +pub(super) fn convert_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + expand_named_port_connection_shorthand(collector, ctx) + .or(collapse_named_port_connection_shorthand(collector, ctx)) +} + +fn expand_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + ctx.find_node_at_offset::()?; + let instance = ctx.find_node_at_offset::()?; + let conns = named_port_connections(instance)?; + let edits = conns + .iter() + .filter(|conn| conn.open_paren().is_none()) + .map(|conn| { + let name = conn.name()?; + Some((name.text_range_in(conn.syntax())?.end(), name.value_text().to_string())) + }) + .collect::>>()?; + if edits.is_empty() { + return None; + } + + let target = instance.syntax().text_range()?; + + collector.add(EXPAND_ID, EXPAND_LABEL, target, |builder| { + for (insert_offset, name) in edits { + builder.insert(insert_offset, format!("({name})")); + } + }) +} + +fn collapse_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + ctx.find_node_at_offset::()?; + let instance = ctx.find_node_at_offset::()?; + let conns = named_port_connections(instance)?; + let edits = conns + .iter() + .filter_map(|conn| collapsible_named_port_connection_range(*conn)) + .collect::>(); + if edits.is_empty() { + return None; + } + + let target = instance.syntax().text_range()?; + collector.add(COLLAPSE_ID, COLLAPSE_LABEL, target, |builder| { + for remove_range in edits { + builder.delete(remove_range); + } + }) +} + +fn named_port_connections( + instance: ast::HierarchicalInstance<'_>, +) -> Option>> { + let conns = instance + .connections() + .children() + .map(|conn| conn.as_named_port_connection()) + .collect::>>()?; + (!conns.is_empty()).then_some(conns) +} + +fn collapsible_named_port_connection_range( + conn: ast::NamedPortConnection<'_>, +) -> Option { + let conn_name = conn.name()?; + let port_name = conn_name.value_text().to_string(); + + let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); + + use ast::{Expression, Name}; + let actual = match expr { + Expression::Name(Name::IdentifierName(ident)) => ident.identifier()?, + Expression::Name(Name::IdentifierSelectName(ident)) + if ident.selectors().children().next().is_none() => + { + ident.identifier()? + } + _ => return None, + }; + if actual.value_text().to_string() != port_name { + return None; + } + + Some(TextRange::new( + conn_name.text_range_in(conn.syntax())?.end(), + conn.close_paren()?.text_range_in(conn.syntax())?.end(), + )) +} diff --git a/crates/ide/src/code_action/handlers/convert_port_declarations.rs b/crates/ide/src/code_action/handlers/convert_port_declarations.rs new file mode 100644 index 00000000..b78908fd --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_port_declarations.rs @@ -0,0 +1,397 @@ +use std::ops::Range; + +use hir::{ + base_db::source_db::SourceDb, + container::{InContainer, InModule}, + db::HirDb, + display::HirDisplay, + hir_def::{ + Ident, + declaration::DeclarationSrc, + expr::declarator::{DeclId, DeclaratorParent}, + module::{ + Module, ModuleId, ModuleSourceMap, + port::{PortDecl, PortDeclSrc, Ports}, + }, + }, + scope::{ModuleEntry, ModuleScope, NonAnsiPortEntry}, + source_map::IsSrc, +}; +use itertools::Itertools; +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; +use utils::{ + get::{Get, GetRef}, + text_edit::TextRange, +}; + +use crate::code_action::{ + CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +}; + +const ANSI_TO_NON_ANSI_ID: CodeActionId = CodeActionId { + name: "convert_ansi_ports_to_non_ansi", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ANSI_TO_NON_ANSI_LABEL: &str = "Convert ANSI port declarations to non-ANSI"; + +const NON_ANSI_TO_ANSI_ID: CodeActionId = CodeActionId { + name: "convert_non_ansi_ports_to_ansi", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const NON_ANSI_TO_ANSI_LABEL: &str = "Convert non-ANSI port declarations to ANSI"; + +pub(super) fn convert_port_declarations( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + convert_ansi_ports_to_non_ansi(collector, ctx) + .or(convert_non_ansi_ports_to_ansi(collector, ctx)) +} + +fn convert_ansi_ports_to_non_ansi( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let ast_module = ctx.find_node_at_offset::()?; + let port_list = ast_module.header().ports()?.as_ansi_port_list()?; + + let module_id = ctx.sema().module_to_def(ctx.file_id().into(), ast_module)?; + let (module, module_src_map) = ctx.sema().db.module_with_source_map(module_id); + let Ports::Ansi(port_decls) = &module.ports else { + return None; + }; + + let mut port_names = Vec::with_capacity(port_decls.len()); + let mut port_items = Vec::with_capacity(port_decls.len()); + for (port_id, port_decl) in port_decls.iter() { + let src = module_src_map.port_srcs.get(port_id)?; + let PortDeclSrc::ImplicitAnsiPort(_) = src else { + return None; + }; + + let name = port_decl_declared_name(&module, port_decl)?; + port_names.push(name); + port_items.push((port_decl, src)); + } + + if port_names.is_empty() { + return None; + } + + let open_paren = port_list.open_paren()?.text_range_in(port_list.syntax())?; + let close_paren = port_list.close_paren()?.text_range_in(port_list.syntax())?; + if !port_list_trigger_range(open_paren, close_paren)?.contains_range(ctx.range()) { + return None; + } + + let body_range = module_body_range(ast_module)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let generated_members = port_items + .iter() + .map(|(port_decl, src)| { + render_ansi_port_declaration(ctx, module_id, port_decl, *src, &text) + }) + .collect::>>()?; + let port_list_replacement = render_port_list(&text, open_paren, close_paren, &port_names)?; + let body_replacement = + render_module_body(&text, ast_module, body_range, &generated_members, &[])?; + let target = port_list.syntax().text_range()?; + + collector.add(ANSI_TO_NON_ANSI_ID, ANSI_TO_NON_ANSI_LABEL, target, |builder| { + builder.replace(target, port_list_replacement); + builder.replace(body_range, body_replacement); + }) +} + +fn convert_non_ansi_ports_to_ansi( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let ast_module = ctx.find_node_at_offset::()?; + let port_list = ast_module.header().ports()?.as_non_ansi_port_list()?; + + let module_id = ctx.sema().module_to_def(ctx.file_id().into(), ast_module)?; + let (module, module_src_map) = ctx.sema().db.module_with_source_map(module_id); + let Ports::NonAnsi { ports, refs, .. } = &module.ports else { + return None; + }; + + let mut port_names = Vec::new(); + for (_, port) in ports.iter() { + let mut ref_ids = port.refs.clone()?; + let ref_id = ref_ids.next()?; + if ref_ids.next().is_some() { + return None; + } + + let port_ref = &refs[ref_id]; + if port_ref.select.is_some() { + return None; + } + + let ident = port_ref.ident.as_ref()?; + if port.label.as_ref() != Some(ident) { + return None; + } + port_names.push(ident.clone()); + } + if port_names.is_empty() { + return None; + } + + let open_paren = port_list.open_paren()?.text_range_in(port_list.syntax())?; + let close_paren = port_list.close_paren()?.text_range_in(port_list.syntax())?; + if !port_list_trigger_range(open_paren, close_paren)?.contains_range(ctx.range()) { + return None; + } + + let body_range = module_body_range(ast_module)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let module_scope = ctx.sema().db.module_scope(module_id); + let port_replacements = port_names + .iter() + .map(|name| { + non_ansi_port_replacement(ctx, &module, &module_src_map, &module_scope, name, &text) + }) + .collect::>>()?; + let ansi_items = port_replacements + .iter() + .map(|replacement| replacement.ansi_item.clone()) + .collect::>(); + let removed_ranges = port_replacements + .into_iter() + .flat_map(|replacement| replacement.remove_ranges) + .collect::>(); + let port_list_replacement = render_port_list(&text, open_paren, close_paren, &ansi_items)?; + let body_replacement = render_module_body(&text, ast_module, body_range, &[], &removed_ranges)?; + let target = port_list.syntax().text_range()?; + + collector.add(NON_ANSI_TO_ANSI_ID, NON_ANSI_TO_ANSI_LABEL, target, |builder| { + builder.replace(target, port_list_replacement); + builder.replace(body_range, body_replacement); + }) +} + +fn port_list_trigger_range(open: TextRange, close: TextRange) -> Option { + (open.end() <= close.start()).then(|| TextRange::new(open.end(), close.start())) +} + +fn port_decl_declared_name(module: &Module, port_decl: &PortDecl) -> Option { + let decl_id = single_port_decl_id(port_decl)?; + Some(module.get(decl_id).name.as_ref()?.to_string()) +} + +fn single_port_decl_id(port_decl: &PortDecl) -> Option { + let mut decls = port_decl.decls.clone(); + let decl_id = decls.next()?; + if decls.next().is_some() { + return None; + } + Some(decl_id) +} + +struct NonAnsiPortReplacement { + ansi_item: String, + remove_ranges: Vec, +} + +fn non_ansi_port_replacement( + ctx: &CodeActionCtx, + module: &Module, + module_src_map: &ModuleSourceMap, + module_scope: &ModuleScope, + name: &Ident, + text: &str, +) -> Option { + let ModuleEntry::NonAnsiPortEntry(NonAnsiPortEntry { + port_decl: Some(port_decl), + data_decl, + .. + }) = module_scope.get(name)? + else { + return None; + }; + let DeclaratorParent::PortDeclId(port_decl_id) = module.get(port_decl).parent else { + return None; + }; + let port_decl = module.get(port_decl_id); + if port_decl_declared_name(module, port_decl).as_deref() != Some(name.as_str()) { + return None; + } + + let port_src = module_src_map.port_srcs.get(port_decl_id)?; + let PortDeclSrc::PortDeclaration(_) = port_src else { + return None; + }; + let port_range = port_src.range(); + + if let Some(data_decl) = data_decl { + let data_range = data_decl_range_for_name(module, module_src_map, data_decl, name)?; + let direction = port_decl.header.dir().display_source(ctx.sema().db).ok()?; + let data_decl = declaration_text_without_semicolon(text, data_range)?; + return Some(NonAnsiPortReplacement { + ansi_item: format!("{direction} {data_decl}"), + remove_ranges: vec![port_range, data_range], + }); + } + + Some(NonAnsiPortReplacement { + ansi_item: declaration_text_without_semicolon(text, port_range)?, + remove_ranges: vec![port_range], + }) +} + +fn data_decl_range_for_name( + module: &Module, + module_src_map: &ModuleSourceMap, + decl_id: DeclId, + name: &Ident, +) -> Option { + let decl = module.get(decl_id); + if decl.name.as_ref() != Some(name) { + return None; + } + + let DeclaratorParent::DeclarationId(declaration_id) = decl.parent else { + return None; + }; + let declaration = module.get(declaration_id); + let mut decls = declaration.decls(); + let single_decl_id = decls.next()?; + if single_decl_id != decl_id || decls.next().is_some() { + return None; + } + + let src = module_src_map.declaration_srcs.get(declaration_id)?; + match src { + DeclarationSrc::DataDeclaration(_) | DeclarationSrc::NetDeclaration(_) => Some(src.range()), + _ => None, + } +} + +fn render_ansi_port_declaration( + ctx: &CodeActionCtx, + module_id: ModuleId, + port_decl: &PortDecl, + src: PortDeclSrc, + text: &str, +) -> Option { + let source = text.get(Range::from(src.range()))?; + if source + .split_ascii_whitespace() + .next() + .is_some_and(|word| matches!(word, "input" | "output" | "inout" | "ref")) + { + return Some(format!("{source};")); + } + + let decl_id = single_port_decl_id(port_decl)?; + let header = InModule::new(module_id, port_decl.header).display_source(ctx.sema().db).ok()?; + let decl = InContainer::new(module_id.into(), decl_id).display_signature(ctx.sema().db).ok()?; + + if header.is_empty() { Some(format!("{decl};")) } else { Some(format!("{header} {decl};")) } +} + +fn declaration_text_without_semicolon(text: &str, range: TextRange) -> Option { + Some(text.get(Range::from(range))?.strip_suffix(';')?.to_owned()) +} + +fn module_body_range(module: ast::ModuleDeclaration<'_>) -> Option { + let header = module.header(); + Some(TextRange::new( + header.semi()?.text_range_in(header.syntax())?.end(), + module.endmodule()?.text_range_in(module.syntax())?.start(), + )) +} + +fn render_port_list( + text: &str, + open: TextRange, + close: TextRange, + items: &[String], +) -> Option { + let content = text.get(usize::from(open.end())..usize::from(close.start()))?; + if content.contains('\n') { + let close_indent = line_indent(text, close.start()); + let item_indent = format!("{close_indent} "); + let rendered = items + .iter() + .enumerate() + .map(|(idx, item)| { + let suffix = if idx + 1 == items.len() { "" } else { "," }; + format!("{item_indent}{item}{suffix}") + }) + .collect::>() + .join("\n"); + Some(format!("(\n{rendered}\n{close_indent})")) + } else { + Some(format!("({})", items.join(", "))) + } +} + +fn render_module_body( + text: &str, + module: ast::ModuleDeclaration<'_>, + body_range: TextRange, + prefix_items: &[String], + remove_ranges: &[TextRange], +) -> Option { + let mut items = prefix_items.to_vec(); + let mut body = text.get(Range::from(body_range))?.to_owned(); + remove_ranges_from_body(&mut body, body_range, remove_ranges)?; + let body = body.trim(); + if !body.is_empty() { + items.push(body.to_owned()); + } + + let endmodule = module.endmodule()?.text_range_in(module.syntax())?; + let module_indent = line_indent(text, endmodule.start()); + if items.is_empty() { + return Some(format!("\n{module_indent}")); + } + + let item_indent = format!("{module_indent} "); + let rendered = items + .into_iter() + .map(|item| indent_block(&item, &item_indent)) + .collect::>() + .join("\n"); + Some(format!("\n{rendered}\n{module_indent}")) +} + +fn remove_ranges_from_body( + body: &mut String, + body_range: TextRange, + remove_ranges: &[TextRange], +) -> Option<()> { + let body_start = usize::from(body_range.start()); + let body_end = usize::from(body_range.end()); + let mut ranges = remove_ranges + .iter() + .filter(|range| body_range.contains_range(**range)) + .map(|range| { + Some(( + usize::from(range.start()).checked_sub(body_start)?, + usize::from(range.end()).checked_sub(body_start)?, + )) + }) + .collect::>>()?; + + ranges.sort_by_key(|(start, _)| *start); + for (start, end) in ranges.into_iter().rev() { + if start > end || body_start + end > body_end { + return None; + } + body.replace_range(start..end, ""); + } + Some(()) +} + +fn indent_block(text: &str, indent: &str) -> String { + text.lines().map(|line| format!("{indent}{line}")).join("\n") +} diff --git a/crates/ide/src/code_action/handlers/extract_variable.rs b/crates/ide/src/code_action/handlers/extract_variable.rs new file mode 100644 index 00000000..50ed287f --- /dev/null +++ b/crates/ide/src/code_action/handlers/extract_variable.rs @@ -0,0 +1,217 @@ +use std::ops::Range; + +use hir::{ + base_db::source_db::SourceDb, + container::InContainer, + display::HirDisplay, + type_infer::{BuiltinTy, Ty, type_of_expr, type_of_path_resolution}, +}; +use syntax::{ + SyntaxAncestors, SyntaxKind, TokenKind, WalkEvent, + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::{ + get::GetRef, + text_edit::{TextRange, TextSize}, +}; + +use crate::code_action::{ + CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +}; + +const ID: CodeActionId = + CodeActionId { name: "extract_variable", kind: CodeActionKind::RefactorExtract, repair: None }; + +pub(super) fn extract_variable( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let text = ctx.sema().db.file_text(ctx.file_id()); + let expr = selected_expression(ctx, &text)?; + let expr_range = expr.syntax().text_range()?; + let target = extract_target(&text, expr)?; + let expr_text = text.get(Range::from(expr_range))?.trim().to_owned(); + let name = fresh_variable_name(&text, "value"); + + 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); + }) +} + +struct ExtractTarget { + insert_offset: TextSize, + indent: String, + declaration_style: DeclarationStyle, +} + +impl ExtractTarget { + fn declaration(&self, ty_text: &str, name: &str, expr_text: &str) -> String { + match self.declaration_style { + DeclarationStyle::Local => { + format!("{}{ty_text} {name} = {expr_text};\n", self.indent) + } + DeclarationStyle::ContinuousNet => { + format!("{}wire {ty_text} {name} = {expr_text};\n", self.indent) + } + } + } +} + +enum DeclarationStyle { + Local, + ContinuousNet, +} + +fn extract_target(text: &str, expr: ast::Expression<'_>) -> Option { + if let Some(stmt) = + SyntaxAncestors::start_from(expr.syntax()).find_map(ast::ExpressionStatement::cast) + && stmt.syntax().parent().and_then(ast::BlockStatement::cast).is_some() + { + let stmt_range = stmt.syntax().text_range()?; + return Some(ExtractTarget { + insert_offset: stmt_range.start(), + indent: line_indent(text, stmt_range.start()), + declaration_style: DeclarationStyle::Local, + }); + } + + let assign = SyntaxAncestors::start_from(expr.syntax()).find_map(ast::ContinuousAssign::cast)?; + expression_is_assignment_rhs(expr)?; + let assign_range = assign.syntax().text_range()?; + Some(ExtractTarget { + insert_offset: assign_range.start(), + indent: line_indent(text, assign_range.start()), + declaration_style: DeclarationStyle::ContinuousNet, + }) +} + +fn expression_is_assignment_rhs(expr: ast::Expression<'_>) -> Option<()> { + assignment_expression_containing_rhs(expr) + .filter(|binary| binary.operator_token().is_some_and(|token| token.kind() == TokenKind::EQUALS)) + .map(|_| ()) +} + +fn assignment_expression_containing_rhs( + expr: ast::Expression<'_>, +) -> Option> { + let expr_range = expr.syntax().text_range()?; + SyntaxAncestors::start_from(expr.syntax()).filter_map(ast::BinaryExpression::cast).find( + |binary| { + is_assignment_expression(binary.syntax().kind()) + && binary + .right() + .syntax() + .text_range() + .is_some_and(|range| range.contains_range(expr_range)) + }, + ) +} + +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 + ) +} + +fn selected_expression<'a>(ctx: &'a CodeActionCtx<'_>, text: &str) -> Option> { + let range = trim_range(text, ctx.range())?; + if range.is_empty() { + return None; + } + + ctx.syntax().node_preorder().find_map(|event| match event { + WalkEvent::Enter(node) => { + let expr = ast::Expression::cast(node)?; + (expr.syntax().text_range()? == range).then_some(expr) + } + WalkEvent::Leave(_) => None, + }) +} + +fn trim_range(text: &str, range: TextRange) -> Option { + let selected = text.get(Range::::from(range))?; + let trimmed_start = selected.trim_start(); + let trimmed = trimmed_start.trim_end(); + + let leading = selected.len() - trimmed_start.len(); + let trailing = trimmed_start.len() - trimmed.len(); + Some(TextRange::new( + range.start() + TextSize::from(leading as u32), + range.end() - TextSize::from(trailing as u32), + )) +} + +fn extracted_variable_type(ctx: &CodeActionCtx<'_>, expr: ast::Expression<'_>) -> Option { + let ty = type_of_expr(ctx.sema().db, ctx.sema().resolve_expr(ctx.file_id().into(), expr)?).ty; + 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(type_of_path_resolution(ctx.sema().db, res).ty) +} + +fn render_ty(ctx: &CodeActionCtx<'_>, ty: &Ty) -> Option { + match ty { + Ty::Builtin(BuiltinTy::Data { id, container }) => { + InContainer::new(*container, hir::hir_def::expr::data_ty::DataTy::Builtin(*id)) + .display_source(ctx.sema().db) + .ok() + } + Ty::Alias { typedef, .. } => { + let container = typedef.cont_id.to_container(ctx.sema().db); + container.get(typedef.value).name.as_ref().map(ToString::to_string) + } + Ty::Struct(struct_ref) => { + let container = struct_ref.cont_id.to_container(ctx.sema().db); + container.get(struct_ref.value).name.as_ref().map(ToString::to_string) + } + Ty::Unknown + | Ty::Error + | Ty::Void + | Ty::Module(_) + | Ty::GenerateBlock(_) + | Ty::Block(_) => None, + } +} + +fn fresh_variable_name(text: &str, base: &str) -> String { + if !text.contains(base) { + return base.to_owned(); + } + + let mut idx = 1usize; + loop { + let candidate = format!("{base}_{idx}"); + if !text.contains(&candidate) { + return candidate; + } + idx += 1; + } +} diff --git a/crates/ide/src/code_action/handlers/merge_nested_if.rs b/crates/ide/src/code_action/handlers/merge_nested_if.rs new file mode 100644 index 00000000..f0d4ee28 --- /dev/null +++ b/crates/ide/src/code_action/handlers/merge_nested_if.rs @@ -0,0 +1,130 @@ +use std::{borrow::Cow, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = + CodeActionId { name: "merge_nested_if", kind: CodeActionKind::RefactorRewrite, repair: None }; + +pub(super) fn merge_nested_if( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let current_if = ctx.find_node_at_offset::()?; + if !in_if_head(current_if, ctx.range()) || current_if.else_clause().is_some() { + return None; + } + + let outer_if = outermost_mergeable_if(current_if); + let chain = nested_if_chain(outer_if); + if chain.len() < 2 { + return None; + } + + let innermost_if = *chain.last()?; + let innermost_body_stmt = single_statement_body(innermost_if.statement())?; + + let text = ctx.sema().db.file_text(ctx.file_id()); + let predicates = chain + .iter() + .map(|if_stmt| { + let range = if_stmt.predicate().syntax().text_range()?; + let predicate = text.get(Range::from(range))?.trim(); + if predicate.contains("||") || predicate.contains('?') { + Some(Cow::Owned(format!("({predicate})"))) + } else { + Some(Cow::Borrowed(predicate)) + } + }) + .collect::>>()?; + + let outer_pred_range = outer_if.predicate().syntax().text_range()?; + let outer_body_range = outer_if.statement().syntax().text_range()?; + + let innermost_body_range = innermost_body_stmt.syntax().text_range()?; + let innermost_body = text.get(Range::from(innermost_body_range))?.trim().to_owned(); + + collector.add(ID, "Merge nested if", outer_if.syntax().text_range()?, |builder| { + let merged_predicate = predicates.join(" && "); + builder.replace(outer_pred_range, merged_predicate); + builder.replace(outer_body_range, innermost_body); + }) +} + +fn in_if_head(if_stmt: ast::ConditionalStatement<'_>, range: TextRange) -> bool { + let Some(if_range) = if_stmt.syntax().text_range() else { return false }; + let Some(pred_range) = if_stmt.predicate().syntax().text_range() else { return false }; + TextRange::new(if_range.start(), pred_range.end()).contains_range(range) +} + +fn outermost_mergeable_if<'a>( + mut if_stmt: ast::ConditionalStatement<'a>, +) -> ast::ConditionalStatement<'a> { + loop { + let Some(parent_if) = parent_conditional_statement(if_stmt) else { break }; + if parent_if.else_clause().is_some() { + break; + } + let Some(body) = single_statement_body(parent_if.statement()) else { break }; + let Some(body_stmt) = body.as_conditional_statement() else { break }; + if body_stmt.syntax() != if_stmt.syntax() { + break; + } + if_stmt = parent_if; + } + + if_stmt +} + +fn parent_conditional_statement<'a>( + if_stmt: ast::ConditionalStatement<'a>, +) -> Option> { + let mut parent = if_stmt.syntax().parent(); + while let Some(node) = parent { + if let Some(parent_if) = ast::ConditionalStatement::cast(node) { + return Some(parent_if); + } + parent = node.parent(); + } + None +} + +fn nested_if_chain<'a>( + outer_if: ast::ConditionalStatement<'a>, +) -> Vec> { + let mut chain = vec![outer_if]; + let mut current_if = outer_if; + loop { + let Some(body) = single_statement_body(current_if.statement()) else { + break; + }; + let Some(nested_if) = body.as_conditional_statement() else { + break; + }; + if nested_if.else_clause().is_some() { + break; + } + chain.push(nested_if); + current_if = nested_if; + } + chain +} + +fn single_statement_body(stmt: ast::Statement<'_>) -> Option> { + let Some(block) = stmt.as_block_statement() else { + return Some(stmt); + }; + + let mut items = block.items().children(); + let item = items.next()?; + if items.next().is_some() { + return None; + } + ast::Statement::cast(item.syntax()) +} diff --git a/crates/ide/src/code_action/handlers/pull_assignment_up.rs b/crates/ide/src/code_action/handlers/pull_assignment_up.rs new file mode 100644 index 00000000..7ae297c4 --- /dev/null +++ b/crates/ide/src/code_action/handlers/pull_assignment_up.rs @@ -0,0 +1,129 @@ +use std::{borrow::Cow, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + TokenKind, + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "pull_assignment_up", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const DOWN_ID: CodeActionId = CodeActionId { + name: "pull_assignment_down", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; + +pub(super) fn pull_assignment_up( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let mut conditional = ctx.find_node_at_offset::()?; + while let Some(parent_if) = conditional + .syntax() + .parent() + .and_then(|node| ast::ElseClause::cast(node)?.syntax().parent()) + .and_then(ast::ConditionalStatement::cast) + { + conditional = parent_if; + } + + let text = ctx.sema().db.file_text(ctx.file_id()); + let (lhs, expr) = conditional_assignment_expression(conditional, &text)?; + + collector.add(ID, "Pull assignment up", conditional.syntax().text_range()?, |builder| { + let replacement = format!("{} = {};", lhs.trim(), expr); + builder.replace(conditional.syntax().text_range().unwrap(), replacement); + }) +} + +pub(super) fn pull_assignment_down( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let assignment = ctx.find_node_at_offset::()?; + if assignment.operator_token()?.kind() != TokenKind::EQUALS { + return None; + } + + let conditional = assignment.right().as_conditional_expression()?; + let stmt = syntax::SyntaxAncestors::start_from(assignment.syntax()) + .find_map(ast::ExpressionStatement::cast)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let lhs = text.get(Range::from(assignment.left().syntax().text_range()?))?.trim(); + let replacement = conditional_assignment_statement(conditional, lhs, &text)?; + + collector.add(DOWN_ID, "Pull assignment down", stmt.syntax().text_range()?, |builder| { + builder.replace(stmt.syntax().text_range().unwrap(), replacement); + }) +} + +fn conditional_assignment_expression<'a>( + conditional: ast::ConditionalStatement<'_>, + text: &'a str, +) -> Option<(&'a str, String)> { + let (lhs, then_rhs) = assignment_rhs_text(conditional.statement(), text)?; + + let else_syntax = conditional.else_clause()?.clause().syntax(); + let (else_lhs, else_expr) = if let Some(nested) = ast::ConditionalStatement::cast(else_syntax) { + conditional_assignment_expression(nested, text)? + } else { + let else_stmt = ast::Statement::cast(else_syntax)?; + let (lhs, expr) = assignment_rhs_text(else_stmt, text)?; + (lhs, expr.to_owned()) + }; + + if else_lhs != lhs { + return None; + } + + let predicate: Cow<'a, str> = { + let predicate = + text.get(Range::from(conditional.predicate().syntax().text_range()?))?.trim(); + + if predicate.contains('?') { format!("({predicate})").into() } else { predicate.into() } + }; + Some((lhs, format!("{predicate} ? {then_rhs} : {else_expr}"))) +} + +fn assignment_rhs_text<'a>(stmt: ast::Statement<'_>, text: &'a str) -> Option<(&'a str, &'a str)> { + if let Some(block) = stmt.as_block_statement() { + let item = block.items().only_children()?; + let stmt = ast::Statement::cast(item.syntax())?; + return assignment_rhs_text(stmt, text); + } + + let assignment = stmt.as_expression_statement()?.expr().as_binary_expression()?; + if assignment.operator_token()?.kind() != TokenKind::EQUALS { + return None; + } + + let lhs = text.get(Range::from(assignment.left().syntax().text_range()?))?.trim(); + let rhs = text.get(Range::from(assignment.right().syntax().text_range()?))?.trim(); + Some((lhs, rhs)) +} + +fn conditional_assignment_statement( + conditional: ast::ConditionalExpression<'_>, + lhs: &str, + text: &str, +) -> Option { + let predicate = text.get(Range::from(conditional.predicate().syntax().text_range()?))?.trim(); + let then_expr = expr_text(conditional.left(), text)?; + let else_expr = if let Some(nested) = conditional.right().as_conditional_expression() { + conditional_assignment_statement(nested, lhs, text)? + } else { + format!("{lhs} = {};", expr_text(conditional.right(), text)?) + }; + Some(format!("if ({predicate}) {lhs} = {then_expr}; else {else_expr}")) +} + +fn expr_text<'a>(expr: ast::Expression<'_>, text: &'a str) -> Option<&'a str> { + text.get(Range::from(expr.syntax().text_range()?)).map(str::trim) +} diff --git a/crates/ide/src/code_action/handlers/reformat_number_literal.rs b/crates/ide/src/code_action/handlers/reformat_number_literal.rs new file mode 100644 index 00000000..d3edc630 --- /dev/null +++ b/crates/ide/src/code_action/handlers/reformat_number_literal.rs @@ -0,0 +1,96 @@ +use std::ops::Range; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "reformat_number_literal", + kind: CodeActionKind::RefactorInline, + repair: None, +}; +const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; + +pub(super) fn reformat_number_literal( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let text = ctx.sema().db.file_text(ctx.file_id()); + let (raw, prefix, digits, group_size, range) = selected_integer_literal(ctx, &text)?; + + if digits.contains('_') { + let replacement = raw.replace('_', ""); + return collector.add(ID, "Remove digit separators", range, |builder| { + builder.replace(range, replacement); + }); + } + + if digits.chars().count() < MIN_NUMBER_OF_DIGITS_TO_FORMAT { + return None; + } + + let replacement = format!("{}{}", prefix, add_group_separators(&digits, group_size)); + let label = format!("Convert {raw} to {replacement}"); + collector.add(ID, label, range, |builder| { + builder.replace(range, replacement); + }) +} + +fn selected_integer_literal<'a>( + ctx: &CodeActionCtx<'_>, + text: &'a str, +) -> Option<(&'a str, &'a str, &'a str, usize, TextRange)> { + if let Some(expr) = ctx.find_node_at_offset::() { + let range = expr.syntax().text_range()?; + let raw = text.get(Range::from(range))?; + return parse_based_literal(raw, range); + } + + let literal = ctx.find_node_at_offset::()?; + let ast::LiteralExpression::IntegerLiteralExpression(integer) = literal else { + return None; + }; + let range = integer.text_range()?; + let raw = text.get(Range::from(range))?; + Some((raw, "", raw, 3, range)) +} + +fn parse_based_literal<'a>( + raw: &'a str, + range: TextRange, +) -> Option<(&'a str, &'a str, &'a str, usize, TextRange)> { + let apostrophe = raw.find('\'')?; + let after_quote = raw.get(apostrophe + 1..)?; + let (sign_len, rest) = match after_quote.as_bytes().first().copied() { + Some(b's' | b'S') => (1usize, after_quote.get(1..)?), + _ => (0usize, after_quote), + }; + let base = rest.as_bytes().first().copied()?; + let group_size = match base.to_ascii_lowercase() { + b'b' => 4, + b'o' => 3, + b'd' => 3, + b'h' => 4, + _ => return None, + }; + let digits_start = apostrophe + 1 + sign_len + 1; + let digits = raw.get(digits_start..)?; + Some((raw, raw.get(..digits_start)?, digits, group_size, range)) +} + +fn add_group_separators(digits: &str, group_size: usize) -> String { + let clean: Vec = digits.chars().filter(|ch| *ch != '_').collect(); + let mut buf = String::with_capacity(clean.len() + clean.len() / group_size); + for (idx, ch) in clean.iter().rev().enumerate() { + if idx != 0 && idx % group_size == 0 { + buf.push('_'); + } + buf.push(*ch); + } + buf.chars().rev().collect() +} diff --git a/crates/ide/src/code_action/handlers/remove_parentheses.rs b/crates/ide/src/code_action/handlers/remove_parentheses.rs new file mode 100644 index 00000000..f2e5c8c6 --- /dev/null +++ b/crates/ide/src/code_action/handlers/remove_parentheses.rs @@ -0,0 +1,148 @@ +use std::{cmp::Ordering, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + SyntaxKind, TokenKind, + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "remove_parentheses", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; + +pub(super) fn remove_parentheses( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let parens = ctx.find_node_at_offset::()?; + let range = parens.syntax().text_range()?; + let left = parens.open_paren()?.text_range_in(parens.syntax())?; + let right = parens.close_paren()?.text_range_in(parens.syntax())?; + if !left.contains_range(ctx.range()) && !right.contains_range(ctx.range()) { + return None; + } + + let expr = parens.expression(); + let parent = parens.syntax().parent()?; + if parentheses_are_required(parens, expr, parent) { + return None; + } + + let expr_range = expr.syntax().text_range()?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let inner = text.get(Range::from(expr_range))?.to_owned(); + collector.add(ID, "Remove redundant parentheses", range, |builder| { + builder.replace(range, inner); + }) +} + +fn parentheses_are_required( + parens: ast::ParenthesizedExpression<'_>, + expr: ast::Expression<'_>, + parent: syntax::SyntaxNode<'_>, +) -> bool { + if ast::ParenthesizedExpression::cast(parent).is_some() { + return false; + } + + if matches!(parent.kind(), SyntaxKind::MEMBER_ACCESS_EXPRESSION | SyntaxKind::SCOPED_NAME) { + return true; + } + + let Some(parent_binary) = ast::BinaryExpression::cast(parent) else { + return ast::Expression::cast(parent).is_some_and(|_| { + expr.as_binary_expression().is_some() || expr.as_conditional_expression().is_some() + }); + }; + let Some(child_binary) = expr.as_binary_expression() else { + return false; + }; + + let (Some(parent_prec), Some(child_prec)) = + (binary_precedence(parent_binary), binary_precedence(child_binary)) + else { + return true; + }; + + match child_prec.cmp(&parent_prec) { + Ordering::Greater => false, + Ordering::Less => true, + Ordering::Equal => { + let same_associative_op = parent_binary + .operator_token() + .zip(child_binary.operator_token()) + .is_some_and(|(parent_op, child_op)| { + parent_op.kind() == child_op.kind() + && associative_binary_operator(parent_op.kind()) + }); + !(parent_binary.left().syntax() == parens.syntax() && same_associative_op) + } + } +} + +fn associative_binary_operator(kind: TokenKind) -> bool { + matches!( + kind, + TokenKind::PLUS + | TokenKind::STAR + | TokenKind::DOUBLE_AND + | TokenKind::DOUBLE_OR + | TokenKind::AND + | TokenKind::OR + | TokenKind::XOR + | TokenKind::TILDE_XOR + | TokenKind::XOR_TILDE + ) +} + +fn binary_precedence(expr: ast::BinaryExpression<'_>) -> Option { + let kind = expr.operator_token()?.kind(); + Some(match kind { + TokenKind::DOUBLE_STAR => 12, + TokenKind::STAR | TokenKind::SLASH | TokenKind::PERCENT => 11, + TokenKind::PLUS | TokenKind::MINUS => 10, + TokenKind::LEFT_SHIFT + | TokenKind::RIGHT_SHIFT + | TokenKind::TRIPLE_LEFT_SHIFT + | TokenKind::TRIPLE_RIGHT_SHIFT => 9, + TokenKind::LESS_THAN_EQUALS + if expr.syntax().kind() == SyntaxKind::NONBLOCKING_ASSIGNMENT_EXPRESSION => + { + 1 + } + TokenKind::GREATER_THAN + | TokenKind::GREATER_THAN_EQUALS + | TokenKind::LESS_THAN + | TokenKind::LESS_THAN_EQUALS => 8, + TokenKind::DOUBLE_EQUALS + | TokenKind::EXCLAMATION_EQUALS + | TokenKind::TRIPLE_EQUALS + | TokenKind::EXCLAMATION_DOUBLE_EQUALS + | TokenKind::DOUBLE_EQUALS_QUESTION + | TokenKind::EXCLAMATION_EQUALS_QUESTION => 7, + TokenKind::AND => 6, + TokenKind::XOR | TokenKind::TILDE_XOR | TokenKind::XOR_TILDE => 5, + TokenKind::OR => 4, + TokenKind::DOUBLE_AND => 3, + TokenKind::DOUBLE_OR => 2, + TokenKind::EQUALS + | TokenKind::PLUS_EQUAL + | TokenKind::MINUS_EQUAL + | TokenKind::STAR_EQUAL + | TokenKind::SLASH_EQUAL + | TokenKind::PERCENT_EQUAL + | TokenKind::AND_EQUAL + | TokenKind::OR_EQUAL + | TokenKind::XOR_EQUAL + | TokenKind::LEFT_SHIFT_EQUAL + | TokenKind::RIGHT_SHIFT_EQUAL + | TokenKind::TRIPLE_LEFT_SHIFT_EQUAL + | TokenKind::TRIPLE_RIGHT_SHIFT_EQUAL => 1, + _ => return None, + }) +} diff --git a/crates/ide/src/code_action/tests.rs b/crates/ide/src/code_action/tests.rs index 2f7161ad..f5cd12fe 100644 --- a/crates/ide/src/code_action/tests.rs +++ b/crates/ide/src/code_action/tests.rs @@ -13,6 +13,11 @@ fn db_with_file(text: &str) -> (RootDb, 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)) +} + +fn db_with_text(text: &str) -> (RootDb, FileId) { let file_id = FileId(0); let mut file_set = FileSet::default(); file_set.insert(file_id, VfsPath::new_virtual_path("/test.sv".to_owned())); @@ -21,12 +26,12 @@ fn db_with_file(text: &str) -> (RootDb, FileId, TextSize) { change.set_roots(vec![SourceRoot::new_local(file_set)]); change.add_changed_file(ChangedFile { file_id, - change_kind: ChangeKind::Create(Arc::from(text.as_str()), LineEnding::Unix), + change_kind: ChangeKind::Create(Arc::from(text), LineEnding::Unix), }); let mut db = RootDb::new(None); db.apply_change(change); - (db, file_id, TextSize::from(offset as u32)) + (db, file_id) } fn apply_action(text: &str, repair: RepairKind) -> Option { @@ -90,6 +95,57 @@ fn apply_action_without_diagnostics_by( Some(text) } +fn apply_action_without_diagnostics_with_selection( + text: &str, + action_name: &str, +) -> Option { + apply_action_without_diagnostics_with_selection_by(text, |action| action.id.name == action_name) +} + +fn apply_action_without_diagnostics_with_selection_by( + text: &str, + 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, + CodeActionDiagnostics::default(), + CodeActionResolveStrategy::All, + ); + let action = actions.into_iter().find(pred)?; + let edit = action.source_change?.text_edits.remove(&file_id)?; + edit.apply(&mut text); + Some(text) +} + +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, + CodeActionDiagnostics::default(), + CodeActionResolveStrategy::All, + ) + .into_iter() + .map(|action| action.label) + .collect() +} + +fn text_with_selection_range(text: &str) -> (String, TextRange) { + let marker = "/*selection*/"; + let start = text.find(marker).expect("missing selection start marker"); + let text = text.replacen(marker, "", 1); + let end = text.find(marker).expect("missing selection end marker"); + let text = text.replacen(marker, "", 1); + let range = TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32)); + (text, range) +} + fn diagnostic_for_repair(repair: RepairKind) -> CodeActionDiagnostic { match repair { RepairKind::MissingConnection => CodeActionDiagnostic { @@ -317,6 +373,53 @@ fn literal_base_is_not_available_for_string_literals() { assert!(!labels.iter().any(|label| label.starts_with("Convert literal to "))); } +#[test] +fn reformat_number_literal_adds_decimal_separators() { + let text = "module top; localparam int value = /*caret*/10000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Convert 10000 to 10_000", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 10_000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_removes_separators() { + let text = "module top; localparam int value = /*caret*/10_000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Remove digit separators", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 10000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_formats_hex_literals() { + let text = "module top; localparam int value = /*caret*/'hff0000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Convert 'hff0000 to 'hff_0000", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 'hff_0000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_requires_enough_digits() { + let labels = action_labels_without_diagnostics( + "module top; localparam int value = /*caret*/999; endmodule\n", + ); + assert!(!labels.iter().any(|label| label.starts_with("Convert 999 to "))); +} + #[test] fn missing_connection_repair_fills_named_connections() { let text = "module child(input a, input b); endmodule\nmodule top; child u(/*caret*/.a()); endmodule\n"; @@ -524,6 +627,139 @@ fn implicit_named_port_repair_is_available_without_diagnostics() { assert_eq!(fixed, "module child(input a); endmodule\nmodule top; child u(.a()); endmodule\n"); } +#[test] +fn named_port_shorthand_expands() { + let text = + "module child(input a); endmodule\nmodule top; logic a; child u(/*caret*/.a); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "expand_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a); endmodule\nmodule top; logic a; child u(.a(a)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_expands_all_named_connections_in_instance() { + let text = "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a, .b); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "expand_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(.a(a), .b(b)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses() { + let text = "module child(input a); endmodule\nmodule top; logic a; child u(/*caret*/.a(a)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a); endmodule\nmodule top; logic a; child u(.a); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses_all_named_connections_in_instance() { + let text = "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a(a), .b(b)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(.a, .b); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses_matching_connections_in_instance() { + let text = "module child(input a, b, c); endmodule\nmodule top; logic sw1, b, gate_out; child u(/*caret*/.a(sw1), .c(c), .b(gate_out)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b, c); endmodule\nmodule top; logic sw1, b, gate_out; child u(.a(sw1), .c, .b(gate_out)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapse_requires_at_least_one_same_name() { + let labels = action_labels_without_diagnostics( + "module child(input a); endmodule\nmodule top; logic b; child u(/*caret*/.a(b)); endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Collapse named port to shorthand")); +} + +#[test] +fn named_port_shorthand_requires_all_connections_named() { + let labels = action_labels_without_diagnostics( + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a, b); endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Expand named port shorthand")); +} + +#[test] +fn convert_always_star_to_always_comb() { + let text = "module top; logic a, y; /*caret*/always @(*) begin y = a; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_comb").unwrap(); + assert_eq!(fixed, "module top; logic a, y; always_comb begin y = a; end endmodule\n"); +} + +#[test] +fn convert_always_comb_to_always_star() { + let text = "module top; logic a, y; /*caret*/always_comb begin y = a; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_comb_to_always").unwrap(); + assert_eq!(fixed, "module top; logic a, y; always @(*) begin y = a; end endmodule\n"); +} + +#[test] +fn convert_always_posedge_to_always_ff() { + let text = "module top; logic clk, d, q; /*caret*/always @(posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_ff").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always_ff @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_event_list_to_always_ff() { + let text = "module top; logic clk, d, q; always @(/*caret*/posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_ff").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always_ff @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_ff_to_plain_always() { + let text = "module top; logic clk, d, q; /*caret*/always_ff @(posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_ff_to_always").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_block_requires_caret_on_keyword_or_event_list() { + let labels = action_labels_without_diagnostics( + "module top; logic a, y; always @(*) begin /*caret*/y = a; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always_comb")); + + let labels = action_labels_without_diagnostics( + "module top; logic a, y; always_comb begin /*caret*/y = a; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always @(*)")); + + let labels = action_labels_without_diagnostics( + "module top; logic clk, d, q; always_ff @(posedge clk) /*caret*/q <= d; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always @(...)")); +} + +#[test] +fn convert_always_to_always_ff_requires_edge_sensitivity() { + let labels = action_labels_without_diagnostics( + "module top; logic clk, d, q; /*caret*/always @(clk) q <= d; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always_ff")); +} + #[test] fn instance_missing_parens_repair_adds_port_list() { let text = "module child; endmodule\nmodule top; child u/*caret*/; endmodule\n"; @@ -538,6 +774,85 @@ fn instance_missing_parens_repair_requires_diagnostics() { assert!(!labels.iter().any(|label| label == "Add empty instance port list")); } +#[test] +fn convert_ansi_ports_to_non_ansi() { + let text = "module top(/*caret*/input a, output logic b);\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert_eq!( + fixed, + "module top(a, b);\n input a;\n output logic b;\n assign b = a;\nendmodule\n" + ); +} + +#[test] +fn convert_ansi_ports_to_non_ansi_uses_inherited_header() { + let text = "module top(/*caret*/input a, b);\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert_eq!( + fixed, + "module top(a, b);\n input a;\n input wire logic b;\n assign b = a;\nendmodule\n" + ); +} + +#[test] +fn convert_non_ansi_ports_to_ansi() { + let text = + "module top(/*caret*/a, b);\ninput wire a;\noutput logic b;\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert_eq!(fixed, "module top(input wire a, output logic b);\n assign b = a;\nendmodule\n"); +} + +#[test] +fn convert_non_ansi_ports_to_ansi_merges_data_declaration() { + let text = "module top (\n /*caret*/c,\n led0\n);\n input wire c;\n output led0;\n reg led0;\n\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert_eq!( + fixed, + "module top (\n input wire c,\n output reg led0\n);\nendmodule\n" + ); +} + +#[test] +fn convert_ansi_ports_to_non_ansi_preserves_body_comments() { + let text = + "module top(/*caret*/input a, output logic b);\n// keep this\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert!(fixed.contains("// keep this"), "{fixed}"); + assert!(fixed.contains("assign b = a;"), "{fixed}"); +} + +#[test] +fn convert_non_ansi_ports_to_ansi_preserves_body_comments() { + let text = "module top(/*caret*/a, b);\n// keep first\ninput wire a;\n// keep second\noutput logic b;\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert!(fixed.contains("// keep first"), "{fixed}"); + assert!(fixed.contains("// keep second"), "{fixed}"); + assert!(fixed.contains("assign b = a;"), "{fixed}"); +} + +#[test] +fn convert_port_declarations_requires_caret_in_port_list() { + let labels = action_labels_without_diagnostics( + "module /*caret*/top(input a, output logic b);\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert ANSI port declarations to non-ANSI")); + + let labels = action_labels_without_diagnostics( + "module top(input a, output logic b);\n/*caret*/assign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert ANSI port declarations to non-ANSI")); + + let labels = action_labels_without_diagnostics( + "module /*caret*/top(a, b);\ninput wire a;\noutput logic b;\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert non-ANSI port declarations to ANSI")); + + let labels = action_labels_without_diagnostics( + "module top(a, b);\ninput wire a;\n/*caret*/output logic b;\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert non-ANSI port declarations to ANSI")); +} + #[test] fn split_declaration_declarators_splits_data_declaration() { let text = "module top; /*caret*/logic [3:0] a, b = 4'h0; endmodule\n"; @@ -620,6 +935,235 @@ fn invert_if_else_swaps_branches_and_negates_condition() { assert_eq!(fixed, "module top; always_comb if (!(a)) y = 0; else y = 1; endmodule\n"); } +#[test] +fn remove_parentheses_removes_redundant_binary_parens() { + let text = "module top; assign y = /*caret*/(a + b) + c; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "remove_parentheses").unwrap(); + assert_eq!(fixed, "module top; assign y = a + b + c; endmodule\n"); +} + +#[test] +fn remove_parentheses_keeps_required_parens() { + let labels = action_labels_without_diagnostics( + "module top; assign y = /*caret*/(a + b) * c; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Remove redundant parentheses")); +} + +#[test] +fn remove_parentheses_requires_cursor_on_paren() { + let labels = action_labels_without_diagnostics( + "module top; assign y = (a /*caret*/+ b) + c; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Remove redundant parentheses")); +} + +#[test] +fn merge_nested_if_merges_simple_nested_if() { + let text = "module top; always_comb if (/*caret*/a) begin if (b) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_wraps_or_conditions() { + let text = + "module top; always_comb if (/*caret*/a || b) begin if (c || d) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if ((a || b) && (c || d)) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_merges_multiple_nested_levels() { + let text = "module top; always_comb if (/*caret*/a) begin if (b) begin if (c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_triggers_from_middle_nested_level() { + let text = "module top; always_comb if (a) begin if (/*caret*/b) begin if (c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_triggers_from_innermost_nested_level() { + let text = "module top; always_comb if (a) begin if (b) begin if (/*caret*/c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_merges_mixed_block_and_unbraced_levels() { + let text = "module top; always_comb if (a) begin if (/*caret*/b) if (c) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_requires_no_else_branches() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (/*caret*/a) begin if (b) y = 1; else y = 0; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Merge nested if")); +} + +#[test] +fn merge_nested_if_rejects_block_with_declarations() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (/*caret*/a) begin logic tmp; if (b) y = tmp; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Merge nested if")); +} + +#[test] +fn extract_variable_inserts_local_before_statement() { + let text = "module top; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; always_comb begin logic value = a + b;\ny = value; end endmodule\n" + ); +} + +#[test] +fn extract_variable_allows_selection_padding() { + let text = "module top; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; always_comb begin logic value = a + b;\ny = value ; end endmodule\n" + ); +} + +#[test] +fn extract_variable_uses_assignment_lhs_type() { + let text = + "module top; logic [7:0] y, a, b; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; logic [7:0] y, a, b; always_comb begin logic [7:0] value = a + b;\ny = value; end endmodule\n" + ); +} + +#[test] +fn extract_variable_from_continuous_assign() { + let text = "module top; assign y = /*selection*/a + b/*selection*/; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; wire logic value = a + b;\nassign y = value; endmodule\n" + ); +} + +#[test] +fn extract_variable_uses_continuous_assign_lhs_type() { + let text = + "module top; logic [7:0] y, a, b; assign y = /*selection*/a + b/*selection*/; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; logic [7:0] y, a, b; wire logic [7:0] value = a + b;\nassign y = value; endmodule\n" + ); +} + +#[test] +fn extract_variable_requires_selection() { + let labels = action_labels_without_diagnostics( + "module top; always_comb begin y = a /*caret*/+ b; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_requires_complete_expression_selection() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; always_comb begin y = a /*selection*/+/*selection*/ b; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_rejects_continuous_assign_lhs() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; assign /*selection*/y/*selection*/ = a + b; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_requires_block_scope() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; always_comb if (a) y = /*selection*/b + c/*selection*/; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn pull_assignment_up_converts_if_else_assignment_to_ternary() { + let text = "module top; always_comb /*caret*/if (a) y = 1; else y = 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : 0; endmodule\n"); +} + +#[test] +fn pull_assignment_up_converts_else_if_chain_to_nested_ternary() { + let text = + "module top; always_comb if (/*caret*/a) y = 1; else if (b) y = 2; else y = 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : b ? 2 : 3; endmodule\n"); +} + +#[test] +fn pull_assignment_up_triggers_from_else_if_chain_body() { + let text = + "module top; always_comb if (a) y = 1; else if (b) /*caret*/y = 2; else y = 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : b ? 2 : 3; endmodule\n"); +} + +#[test] +fn pull_assignment_up_wraps_conditional_predicate() { + let text = "module top; always_comb if (a ? b : c) /*caret*/y = 1; else y = 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = (a ? b : c) ? 1 : 0; endmodule\n"); +} + +#[test] +fn pull_assignment_up_requires_single_assignment_branches() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (a) begin /*caret*/y = 1; z = 0; end else y = 2; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Pull assignment up")); +} + +#[test] +fn pull_assignment_up_rejects_block_with_declarations() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (a) begin logic tmp; /*caret*/y = tmp; end else y = 0; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Pull assignment up")); +} + +#[test] +fn pull_assignment_down_converts_ternary_assignment_to_if_else() { + let text = "module top; always_comb /*caret*/y = a ? 1 : 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_down").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a) y = 1; else y = 0; endmodule\n"); +} + +#[test] +fn pull_assignment_down_converts_nested_ternary_to_else_if_chain() { + let text = "module top; always_comb /*caret*/y = a ? 1 : b ? 2 : 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_down").unwrap(); + assert_eq!( + fixed, + "module top; always_comb if (a) y = 1; else if (b) y = 2; else y = 3; endmodule\n" + ); +} + #[test] fn unwrap_single_statement_block_unwraps_single_statement() { let text = "module top; always_comb if (a) /*caret*/begin y = 1; end endmodule\n"; diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index 04cef1b4..948700af 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -1,6 +1,4 @@ -use hir::{ - base_db::source_db::SourceDb, container::InFile, hir_def::lower_ident, semantics::Semantics, -}; +use hir::{base_db::source_db::SourceDb, container::InFile, semantics::Semantics}; use nohash_hasher::IntMap; use rustc_hash::FxHashMap; use smol_str::SmolStr; @@ -427,7 +425,7 @@ fn check_same_name_conn( DefinitionClass::PortConnShorthand { port, .. } => port, DefinitionClass::Ambiguous(_) => return None, }; - let port_name = lower_ident(Some(name_token))?; + let port_name = name_token.value_text().to_string(); let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); let actual_token = match expr { Expression::Name(Name::IdentifierName(ident)) => ident.identifier()?, @@ -438,7 +436,7 @@ fn check_same_name_conn( } _ => return None, }; - if lower_ident(Some(actual_token))?.as_str() != port_name.as_str() { + if actual_token.value_text().to_string() != port_name { return None; } let actual_token = SyntaxTokenWithParent { parent: expr.syntax(), tok: actual_token }; @@ -548,7 +546,7 @@ fn edits_from_refs( && conn_data_range(port_conn).is_some_and(|r| r == range) && let Some(port_name) = port_conn .name() - .filter(|n| lower_ident(Some(*n)).is_some_and(|name| name == new_name)) { + .filter(|n| n.value_text().to_string() == new_name) { // .new(data) => .new let Some(start) = port_name.text_range_in(port_conn.syntax()).map(|range| range.start()) else { diff --git a/crates/slang/bindings/rust/ast.rs b/crates/slang/bindings/rust/ast.rs old mode 100644 new mode 100755 index fdad1b27..5e8328e8 --- a/crates/slang/bindings/rust/ast.rs +++ b/crates/slang/bindings/rust/ast.rs @@ -51,6 +51,15 @@ impl<'a, T: AstNode<'a>> SyntaxList<'a, T> { pub fn children(&self) -> impl Iterator + 'a { SyntaxChildren::new(self.syntax).map(|elem| T::cast(elem.as_node().unwrap()).unwrap()) } + + pub fn only_children(&self) -> Option { + let mut children = SyntaxChildren::new(self.syntax); + let first = children.next()?; + if children.next().is_some() { + return None; + } + T::cast(first.as_node().unwrap()) + } } impl<'a, T: AstNode<'a>> AstNode<'a> for SyntaxList<'a, T> { @@ -79,6 +88,15 @@ impl<'a, T: AstNode<'a>> SeparatedList<'a, T> { .step_by(2) .map(|elem| T::cast(elem.as_node().unwrap()).unwrap()) } + + pub fn only_children(&self) -> Option { + let mut children = SyntaxChildren::new(self.syntax); + let first = children.next()?; + if children.next().is_some() { + return None; + } + T::cast(first.as_node().unwrap()) + } } impl<'a, T: AstNode<'a>> AstNode<'a> for SeparatedList<'a, T> { diff --git a/src/i18n.rs b/src/i18n.rs index 28fac9cc..66b6efc8 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -95,10 +95,32 @@ pub(crate) mod keys { "code_action.sort_named_port_connections"; pub(crate) const CODE_ACTION_ADD_DEFAULT_CASE_ITEM: &str = "code_action.add_default_case_item"; pub(crate) const CODE_ACTION_INVERT_IF_ELSE: &str = "code_action.invert_if_else"; + pub(crate) const CODE_ACTION_EXTRACT_VARIABLE: &str = "code_action.extract_variable"; + pub(crate) const CODE_ACTION_REMOVE_REDUNDANT_PARENTHESES: &str = + "code_action.remove_redundant_parentheses"; pub(crate) const CODE_ACTION_UNWRAP_SINGLE_STATEMENT_BLOCK: &str = "code_action.unwrap_single_statement_block"; pub(crate) const CODE_ACTION_WRAP_STATEMENT_IN_BEGIN_END: &str = "code_action.wrap_statement_in_begin_end"; + pub(crate) const CODE_ACTION_EXPAND_NAMED_PORT_CONNECTION_SHORTHAND: &str = + "code_action.expand_named_port_connection_shorthand"; + pub(crate) const CODE_ACTION_COLLAPSE_NAMED_PORT_CONNECTION_SHORTHAND: &str = + "code_action.collapse_named_port_connection_shorthand"; + pub(crate) const CODE_ACTION_CONVERT_ANSI_PORTS_TO_NON_ANSI: &str = + "code_action.convert_ansi_ports_to_non_ansi"; + pub(crate) const CODE_ACTION_CONVERT_NON_ANSI_PORTS_TO_ANSI: &str = + "code_action.convert_non_ansi_ports_to_ansi"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_COMB: &str = + "code_action.convert_always_to_always_comb"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_FF: &str = + "code_action.convert_always_to_always_ff"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_COMB_TO_ALWAYS: &str = + "code_action.convert_always_comb_to_always"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_FF_TO_ALWAYS: &str = + "code_action.convert_always_ff_to_always"; + pub(crate) const CODE_ACTION_MERGE_NESTED_IF: &str = "code_action.merge_nested_if"; + pub(crate) const CODE_ACTION_PULL_ASSIGNMENT_UP: &str = "code_action.pull_assignment_up"; + pub(crate) const CODE_ACTION_PULL_ASSIGNMENT_DOWN: &str = "code_action.pull_assignment_down"; pub(crate) const CODE_ACTION_EXPAND_POSTFIX_INC_DEC: &str = "code_action.expand_postfix_inc_dec"; pub(crate) const CODE_ACTION_EXPAND_PREFIX_INC_DEC: &str = "code_action.expand_prefix_inc_dec"; @@ -124,6 +146,8 @@ pub(crate) mod keys { "code_action.collapse_compound_assignment"; pub(crate) const CODE_ACTION_APPLY_DE_MORGAN: &str = "code_action.apply_de_morgan"; pub(crate) const CODE_ACTION_FACTOR_DE_MORGAN: &str = "code_action.factor_de_morgan"; + pub(crate) const CODE_ACTION_REMOVE_DIGIT_SEPARATORS: &str = + "code_action.remove_digit_separators"; pub(crate) const CODE_ACTION_INSERT_MISSING_TOKEN: &str = "code_action.insert_missing_token"; pub(crate) const CODE_ACTION_CONVERT_LITERAL_TO_BINARY: &str = "code_action.convert_literal_to_binary"; diff --git a/src/i18n/en.toml b/src/i18n/en.toml index fd942584..d695beb7 100644 --- a/src/i18n/en.toml +++ b/src/i18n/en.toml @@ -53,8 +53,21 @@ sort_named_parameter_assignments = "Sort named parameter assignments" sort_named_port_connections = "Sort named port connections" add_default_case_item = "Add default case item" invert_if_else = "Invert if/else" +extract_variable = "Extract into variable" +remove_redundant_parentheses = "Remove redundant parentheses" unwrap_single_statement_block = "Unwrap single-statement begin/end" wrap_statement_in_begin_end = "Wrap statement in begin/end" +expand_named_port_connection_shorthand = "Expand named port shorthand" +collapse_named_port_connection_shorthand = "Collapse named port to shorthand" +convert_ansi_ports_to_non_ansi = "Convert ANSI port declarations to non-ANSI" +convert_non_ansi_ports_to_ansi = "Convert non-ANSI port declarations to ANSI" +convert_always_to_always_comb = "Convert to always_comb" +convert_always_to_always_ff = "Convert to always_ff" +convert_always_comb_to_always = "Convert to always @(*)" +convert_always_ff_to_always = "Convert to always @(...)" +merge_nested_if = "Merge nested if" +pull_assignment_up = "Pull assignment up" +pull_assignment_down = "Pull assignment down" expand_postfix_inc_dec = "Expand postfix expression" expand_prefix_inc_dec = "Expand prefix expression" convert_postfix_to_prefix_inc_dec = "Convert postfix to prefix expression" @@ -69,6 +82,7 @@ expand_compound_assignment = "Expand compound assignment" collapse_compound_assignment = "Collapse compound assignment" apply_de_morgan = "Apply De Morgan's law" factor_de_morgan = "Factor De Morgan's law" +remove_digit_separators = "Remove digit separators" insert_missing_token = "Insert missing '{token}'" convert_literal_to_binary = "Convert literal to binary" convert_literal_to_octal = "Convert literal to octal" diff --git a/src/i18n/zh-CN.toml b/src/i18n/zh-CN.toml index c25d855b..30e33df6 100644 --- a/src/i18n/zh-CN.toml +++ b/src/i18n/zh-CN.toml @@ -53,8 +53,21 @@ sort_named_parameter_assignments = "排序命名参数赋值" sort_named_port_connections = "排序命名端口连接" add_default_case_item = "添加 default case 分支项" invert_if_else = "反转 if/else" +extract_variable = "提取为变量" +remove_redundant_parentheses = "移除冗余括号" unwrap_single_statement_block = "展开单语句 begin/end" wrap_statement_in_begin_end = "用 begin/end 包裹语句" +expand_named_port_connection_shorthand = "展开命名端口简写" +collapse_named_port_connection_shorthand = "折叠命名端口为简写" +convert_ansi_ports_to_non_ansi = "将 ANSI 端口声明转换为非 ANSI" +convert_non_ansi_ports_to_ansi = "将非 ANSI 端口声明转换为 ANSI" +convert_always_to_always_comb = "转换为 always_comb" +convert_always_to_always_ff = "转换为 always_ff" +convert_always_comb_to_always = "转换为 always @(*)" +convert_always_ff_to_always = "转换为 always @(...)" +merge_nested_if = "合并嵌套 if" +pull_assignment_up = "转换为嵌套三元表达式 ?:" +pull_assignment_down = "转换为 if/else 赋值" expand_postfix_inc_dec = "展开后缀表达式" expand_prefix_inc_dec = "展开前缀表达式" convert_postfix_to_prefix_inc_dec = "将后缀表达式转换为前缀表达式" @@ -67,8 +80,9 @@ convert_assignment_to_postfix_inc_dec = "将赋值转换为后缀表达式" convert_assignment_to_prefix_inc_dec = "将赋值转换为前缀表达式" expand_compound_assignment = "展开复合赋值" collapse_compound_assignment = "折叠复合赋值" -apply_de_morgan = "应用德摩根律" -factor_de_morgan = "提取德摩根律" +apply_de_morgan = "应用德摩根律,将取反操作分配到内层" +factor_de_morgan = "应用德摩根律,将内层取反提取到外层" +remove_digit_separators = "移除数字分隔符" insert_missing_token = "插入缺失的 '{token}'" convert_literal_to_binary = "将字面量转换为二进制" convert_literal_to_octal = "将字面量转换为八进制" diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 4209f6cf..98fac21a 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -946,8 +946,25 @@ fn code_action_title_key(id: &str, label: &str) -> Option<&'static str> { "sort_named_port_connections" => keys::CODE_ACTION_SORT_NAMED_PORT_CONNECTIONS, "add_default_case_item" => keys::CODE_ACTION_ADD_DEFAULT_CASE_ITEM, "invert_if_else" => keys::CODE_ACTION_INVERT_IF_ELSE, + "extract_variable" => keys::CODE_ACTION_EXTRACT_VARIABLE, + "remove_parentheses" => keys::CODE_ACTION_REMOVE_REDUNDANT_PARENTHESES, "unwrap_single_statement_block" => keys::CODE_ACTION_UNWRAP_SINGLE_STATEMENT_BLOCK, "wrap_statement_in_begin_end" => keys::CODE_ACTION_WRAP_STATEMENT_IN_BEGIN_END, + "expand_named_port_connection_shorthand" => { + keys::CODE_ACTION_EXPAND_NAMED_PORT_CONNECTION_SHORTHAND + } + "collapse_named_port_connection_shorthand" => { + keys::CODE_ACTION_COLLAPSE_NAMED_PORT_CONNECTION_SHORTHAND + } + "convert_ansi_ports_to_non_ansi" => keys::CODE_ACTION_CONVERT_ANSI_PORTS_TO_NON_ANSI, + "convert_non_ansi_ports_to_ansi" => keys::CODE_ACTION_CONVERT_NON_ANSI_PORTS_TO_ANSI, + "convert_always_to_always_comb" => keys::CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_COMB, + "convert_always_to_always_ff" => keys::CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_FF, + "convert_always_comb_to_always" => keys::CODE_ACTION_CONVERT_ALWAYS_COMB_TO_ALWAYS, + "convert_always_ff_to_always" => keys::CODE_ACTION_CONVERT_ALWAYS_FF_TO_ALWAYS, + "merge_nested_if" => keys::CODE_ACTION_MERGE_NESTED_IF, + "pull_assignment_up" => keys::CODE_ACTION_PULL_ASSIGNMENT_UP, + "pull_assignment_down" => keys::CODE_ACTION_PULL_ASSIGNMENT_DOWN, "expand_postfix_inc_dec" => keys::CODE_ACTION_EXPAND_POSTFIX_INC_DEC, "expand_prefix_inc_dec" => keys::CODE_ACTION_EXPAND_PREFIX_INC_DEC, "convert_postfix_to_prefix_inc_dec" => keys::CODE_ACTION_CONVERT_POSTFIX_TO_PREFIX_INC_DEC, @@ -974,6 +991,9 @@ fn code_action_title_key(id: &str, label: &str) -> Option<&'static str> { "collapse_compound_assignment" => keys::CODE_ACTION_COLLAPSE_COMPOUND_ASSIGNMENT, "apply_de_morgan" => keys::CODE_ACTION_APPLY_DE_MORGAN, "factor_de_morgan" => keys::CODE_ACTION_FACTOR_DE_MORGAN, + "reformat_number_literal" if label == "Remove digit separators" => { + keys::CODE_ACTION_REMOVE_DIGIT_SEPARATORS + } "convert_literal_base" => match label { "Convert literal to binary" => keys::CODE_ACTION_CONVERT_LITERAL_TO_BINARY, "Convert literal to octal" => keys::CODE_ACTION_CONVERT_LITERAL_TO_OCTAL, diff --git a/src/tests.rs b/src/tests.rs index ac8ab1f2..e9f14354 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -673,11 +673,20 @@ fn goto_definition_response_uris(response: GotoDefinitionResponse) -> Vec { fn position_of(text: &str, needle: &str) -> Position { let offset = text.find(needle).unwrap_or_else(|| panic!("missing {needle:?}")); + position_at_offset(text, offset) +} + +fn position_at_offset(text: &str, offset: usize) -> Position { let line = text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32; let line_start = text[..offset].rfind('\n').map(|idx| idx + 1).unwrap_or(0); Position { line, character: (offset - line_start) as u32 } } +fn range_of(text: &str, needle: &str) -> Range { + let start = text.find(needle).unwrap_or_else(|| panic!("missing {needle:?}")); + Range::new(position_at_offset(text, start), position_at_offset(text, start + needle.len())) +} + fn code_action_client_caps() -> ClientCapabilities { ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { @@ -688,6 +697,7 @@ fn code_action_client_caps() -> ClientCapabilities { CodeActionKind::EMPTY, CodeActionKind::QUICKFIX, CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, CodeActionKind::REFACTOR_REWRITE, ] .into_iter() @@ -751,6 +761,48 @@ fn request_code_actions( unreachable!("codeAction retries should either return or panic") } +fn request_code_actions_with_range( + client: &Connection, + uri: Url, + range: Range, + context: CodeActionContext, + request_id: i32, +) -> Vec { + const CONTENT_MODIFIED_RETRIES: i32 = 5; + + for attempt in 0..=CONTENT_MODIFIED_RETRIES { + let request_id = lsp_server::RequestId::from(request_id + attempt); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + CodeActionRequest::METHOD.to_string(), + CodeActionParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + range, + context: context.clone(), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + + let response = recv_raw_response(client, request_id, "codeAction"); + if response.error.is_none() { + return serde_json::from_value(response.result.unwrap_or(serde_json::Value::Null)) + .unwrap_or_else(|err| panic!("failed to decode codeAction response: {err}")); + } + + if is_content_modified(&response) && attempt < CONTENT_MODIFIED_RETRIES { + continue; + } + + panic!("codeAction returned error: {:?}", response.error); + } + + unreachable!("codeAction retries should either return or panic") +} + fn is_content_modified(response: &lsp_server::Response) -> bool { response .error @@ -1158,6 +1210,77 @@ endmodule shutdown_test_server(&client, server_thread); } +#[test] +fn code_action_request_returns_extract_variable_for_selected_expression() { + let text = "\ +module top; + always_comb begin + y = a + b; + end +endmodule +"; + let (_temp_dir, client, server_thread, uri) = + setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + + let actions = request_code_actions_with_range( + &client, + uri, + range_of(text, "a + b"), + CodeActionContext { + diagnostics: Vec::new(), + only: Some(vec![CodeActionKind::REFACTOR_EXTRACT]), + trigger_kind: None, + }, + 201, + ); + let titles = code_action_titles(&actions); + + assert!( + titles.iter().any(|title| title == "Extract into variable"), + "expected extract variable refactor, got {titles:?}" + ); + + shutdown_test_server(&client, server_thread); +} + +#[test] +fn code_action_request_returns_extract_variable_for_selected_continuous_assign_rhs() { + let text = "\ +module top ( + c, + led0 +); + input wire c; + output led0; + reg led0; + + assign led0 = c * 2 + c; +endmodule +"; + let (_temp_dir, client, server_thread, uri) = + setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + + let actions = request_code_actions_with_range( + &client, + uri, + range_of(text, "c * 2 + c"), + CodeActionContext { + diagnostics: Vec::new(), + only: Some(vec![CodeActionKind::REFACTOR_EXTRACT]), + trigger_kind: None, + }, + 202, + ); + let titles = code_action_titles(&actions); + + assert!( + titles.iter().any(|title| title == "Extract into variable"), + "expected extract variable refactor, got {titles:?}" + ); + + shutdown_test_server(&client, server_thread); +} + #[test] fn code_action_request_uses_server_diagnostics_when_client_diagnostic_has_no_data() { let text = "\ From d24c4d96164464a6bc5f69eaf3322ce6cd657a7c Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 02:29:05 +0800 Subject: [PATCH 48/80] Document SystemVerilog assists --- .../handlers/add_default_case_item.rs | 11 ++++++++++ .../add_implicit_named_port_parens.rs | 11 ++++++++++ .../handlers/add_instance_parens.rs | 11 ++++++++++ .../handlers/add_missing_connections.rs | 11 ++++++++++ .../handlers/add_missing_parameters.rs | 11 ++++++++++ .../code_action/handlers/apply_de_morgan.rs | 11 ++++++++++ .../handlers/convert_always_block.rs | 11 ++++++++++ .../handlers/convert_literal_base.rs | 11 ++++++++++ .../convert_named_port_connections.rs | 11 ++++++++++ .../handlers/convert_ordered_connections.rs | 22 +++++++++++++++++++ .../handlers/convert_port_declarations.rs | 11 ++++++++++ .../handlers/expand_compound_assignment.rs | 11 ++++++++++ .../handlers/expand_postfix_inc_dec.rs | 11 ++++++++++ .../code_action/handlers/extract_variable.rs | 12 ++++++++++ .../handlers/insert_expected_token.rs | 11 ++++++++++ .../code_action/handlers/invert_if_else.rs | 11 ++++++++++ .../code_action/handlers/merge_nested_if.rs | 11 ++++++++++ .../handlers/pull_assignment_up.rs | 22 +++++++++++++++++++ .../handlers/reformat_number_literal.rs | 11 ++++++++++ .../handlers/remove_empty_port_connections.rs | 11 ++++++++++ .../handlers/remove_parentheses.rs | 11 ++++++++++ .../sort_named_instantiation_items.rs | 22 +++++++++++++++++++ .../handlers/split_declaration_declarators.rs | 12 ++++++++++ .../handlers/wrap_statement_in_begin_end.rs | 22 +++++++++++++++++++ 24 files changed, 310 insertions(+) diff --git a/crates/ide/src/code_action/handlers/add_default_case_item.rs b/crates/ide/src/code_action/handlers/add_default_case_item.rs index 3e635346..a3f2d77c 100644 --- a/crates/ide/src/code_action/handlers/add_default_case_item.rs +++ b/crates/ide/src/code_action/handlers/add_default_case_item.rs @@ -12,6 +12,17 @@ const ID: CodeActionId = CodeActionId { name: "add_default_case_item", kind: CodeActionKind::Generate, repair: None }; const LABEL: &str = "Add default case item"; +// Assist: add_default_case_item +// +// This adds a `default` item to a case statement that does not already have one. +// +// ``` +// module top; always_comb case$0 (sel) 1'b0: y = a; endcase endmodule +// ``` +// -> +// ``` +// module top; always_comb case (sel) 1'b0: y = a; default: ; endcase endmodule +// ``` pub(super) fn add_default_case_item( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs index 31ec47aa..4490d3c6 100644 --- a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs +++ b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs @@ -14,6 +14,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Add explicit empty port connection"; +// Assist: add_implicit_named_port_parens +// +// This makes an implicit named port connection explicit by adding empty parentheses. +// +// ``` +// child u(.ready$0); +// ``` +// -> +// ``` +// child u(.ready()); +// ``` pub(super) fn add_implicit_named_port_parens( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_instance_parens.rs b/crates/ide/src/code_action/handlers/add_instance_parens.rs index a25aeee8..56de01d1 100644 --- a/crates/ide/src/code_action/handlers/add_instance_parens.rs +++ b/crates/ide/src/code_action/handlers/add_instance_parens.rs @@ -11,6 +11,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Add empty instance port list"; +// Assist: add_instance_parens +// +// This adds an empty port list to an instance that is missing one. +// +// ``` +// child u$0; +// ``` +// -> +// ``` +// child u(); +// ``` pub(super) fn add_instance_parens( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_missing_connections.rs b/crates/ide/src/code_action/handlers/add_missing_connections.rs index 4bc703f4..49bc8f6c 100644 --- a/crates/ide/src/code_action/handlers/add_missing_connections.rs +++ b/crates/ide/src/code_action/handlers/add_missing_connections.rs @@ -25,6 +25,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Fill connections"; +// Assist: add_missing_connections +// +// This fills the missing port connections for an instance from the target module definition. +// +// ``` +// child u($0.a(a)); +// ``` +// -> +// ``` +// child u(.a(a), .b('0)); +// ``` pub(super) fn add_missing_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_missing_parameters.rs b/crates/ide/src/code_action/handlers/add_missing_parameters.rs index f0dee54f..cf5f4c59 100644 --- a/crates/ide/src/code_action/handlers/add_missing_parameters.rs +++ b/crates/ide/src/code_action/handlers/add_missing_parameters.rs @@ -25,6 +25,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Fill parameters"; +// Assist: add_missing_parameters +// +// This fills the missing parameter assignments for an instantiation from the target module definition. +// +// ``` +// child #($0.A(1)) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(0)) u(); +// ``` pub(super) fn add_missing_parameters( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/apply_de_morgan.rs b/crates/ide/src/code_action/handlers/apply_de_morgan.rs index 465edb75..7f45d2d3 100644 --- a/crates/ide/src/code_action/handlers/apply_de_morgan.rs +++ b/crates/ide/src/code_action/handlers/apply_de_morgan.rs @@ -16,6 +16,17 @@ const FACTOR_ID: CodeActionId = CodeActionId { name: "factor_de_morgan", kind: CodeActionKind::RefactorRewrite, repair: None }; const FACTOR_LABEL: &str = "Factor De Morgan's law"; +// Assist: apply_de_morgan +// +// This applies or factors De Morgan's law for boolean expressions and if conditions. +// +// ``` +// assign y = $0!(a && b); +// ``` +// -> +// ``` +// assign y = !a || !b; +// ``` pub(super) fn apply_de_morgan( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_always_block.rs b/crates/ide/src/code_action/handlers/convert_always_block.rs index 169cad5b..db79c77f 100644 --- a/crates/ide/src/code_action/handlers/convert_always_block.rs +++ b/crates/ide/src/code_action/handlers/convert_always_block.rs @@ -36,6 +36,17 @@ const ALWAYS_FF_TO_ALWAYS_ID: CodeActionId = CodeActionId { }; const ALWAYS_FF_TO_ALWAYS_LABEL: &str = "Convert to always @(...)"; +// Assist: convert_always_block +// +// This converts compatible procedural blocks between `always`, `always_comb`, and `always_ff`. +// +// ``` +// always$0 @(*) begin y = a; end +// ``` +// -> +// ``` +// always_comb begin y = a; end +// ``` pub(super) fn convert_always_block( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_literal_base.rs b/crates/ide/src/code_action/handlers/convert_literal_base.rs index 55cc9420..70ff8c56 100644 --- a/crates/ide/src/code_action/handlers/convert_literal_base.rs +++ b/crates/ide/src/code_action/handlers/convert_literal_base.rs @@ -13,6 +13,17 @@ const ACTION_ID: CodeActionId = CodeActionId { repair: None, }; +// Assist: convert_literal_base +// +// This converts an integer literal between binary, octal, decimal, and hexadecimal notation. +// +// ``` +// localparam int value = 8'h0f$0; +// ``` +// -> +// ``` +// localparam int value = 8'b1111; +// ``` pub(super) fn convert_literal_base( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs index 81a42328..83ea401f 100644 --- a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs @@ -20,6 +20,17 @@ const COLLAPSE_ID: CodeActionId = CodeActionId { }; const COLLAPSE_LABEL: &str = "Collapse named port to shorthand"; +// Assist: convert_named_port_connection_shorthand +// +// This expands named port connection shorthand, or collapses same-name connections to shorthand. +// +// ``` +// child u(.ready$0); +// ``` +// -> +// ``` +// child u(.ready(ready)); +// ``` pub(super) fn convert_named_port_connection_shorthand( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs index df2fbb29..75e57ffd 100644 --- a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs @@ -29,6 +29,17 @@ const PARAMS_ID: CodeActionId = CodeActionId { }; const PARAMS_LABEL: &str = "Convert ordered parameter assignments to named assignments"; +// Assist: convert_ordered_ports +// +// This converts ordered port connections to named port connections using the target module's port order. +// +// ``` +// child u($0a, b); +// ``` +// -> +// ``` +// child u(.a(a), .b(b)); +// ``` pub(super) fn convert_ordered_ports( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -69,6 +80,17 @@ pub(super) fn convert_ordered_ports( Some(()) } +// Assist: convert_ordered_params +// +// This converts ordered parameter assignments to named parameter assignments using the target module's parameter order. +// +// ``` +// child #($01, 2) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(2)) u(); +// ``` pub(super) fn convert_ordered_params( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, 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 b78908fd..992db61c 100644 --- a/crates/ide/src/code_action/handlers/convert_port_declarations.rs +++ b/crates/ide/src/code_action/handlers/convert_port_declarations.rs @@ -45,6 +45,17 @@ const NON_ANSI_TO_ANSI_ID: CodeActionId = CodeActionId { }; const NON_ANSI_TO_ANSI_LABEL: &str = "Convert non-ANSI port declarations to ANSI"; +// Assist: convert_port_declarations +// +// This converts module ports between ANSI declarations and non-ANSI declarations. +// +// ``` +// module top($0input a, output logic b); endmodule +// ``` +// -> +// ``` +// module top(a, b); input a; output logic b; endmodule +// ``` pub(super) fn convert_port_declarations( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs index e2275ee8..4d77e1e6 100644 --- a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs +++ b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs @@ -21,6 +21,17 @@ const COLLAPSE_ID: CodeActionId = CodeActionId { }; const COLLAPSE_LABEL: &str = "Collapse compound assignment"; +// Assist: expand_compound_assignment +// +// This expands compound assignments, or collapses simple self-assignments into compound assignments. +// +// ``` +// always_comb a $0+= b; +// ``` +// -> +// ``` +// always_comb a = a + b; +// ``` pub(super) fn expand_compound_assignment( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs index 46619c77..5ca61dc1 100644 --- a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs +++ b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs @@ -78,6 +78,17 @@ const ASSIGNMENT_TO_PREFIX_ID: CodeActionId = CodeActionId { }; const ASSIGNMENT_TO_PREFIX_LABEL: &str = "Convert assignment to prefix expression"; +// Assist: expand_postfix_inc_dec +// +// This converts between postfix, prefix, compound assignment, and expanded assignment forms of increment/decrement expressions. +// +// ``` +// always_ff @(posedge clk) count$0++; +// ``` +// -> +// ``` +// always_ff @(posedge clk) count = count + 1; +// ``` pub(super) fn expand_postfix_inc_dec( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/extract_variable.rs b/crates/ide/src/code_action/handlers/extract_variable.rs index 50ed287f..ff876339 100644 --- a/crates/ide/src/code_action/handlers/extract_variable.rs +++ b/crates/ide/src/code_action/handlers/extract_variable.rs @@ -23,6 +23,18 @@ use crate::code_action::{ const ID: CodeActionId = CodeActionId { name: "extract_variable", kind: CodeActionKind::RefactorExtract, repair: None }; +// Assist: extract_variable +// +// This extracts a selected expression into a new local variable or continuous net declaration. +// +// ``` +// always_comb begin y = $0a + b$0; end +// ``` +// -> +// ``` +// always_comb begin logic value = a + b; +// y = value; end +// ``` pub(super) fn extract_variable( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/insert_expected_token.rs b/crates/ide/src/code_action/handlers/insert_expected_token.rs index 3d213b55..64be8952 100644 --- a/crates/ide/src/code_action/handlers/insert_expected_token.rs +++ b/crates/ide/src/code_action/handlers/insert_expected_token.rs @@ -11,6 +11,17 @@ const ID: CodeActionId = CodeActionId { repair: Some(RepairKind::InsertExpectedToken), }; +// Assist: insert_expected_token +// +// This inserts a token that the parser expected at the diagnostic location. +// +// ``` +// module top$0 endmodule +// ``` +// -> +// ``` +// module top; endmodule +// ``` pub(super) fn insert_expected_token( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/invert_if_else.rs b/crates/ide/src/code_action/handlers/invert_if_else.rs index 0dfb2873..c7d94748 100644 --- a/crates/ide/src/code_action/handlers/invert_if_else.rs +++ b/crates/ide/src/code_action/handlers/invert_if_else.rs @@ -12,6 +12,17 @@ const ID: CodeActionId = CodeActionId { name: "invert_if_else", kind: CodeActionKind::RefactorRewrite, repair: None }; const LABEL: &str = "Invert if/else"; +// Assist: invert_if_else +// +// This swaps the then and else branches of an if statement and negates the condition. +// +// ``` +// always_comb if$0 (ready) y = a; else y = b; +// ``` +// -> +// ``` +// always_comb if (!(ready)) y = b; else y = a; +// ``` pub(super) fn invert_if_else( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/merge_nested_if.rs b/crates/ide/src/code_action/handlers/merge_nested_if.rs index f0d4ee28..be7e69f7 100644 --- a/crates/ide/src/code_action/handlers/merge_nested_if.rs +++ b/crates/ide/src/code_action/handlers/merge_nested_if.rs @@ -12,6 +12,17 @@ use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeA const ID: CodeActionId = CodeActionId { name: "merge_nested_if", kind: CodeActionKind::RefactorRewrite, repair: None }; +// Assist: merge_nested_if +// +// This merges nested if statements without else branches into one if statement with a combined condition. +// +// ``` +// always_comb if$0 (a) begin if (b) y = 1; end +// ``` +// -> +// ``` +// always_comb if (a && b) y = 1; +// ``` pub(super) fn merge_nested_if( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/pull_assignment_up.rs b/crates/ide/src/code_action/handlers/pull_assignment_up.rs index 7ae297c4..8d932f92 100644 --- a/crates/ide/src/code_action/handlers/pull_assignment_up.rs +++ b/crates/ide/src/code_action/handlers/pull_assignment_up.rs @@ -20,6 +20,17 @@ const DOWN_ID: CodeActionId = CodeActionId { repair: None, }; +// Assist: pull_assignment_up +// +// This pulls matching assignments out of an if/else chain into a single ternary assignment. +// +// ``` +// always_comb if$0 (a) y = 1; else y = 0; +// ``` +// -> +// ``` +// always_comb y = a ? 1 : 0; +// ``` pub(super) fn pull_assignment_up( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -43,6 +54,17 @@ pub(super) fn pull_assignment_up( }) } +// Assist: pull_assignment_down +// +// This expands a ternary assignment into an if/else assignment chain. +// +// ``` +// always_comb $0y = a ? 1 : 0; +// ``` +// -> +// ``` +// always_comb if (a) y = 1; else y = 0; +// ``` pub(super) fn pull_assignment_down( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/reformat_number_literal.rs b/crates/ide/src/code_action/handlers/reformat_number_literal.rs index d3edc630..deeffaac 100644 --- a/crates/ide/src/code_action/handlers/reformat_number_literal.rs +++ b/crates/ide/src/code_action/handlers/reformat_number_literal.rs @@ -16,6 +16,17 @@ const ID: CodeActionId = CodeActionId { }; const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; +// Assist: reformat_number_literal +// +// This adds digit separators to long integer literals or removes existing digit separators. +// +// ``` +// localparam int value = 10000$0; +// ``` +// -> +// ``` +// localparam int value = 10_000; +// ``` pub(super) fn reformat_number_literal( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs b/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs index 29a31812..82a2c5f0 100644 --- a/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs +++ b/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs @@ -17,6 +17,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Remove empty port connections"; +// Assist: remove_empty_port_connections +// +// This removes empty ordered port connections from an instance port list. +// +// ``` +// child u(a, $0, b); +// ``` +// -> +// ``` +// child u(a, b); +// ``` pub(super) fn remove_empty_port_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/remove_parentheses.rs b/crates/ide/src/code_action/handlers/remove_parentheses.rs index f2e5c8c6..2ad7bd33 100644 --- a/crates/ide/src/code_action/handlers/remove_parentheses.rs +++ b/crates/ide/src/code_action/handlers/remove_parentheses.rs @@ -15,6 +15,17 @@ const ID: CodeActionId = CodeActionId { repair: None, }; +// Assist: remove_parentheses +// +// This removes parentheses when they are redundant for the surrounding expression. +// +// ``` +// assign y = $0(a + b) + c; +// ``` +// -> +// ``` +// assign y = a + b + c; +// ``` pub(super) fn remove_parentheses( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs index 917f7699..3a478a09 100644 --- a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs +++ b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs @@ -25,6 +25,17 @@ const SORT_NAMED_PARAMETER_ASSIGNMENTS_ID: CodeActionId = CodeActionId { }; const SORT_NAMED_PARAMETER_ASSIGNMENTS_LABEL: &str = "Sort named parameter assignments"; +// Assist: sort_named_parameter_assignments +// +// This sorts named parameter assignments to match the target module's parameter declaration order. +// +// ``` +// child #(.B(2), $0.A(1)) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(2)) u(); +// ``` pub(super) fn sort_named_parameter_assignments( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -69,6 +80,17 @@ const SORT_NAMED_PORT_CONNECTIONS_ID: CodeActionId = CodeActionId { }; const SORT_NAMED_PORT_CONNECTIONS_LABEL: &str = "Sort named port connections"; +// Assist: sort_named_port_connections +// +// This sorts named port connections to match the target module's port declaration order. +// +// ``` +// child u(.b(b), $0.a(a)); +// ``` +// -> +// ``` +// child u(.a(a), .b(b)); +// ``` pub(super) fn sort_named_port_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs index 2963bc71..da9007a2 100644 --- a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs +++ b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs @@ -19,6 +19,18 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Split declaration"; +// Assist: split_declaration_declarators +// +// This splits a declaration with multiple declarators into one declaration per declarator. +// +// ``` +// logic $0a, b; +// ``` +// -> +// ``` +// logic a; +// logic b; +// ``` pub(super) fn split_declaration_declarators( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs b/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs index 0c134a1b..4bfe2402 100644 --- a/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs +++ b/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs @@ -18,6 +18,17 @@ const WRAP_ID: CodeActionId = CodeActionId { }; const WRAP_LABEL: &str = "Wrap statement in begin/end"; +// Assist: wrap_statement_in_begin_end +// +// This wraps a control-flow body statement in a `begin`/`end` block. +// +// ``` +// always_comb if (a) $0y = 1; +// ``` +// -> +// ``` +// always_comb if (a) begin y = 1; end +// ``` pub(super) fn wrap_statement_in_begin_end( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -47,6 +58,17 @@ const UNWRAP_ID: CodeActionId = CodeActionId { }; const UNWRAP_LABEL: &str = "Unwrap single-statement begin/end"; +// Assist: unwrap_single_statement_block +// +// This unwraps a `begin`/`end` block that contains exactly one statement. +// +// ``` +// always_comb if (a) $0begin y = 1; end +// ``` +// -> +// ``` +// always_comb if (a) y = 1; +// ``` pub(super) fn unwrap_single_statement_block( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, From 70cf6e047ae1af5fea46120d7185e1c4c201d70c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:29:06 +0800 Subject: [PATCH 49/80] refactor(hir): lazily resolve preproc contexts --- crates/hir/src/base_db/source_db.rs | 13 ++-- crates/hir/src/base_db/source_db/preproc.rs | 86 +++++++-------------- crates/hir/src/preproc/helpers/context.rs | 6 +- 3 files changed, 34 insertions(+), 71 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 8fac3da4..1192215e 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -21,14 +21,13 @@ pub use self::preproc::{ MappedSourcePreprocModel, PreprocExpansionDisplay, PreprocExpansionMapping, PreprocExpansionSourceBuffer, PreprocManifestSource, PreprocSourceMap, PreprocSourceMapError, PreprocSourceMapping, PreprocSpeculativeUniverseId, PreprocVirtualOrigin, - SourcePreprocContextIndex, SourcePreprocContextIndexIssue, SourcePreprocContextStatus, - SourcePreprocQueryError, SourcePreprocRelevantContexts, preproc_virtual_builtin_path, - preproc_virtual_expansion_path, preproc_virtual_predefines_path, + SourcePreprocContextStatus, SourcePreprocQueryError, SourcePreprocRelevantContexts, + preproc_virtual_builtin_path, preproc_virtual_expansion_path, preproc_virtual_predefines_path, preproc_virtual_speculative_path, }; #[cfg(test)] use self::preproc::{materialized_predefine_text, source_preproc_file_ids}; -use self::preproc::{source_preproc_context_index_for_profile, source_preproc_model}; +use self::preproc::{source_preproc_contexts_for_file, source_preproc_model}; pub trait FileLoader { fn resolve_path(&self, path: AnchoredPath<'_>) -> Option; @@ -364,10 +363,10 @@ pub trait SourceRootDb: SourceDb { &self, file_id: FileId, ) -> Arc>; - fn source_preproc_context_index_for_profile( + fn source_preproc_contexts_for_file( &self, - profile_id: Option, - ) -> Arc; + file_id: FileId, + ) -> Arc; fn macro_reference_index_for_profile( &self, profile_id: Option, diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs index 4f5b5562..dcfa33aa 100644 --- a/crates/hir/src/base_db/source_db/preproc.rs +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -47,18 +47,6 @@ pub struct PreprocSourceMap { range_offsets: FxHashMap, } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourcePreprocContextIndex { - contexts_by_file: FxHashMap>, - issues: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourcePreprocContextIndexIssue { - pub model_file_id: FileId, - pub error: SourcePreprocQueryError, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourcePreprocRelevantContexts { pub model_file_ids: Vec, @@ -458,39 +446,6 @@ impl PreprocSourceMap { } } -impl SourcePreprocContextIndex { - fn push_context(&mut self, file_id: FileId, model_file_id: FileId) { - let contexts = self.contexts_by_file.entry(file_id).or_default(); - if !contexts.contains(&model_file_id) { - contexts.push(model_file_id); - contexts.sort(); - } - } - - fn push_issue(&mut self, issue: SourcePreprocContextIndexIssue) { - self.issues.push(issue); - } - - pub fn relevant_contexts(&self, file_id: FileId) -> SourcePreprocRelevantContexts { - SourcePreprocRelevantContexts { - model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), - status: self.status(), - } - } - - pub fn status(&self) -> SourcePreprocContextStatus { - if self.issues.is_empty() { - SourcePreprocContextStatus::Complete - } else { - SourcePreprocContextStatus::Partial { skipped_models: self.issues.len() } - } - } - - pub fn issues(&self) -> &[SourcePreprocContextIndexIssue] { - &self.issues - } -} - fn preproc_context_file_ids( mapped: &MappedSourcePreprocModel, model_file_id: FileId, @@ -692,29 +647,40 @@ pub(super) fn source_preproc_model( Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) } -pub(super) fn source_preproc_context_index_for_profile( +pub(super) fn source_preproc_contexts_for_file( db: &dyn SourceRootDb, - profile_id: Option, -) -> Arc { - let mut index = SourcePreprocContextIndex::default(); + file_id: FileId, +) -> Arc { + let profile_id = db.file_compilation_profile(file_id); + let plan = db.compilation_plan_for_profile(profile_id); + let mut model_file_ids = UniqVec::::default(); + let mut skipped_models = 0usize; - for model_file_id in workspace_preproc_model_file_ids(db, profile_id) { - index.push_context(model_file_id, model_file_id); + for model_file_id in plan.roots.iter().copied() { + if model_file_id == file_id { + continue; + } + if !matches!( + db.file_kind(model_file_id), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + continue; + } let mapped = db.source_preproc_model(model_file_id); match mapped.as_ref() { Ok(mapped) => { - for file_id in preproc_context_file_ids(mapped, model_file_id) { - index.push_context(file_id, model_file_id); + if preproc_context_file_ids(mapped, model_file_id).contains(&file_id) { + model_file_ids.push_unique(model_file_id); } } - Err(error) => { - index.push_issue(SourcePreprocContextIndexIssue { - model_file_id, - error: error.clone(), - }); - } + Err(_) => skipped_models += 1, } } - Arc::new(index) + let status = if skipped_models == 0 { + SourcePreprocContextStatus::Complete + } else { + SourcePreprocContextStatus::Partial { skipped_models } + }; + Arc::new(SourcePreprocRelevantContexts { model_file_ids: model_file_ids.into_vec(), status }) } diff --git a/crates/hir/src/preproc/helpers/context.rs b/crates/hir/src/preproc/helpers/context.rs index 14ecc82e..bce852e2 100644 --- a/crates/hir/src/preproc/helpers/context.rs +++ b/crates/hir/src/preproc/helpers/context.rs @@ -27,9 +27,7 @@ pub(in crate::preproc) fn source_preproc_single_query_contexts( db: &dyn SourceRootDb, file_id: FileId, ) -> SourcePreprocQueryContexts { - let profile_id = db.file_compilation_profile(file_id); - let index = db.source_preproc_context_index_for_profile(profile_id); - let relevant = index.relevant_contexts(file_id); + let relevant = db.source_preproc_contexts_for_file(file_id); let mut file_ids = UniqVec::::default(); if matches!( db.file_kind(file_id), @@ -37,7 +35,7 @@ pub(in crate::preproc) fn source_preproc_single_query_contexts( ) { file_ids.push_unique(file_id); } - for model_file_id in relevant.model_file_ids { + for model_file_id in relevant.model_file_ids.iter().copied() { file_ids.push_unique(model_file_id); } SourcePreprocQueryContexts { model_file_ids: file_ids.into_vec(), status: relevant.status } From d979b7d3f569bb10d11091b9374658d5399c97fa Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 02:33:05 +0800 Subject: [PATCH 50/80] docs: add comments for code actions --- .../handlers/add_default_case_item.rs | 3 ++- .../handlers/add_implicit_named_port_parens.rs | 3 ++- .../handlers/add_missing_connections.rs | 3 ++- .../handlers/add_missing_parameters.rs | 3 ++- .../src/code_action/handlers/apply_de_morgan.rs | 3 ++- .../code_action/handlers/convert_always_block.rs | 3 ++- .../code_action/handlers/convert_literal_base.rs | 3 ++- .../handlers/convert_named_port_connections.rs | 3 ++- .../handlers/convert_ordered_connections.rs | 6 ++++-- .../handlers/convert_port_declarations.rs | 3 ++- .../handlers/expand_compound_assignment.rs | 3 ++- .../handlers/expand_postfix_inc_dec.rs | 3 ++- .../src/code_action/handlers/extract_variable.rs | 15 +++++++++------ .../src/code_action/handlers/invert_if_else.rs | 3 ++- .../src/code_action/handlers/merge_nested_if.rs | 3 ++- .../code_action/handlers/pull_assignment_up.rs | 3 ++- .../handlers/reformat_number_literal.rs | 3 ++- .../code_action/handlers/remove_parentheses.rs | 3 ++- .../handlers/sort_named_instantiation_items.rs | 6 ++++-- .../handlers/split_declaration_declarators.rs | 3 ++- crates/ide/src/code_action/tests.rs | 16 +++++----------- 21 files changed, 56 insertions(+), 38 deletions(-) diff --git a/crates/ide/src/code_action/handlers/add_default_case_item.rs b/crates/ide/src/code_action/handlers/add_default_case_item.rs index a3f2d77c..928063ef 100644 --- a/crates/ide/src/code_action/handlers/add_default_case_item.rs +++ b/crates/ide/src/code_action/handlers/add_default_case_item.rs @@ -14,7 +14,8 @@ const LABEL: &str = "Add default case item"; // Assist: add_default_case_item // -// This adds a `default` item to a case statement that does not already have one. +// This adds a `default` item to a case statement that does not already have +// one. // // ``` // module top; always_comb case$0 (sel) 1'b0: y = a; endcase endmodule diff --git a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs index 4490d3c6..660ccae2 100644 --- a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs +++ b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs @@ -16,7 +16,8 @@ const LABEL: &str = "Add explicit empty port connection"; // Assist: add_implicit_named_port_parens // -// This makes an implicit named port connection explicit by adding empty parentheses. +// This makes an implicit named port connection explicit by adding empty +// parentheses. // // ``` // child u(.ready$0); diff --git a/crates/ide/src/code_action/handlers/add_missing_connections.rs b/crates/ide/src/code_action/handlers/add_missing_connections.rs index 49bc8f6c..fec16050 100644 --- a/crates/ide/src/code_action/handlers/add_missing_connections.rs +++ b/crates/ide/src/code_action/handlers/add_missing_connections.rs @@ -27,7 +27,8 @@ const LABEL: &str = "Fill connections"; // Assist: add_missing_connections // -// This fills the missing port connections for an instance from the target module definition. +// This fills the missing port connections for an instance from the target +// module definition. // // ``` // child u($0.a(a)); diff --git a/crates/ide/src/code_action/handlers/add_missing_parameters.rs b/crates/ide/src/code_action/handlers/add_missing_parameters.rs index cf5f4c59..dc8c03f4 100644 --- a/crates/ide/src/code_action/handlers/add_missing_parameters.rs +++ b/crates/ide/src/code_action/handlers/add_missing_parameters.rs @@ -27,7 +27,8 @@ const LABEL: &str = "Fill parameters"; // Assist: add_missing_parameters // -// This fills the missing parameter assignments for an instantiation from the target module definition. +// This fills the missing parameter assignments for an instantiation from the +// target module definition. // // ``` // child #($0.A(1)) u(); diff --git a/crates/ide/src/code_action/handlers/apply_de_morgan.rs b/crates/ide/src/code_action/handlers/apply_de_morgan.rs index 7f45d2d3..5f5d8ff3 100644 --- a/crates/ide/src/code_action/handlers/apply_de_morgan.rs +++ b/crates/ide/src/code_action/handlers/apply_de_morgan.rs @@ -18,7 +18,8 @@ const FACTOR_LABEL: &str = "Factor De Morgan's law"; // Assist: apply_de_morgan // -// This applies or factors De Morgan's law for boolean expressions and if conditions. +// This applies or factors De Morgan's law for boolean expressions and if +// conditions. // // ``` // assign y = $0!(a && b); diff --git a/crates/ide/src/code_action/handlers/convert_always_block.rs b/crates/ide/src/code_action/handlers/convert_always_block.rs index db79c77f..36a713c4 100644 --- a/crates/ide/src/code_action/handlers/convert_always_block.rs +++ b/crates/ide/src/code_action/handlers/convert_always_block.rs @@ -38,7 +38,8 @@ const ALWAYS_FF_TO_ALWAYS_LABEL: &str = "Convert to always @(...)"; // Assist: convert_always_block // -// This converts compatible procedural blocks between `always`, `always_comb`, and `always_ff`. +// This converts compatible procedural blocks between `always`, `always_comb`, +// and `always_ff`. // // ``` // always$0 @(*) begin y = a; end diff --git a/crates/ide/src/code_action/handlers/convert_literal_base.rs b/crates/ide/src/code_action/handlers/convert_literal_base.rs index 70ff8c56..51b442bf 100644 --- a/crates/ide/src/code_action/handlers/convert_literal_base.rs +++ b/crates/ide/src/code_action/handlers/convert_literal_base.rs @@ -15,7 +15,8 @@ const ACTION_ID: CodeActionId = CodeActionId { // Assist: convert_literal_base // -// This converts an integer literal between binary, octal, decimal, and hexadecimal notation. +// This converts an integer literal between binary, octal, decimal, and +// hexadecimal notation. // // ``` // localparam int value = 8'h0f$0; diff --git a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs index 83ea401f..18435ae3 100644 --- a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs @@ -22,7 +22,8 @@ const COLLAPSE_LABEL: &str = "Collapse named port to shorthand"; // Assist: convert_named_port_connection_shorthand // -// This expands named port connection shorthand, or collapses same-name connections to shorthand. +// This expands named port connection shorthand, or collapses same-name +// connections to shorthand. // // ``` // child u(.ready$0); diff --git a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs index 75e57ffd..c8bc2090 100644 --- a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs @@ -31,7 +31,8 @@ const PARAMS_LABEL: &str = "Convert ordered parameter assignments to named assig // Assist: convert_ordered_ports // -// This converts ordered port connections to named port connections using the target module's port order. +// This converts ordered port connections to named port connections using the +// target module's port order. // // ``` // child u($0a, b); @@ -82,7 +83,8 @@ pub(super) fn convert_ordered_ports( // Assist: convert_ordered_params // -// This converts ordered parameter assignments to named parameter assignments using the target module's parameter order. +// This converts ordered parameter assignments to named parameter assignments +// using the target module's parameter order. // // ``` // child #($01, 2) u(); 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 992db61c..a159330d 100644 --- a/crates/ide/src/code_action/handlers/convert_port_declarations.rs +++ b/crates/ide/src/code_action/handlers/convert_port_declarations.rs @@ -47,7 +47,8 @@ const NON_ANSI_TO_ANSI_LABEL: &str = "Convert non-ANSI port declarations to ANSI // Assist: convert_port_declarations // -// This converts module ports between ANSI declarations and non-ANSI declarations. +// This converts module ports between ANSI declarations and non-ANSI +// declarations. // // ``` // module top($0input a, output logic b); endmodule diff --git a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs index 4d77e1e6..61e2feae 100644 --- a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs +++ b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs @@ -23,7 +23,8 @@ const COLLAPSE_LABEL: &str = "Collapse compound assignment"; // Assist: expand_compound_assignment // -// This expands compound assignments, or collapses simple self-assignments into compound assignments. +// This expands compound assignments, or collapses simple self-assignments into +// compound assignments. // // ``` // always_comb a $0+= b; diff --git a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs index 5ca61dc1..307ee17a 100644 --- a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs +++ b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs @@ -80,7 +80,8 @@ const ASSIGNMENT_TO_PREFIX_LABEL: &str = "Convert assignment to prefix expressio // Assist: expand_postfix_inc_dec // -// This converts between postfix, prefix, compound assignment, and expanded assignment forms of increment/decrement expressions. +// This converts between postfix, prefix, compound assignment, and expanded +// assignment forms of increment/decrement expressions. // // ``` // always_ff @(posedge clk) count$0++; diff --git a/crates/ide/src/code_action/handlers/extract_variable.rs b/crates/ide/src/code_action/handlers/extract_variable.rs index ff876339..1b260627 100644 --- a/crates/ide/src/code_action/handlers/extract_variable.rs +++ b/crates/ide/src/code_action/handlers/extract_variable.rs @@ -25,7 +25,8 @@ const ID: CodeActionId = // Assist: extract_variable // -// This extracts a selected expression into a new local variable or continuous net declaration. +// This extracts a selected expression into a new local variable or continuous +// net declaration. // // ``` // always_comb begin y = $0a + b$0; end @@ -91,7 +92,8 @@ fn extract_target(text: &str, expr: ast::Expression<'_>) -> Option) -> Option) -> Option<()> { assignment_expression_containing_rhs(expr) - .filter(|binary| binary.operator_token().is_some_and(|token| token.kind() == TokenKind::EQUALS)) + .filter(|binary| { + binary.operator_token().is_some_and(|token| token.kind() == TokenKind::EQUALS) + }) .map(|_| ()) } @@ -173,9 +177,8 @@ fn trim_range(text: &str, range: TextRange) -> Option { fn extracted_variable_type(ctx: &CodeActionCtx<'_>, expr: ast::Expression<'_>) -> Option { let ty = type_of_expr(ctx.sema().db, ctx.sema().resolve_expr(ctx.file_id().into(), expr)?).ty; - render_ty(ctx, &ty).or_else(|| { - expected_type_for_assignment_rhs(ctx, expr).and_then(|ty| render_ty(ctx, &ty)) - }) + 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( diff --git a/crates/ide/src/code_action/handlers/invert_if_else.rs b/crates/ide/src/code_action/handlers/invert_if_else.rs index c7d94748..de3876ea 100644 --- a/crates/ide/src/code_action/handlers/invert_if_else.rs +++ b/crates/ide/src/code_action/handlers/invert_if_else.rs @@ -14,7 +14,8 @@ const LABEL: &str = "Invert if/else"; // Assist: invert_if_else // -// This swaps the then and else branches of an if statement and negates the condition. +// This swaps the then and else branches of an if statement and negates the +// condition. // // ``` // always_comb if$0 (ready) y = a; else y = b; diff --git a/crates/ide/src/code_action/handlers/merge_nested_if.rs b/crates/ide/src/code_action/handlers/merge_nested_if.rs index be7e69f7..51fe3d87 100644 --- a/crates/ide/src/code_action/handlers/merge_nested_if.rs +++ b/crates/ide/src/code_action/handlers/merge_nested_if.rs @@ -14,7 +14,8 @@ const ID: CodeActionId = // Assist: merge_nested_if // -// This merges nested if statements without else branches into one if statement with a combined condition. +// This merges nested if statements without else branches into one if statement +// with a combined condition. // // ``` // always_comb if$0 (a) begin if (b) y = 1; end diff --git a/crates/ide/src/code_action/handlers/pull_assignment_up.rs b/crates/ide/src/code_action/handlers/pull_assignment_up.rs index 8d932f92..feb203a5 100644 --- a/crates/ide/src/code_action/handlers/pull_assignment_up.rs +++ b/crates/ide/src/code_action/handlers/pull_assignment_up.rs @@ -22,7 +22,8 @@ const DOWN_ID: CodeActionId = CodeActionId { // Assist: pull_assignment_up // -// This pulls matching assignments out of an if/else chain into a single ternary assignment. +// This pulls matching assignments out of an if/else chain into a single ternary +// assignment. // // ``` // always_comb if$0 (a) y = 1; else y = 0; diff --git a/crates/ide/src/code_action/handlers/reformat_number_literal.rs b/crates/ide/src/code_action/handlers/reformat_number_literal.rs index deeffaac..e63eca34 100644 --- a/crates/ide/src/code_action/handlers/reformat_number_literal.rs +++ b/crates/ide/src/code_action/handlers/reformat_number_literal.rs @@ -18,7 +18,8 @@ const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; // Assist: reformat_number_literal // -// This adds digit separators to long integer literals or removes existing digit separators. +// This adds digit separators to long integer literals or removes existing digit +// separators. // // ``` // localparam int value = 10000$0; diff --git a/crates/ide/src/code_action/handlers/remove_parentheses.rs b/crates/ide/src/code_action/handlers/remove_parentheses.rs index 2ad7bd33..e6e431da 100644 --- a/crates/ide/src/code_action/handlers/remove_parentheses.rs +++ b/crates/ide/src/code_action/handlers/remove_parentheses.rs @@ -17,7 +17,8 @@ const ID: CodeActionId = CodeActionId { // Assist: remove_parentheses // -// This removes parentheses when they are redundant for the surrounding expression. +// This removes parentheses when they are redundant for the surrounding +// expression. // // ``` // assign y = $0(a + b) + c; diff --git a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs index 3a478a09..16dd807d 100644 --- a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs +++ b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs @@ -27,7 +27,8 @@ const SORT_NAMED_PARAMETER_ASSIGNMENTS_LABEL: &str = "Sort named parameter assig // Assist: sort_named_parameter_assignments // -// This sorts named parameter assignments to match the target module's parameter declaration order. +// This sorts named parameter assignments to match the target module's parameter +// declaration order. // // ``` // child #(.B(2), $0.A(1)) u(); @@ -82,7 +83,8 @@ const SORT_NAMED_PORT_CONNECTIONS_LABEL: &str = "Sort named port connections"; // Assist: sort_named_port_connections // -// This sorts named port connections to match the target module's port declaration order. +// This sorts named port connections to match the target module's port +// declaration order. // // ``` // child u(.b(b), $0.a(a)); diff --git a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs index da9007a2..78fb4a10 100644 --- a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs +++ b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs @@ -21,7 +21,8 @@ const LABEL: &str = "Split declaration"; // Assist: split_declaration_declarators // -// This splits a declaration with multiple declarators into one declaration per declarator. +// This splits a declaration with multiple declarators into one declaration per +// declarator. // // ``` // logic $0a, b; diff --git a/crates/ide/src/code_action/tests.rs b/crates/ide/src/code_action/tests.rs index f5cd12fe..1bd3f82b 100644 --- a/crates/ide/src/code_action/tests.rs +++ b/crates/ide/src/code_action/tests.rs @@ -806,10 +806,7 @@ fn convert_non_ansi_ports_to_ansi() { fn convert_non_ansi_ports_to_ansi_merges_data_declaration() { let text = "module top (\n /*caret*/c,\n led0\n);\n input wire c;\n output led0;\n reg led0;\n\nendmodule\n"; let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); - assert_eq!( - fixed, - "module top (\n input wire c,\n output reg led0\n);\nendmodule\n" - ); + assert_eq!(fixed, "module top (\n input wire c,\n output reg led0\n);\nendmodule\n"); } #[test] @@ -1029,7 +1026,8 @@ fn extract_variable_inserts_local_before_statement() { #[test] fn extract_variable_allows_selection_padding() { - let text = "module top; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule\n"; + let text = + "module top; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule\n"; let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); assert_eq!( fixed, @@ -1039,8 +1037,7 @@ fn extract_variable_allows_selection_padding() { #[test] fn extract_variable_uses_assignment_lhs_type() { - let text = - "module top; logic [7:0] y, a, b; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; + let text = "module top; logic [7:0] y, a, b; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); assert_eq!( fixed, @@ -1052,10 +1049,7 @@ fn extract_variable_uses_assignment_lhs_type() { fn extract_variable_from_continuous_assign() { let text = "module top; assign y = /*selection*/a + b/*selection*/; endmodule\n"; let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); - assert_eq!( - fixed, - "module top; wire logic value = a + b;\nassign y = value; endmodule\n" - ); + assert_eq!(fixed, "module top; wire logic value = a + b;\nassign y = value; endmodule\n"); } #[test] From b3973011f4dff97c15c2745e1b5a87fdf41294eb Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:33:16 +0800 Subject: [PATCH 51/80] perf(hir): index preproc reference ranges --- crates/hir/src/base_db/source_db/preproc.rs | 159 +++++++++++++++++++- crates/hir/src/preproc/helpers/expansion.rs | 27 +--- crates/hir/src/preproc/helpers/source.rs | 9 -- crates/hir/src/preproc/reference_queries.rs | 36 ++--- 4 files changed, 171 insertions(+), 60 deletions(-) diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs index dcfa33aa..49155b32 100644 --- a/crates/hir/src/base_db/source_db/preproc.rs +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -1,7 +1,7 @@ use ::preproc::source::{ - PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroExpansionId, - SourcePosition, SourcePreprocError, SourcePreprocModel, SourcePreprocUnavailable, SourceRange, - SourceTokenProvenance, + PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroCallId, + SourceMacroExpansionId, SourceMacroReferenceId, SourcePosition, SourcePreprocError, + SourcePreprocModel, SourcePreprocUnavailable, SourceRange, SourceTokenProvenance, }; use rustc_hash::{FxHashMap, FxHashSet}; use smol_str::SmolStr; @@ -36,6 +36,156 @@ pub use self::source_mapping::{ pub struct MappedSourcePreprocModel { pub model: SourcePreprocModel, pub source_map: PreprocSourceMap, + range_index: PreprocRangeIndex, +} + +impl MappedSourcePreprocModel { + pub(crate) fn macro_reference_ids_at( + &self, + file_id: FileId, + offset: TextSize, + ) -> Vec { + self.range_index.reference_ids_at(file_id, offset) + } + + pub(crate) fn macro_reference_ids_intersecting_range( + &self, + file_id: FileId, + range: TextRange, + ) -> Vec { + self.range_index.reference_ids_intersecting_range(file_id, range) + } + + pub(crate) fn macro_call_ids_at( + &self, + file_id: FileId, + offset: TextSize, + ) -> Vec { + self.range_index.call_ids_at(file_id, offset) + } + + pub(crate) fn macro_call_ids_intersecting_range( + &self, + file_id: FileId, + range: TextRange, + ) -> Vec { + self.range_index.call_ids_intersecting_range(file_id, range) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct PreprocRangeIndex { + references_by_file: FxHashMap>>, + calls_by_file: FxHashMap>>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct IndexedRange { + range: TextRange, + id: T, +} + +impl PreprocRangeIndex { + fn from_model(model: &SourcePreprocModel, source_map: &PreprocSourceMap) -> Self { + let mut index = Self::default(); + for reference in model.macro_references().iter() { + if let Some((file_id, range)) = mapped_file_range(source_map, reference.name_range) { + index + .references_by_file + .entry(file_id) + .or_default() + .push(IndexedRange { range, id: reference.id }); + } + } + for call in model.macro_calls().iter() { + if let Some((file_id, range)) = mapped_file_range(source_map, call.call_range) { + index + .calls_by_file + .entry(file_id) + .or_default() + .push(IndexedRange { range, id: call.id }); + } + } + for references in index.references_by_file.values_mut() { + sort_indexed_ranges(references); + } + for calls in index.calls_by_file.values_mut() { + sort_indexed_ranges(calls); + } + index + } + + fn reference_ids_at(&self, file_id: FileId, offset: TextSize) -> Vec { + ids_at(self.references_by_file.get(&file_id), offset) + } + + fn reference_ids_intersecting_range( + &self, + file_id: FileId, + range: TextRange, + ) -> Vec { + ids_intersecting_range(self.references_by_file.get(&file_id), range) + } + + fn call_ids_at(&self, file_id: FileId, offset: TextSize) -> Vec { + ids_at(self.calls_by_file.get(&file_id), offset) + } + + fn call_ids_intersecting_range( + &self, + file_id: FileId, + range: TextRange, + ) -> Vec { + ids_intersecting_range(self.calls_by_file.get(&file_id), range) + } +} + +fn mapped_file_range( + source_map: &PreprocSourceMap, + source_range: SourceRange, +) -> Option<(FileId, TextRange)> { + let range = source_map.map_range(source_range).ok()?; + let file_id = source_map.file_id(source_range.source).ok()?; + Some((file_id, range)) +} + +fn sort_indexed_ranges(ranges: &mut [IndexedRange]) { + ranges.sort_by_key(|entry| (entry.range.start(), entry.range.end())); +} + +fn ids_at(ranges: Option<&Vec>>, offset: TextSize) -> Vec { + let Some(ranges) = ranges else { + return Vec::new(); + }; + let mut ids = Vec::new(); + for entry in ranges { + if entry.range.start() > offset { + break; + } + if entry.range.contains(offset) { + ids.push(entry.id); + } + } + ids +} + +fn ids_intersecting_range( + ranges: Option<&Vec>>, + range: TextRange, +) -> Vec { + let Some(ranges) = ranges else { + return Vec::new(); + }; + let mut ids = Vec::new(); + for entry in ranges { + if entry.range.start() >= range.end() { + break; + } + if entry.range.intersect(range).is_some_and(|intersection| !intersection.is_empty()) { + ids.push(entry.id); + } + } + ids } #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -643,8 +793,9 @@ pub(super) fn source_preproc_model( Err(err) => return Arc::new(Err(SourcePreprocQueryError::Model(err))), }; record_expansion_display_texts(profile_id, &model, &mut source_map); + let range_index = PreprocRangeIndex::from_model(&model, &source_map); - Arc::new(Ok(MappedSourcePreprocModel { model, source_map })) + Arc::new(Ok(MappedSourcePreprocModel { model, source_map, range_index })) } pub(super) fn source_preproc_contexts_for_file( diff --git a/crates/hir/src/preproc/helpers/expansion.rs b/crates/hir/src/preproc/helpers/expansion.rs index 7dde8b71..37ba6f54 100644 --- a/crates/hir/src/preproc/helpers/expansion.rs +++ b/crates/hir/src/preproc/helpers/expansion.rs @@ -94,15 +94,9 @@ pub(in crate::preproc) fn source_macro_calls_at( offset: TextSize, ) -> Vec<&SourceMacroCallFact> { mapped - .model - .macro_calls() - .iter() - .filter(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) && range.contains(offset) - }) + .macro_call_ids_at(file_id, offset) + .into_iter() + .filter_map(|call| mapped.model.macro_calls().get(call)) .collect() } @@ -112,18 +106,9 @@ pub(in crate::preproc) fn source_macro_calls_intersecting_range( source_range: TextRange, ) -> Vec<&SourceMacroCallFact> { mapped - .model - .macro_calls() - .iter() - .filter(|call| { - let Ok((source, range)) = map_mapped_source_range(mapped, call.call_range) else { - return false; - }; - source.file_id() == Some(file_id) - && range - .intersect(source_range) - .is_some_and(|intersection| !intersection.is_empty()) - }) + .macro_call_ids_intersecting_range(file_id, source_range) + .into_iter() + .filter_map(|call| mapped.model.macro_calls().get(call)) .collect() } diff --git a/crates/hir/src/preproc/helpers/source.rs b/crates/hir/src/preproc/helpers/source.rs index 8693144f..3e6d5895 100644 --- a/crates/hir/src/preproc/helpers/source.rs +++ b/crates/hir/src/preproc/helpers/source.rs @@ -48,15 +48,6 @@ pub(in crate::preproc) fn mapped_source_range_at_offset( Ok((source.file_id() == Some(file_id) && range.contains(offset)).then_some((source, range))) } -pub(in crate::preproc) fn mapped_source_range_contains_provenance_offset( - mapped: &MappedSourcePreprocModel, - source_range: SourceRange, - file_id: FileId, - offset: TextSize, -) -> PreprocResult { - Ok(mapped_source_range_at_offset(mapped, source_range, file_id, offset)?.is_some()) -} - pub(in crate::preproc) fn map_mapped_source_id( mapped: &MappedSourcePreprocModel, source: PreprocSourceId, diff --git a/crates/hir/src/preproc/reference_queries.rs b/crates/hir/src/preproc/reference_queries.rs index 0408b9e9..dfecf0fd 100644 --- a/crates/hir/src/preproc/reference_queries.rs +++ b/crates/hir/src/preproc/reference_queries.rs @@ -30,23 +30,13 @@ pub fn macro_usage_resolutions_at( } }; - for reference in mapped.model.macro_references().iter() { + for reference_id in mapped.macro_reference_ids_at(file_id, offset) { + let Some(reference) = mapped.model.macro_references().get(reference_id) else { + continue; + }; let SourceMacroReferenceSite::Usage { usage_index } = reference.site else { continue; }; - match mapped_source_range_contains_provenance_offset( - mapped, - reference.name_range, - file_id, - offset, - ) { - Ok(true) => {} - Ok(false) => continue, - Err(error) => { - record_first_error(&mut first_error, error); - continue; - } - } let SourceMacroResolutionFact::Resolved { definition, include_chain, .. } = &reference.resolution @@ -135,19 +125,10 @@ pub fn macro_references_in_range( } }; - for reference in mapped.model.macro_references().iter() { - let Ok((source, reference_range)) = - map_mapped_source_range(mapped, reference.name_range) - else { + for reference_id in mapped.macro_reference_ids_intersecting_range(file_id, range) { + let Some(reference) = mapped.model.macro_references().get(reference_id) else { continue; }; - if source.file_id() != Some(file_id) - || reference_range - .intersect(range) - .is_none_or(|intersection| intersection.is_empty()) - { - continue; - } match map_macro_reference(mapped, reference) { Ok(reference) => { @@ -210,7 +191,10 @@ pub fn macro_reference_definitions_at( } }; - for reference in mapped.model.macro_references().iter() { + for reference_id in mapped.macro_reference_ids_at(file_id, offset) { + let Some(reference) = mapped.model.macro_references().get(reference_id) else { + continue; + }; let (_, range) = match mapped_source_range_at_offset( mapped, reference.name_range, From af46fd6418c6d1c882604a3c72dd0e877b0fb3d9 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:35:12 +0800 Subject: [PATCH 52/80] perf(preproc): memoize recursive expansion tokens --- crates/preproc/src/source/provenance/builder.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index 800dd64c..12c2799d 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -699,12 +699,14 @@ impl<'a> SourcePreprocModelBuilder<'a> { let child_calls_by_parent = self.child_calls_by_parent(); let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); let mut expansion_tokens_by_call = BTreeMap::new(); + let mut recursive_tokens_by_call = BTreeMap::new(); for call in &call_ids { let mut visiting = Vec::new(); let tokens = self.recursive_emitted_tokens_for_call( *call, &direct_tokens_by_call, &child_calls_by_parent, + &mut recursive_tokens_by_call, &mut visiting, ); expansion_tokens_by_call.insert(*call, tokens); @@ -863,8 +865,12 @@ impl<'a> SourcePreprocModelBuilder<'a> { call: SourceMacroCallId, direct_tokens_by_call: &BTreeMap>, child_calls_by_parent: &BTreeMap>, + recursive_tokens_by_call: &mut BTreeMap>, visiting: &mut Vec, ) -> Vec { + if let Some(tokens) = recursive_tokens_by_call.get(&call) { + return tokens.clone(); + } if visiting.contains(&call) { self.expansions_partial = true; return Vec::new(); @@ -878,6 +884,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { *child, direct_tokens_by_call, child_calls_by_parent, + recursive_tokens_by_call, visiting, )); } @@ -885,6 +892,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { visiting.pop(); tokens.sort_by_key(|token| token.raw()); tokens.dedup(); + recursive_tokens_by_call.insert(call, tokens.clone()); tokens } From ebe00eadd2f2682e2347ca051409c2c990ba7592 Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 02:36:49 +0800 Subject: [PATCH 53/80] fix: clippy --- crates/ide/src/code_action/handlers/merge_nested_if.rs | 8 ++------ .../src/code_action/handlers/reformat_number_literal.rs | 8 ++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/ide/src/code_action/handlers/merge_nested_if.rs b/crates/ide/src/code_action/handlers/merge_nested_if.rs index 51fe3d87..061b8787 100644 --- a/crates/ide/src/code_action/handlers/merge_nested_if.rs +++ b/crates/ide/src/code_action/handlers/merge_nested_if.rs @@ -78,8 +78,7 @@ fn in_if_head(if_stmt: ast::ConditionalStatement<'_>, range: TextRange) -> bool fn outermost_mergeable_if<'a>( mut if_stmt: ast::ConditionalStatement<'a>, ) -> ast::ConditionalStatement<'a> { - loop { - let Some(parent_if) = parent_conditional_statement(if_stmt) else { break }; + while let Some(parent_if) = parent_conditional_statement(if_stmt) { if parent_if.else_clause().is_some() { break; } @@ -112,10 +111,7 @@ fn nested_if_chain<'a>( ) -> Vec> { let mut chain = vec![outer_if]; let mut current_if = outer_if; - loop { - let Some(body) = single_statement_body(current_if.statement()) else { - break; - }; + while let Some(body) = single_statement_body(current_if.statement()) { let Some(nested_if) = body.as_conditional_statement() else { break; }; diff --git a/crates/ide/src/code_action/handlers/reformat_number_literal.rs b/crates/ide/src/code_action/handlers/reformat_number_literal.rs index e63eca34..167477db 100644 --- a/crates/ide/src/code_action/handlers/reformat_number_literal.rs +++ b/crates/ide/src/code_action/handlers/reformat_number_literal.rs @@ -46,7 +46,7 @@ pub(super) fn reformat_number_literal( return None; } - let replacement = format!("{}{}", prefix, add_group_separators(&digits, group_size)); + let replacement = format!("{}{}", prefix, add_group_separators(digits, group_size)); let label = format!("Convert {raw} to {replacement}"); collector.add(ID, label, range, |builder| { builder.replace(range, replacement); @@ -72,10 +72,10 @@ fn selected_integer_literal<'a>( Some((raw, "", raw, 3, range)) } -fn parse_based_literal<'a>( - raw: &'a str, +fn parse_based_literal( + raw: &str, range: TextRange, -) -> Option<(&'a str, &'a str, &'a str, usize, TextRange)> { +) -> Option<(&str, &str, &str, usize, TextRange)> { let apostrophe = raw.find('\'')?; let after_quote = raw.get(apostrophe + 1..)?; let (sign_len, rest) = match after_quote.as_bytes().first().copied() { From 9dea282a45d68c5642d36ddc3fdf2fe870432398 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:37:42 +0800 Subject: [PATCH 54/80] perf(ide): reuse macro hover reference lookup --- crates/ide/src/hover.rs | 69 +++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index b646b182..0673edd5 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -8,8 +8,8 @@ use hir::{ hir_def::expr::Expr, preproc::{ EmittedTokenProvenance, IncludeTarget, MacroDefinition, MacroParamDefinition, - RecursiveMacroExpansionProvenance, include_directives_at, macro_definition_at, - macro_param_definition_at, macro_param_reference_definitions_at, + MacroReferenceDefinitions, RecursiveMacroExpansionProvenance, include_directives_at, + macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, macro_reference_definitions_at, recursive_macro_expansion_provenances_at, }, semantics::Semantics, @@ -41,6 +41,11 @@ struct MacroSourceLink { target: String, } +struct PreprocMacroHover { + hover: RangeInfo, + reference_definitions: Option, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HoverFormat { Markdown, @@ -58,7 +63,10 @@ pub(crate) fn hover( _config: HoverConfig, ) -> Option> { if let Some(macro_hover) = handle_preproc_macro(db, file_id, offset) { - return Some(expanded_macro_hover(db, file_id, offset).unwrap_or(macro_hover)); + return Some( + expanded_macro_hover(db, file_id, offset, macro_hover.reference_definitions.as_ref()) + .unwrap_or(macro_hover.hover), + ); } if let Some(include) = handle_preproc_include(db, file_id, offset) { @@ -162,7 +170,7 @@ fn with_expanded_macro_hover( offset: TextSize, mut hover: RangeInfo, ) -> RangeInfo { - let Some(expanded) = expanded_macro_hover(db, file_id, offset) else { + let Some(expanded) = expanded_macro_hover(db, file_id, offset, None) else { return hover; }; if let Some(range) = covering_range(&[hover.range, expanded.range]) { @@ -177,14 +185,19 @@ fn expanded_macro_hover( db: &RootDb, file_id: FileId, offset: TextSize, + reference_definitions: Option<&MacroReferenceDefinitions>, ) -> Option> { - let reference_ids = macro_reference_definitions_at(db, file_id, offset) - .ok() - .flatten()? - .references - .into_iter() - .map(|reference| reference.id) - .collect::>(); + let reference_ids = if let Some(reference_definitions) = reference_definitions { + reference_definitions.references.iter().map(|reference| reference.id).collect::>() + } else { + macro_reference_definitions_at(db, file_id, offset) + .ok() + .flatten()? + .references + .into_iter() + .map(|reference| reference.id) + .collect::>() + }; if reference_ids.is_empty() { return None; } @@ -379,36 +392,46 @@ fn handle_preproc_macro( db: &RootDb, file_id: FileId, offset: TextSize, -) -> Option> { +) -> Option { if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { - return Some(RangeInfo::new(definition.range, macro_param_definition_markup(&definition))); + return Some(PreprocMacroHover { + hover: RangeInfo::new(definition.range, macro_param_definition_markup(&definition)), + reference_definitions: None, + }); } if let Ok(Some(param_resolution)) = macro_param_reference_definitions_at(db, file_id, offset) { if param_resolution.definitions.is_empty() { return None; } - return Some(RangeInfo::new( - param_resolution.range, - macro_param_definitions_markup(¶m_resolution.definitions), - )); + return Some(PreprocMacroHover { + hover: RangeInfo::new( + param_resolution.range, + macro_param_definitions_markup(¶m_resolution.definitions), + ), + reference_definitions: None, + }); } if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { - return Some(RangeInfo::new( - definition.name_range, - macro_definition_markup(db, file_id, &definition), - )); + return Some(PreprocMacroHover { + hover: RangeInfo::new( + definition.name_range, + macro_definition_markup(db, file_id, &definition), + ), + reference_definitions: None, + }); } if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { if resolution.definitions.is_empty() { return None; } - return Some(RangeInfo::new( + let hover = RangeInfo::new( resolution.range, macro_definitions_markup(db, file_id, &resolution.definitions), - )); + ); + return Some(PreprocMacroHover { hover, reference_definitions: Some(resolution) }); } None From 5e64fccb3a3d8ff635bf17d724464e3a11bbafdf Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:53:57 +0800 Subject: [PATCH 55/80] fix(preproc): bound macro state to include scope --- crates/preproc/src/source/model/tests.rs | 46 +++++++--- crates/preproc/src/source/provenance.rs | 18 ++-- .../preproc/src/source/provenance/builder.rs | 88 +++++++++++++++++++ 3 files changed, 133 insertions(+), 19 deletions(-) diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index a6b0fef4..b2678111 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -40,24 +40,11 @@ fn source_model( }; let trace = preprocessor_trace(root_text, "source", ROOT_PATH, &options); let root_source = PreprocSourceId::from(trace.root_buffer_id); - let header_source = first_non_root_source(&trace, root_source); let model = SourcePreprocModel::from_trace(trace).unwrap(); + let header_source = source_by_path_suffix(&model, "defs.vh"); (model, root_source, header_source) } -fn first_non_root_source( - trace: &PreprocessorTrace, - root_source: PreprocSourceId, -) -> PreprocSourceId { - trace - .events - .iter() - .filter_map(|directive| directive.range.as_ref()) - .map(|range| PreprocSourceId::from(range.buffer_id)) - .find(|source| *source != root_source) - .expect("included source directive should be traced") -} - fn source_by_path_suffix(model: &SourcePreprocModel, suffix: &str) -> PreprocSourceId { model .sources() @@ -295,6 +282,37 @@ fn visible_macro_query_reads_timeline_without_event_records() { ); } +#[test] +fn included_plain_source_uses_include_scope_macro_state() { + let root_text = r#"`define BEFORE 1 +`include "defs.vh" +`define AFTER 1 +"#; + let header_text = "wire x;\n"; + let (model, _, header_source) = source_model(root_text, header_text); + + let names = visible_macro_names(&model, header_source, offset_after(header_text, "wire x")); + + assert!(names.iter().any(|name| name == "BEFORE"), "{names:?}"); + assert!(!names.iter().any(|name| name == "AFTER"), "{names:?}"); +} + +#[test] +fn included_source_after_last_directive_uses_include_scope_macro_state() { + let root_text = r#"`define BEFORE 1 +`include "defs.vh" +`define AFTER 1 +"#; + let header_text = "`define FROM_HEADER 1\nwire x;\n"; + let (model, _, header_source) = source_model(root_text, header_text); + + let names = visible_macro_names(&model, header_source, offset_after(header_text, "wire x")); + + assert!(names.iter().any(|name| name == "BEFORE"), "{names:?}"); + assert!(names.iter().any(|name| name == "FROM_HEADER"), "{names:?}"); + assert!(!names.iter().any(|name| name == "AFTER"), "{names:?}"); +} + #[test] fn source_model_preserves_inactive_range_sources() { let root_text = r#"`include "defs.vh" diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 61efd597..773663e0 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -176,6 +176,7 @@ pub enum SourceIncludeStatus { pub struct SourceMacroStateTimeline { states: Vec, checkpoints: Vec, + source_order_scopes: BTreeMap, source_order_boundaries: BTreeMap>, final_source_order: usize, } @@ -199,6 +200,11 @@ struct SourceMacroStatePositionBoundary { boundary: SourcePosition, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceMacroStateSourceScope { + end_order: usize, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceMacroCall { pub id: SourceMacroCallId, @@ -468,15 +474,17 @@ impl SourceMacroStateTimeline { } fn source_order_at_position(&self, position: SourcePosition) -> usize { + let source_end_order = self + .source_order_scopes + .get(&position.source) + .map(|scope| scope.end_order) + .unwrap_or(self.final_source_order); let Some(boundaries) = self.source_order_boundaries.get(&position.source) else { - return self.final_source_order; + return source_end_order; }; let index = boundaries.partition_point(|boundary| boundary.boundary.offset <= position.offset); - boundaries - .get(index) - .map(|boundary| boundary.source_order) - .unwrap_or(self.final_source_order) + boundaries.get(index).map(|boundary| boundary.source_order).unwrap_or(source_end_order) } fn state_at_source_order(&self, source_order: usize) -> Option<&SourceMacroState> { diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index 12c2799d..5a60a06d 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -112,6 +112,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { fn record_position_boundaries(&mut self) { self.tables.state_timeline.final_source_order = self.index.event_records.len(); + self.record_source_order_scopes(); for (source_order, directive) in self.index.event_records.iter().enumerate() { self.tables .state_timeline @@ -129,6 +130,77 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } + fn record_source_order_scopes(&mut self) { + let event_orders_by_id = self + .index + .event_records + .iter() + .enumerate() + .map(|(source_order, directive)| (directive.event_id, source_order)) + .collect::>(); + let source_parents = self.source_parents_by_include(); + + for source in &self.index.sources { + let end_order = match source.origin { + PreprocSourceOrigin::Root + | PreprocSourceOrigin::Predefine + | PreprocSourceOrigin::Detached => self.index.event_records.len(), + PreprocSourceOrigin::Included { include_event_id } => { + let Some(include_order) = event_orders_by_id.get(&include_event_id).copied() + else { + continue; + }; + self.included_source_end_order(source.id, include_order, &source_parents) + } + }; + self.tables + .state_timeline + .source_order_scopes + .insert(source.id, SourceMacroStateSourceScope { end_order }); + } + } + + fn source_parents_by_include(&self) -> BTreeMap { + let include_sources_by_event = self + .index + .event_records + .iter() + .map(|directive| (directive.event_id, directive.range.source)) + .collect::>(); + + self.index + .sources + .iter() + .filter_map(|source| match source.origin { + PreprocSourceOrigin::Included { include_event_id } => include_sources_by_event + .get(&include_event_id) + .copied() + .map(|parent| (source.id, parent)), + PreprocSourceOrigin::Root + | PreprocSourceOrigin::Predefine + | PreprocSourceOrigin::Detached => None, + }) + .collect() + } + + fn included_source_end_order( + &self, + source: PreprocSourceId, + include_order: usize, + source_parents: &BTreeMap, + ) -> usize { + self.index + .event_records + .iter() + .enumerate() + .skip(include_order + 1) + .find_map(|(source_order, directive)| { + (!source_is_descendant_or_same(directive.range.source, source, source_parents)) + .then_some(source_order) + }) + .unwrap_or_else(|| self.index.event_records.len()) + } + fn build_include_graph(&mut self) { self.tables.inactive_ranges = self.index.inactive_ranges.clone(); let mut resolved_sources_by_event = BTreeMap::new(); @@ -1114,6 +1186,22 @@ fn boundary_after(directive_range: SourceRange) -> SourcePosition { SourcePosition { source: directive_range.source, offset: directive_range.range.end() } } +fn source_is_descendant_or_same( + mut source: PreprocSourceId, + ancestor: PreprocSourceId, + source_parents: &BTreeMap, +) -> bool { + loop { + if source == ancestor { + return true; + } + let Some(parent) = source_parents.get(&source).copied() else { + return false; + }; + source = parent; + } +} + fn partial_status(is_partial: bool) -> CapabilityStatus { if is_partial { CapabilityStatus::Partial } else { CapabilityStatus::Complete } } From f23e7d886b9c305caa09be5afb7e52ea6d731024 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:56:13 +0800 Subject: [PATCH 56/80] fix(hir): prefer including contexts for header queries --- crates/hir/src/preproc/helpers/context.rs | 10 +++--- crates/hir/src/preproc/tests.rs | 39 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/crates/hir/src/preproc/helpers/context.rs b/crates/hir/src/preproc/helpers/context.rs index bce852e2..e779e44f 100644 --- a/crates/hir/src/preproc/helpers/context.rs +++ b/crates/hir/src/preproc/helpers/context.rs @@ -29,10 +29,12 @@ pub(in crate::preproc) fn source_preproc_single_query_contexts( ) -> SourcePreprocQueryContexts { let relevant = db.source_preproc_contexts_for_file(file_id); let mut file_ids = UniqVec::::default(); - if matches!( - db.file_kind(file_id), - SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader - ) { + let include_self = match db.file_kind(file_id) { + SourceFileKind::SystemVerilog => true, + SourceFileKind::IncludeHeader => relevant.model_file_ids.is_empty(), + _ => false, + }; + if include_self { file_ids.push_unique(file_id); } for model_file_id in relevant.model_file_ids.iter().copied() { diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 490fac51..0f31974c 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -697,13 +697,50 @@ endmodule let contexts = source_preproc_single_query_contexts(&db, HEADER); assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); - assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); + assert!(!contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); assert!( !contexts.model_file_ids.contains(&LEAF), "single-offset query contexts should not include unrelated profile model: {contexts:?}" ); } +#[test] +fn preproc_header_query_uses_including_context_over_standalone_model() { + let root_text = r#"`define FEATURE 1 +`include "defs.vh" +"#; + let header_text = r#"`ifdef FEATURE +`define WIDTH 8 +`endif +localparam int W = `WIDTH; +"#; + let db = db_with_files(root_text, header_text); + + let reference = macro_reference_at(&db, HEADER, offset(header_text, "WIDTH;")) + .unwrap() + .expect("included context should resolve the header reference without ambiguity"); + assert_eq!(text_at_range(header_text, reference.range), "`WIDTH"); + assert!(matches!(reference.resolution, MacroResolution::Resolved { .. })); + + let resolution = macro_reference_resolution_at(&db, HEADER, offset(header_text, "WIDTH;")) + .unwrap() + .expect("header macro reference should resolve through the including root"); + assert_eq!(resolution.definition.file_id, HEADER); + assert_eq!(text_at_range(header_text, resolution.definition.name_range), "WIDTH"); +} + +#[test] +fn preproc_header_without_including_context_uses_standalone_model() { + let root_text = "module top; endmodule\n"; + let header_text = "`define WIDTH 8\n"; + let db = db_with_files(root_text, header_text); + + let contexts = source_preproc_single_query_contexts(&db, HEADER); + + assert!(contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); + assert!(!contexts.model_file_ids.contains(&TOP), "{contexts:?}"); +} + #[test] fn preproc_partial_context_index_is_structured_unavailable() { let contexts = SourcePreprocQueryContexts { From f25be9b8f3a47fedeae9cb71e758c8c6ffd53602 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:59:02 +0800 Subject: [PATCH 57/80] fix(hir): decode manifest predefine source tokens --- crates/hir/Cargo.toml | 1 + .../source_db/preproc/source_mapping.rs | 16 ++++----- crates/hir/src/preproc/tests.rs | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml index d3092789..4d393edd 100644 --- a/crates/hir/Cargo.toml +++ b/crates/hir/Cargo.toml @@ -14,6 +14,7 @@ salsa.workspace = true smallvec.workspace = true smol_str.workspace = true syntax.workspace = true +toml.workspace = true tracing.workspace = true triomphe.workspace = true utils.workspace = true diff --git a/crates/hir/src/base_db/source_db/preproc/source_mapping.rs b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs index 524c9289..262dcf11 100644 --- a/crates/hir/src/base_db/source_db/preproc/source_mapping.rs +++ b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs @@ -349,19 +349,19 @@ fn manifest_predefine_source_matches(text: &str, range: TextRange, predefine: &P let Some(raw_source) = text.get(start..end) else { return false; }; - let Some(source_definition) = unquote_manifest_predefine(raw_source) else { + let Some(source_definition) = decode_manifest_predefine_source(raw_source) else { return false; }; - source_definition == predefine.as_str() - && predefine_definition_name(source_definition) + source_definition.as_str() == predefine.as_str() + && predefine_definition_name(source_definition.as_str()) == predefine_definition_name(predefine.as_str()) } -fn unquote_manifest_predefine(text: &str) -> Option<&str> { - let text = text.trim(); - text.strip_prefix('"') - .and_then(|text| text.strip_suffix('"')) - .or_else(|| text.strip_prefix('\'').and_then(|text| text.strip_suffix('\''))) +fn decode_manifest_predefine_source(text: &str) -> Option { + let document = format!("value = {}", text.trim()); + toml::from_str::(&document) + .ok() + .and_then(|document| document.get("value").and_then(toml::Value::as_str).map(str::to_owned)) } fn predefine_definition_name(predefine: &str) -> Option { diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 0f31974c..4942da78 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -815,6 +815,42 @@ endmodule ); } +#[test] +fn preproc_manifest_escaped_predefine_definition_uses_manifest_provenance() { + let root_text = r#"`ifdef MSG +module top; +localparam string S = `MSG; +endmodule +`endif +"#; + let manifest_text = r#"defines = ["MSG=\"hello\""] +"#; + let raw_define = r#""MSG=\"hello\"""#; + let manifest_range = + TextRange::new(offset(manifest_text, raw_define), offset_after(manifest_text, raw_define)); + let predefine = Predefine::with_source( + r#"MSG="hello""#, + PredefineSource { path: abs_path("vide.toml"), range: manifest_range }, + ); + let db = db_with_entries_and_predefine_entries( + &[(TOP, "rtl/top.v", root_text), (MANIFEST, "vide.toml", manifest_text)], + vec![predefine], + ); + + let definition = macro_definition_at(&db, MANIFEST, manifest_range.start()).unwrap().unwrap(); + assert_eq!(definition.file_id, MANIFEST); + assert_eq!(definition.name.as_str(), "MSG"); + assert_eq!(definition.name_range, manifest_range); + assert_eq!(text_at_range(manifest_text, definition.name_range), raw_define); + + let references = macro_references(&db, MANIFEST, &definition).unwrap(); + assert!( + references.references.iter().any(|reference| reference.file_id == TOP + && text_at_range(root_text, reference.range) == "MSG"), + "escaped manifest predefine should still find source references: {references:?}" + ); +} + #[test] fn preproc_visible_macro_names_follow_define_undef_boundaries() { let root_text = r#"`define A005_LOCAL 1 From 220d5100311eb37eb82d256a47d164763c0b55c1 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 03:03:51 +0800 Subject: [PATCH 58/80] fix(ide): preserve partial macro reference status --- crates/ide/src/references.rs | 92 +++++++++++++++++++++++++--- src/global_state/handlers/request.rs | 7 ++- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 78e7f996..bb97e8f6 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1,8 +1,8 @@ use hir::{ file::HirFileId, preproc::{ - MacroDefinition, MacroParamDefinition, macro_definition_at, macro_param_definition_at, - macro_param_reference_definitions_at, macro_param_references, + MacroDefinition, MacroParamDefinition, MacroReferenceIndexStatus, macro_definition_at, + macro_param_definition_at, macro_param_reference_definitions_at, macro_param_references, macro_reference_definitions_at, macro_references, }, semantics::Semantics, @@ -62,6 +62,31 @@ impl ReferencesConfig { pub struct References { pub def: Option>, pub refs: IntMap>, + pub status: ReferencesStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReferencesStatus { + Complete, + Partial { reason: ReferencesPartialReason, issue_count: usize }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReferencesPartialReason { + PreprocMacroIndex, +} + +impl ReferencesStatus { + pub fn is_partial(self) -> bool { + matches!(self, ReferencesStatus::Partial { .. }) + } + + pub fn issue_count(self) -> usize { + match self { + ReferencesStatus::Complete => 0, + ReferencesStatus::Partial { issue_count, .. } => issue_count, + } + } } pub(crate) fn references( @@ -185,7 +210,11 @@ fn macro_param_references_for_definition( ) }) .collect(); - Some(References { def: Some(vec![macro_param_nav_target(definition)]), refs }) + Some(References { + def: Some(vec![macro_param_nav_target(definition)]), + refs, + status: ReferencesStatus::Complete, + }) } fn macro_references_for_definition( @@ -194,8 +223,9 @@ fn macro_references_for_definition( definition: MacroDefinition, config: &ReferencesConfig, ) -> Option { - let refs = macro_references(db, file_id, &definition) - .ok()? + let references = macro_references(db, file_id, &definition).ok()?; + let status = references_status_from_macro_index(references.status); + let refs = references .references .into_iter() .filter(|usage| { @@ -217,7 +247,17 @@ fn macro_references_for_definition( ) }) .collect(); - Some(References { def: Some(vec![macro_nav_target(definition)]), refs }) + Some(References { def: Some(vec![macro_nav_target(definition)]), refs, status }) +} + +fn references_status_from_macro_index(status: MacroReferenceIndexStatus) -> ReferencesStatus { + match status { + MacroReferenceIndexStatus::Complete => ReferencesStatus::Complete, + MacroReferenceIndexStatus::Partial { issues } => ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count: issues.len(), + }, + } } fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { @@ -267,7 +307,11 @@ pub(crate) fn handle_ctrl_flow_kw( _ => return None, } - Some(vec![References { def: None, refs: IntMap::from_iter([(file_id.file_id(), refs)]) }]) + Some(vec![References { + def: None, + refs: IntMap::from_iter([(file_id.file_id(), refs)]), + status: ReferencesStatus::Complete, + }]) } fn search_refs<'a>( @@ -284,7 +328,7 @@ fn search_refs<'a>( }) .collect(); let def = def.origins().into_iter().filter_map(|def| def.to_nav(sema.db)).collect_vec().into(); - References { def, refs } + References { def, refs, status: ReferencesStatus::Complete } } fn token_precedence(kind: TokenKind) -> usize { @@ -294,3 +338,35 @@ fn token_precedence(kind: TokenKind) -> usize { _ => 1, } } + +#[cfg(test)] +mod tests { + use hir::preproc::{MacroReferenceIndexIssue, PreprocError}; + + use super::*; + + #[test] + fn macro_reference_index_status_maps_to_reference_status() { + assert_eq!( + references_status_from_macro_index(MacroReferenceIndexStatus::Complete), + ReferencesStatus::Complete + ); + + let status = references_status_from_macro_index(MacroReferenceIndexStatus::Partial { + issues: vec![MacroReferenceIndexIssue::SkippedModel { + file_id: FileId(0), + error: PreprocError::MissingRootSource, + }], + }); + + assert_eq!( + status, + ReferencesStatus::Partial { + reason: ReferencesPartialReason::PreprocMacroIndex, + issue_count: 1, + } + ); + assert!(status.is_partial()); + assert_eq!(status.issue_count(), 1); + } +} diff --git a/src/global_state/handlers/request.rs b/src/global_state/handlers/request.rs index ba159a76..465de975 100644 --- a/src/global_state/handlers/request.rs +++ b/src/global_state/handlers/request.rs @@ -446,10 +446,15 @@ pub(crate) fn handle_references( let Some(refs) = snap.analysis.references(position, config)? else { return Ok(None); }; + let partial_issue_count: usize = + refs.iter().map(|references| references.status.issue_count()).sum(); + if partial_issue_count > 0 { + tracing::debug!(partial_issue_count, "references result is partial"); + } let locations = refs .into_iter() - .flat_map(|References { def, refs }| { + .flat_map(|References { def, refs, .. }| { let decl = if include_declaration { def.unwrap_or_default() } else { Vec::new() } .into_iter() .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }); From 6c7427bed1600980caa97869ae61b40288f6509f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 09:25:40 +0800 Subject: [PATCH 59/80] fix(preproc): satisfy include scope clippy lint --- crates/preproc/src/source/provenance/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index 5a60a06d..ba3fc911 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -198,7 +198,7 @@ impl<'a> SourcePreprocModelBuilder<'a> { (!source_is_descendant_or_same(directive.range.source, source, source_parents)) .then_some(source_order) }) - .unwrap_or_else(|| self.index.event_records.len()) + .unwrap_or(self.index.event_records.len()) } fn build_include_graph(&mut self) { From 7d9a7a8c46c45238a432418bd2004920fa1ab49c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 09:29:55 +0800 Subject: [PATCH 60/80] perf(ide): gate macro provenance token lookup --- crates/ide/src/source_tokens.rs | 131 +++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index 49214ecb..51ff4b72 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -1,5 +1,5 @@ use hir::{ - base_db::source_db::SourcePreprocQueryError, + base_db::source_db::{SourceDb, SourcePreprocQueryError}, preproc::{ EmittedTokenProvenance, MacroDefinitionId, MacroExpansionProvenance, MappedPreprocSource, PreprocError, TokenProvenance, macro_expansion_provenances_at, @@ -203,6 +203,10 @@ fn provenance_token_candidates_at_offset<'tree>( precedence: &impl Fn(TokenKind) -> usize, cache: &mut SourceTokenRequestCache, ) -> ProvenanceTokenLookup<'tree> { + if !source_macro_invocation_may_cover_offset(db.file_text(file_id).as_ref(), offset) { + return ProvenanceTokenLookup::NotApplicable; + } + let provenances = match cache.macro_expansion_provenances_at(db, file_id, offset) { Ok(provenances) => provenances, Err(PreprocError::SourceQuery(SourcePreprocQueryError::UnsupportedFileKind(_))) => { @@ -379,6 +383,105 @@ fn syntax_tokens_for_preproc_hit<'tree>( (!tokens.is_empty()).then_some(tokens) } +fn source_macro_invocation_may_cover_offset(text: &str, offset: TextSize) -> bool { + let offset = usize::from(offset); + if offset > text.len() || !text.is_char_boundary(offset) { + return false; + } + + let search_end = text[offset..].chars().next().map_or(offset, |ch| offset + ch.len_utf8()); + let prefix = &text[..search_end]; + for (tick, _) in prefix.match_indices('`').rev() { + match macro_invocation_candidate_end(text, tick) { + MacroInvocationCandidate::RangeEnd(end) if offset <= end => return true, + MacroInvocationCandidate::RangeEnd(_) => {} + MacroInvocationCandidate::Malformed => return true, + MacroInvocationCandidate::NotMacro => {} + } + } + false +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MacroInvocationCandidate { + RangeEnd(usize), + Malformed, + NotMacro, +} + +fn macro_invocation_candidate_end(text: &str, tick: usize) -> MacroInvocationCandidate { + let Some(after_tick) = text.get(tick + 1..) else { + return MacroInvocationCandidate::Malformed; + }; + let Some((name_start_offset, first)) = after_tick.char_indices().next() else { + return MacroInvocationCandidate::Malformed; + }; + let name_start = tick + 1 + name_start_offset; + let name_end = if first == '\\' { + let Some((end, _)) = text[name_start..].char_indices().find(|(_, ch)| ch.is_whitespace()) + else { + return MacroInvocationCandidate::Malformed; + }; + name_start + end + } else if is_macro_ident_start(first) { + text[name_start..] + .char_indices() + .find_map(|(index, ch)| (!is_macro_ident_continue(ch)).then_some(name_start + index)) + .unwrap_or(text.len()) + } else { + return MacroInvocationCandidate::NotMacro; + }; + + let after_name = &text[name_end..]; + let Some((next_offset, next)) = after_name.char_indices().find(|(_, ch)| !ch.is_whitespace()) + else { + return MacroInvocationCandidate::RangeEnd(name_end); + }; + if next != '(' { + return MacroInvocationCandidate::RangeEnd(name_end); + } + let open = name_end + next_offset; + match balanced_paren_end(text, open) { + Some(end) => MacroInvocationCandidate::RangeEnd(end), + None => MacroInvocationCandidate::Malformed, + } +} + +fn balanced_paren_end(text: &str, open: usize) -> Option { + let mut depth = 0usize; + let mut chars = text[open..].char_indices(); + while let Some((relative, ch)) = chars.next() { + match ch { + '(' => depth += 1, + ')' => { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(open + relative + ch.len_utf8()); + } + } + '"' => { + while let Some((_, string_ch)) = chars.next() { + if string_ch == '\\' { + let _ = chars.next(); + } else if string_ch == '"' { + break; + } + } + } + _ => {} + } + } + None +} + +fn is_macro_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +fn is_macro_ident_continue(ch: char) -> bool { + is_macro_ident_start(ch) || ch.is_ascii_digit() || ch == '$' +} + fn covering_range(ranges: &[TextRange]) -> Option { let start = ranges.iter().map(|range| range.start()).min()?; let end = ranges.iter().map(|range| range.end()).max()?; @@ -467,6 +570,28 @@ mod tests { assert_eq!(selection.tokens.len(), 1); } + #[test] + fn source_tokens_macro_provenance_gate_skips_plain_identifiers() { + let text = "module m; wire payload_i; endmodule\n"; + + assert!(!source_macro_invocation_may_cover_offset(text, offset(text, "payload_i"))); + } + + #[test] + fn source_tokens_macro_provenance_gate_keeps_macro_names_and_arguments() { + let text = "module m; wire `MAKE_DECL(payload_i); endmodule\n"; + + assert!(source_macro_invocation_may_cover_offset(text, offset(text, "`MAKE_DECL"))); + assert!(source_macro_invocation_may_cover_offset(text, offset(text, "payload_i"))); + } + + #[test] + fn source_tokens_macro_provenance_gate_keeps_outer_arguments_after_nested_macros() { + let text = "assign y = `OUTER(a, `INNER(b), payload_i);\n"; + + assert!(source_macro_invocation_may_cover_offset(text, offset(text, "payload_i"))); + } + #[test] fn source_tokens_dedups_preproc_hits_for_same_semantic_target() { let (root, offset, parser_range) = @@ -547,6 +672,10 @@ mod tests { (root, range.start() + TextSize::from(delta), range) } + fn offset(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).expect("needle should exist")).unwrap()) + } + fn test_source_hit(file_id: FileId, range: TextRange, emitted_token: usize) -> PreprocTokenHit { let source = MappedPreprocSource::RealFile { file_id }; PreprocTokenHit { From f621ee47b2fa1ea53f6b1824c812a2721c8a09d5 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 09:34:38 +0800 Subject: [PATCH 61/80] perf(hir): index preproc contexts by profile --- crates/hir/src/base_db/source_db.rs | 15 ++++-- crates/hir/src/base_db/source_db/preproc.rs | 58 +++++++++++++++++---- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 1192215e..0d8cc40e 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -21,13 +21,16 @@ pub use self::preproc::{ MappedSourcePreprocModel, PreprocExpansionDisplay, PreprocExpansionMapping, PreprocExpansionSourceBuffer, PreprocManifestSource, PreprocSourceMap, PreprocSourceMapError, PreprocSourceMapping, PreprocSpeculativeUniverseId, PreprocVirtualOrigin, - SourcePreprocContextStatus, SourcePreprocQueryError, SourcePreprocRelevantContexts, - preproc_virtual_builtin_path, preproc_virtual_expansion_path, preproc_virtual_predefines_path, - preproc_virtual_speculative_path, + SourcePreprocContextIndex, SourcePreprocContextStatus, SourcePreprocQueryError, + SourcePreprocRelevantContexts, preproc_virtual_builtin_path, preproc_virtual_expansion_path, + preproc_virtual_predefines_path, preproc_virtual_speculative_path, }; #[cfg(test)] use self::preproc::{materialized_predefine_text, source_preproc_file_ids}; -use self::preproc::{source_preproc_contexts_for_file, source_preproc_model}; +use self::preproc::{ + source_preproc_context_index_for_profile, source_preproc_contexts_for_file, + source_preproc_model, +}; pub trait FileLoader { fn resolve_path(&self, path: AnchoredPath<'_>) -> Option; @@ -363,6 +366,10 @@ pub trait SourceRootDb: SourceDb { &self, file_id: FileId, ) -> Arc>; + fn source_preproc_context_index_for_profile( + &self, + profile_id: Option, + ) -> Arc; fn source_preproc_contexts_for_file( &self, file_id: FileId, diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs index 49155b32..7aaf5ff3 100644 --- a/crates/hir/src/base_db/source_db/preproc.rs +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -203,12 +203,33 @@ pub struct SourcePreprocRelevantContexts { pub status: SourcePreprocContextStatus, } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourcePreprocContextIndex { + contexts_by_file: FxHashMap>, + status: SourcePreprocContextStatus, +} + +impl SourcePreprocContextIndex { + fn contexts_for_file(&self, file_id: FileId) -> SourcePreprocRelevantContexts { + SourcePreprocRelevantContexts { + model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), + status: self.status, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SourcePreprocContextStatus { Complete, Partial { skipped_models: usize }, } +impl Default for SourcePreprocContextStatus { + fn default() -> Self { + Self::Complete + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreprocSourceMapping { RealFile(FileId), @@ -798,19 +819,15 @@ pub(super) fn source_preproc_model( Arc::new(Ok(MappedSourcePreprocModel { model, source_map, range_index })) } -pub(super) fn source_preproc_contexts_for_file( +pub(super) fn source_preproc_context_index_for_profile( db: &dyn SourceRootDb, - file_id: FileId, -) -> Arc { - let profile_id = db.file_compilation_profile(file_id); + profile_id: Option, +) -> Arc { let plan = db.compilation_plan_for_profile(profile_id); - let mut model_file_ids = UniqVec::::default(); + let mut contexts_by_file = FxHashMap::>::default(); let mut skipped_models = 0usize; for model_file_id in plan.roots.iter().copied() { - if model_file_id == file_id { - continue; - } if !matches!( db.file_kind(model_file_id), SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader @@ -820,18 +837,37 @@ pub(super) fn source_preproc_contexts_for_file( let mapped = db.source_preproc_model(model_file_id); match mapped.as_ref() { Ok(mapped) => { - if preproc_context_file_ids(mapped, model_file_id).contains(&file_id) { - model_file_ids.push_unique(model_file_id); + for file_id in preproc_context_file_ids(mapped, model_file_id) { + if file_id == model_file_id { + continue; + } + contexts_by_file.entry(file_id).or_default().push_unique(model_file_id); } } Err(_) => skipped_models += 1, } } + let contexts_by_file = contexts_by_file + .into_iter() + .map(|(file_id, model_file_ids)| { + let mut model_file_ids = model_file_ids.into_vec(); + model_file_ids.sort(); + (file_id, model_file_ids) + }) + .collect(); let status = if skipped_models == 0 { SourcePreprocContextStatus::Complete } else { SourcePreprocContextStatus::Partial { skipped_models } }; - Arc::new(SourcePreprocRelevantContexts { model_file_ids: model_file_ids.into_vec(), status }) + Arc::new(SourcePreprocContextIndex { contexts_by_file, status }) +} + +pub(super) fn source_preproc_contexts_for_file( + db: &dyn SourceRootDb, + file_id: FileId, +) -> Arc { + let profile_id = db.file_compilation_profile(file_id); + Arc::new(db.source_preproc_context_index_for_profile(profile_id).contexts_for_file(file_id)) } From ce01c934d48e0f59693ac794b6fd3af0335bc4ff Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 09:44:00 +0800 Subject: [PATCH 62/80] fix(hir): preserve partial preproc context status --- crates/hir/src/base_db/source_db/preproc.rs | 13 +-- crates/hir/src/preproc/conditionals.rs | 11 +- crates/hir/src/preproc/definitions.rs | 29 +++-- crates/hir/src/preproc/expansion.rs | 114 +++++++++++++++++++- crates/hir/src/preproc/helpers/context.rs | 15 +++ crates/hir/src/preproc/includes.rs | 5 +- crates/hir/src/preproc/predefines.rs | 6 +- crates/hir/src/preproc/reference_queries.rs | 18 +++- crates/hir/src/preproc/tests.rs | 17 +++ crates/hir/src/preproc/types.rs | 1 + 10 files changed, 197 insertions(+), 32 deletions(-) diff --git a/crates/hir/src/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs index 7aaf5ff3..8421db00 100644 --- a/crates/hir/src/base_db/source_db/preproc.rs +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -218,16 +218,13 @@ impl SourcePreprocContextIndex { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum SourcePreprocContextStatus { + #[default] Complete, - Partial { skipped_models: usize }, -} - -impl Default for SourcePreprocContextStatus { - fn default() -> Self { - Self::Complete - } + Partial { + skipped_models: usize, + }, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/hir/src/preproc/conditionals.rs b/crates/hir/src/preproc/conditionals.rs index dc7426d4..e42c131c 100644 --- a/crates/hir/src/preproc/conditionals.rs +++ b/crates/hir/src/preproc/conditionals.rs @@ -30,12 +30,11 @@ pub fn inactive_branches( continue; }; if branch_file_id == file_id { - let branch = InactiveBranch { - source, - capability: capability_status(&mapped.model.capabilities().inactive_ranges), - file_id: branch_file_id, - range, - }; + let capability = context_query_capability( + &contexts, + capability_status(&mapped.model.capabilities().inactive_ranges), + ); + let branch = InactiveBranch { source, capability, file_id: branch_file_id, range }; branches.push_keyed(branch, InactiveBranchKey::from_branch); } } diff --git a/crates/hir/src/preproc/definitions.rs b/crates/hir/src/preproc/definitions.rs index 1bf5a6e5..2aca2ba1 100644 --- a/crates/hir/src/preproc/definitions.rs +++ b/crates/hir/src/preproc/definitions.rs @@ -24,7 +24,9 @@ pub fn visible_macros_at( for position in mapped.source_map.source_positions_for_file_offset(file_id, offset) { for definition in mapped.model.visible_macros_at(position) { match map_macro_definition(mapped, definition) { - Ok(definition) => { + Ok(mut definition) => { + definition.capability = + context_query_capability(&contexts, definition.capability); definitions.push_keyed(definition, MacroDefinitionKey::from_definition); } Err(error) => record_first_error(&mut first_error, error), @@ -76,9 +78,11 @@ pub fn macro_definition_at( }; for definition in mapped.model.macro_definitions().iter() { - let mapped_definition = map_macro_definition(mapped, definition)?; + let mut mapped_definition = map_macro_definition(mapped, definition)?; if mapped_definition.file_id == file_id && mapped_definition.name_range.contains(offset) { + mapped_definition.capability = + context_query_capability(&contexts, mapped_definition.capability); return Ok(Some(mapped_definition)); } } @@ -131,7 +135,7 @@ pub fn macro_param_definitions_at( continue; }; for (param_index, param) in params.iter().enumerate() { - let Some(param_definition) = + let Some(mut param_definition) = map_macro_param_definition(mapped, definition, param_index, param)? else { continue; @@ -139,6 +143,10 @@ pub fn macro_param_definitions_at( if param_definition.macro_definition.file_id == file_id && param_definition.range.contains(offset) { + param_definition.macro_definition.capability = context_query_capability( + &contexts, + param_definition.macro_definition.capability, + ); definitions .push_keyed(param_definition, MacroParamDefinitionKey::from_definition); } @@ -198,18 +206,24 @@ pub fn macro_param_reference_definitions_at( if param.name.as_ref() != Some(&token.value) { continue; } - let Some(param_definition) = + let Some(mut param_definition) = map_macro_param_definition(mapped, definition, param_index, param)? else { continue; }; - let reference = map_macro_param_reference( + param_definition.macro_definition.capability = context_query_capability( + &contexts, + param_definition.macro_definition.capability, + ); + let mut reference = map_macro_param_reference( mapped, definition, param_index, token_index, token_range, )?; + reference.capability = + context_query_capability(&contexts, reference.capability); query_range.get_or_insert(range); definitions .push_keyed(param_definition, MacroParamDefinitionKey::from_definition); @@ -227,7 +241,10 @@ pub fn macro_param_reference_definitions_at( let references = references.into_vec(); let definitions = definitions.into_vec(); Ok(Some(MacroParamReferenceDefinitions { - capability: macro_param_reference_context_capability(&references), + capability: context_query_capability( + &contexts, + macro_param_reference_context_capability(&references), + ), references, range, definitions, diff --git a/crates/hir/src/preproc/expansion.rs b/crates/hir/src/preproc/expansion.rs index 935020ff..2ce47c6f 100644 --- a/crates/hir/src/preproc/expansion.rs +++ b/crates/hir/src/preproc/expansion.rs @@ -46,7 +46,8 @@ pub fn macro_expansion_queries_at( } }; for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let query = immediate_macro_expansion_for_call(mapped, call_fact)?; + let mut query = immediate_macro_expansion_for_call(mapped, call_fact)?; + apply_context_capability_to_macro_expansion_query(&contexts, &mut query); queries.push_unique_eq(query); } } @@ -88,7 +89,8 @@ pub fn recursive_macro_expansions_at( } }; for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; + let mut recursive = recursive_macro_expansion_for_call(mapped, call_fact)?; + apply_context_capability_to_recursive_macro_expansion(&contexts, &mut recursive); expansions.push_unique_eq(recursive); } } @@ -120,7 +122,11 @@ pub fn recursive_macro_expansion_provenances_at( } }; for call_fact in source_macro_calls_at(mapped, file_id, offset) { - let recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; + let mut recursive = recursive_macro_expansion_provenance_for_call(mapped, call_fact)?; + apply_context_capability_to_recursive_macro_expansion_provenance( + &contexts, + &mut recursive, + ); expansions.push_unique_eq(recursive); } } @@ -164,7 +170,12 @@ pub fn macro_expansion_provenances_at( for call_fact in source_macro_calls_at(mapped, file_id, offset) { match macro_expansion_provenance_for_call(mapped, call_fact)? { MacroExpansionProvenanceForCall::Available(provenance) => { - provenances.push_unique_eq(*provenance); + let mut provenance = *provenance; + apply_context_capability_to_macro_expansion_provenance( + &contexts, + &mut provenance, + ); + provenances.push_unique_eq(provenance); } MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), } @@ -216,7 +227,12 @@ pub fn macro_expansion_provenances_for_range( [] => continue, [call_fact] => match macro_expansion_provenance_for_call(mapped, call_fact)? { MacroExpansionProvenanceForCall::Available(provenance) => { - provenances.push_unique_eq(*provenance); + let mut provenance = *provenance; + apply_context_capability_to_macro_expansion_provenance( + &contexts, + &mut provenance, + ); + provenances.push_unique_eq(provenance); } MacroExpansionProvenanceForCall::Unavailable(reason) => unavailable.push(reason), }, @@ -257,6 +273,94 @@ fn unavailable_or_ambiguous_macro_expansion_provenance( }) } +fn apply_context_capability_to_macro_call( + contexts: &SourcePreprocQueryContexts, + call: &mut MacroCall, +) { + call.capability = context_query_capability(contexts, call.capability.clone()); +} + +fn apply_context_capability_to_macro_expansion( + contexts: &SourcePreprocQueryContexts, + expansion: &mut MacroExpansion, +) { + apply_context_capability_to_macro_call(contexts, &mut expansion.call); + expansion.definition.capability = + context_query_capability(contexts, expansion.definition.capability.clone()); + expansion.capability = context_query_capability(contexts, expansion.capability.clone()); +} + +fn apply_context_capability_to_macro_expansion_unavailable( + contexts: &SourcePreprocQueryContexts, + unavailable: &mut MacroExpansionUnavailable, +) { + apply_context_capability_to_macro_call(contexts, &mut unavailable.call); +} + +fn apply_context_capability_to_macro_expansion_query( + contexts: &SourcePreprocQueryContexts, + query: &mut MacroExpansionQuery, +) { + match query { + MacroExpansionQuery::Available(expansion) => { + apply_context_capability_to_macro_expansion(contexts, expansion); + } + MacroExpansionQuery::Ambiguous(expansions) => { + for expansion in expansions { + apply_context_capability_to_macro_expansion(contexts, expansion); + } + } + MacroExpansionQuery::Unavailable(unavailable) => { + apply_context_capability_to_macro_expansion_unavailable(contexts, unavailable); + } + } +} + +fn apply_context_capability_to_recursive_macro_expansion( + contexts: &SourcePreprocQueryContexts, + recursive: &mut RecursiveMacroExpansion, +) { + apply_context_capability_to_macro_call(contexts, &mut recursive.root_call); + for expansion in &mut recursive.expansions { + apply_context_capability_to_macro_expansion(contexts, expansion); + } + for unavailable in &mut recursive.unavailable { + apply_context_capability_to_macro_expansion_unavailable(contexts, unavailable); + } +} + +fn apply_context_capability_to_recursive_macro_expansion_provenance( + contexts: &SourcePreprocQueryContexts, + recursive: &mut RecursiveMacroExpansionProvenance, +) { + apply_context_capability_to_macro_call(contexts, &mut recursive.root_call); + for expansion in &mut recursive.expansions { + apply_context_capability_to_macro_expansion_provenance(contexts, expansion); + } + for unavailable in &mut recursive.unavailable { + apply_context_capability_to_macro_expansion_unavailable(contexts, unavailable); + } +} + +fn apply_context_capability_to_macro_expansion_provenance( + contexts: &SourcePreprocQueryContexts, + provenance: &mut MacroExpansionProvenance, +) { + apply_context_capability_to_macro_expansion(contexts, &mut provenance.expansion); + for token in &mut provenance.tokens { + match &mut token.provenance { + TokenProvenance::MacroBody { call, .. } + | TokenProvenance::MacroArgument { call, .. } => { + apply_context_capability_to_macro_call(contexts, call); + } + TokenProvenance::SourceToken { .. } + | TokenProvenance::Predefine { .. } + | TokenProvenance::Builtin { .. } + | TokenProvenance::Unavailable(_) => {} + } + } +} + pub fn diagnostic_provenance_for_range( db: &dyn SourceRootDb, file_id: FileId, diff --git a/crates/hir/src/preproc/helpers/context.rs b/crates/hir/src/preproc/helpers/context.rs index e779e44f..ddb5e563 100644 --- a/crates/hir/src/preproc/helpers/context.rs +++ b/crates/hir/src/preproc/helpers/context.rs @@ -23,6 +23,21 @@ impl SourcePreprocQueryContexts { } } +pub(in crate::preproc) fn context_query_capability( + contexts: &SourcePreprocQueryContexts, + capability: PreprocAvailability, +) -> PreprocAvailability { + match contexts.status { + SourcePreprocContextStatus::Complete => capability, + SourcePreprocContextStatus::Partial { .. } => match capability { + PreprocAvailability::Unavailable(reason) => PreprocAvailability::Unavailable(reason), + PreprocAvailability::Complete | PreprocAvailability::Partial => { + PreprocAvailability::Partial + } + }, + } +} + pub(in crate::preproc) fn source_preproc_single_query_contexts( db: &dyn SourceRootDb, file_id: FileId, diff --git a/crates/hir/src/preproc/includes.rs b/crates/hir/src/preproc/includes.rs index bfa463b5..27efd7ec 100644 --- a/crates/hir/src/preproc/includes.rs +++ b/crates/hir/src/preproc/includes.rs @@ -53,7 +53,10 @@ pub fn include_directives_at( let directive = IncludeDirective { id: include.id.into(), source, - capability: capability_status(&mapped.model.capabilities().include_edges), + capability: context_query_capability( + &contexts, + capability_status(&mapped.model.capabilities().include_edges), + ), file_id, include_index: include.id.raw(), range, diff --git a/crates/hir/src/preproc/predefines.rs b/crates/hir/src/preproc/predefines.rs index 0f1fe432..7f38b418 100644 --- a/crates/hir/src/preproc/predefines.rs +++ b/crates/hir/src/preproc/predefines.rs @@ -57,16 +57,18 @@ pub(super) fn configured_predefine_definitions_at( let profile_id = db.file_compilation_profile(context_file_id); let project_preprocess = db.project_config().preprocess_for_profile(profile_id); for predefine in &project_preprocess.predefines { - if let Some(definition) = + if let Some(mut definition) = configured_predefine_definition_at(db, predefine, file_id, offset) { + definition.capability = context_query_capability(&contexts, definition.capability); definitions.push_keyed(definition, MacroDefinitionKey::from_definition); } } for predefine in &db.file_preprocess_config(context_file_id).predefines { - if let Some(definition) = + if let Some(mut definition) = configured_predefine_definition_at(db, predefine, file_id, offset) { + definition.capability = context_query_capability(&contexts, definition.capability); definitions.push_keyed(definition, MacroDefinitionKey::from_definition); } } diff --git a/crates/hir/src/preproc/reference_queries.rs b/crates/hir/src/preproc/reference_queries.rs index dfecf0fd..fa8428ee 100644 --- a/crates/hir/src/preproc/reference_queries.rs +++ b/crates/hir/src/preproc/reference_queries.rs @@ -58,12 +58,15 @@ pub fn macro_usage_resolutions_at( let definition_provenance = map_definition_provenance_from_definition(mapped, definition_fact)?; let include_chain = map_include_chain(mapped, include_chain)?; + let capability = + context_query_capability(&contexts, mapped_reference.capability.clone()); resolutions.push_unique_eq(MacroUsageResolution { + capability: capability.clone(), usage: MacroUsage { reference_id: mapped_reference.id, source: mapped_reference.source, - capability: mapped_reference.capability.clone(), + capability, file_id: mapped_reference.file_id, name: mapped_reference.name, usage_index, @@ -131,7 +134,9 @@ pub fn macro_references_in_range( }; match map_macro_reference(mapped, reference) { - Ok(reference) => { + Ok(mut reference) => { + reference.capability = + context_query_capability(&contexts, reference.capability); references.push_unique_eq(reference); } Err(error) => record_first_error(&mut first_error, error), @@ -210,13 +215,15 @@ pub fn macro_reference_definitions_at( }; query_range.get_or_insert(range); - let mapped_reference = match map_macro_reference(mapped, reference) { + let mut mapped_reference = match map_macro_reference(mapped, reference) { Ok(reference) => reference, Err(error) => { record_first_error(&mut first_error, error); continue; } }; + mapped_reference.capability = + context_query_capability(&contexts, mapped_reference.capability); references.push_unique_eq(mapped_reference.clone()); match &reference.resolution { @@ -262,7 +269,10 @@ pub fn macro_reference_definitions_at( }; Ok(Some(MacroReferenceDefinitions { - capability: macro_reference_context_capability(references.as_slice()), + capability: context_query_capability( + &contexts, + macro_reference_context_capability(references.as_slice()), + ), references: references.into_vec(), range, definitions: definitions.into_vec(), diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 4942da78..46701cb9 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -758,6 +758,23 @@ fn preproc_partial_context_index_is_structured_unavailable() { )); } +#[test] +fn preproc_partial_context_index_marks_nonempty_results_partial() { + let contexts = SourcePreprocQueryContexts { + model_file_ids: vec![TOP], + status: SourcePreprocContextStatus::Partial { skipped_models: 2 }, + }; + + assert_eq!( + context_query_capability(&contexts, PreprocAvailability::Complete), + PreprocAvailability::Partial + ); + assert_eq!( + context_query_capability(&contexts, PreprocAvailability::Partial), + PreprocAvailability::Partial + ); +} + #[test] fn preproc_manifest_predefine_definition_uses_manifest_provenance() { let root_text = r#"`ifdef Z_FROM_MANIFEST diff --git a/crates/hir/src/preproc/types.rs b/crates/hir/src/preproc/types.rs index 3a446463..da234d16 100644 --- a/crates/hir/src/preproc/types.rs +++ b/crates/hir/src/preproc/types.rs @@ -207,6 +207,7 @@ pub struct MacroUsage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroUsageResolution { + pub capability: PreprocAvailability, pub usage: MacroUsage, pub definition: MacroDefinition, pub definition_provenance: MacroDefinitionProvenance, From 8cfb480012567afc50668efdbf4077288804017e Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 10:08:48 +0800 Subject: [PATCH 63/80] fix(preproc): keep macro body references per call --- crates/preproc/src/source/model/tests.rs | 36 +++++++++++++++++++ .../preproc/src/source/provenance/builder.rs | 7 ++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index b2678111..a0e026b8 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -1036,6 +1036,42 @@ endmodule assert!(recursive.unavailable.is_empty()); } +#[test] +fn source_model_keeps_macro_body_references_for_each_call_site() { + let root_text = r#"`define LEAF 3 +`define WRAP `LEAF +module m; +localparam int A = `WRAP; +localparam int B = `WRAP; +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let references = model + .macro_references() + .iter() + .filter(|reference| { + reference.name.as_str() == "LEAF" + && matches!(reference.site, SourceMacroReferenceSite::MacroBodyToken { .. }) + }) + .collect::>(); + + assert_eq!(references.len(), 2); + let first_site = references[0].site; + let second_site = references[1].site; + let ( + SourceMacroReferenceSite::MacroBodyToken { call: first_call, token_index: first_token }, + SourceMacroReferenceSite::MacroBodyToken { call: second_call, token_index: second_token }, + ) = (first_site, second_site) + else { + unreachable!(); + }; + assert_ne!(first_call, second_call); + assert_eq!(first_token, second_token); + assert_eq!(references[0].name_range, references[1].name_range); + assert_eq!(references[0].resolution, references[1].resolution); +} + #[test] fn source_model_marks_unsupported_macro_ops_unavailable_without_dropping_tokens() { let root_text = r#"`define JOIN(a,b) a``b diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index ba3fc911..0d0a8292 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -854,12 +854,13 @@ impl<'a> SourcePreprocModelBuilder<'a> { }; let resolution = self.resolve_visible_reference_at_position(name.as_str(), call_position); - if self.macro_reference_exists(name.as_str(), name_range, &resolution) { + let site = SourceMacroReferenceSite::MacroBodyToken { call: call.id, token_index }; + if self.macro_reference_exists(name.as_str(), name_range, &site, &resolution) { continue; } self.push_reference( definition.event_id, - SourceMacroReferenceSite::MacroBodyToken { call: call.id, token_index }, + site, name, name_range, definition.directive_range, @@ -873,11 +874,13 @@ impl<'a> SourcePreprocModelBuilder<'a> { &self, name: &str, name_range: SourceRange, + site: &SourceMacroReferenceSite, resolution: &SourceMacroResolution, ) -> bool { self.tables.macro_references.iter().any(|reference| { reference.name.as_str() == name && reference.name_range == name_range + && &reference.site == site && &reference.resolution == resolution }) } From 7d206b24242d1ad2b12fce34fafc1b3dfefc53a8 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 10:13:42 +0800 Subject: [PATCH 64/80] fix(hir): map duplicate predefine sources --- crates/hir/src/base_db/source_db.rs | 90 +++++++++++++++++++ .../source_db/preproc/source_mapping.rs | 11 +-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index 0d8cc40e..128feb32 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -983,6 +983,96 @@ mod tests { ); } + #[test] + fn source_preproc_mapping_records_duplicate_predefine_occurrences() { + let manifest_text = "defines = [\"FOO\", \"FOO=1\"]\n"; + let first_start = manifest_text.find("\"FOO\"").unwrap(); + let second_start = manifest_text.find("\"FOO=1\"").unwrap(); + let first_range = TextRange::new( + TextSize::from(u32::try_from(first_start).unwrap()), + TextSize::from(u32::try_from(first_start + "\"FOO\"".len()).unwrap()), + ); + let second_range = TextRange::new( + TextSize::from(u32::try_from(second_start).unwrap()), + TextSize::from(u32::try_from(second_start + "\"FOO=1\"".len()).unwrap()), + ); + let db = db_with_root_and_manifest(manifest_text); + let predefine_text = materialized_predefine_text("FOO"); + let trace = PreprocessorTrace { + root_buffer_id: 1, + source_buffers: vec![ + SourceBufferId { + path: abs_path("rtl/top.v").to_string(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: "".to_owned(), + text: Some(predefine_text.clone()), + buffer_id: 2, + origin: SourceBufferOrigin::Predefine, + }, + SourceBufferId { + path: "".to_owned(), + text: Some(predefine_text.clone()), + buffer_id: 3, + origin: SourceBufferOrigin::Predefine, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let options = SyntaxTreeOptions { + predefines: vec!["FOO".to_owned(), "FOO=1".to_owned()], + ..SyntaxTreeOptions::default() + }; + let preprocess = PreprocessConfig { + predefines: vec![ + Predefine::with_source( + "FOO", + PredefineSource { path: abs_path("vide.toml"), range: first_range }, + ), + Predefine::with_source( + "FOO=1", + PredefineSource { path: abs_path("vide.toml"), range: second_range }, + ), + ], + include_dirs: Vec::new(), + }; + + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).unwrap(); + let first = PreprocSourceId::from(2); + let second = PreprocSourceId::from(3); + + assert!(matches!(source_map.get(first), Some(PreprocSourceMapping::VirtualDisplay { .. }))); + assert!(matches!( + source_map.get(second), + Some(PreprocSourceMapping::VirtualDisplay { .. }) + )); + assert_eq!(source_map.predefine_manifest_source(first).unwrap().range, first_range); + assert_eq!(source_map.predefine_manifest_source(second).unwrap().range, second_range); + assert_eq!( + source_map.map_range(SourceRange { + source: first, + range: TextRange::new(TextSize::from(0), TextSize::from(1)), + }), + Ok(TextRange::new(TextSize::from(0), TextSize::from(1))) + ); + assert_eq!( + source_map.map_range(SourceRange { + source: second, + range: TextRange::new(TextSize::from(0), TextSize::from(1)), + }), + Ok(TextRange::new( + TextSize::from(u32::try_from(predefine_text.len()).unwrap()), + TextSize::from(u32::try_from(predefine_text.len() + 1).unwrap()), + )) + ); + } + #[test] fn source_preproc_mapping_rejects_predefine_source_text_mismatch() { let db = db_with_root_file(); diff --git a/crates/hir/src/base_db/source_db/preproc/source_mapping.rs b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs index 262dcf11..17b92415 100644 --- a/crates/hir/src/base_db/source_db/preproc/source_mapping.rs +++ b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs @@ -249,12 +249,9 @@ impl PredefineVirtualMapping { range_offset += text.len(); } - let mut configs_by_text = FxHashMap::>::default(); - for (index, config) in configs.iter().enumerate() { - let slot = configs_by_text.entry(config.text.clone()).or_insert(Some(index)); - if *slot != Some(index) { - *slot = None; - } + let mut config_indexes_by_text = FxHashMap::>::default(); + for (index, config) in configs.iter().enumerate().rev() { + config_indexes_by_text.entry(config.text.clone()).or_default().push(index); } let mut entries = FxHashMap::default(); @@ -267,7 +264,7 @@ impl PredefineVirtualMapping { ); continue; }; - let Some(config_index) = configs_by_text.get(source_text).and_then(|index| *index) + let Some(config_index) = config_indexes_by_text.get_mut(source_text).and_then(Vec::pop) else { unavailable.insert( source.source, From 7cd6c05ae9e581678cc8ef257a2a0252ecbdaf51 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 10:23:43 +0800 Subject: [PATCH 65/80] fix(preproc): expose builtin token provenance --- crates/preproc/src/source/model/tests.rs | 6 ++---- crates/preproc/src/source/trace.rs | 4 ++++ crates/slang/bindings/rust/ffi/wrapper.cc | 12 ++++++++++++ crates/slang/bindings/rust/lib.rs | 5 +++++ crates/slang/bindings/rust/tests.rs | 5 ++++- crates/slang/include/slang/text/SourceManager.h | 5 ++++- .../slang/source/parsing/Preprocessor_macros.cpp | 16 ++++++++++++++-- 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index a0e026b8..ddf4e5ee 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -1208,7 +1208,7 @@ fn source_model_does_not_create_expansion_without_emitted_token_authority() { } #[test] -fn source_model_maps_predefine_and_marks_intrinsic_unavailable() { +fn source_model_maps_predefine_and_intrinsic_provenance() { let root_text = r#"module m; localparam int P = `FROM_API; localparam int L = `__LINE__; @@ -1243,9 +1243,7 @@ endmodule .expect("intrinsic macro token should stay in emitted stream"); assert!(matches!( model.token_provenance().get(intrinsic.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) + SourceTokenProvenance::Builtin { name } if name.as_str() == "__LINE__" )); } diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index d7e1e190..565a2b72 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -326,6 +326,10 @@ fn emitted_token_provenance_from_trace( argument_token_range, } } + PreprocessorTraceTokenProvenance::Builtin { name } if !name.is_empty() => { + SourceTokenProvenanceFact::Builtin { name: name.to_smolstr() } + } + PreprocessorTraceTokenProvenance::Builtin { .. } => SourceTokenProvenanceFact::Unavailable, PreprocessorTraceTokenProvenance::Unavailable => SourceTokenProvenanceFact::Unavailable, } } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index 1565cd1e..a40ec58d 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -167,6 +167,7 @@ constexpr uint8_t TRACE_TOKEN_PROVENANCE_UNAVAILABLE = 0; constexpr uint8_t TRACE_TOKEN_PROVENANCE_SOURCE = 1; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_BODY = 2; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT = 3; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_BUILTIN = 4; ::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { ::RawPreprocessorTraceEmittedToken token; @@ -303,6 +304,12 @@ bool has_direct_macro_token_provenance( provenance->definitionId != 0; } +bool has_builtin_macro_token_provenance( + const std::optional& provenance) { + return provenance && provenance->expansionId != 0 && provenance->callId != 0 && + !provenance->builtinName.empty(); +} + void apply_direct_macro_token_provenance( ::RawPreprocessorTraceEmittedToken& token, const slang::SourceManager::MacroTokenProvenance& provenance) { @@ -503,6 +510,11 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( auto macroName = std::string(sourceManager.getMacroName(location)); result.macro_name = rust::String(macroName); auto directProvenance = sourceManager.getMacroTokenProvenance(location); + if (has_builtin_macro_token_provenance(directProvenance)) { + result.macro_name = rust::String(directProvenance->builtinName); + result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; + return result; + } if (apply_original_macro_loc_provenance_for_nested_argument( result, token, sourceManager, location)) diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 037bddd2..e0032a78 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -208,6 +208,9 @@ pub enum PreprocessorTraceTokenProvenance { body_token_range: SourceBufferRange, argument_token_range: SourceBufferRange, }, + Builtin { + name: String, + }, Unavailable, } @@ -541,6 +544,7 @@ struct RawPreprocessorTraceMacroIdentity { } impl PreprocessorTraceTokenProvenance { + const BUILTIN: u8 = 4; const MACRO_ARGUMENT: u8 = 3; const MACRO_BODY: u8 = 2; const SOURCE: u8 = 1; @@ -597,6 +601,7 @@ impl PreprocessorTraceTokenProvenance { argument_token_range, } } + Self::BUILTIN if !raw.macro_name.is_empty() => Self::Builtin { name: raw.macro_name }, Self::UNAVAILABLE => Self::Unavailable, _ => Self::Unavailable, } diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index c38f4334..3568f773 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1613,7 +1613,10 @@ endmodule .iter() .find(|token| token.raw_text == "3") .expect("intrinsic macro token should stay in emitted stream"); - assert!(matches!(intrinsic.provenance, PreprocessorTraceTokenProvenance::Unavailable)); + assert!(matches!( + &intrinsic.provenance, + PreprocessorTraceTokenProvenance::Builtin { name } if name == "__LINE__" + )); } #[test] diff --git a/crates/slang/include/slang/text/SourceManager.h b/crates/slang/include/slang/text/SourceManager.h index 5bbb7dbe..2cf83413 100644 --- a/crates/slang/include/slang/text/SourceManager.h +++ b/crates/slang/include/slang/text/SourceManager.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -66,8 +67,10 @@ class SLANG_EXPORT SourceManager { uint32_t bodyTokenIndex = InvalidIndex; uint32_t argumentIndex = InvalidIndex; uint32_t argumentTokenIndex = InvalidIndex; + std::string builtinName; - bool valid() const { return expansionId != 0 && callId != 0; } + bool valid() const { return expansionId != 0 && callId != 0 && + (definitionId != 0 || !builtinName.empty()); } }; /// Default constructor. diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index baaae174..881d5ca6 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -807,6 +807,18 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp loc, expansion.getRange(), SourceManager::MacroExpansionKind::Body, expansion.getMetadata()); expansion.setExpansionLoc(macroLoc); + auto provenance = + expansion.tokenProvenance(SourceManager::MacroTokenProvenance::InvalidIndex); + switch (intrinsic) { + case MacroIntrinsic::File: + provenance.builtinName = "__FILE__"; + break; + case MacroIntrinsic::Line: + provenance.builtinName = "__LINE__"; + break; + case MacroIntrinsic::None: + SLANG_UNREACHABLE; + } SmallVector text; switch (intrinsic) { case MacroIntrinsic::File: { @@ -817,7 +829,7 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp std::string_view rawText = toStringView(text.copy(alloc)); Token token(alloc, TokenKind::StringLiteral, {}, rawText, loc, fileName); - expansion.append(token, macroLoc); + expansion.append(token, macroLoc, false, provenance); break; } case MacroIntrinsic::Line: { @@ -826,7 +838,7 @@ bool Preprocessor::expandIntrinsic(MacroIntrinsic intrinsic, MacroExpansion& exp std::string_view rawText = toStringView(text.copy(alloc)); Token token(alloc, TokenKind::IntegerLiteral, {}, rawText, loc, lineNum); - expansion.append(token, macroLoc); + expansion.append(token, macroLoc, false, provenance); break; } case MacroIntrinsic::None: From 06a2f2ea6fbab0ee1b98e38ed88ba7752f2e143d Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 10:28:43 +0800 Subject: [PATCH 66/80] fix(hir): scope include-only preproc contexts --- crates/hir/src/preproc/helpers/context.rs | 9 ++++-- crates/hir/src/preproc/tests.rs | 38 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/hir/src/preproc/helpers/context.rs b/crates/hir/src/preproc/helpers/context.rs index ddb5e563..ae7e3a72 100644 --- a/crates/hir/src/preproc/helpers/context.rs +++ b/crates/hir/src/preproc/helpers/context.rs @@ -44,9 +44,14 @@ pub(in crate::preproc) fn source_preproc_single_query_contexts( ) -> SourcePreprocQueryContexts { let relevant = db.source_preproc_contexts_for_file(file_id); let mut file_ids = UniqVec::::default(); + let profile_id = db.file_compilation_profile(file_id); + let plan = db.compilation_plan_for_profile(profile_id); + let is_include_only = plan.include_only.contains(&file_id); let include_self = match db.file_kind(file_id) { - SourceFileKind::SystemVerilog => true, - SourceFileKind::IncludeHeader => relevant.model_file_ids.is_empty(), + SourceFileKind::SystemVerilog if !is_include_only => true, + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader => { + relevant.model_file_ids.is_empty() + } _ => false, }; if include_self { diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 46701cb9..96d79865 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -704,6 +704,44 @@ endmodule ); } +#[test] +fn preproc_include_only_sv_query_uses_all_including_roots() { + let top_a_text = r#"`define WIDTH 8 +`include "shared.sv" +"#; + let shared_text = "localparam int W = `WIDTH;\n"; + let top_b_text = r#"`define WIDTH 16 +`include "shared.sv" +"#; + let db = db_with_entries(&[ + (TOP, "rtl/top_a.sv", top_a_text), + (HEADER, "include/shared.sv", shared_text), + (LEAF, "rtl/top_b.sv", top_b_text), + ]); + + let plan = db.compilation_plan_for_profile(Some(PROFILE)); + assert!(plan.include_only.contains(&HEADER), "{plan:?}"); + assert!(plan.roots.contains(&TOP), "{plan:?}"); + assert!(plan.roots.contains(&LEAF), "{plan:?}"); + assert!(!plan.roots.contains(&HEADER), "{plan:?}"); + + let contexts = source_preproc_single_query_contexts(&db, HEADER); + assert!(contexts.model_file_ids.contains(&TOP), "{contexts:?}"); + assert!(contexts.model_file_ids.contains(&LEAF), "{contexts:?}"); + assert!(!contexts.model_file_ids.contains(&HEADER), "{contexts:?}"); + + let definitions = + macro_reference_definitions_at(&db, HEADER, offset(shared_text, "WIDTH")).unwrap().unwrap(); + + assert_eq!(definitions.definitions.len(), 2); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == TOP && text_at_range(top_a_text, definition.name_range) == "WIDTH" + })); + assert!(definitions.definitions.iter().any(|definition| { + definition.file_id == LEAF && text_at_range(top_b_text, definition.name_range) == "WIDTH" + })); +} + #[test] fn preproc_header_query_uses_including_context_over_standalone_model() { let root_text = r#"`define FEATURE 1 From e6c550efc843f087829ee892f4a31f4beee718aa Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 11:11:36 +0800 Subject: [PATCH 67/80] refactor: prefer using hir --- .../handlers/add_missing_connections.rs | 19 +++-- .../handlers/add_missing_parameters.rs | 17 ++-- .../handlers/convert_ordered_connections.rs | 68 +++++++++------- .../sort_named_instantiation_items.rs | 77 +++++++++++-------- crates/ide/src/module_resolution.rs | 15 +++- 5 files changed, 117 insertions(+), 79 deletions(-) diff --git a/crates/ide/src/code_action/handlers/add_missing_connections.rs b/crates/ide/src/code_action/handlers/add_missing_connections.rs index fec16050..607db3f6 100644 --- a/crates/ide/src/code_action/handlers/add_missing_connections.rs +++ b/crates/ide/src/code_action/handlers/add_missing_connections.rs @@ -1,13 +1,13 @@ use hir::{ base_db::source_db::SourceDb, container::InModule, db::HirDb, - hir_def::module::instantiation::PortConn, + hir_def::module::instantiation::PortConn, source_map::IsSrc, }; use rustc_hash::FxHashSet; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, }; -use utils::get::GetRef; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ @@ -15,7 +15,7 @@ use crate::{ apply_missing_list_edit, missing_member_entry_text, port_names, remaining_ordered_port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const ID: CodeActionId = CodeActionId { @@ -48,14 +48,13 @@ pub(super) fn add_missing_connections( let ast_instance = ctx.find_node_at_offset::()?; let InModule { value: instance_id, module_id } = sema.resolve_instance(file_id, ast_instance)?; - let module = db.module(module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); let instance = module.get(instance_id); let open_paren = ast_instance.open_paren()?.text_range_in(ast_instance.syntax())?; let close_paren = ast_instance.close_paren()?.text_range_in(ast_instance.syntax())?; - let instantiation = ast::HierarchyInstantiation::cast(ast_instance.syntax().parent()?)?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; + let instantiation = module.get(instance.parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let is_ordered = instance @@ -95,8 +94,8 @@ pub(super) fn add_missing_connections( .collect(); let text = sema.db.file_text(ctx.file_id()); - let item_ranges = ast_instance.connections().children().filter_map(|conn| { - let range = conn.syntax().text_range()?; + let item_ranges = instance.connections.iter().filter_map(|conn_id| { + let range = module_src_map.get(*conn_id)?.range(); (!range.is_empty()).then_some(range) }); apply_missing_list_edit(builder, &text, open_paren, close_paren, item_ranges, entries); diff --git a/crates/ide/src/code_action/handlers/add_missing_parameters.rs b/crates/ide/src/code_action/handlers/add_missing_parameters.rs index dc8c03f4..b9f466d7 100644 --- a/crates/ide/src/code_action/handlers/add_missing_parameters.rs +++ b/crates/ide/src/code_action/handlers/add_missing_parameters.rs @@ -1,13 +1,13 @@ use hir::{ base_db::source_db::SourceDb, container::InModule, db::HirDb, - hir_def::module::instantiation::ParamAssign, + hir_def::module::instantiation::ParamAssign, source_map::IsSrc, }; use rustc_hash::FxHashSet; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, }; -use utils::get::GetRef; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ @@ -15,7 +15,7 @@ use crate::{ all_parameter_names, apply_missing_list_edit, leading_parameter_names, missing_member_entry_text, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const ID: CodeActionId = CodeActionId { @@ -48,15 +48,14 @@ pub(super) fn add_missing_parameters( let ast_instantiation = ctx.find_node_at_offset::()?; let InModule { value: instantiation_id, module_id } = sema.resolve_instantiation(file_id, ast_instantiation)?; - let module = db.module(module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); let instantiation = module.get(instantiation_id); let params_node = ast_instantiation.parameters()?; let open_paren = params_node.open_paren()?.text_range_in(params_node.syntax())?; let close_paren = params_node.close_paren()?.text_range_in(params_node.syntax())?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), ast_instantiation).unique()?; + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let is_ordered = instantiation @@ -99,8 +98,8 @@ pub(super) fn add_missing_parameters( .collect(); let text = sema.db.file_text(ctx.file_id()); - let item_ranges = params_node.parameters().children().filter_map(|assign| { - let range = assign.syntax().text_range()?; + let item_ranges = instantiation.param_assigns.iter().filter_map(|assign_id| { + let range = module_src_map.get(*assign_id)?.range(); (!range.is_empty()).then_some(range) }); apply_missing_list_edit(builder, &text, open_paren, close_paren, item_ranges, entries); diff --git a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs index c8bc2090..52eb0ae3 100644 --- a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs @@ -1,18 +1,22 @@ use std::ops::Range; -use hir::{base_db::source_db::SourceDb, db::HirDb}; -use itertools::Itertools; -use syntax::{ - ast::{self, AstNode}, - has_text_range::HasTextRange, +use hir::{ + base_db::source_db::SourceDb, + container::InModule, + db::HirDb, + hir_def::module::instantiation::{ParamAssign, PortConn}, + source_map::IsSrc, }; +use itertools::Itertools; +use syntax::ast; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, RepairKind, leading_parameter_names, port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const PORTS_ID: CodeActionId = CodeActionId { @@ -49,21 +53,25 @@ pub(super) fn convert_ordered_ports( let db = sema.db; let text = db.file_text(ctx.file_id()); let ast_instance = ctx.find_node_at_offset::()?; - let instantiation = ast::HierarchyInstantiation::cast(ast_instance.syntax().parent()?)?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; - let target_module = db.module(target_module_id); - let port_names = port_names(&target_module); + let InModule { value: instance_id, module_id } = + sema.resolve_instance(ctx.file_id().into(), ast_instance)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(module.get(instance_id).parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let port_names = port_names(&db.module(target_module_id)); - let replacements = ast_instance - .connections() - .children() + let replacements = module + .get(instance_id) + .connections + .iter() .enumerate() - .filter_map(|(idx, conn)| { - let ordered = conn.as_ordered_port_connection()?; + .filter_map(|(idx, conn_id)| { + let PortConn::Ordered(expr_id) = module.get(*conn_id) else { + return None; + }; let name = port_names.get(idx)?; - let expr = ordered.expr().syntax().text_range()?; - let range = ordered.syntax().text_range()?; + let expr = module_src_map.get(*expr_id)?.range(); + let range = module_src_map.get(*conn_id)?.range(); Some((range, format!(".{name}({})", text.get(Range::from(expr))?))) }) .collect_vec(); @@ -101,21 +109,25 @@ pub(super) fn convert_ordered_params( let db = sema.db; let text = db.file_text(ctx.file_id()); let ast_instantiation = ctx.find_node_at_offset::()?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), ast_instantiation).unique()?; + let InModule { value: instantiation_id, module_id } = + sema.resolve_instantiation(ctx.file_id().into(), ast_instantiation)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(instantiation_id); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let param_names = leading_parameter_names(&target_module); - let replacements = ast_instantiation - .parameters()? - .parameters() - .children() + let replacements = instantiation + .param_assigns + .iter() .enumerate() - .filter_map(|(idx, assign)| { - let ordered = assign.as_ordered_param_assignment()?; + .filter_map(|(idx, assign_id)| { + let ParamAssign::Ordered(expr_id) = module.get(*assign_id) else { + return None; + }; let name = param_names.get(idx)?; - let expr = ordered.expr().syntax().text_range()?; - let range = ordered.syntax().text_range()?; + let expr = module_src_map.get(*expr_id)?.range(); + let range = module_src_map.get(*assign_id)?.range(); Some((range, format!(".{name}({})", text.get(Range::from(expr))?))) }) .collect_vec(); diff --git a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs index 16dd807d..9ee0c222 100644 --- a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs +++ b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs @@ -1,21 +1,29 @@ use std::ops::Range; -use hir::{base_db::source_db::SourceDb, db::HirDb}; +use hir::{ + base_db::source_db::SourceDb, + container::InModule, + db::HirDb, + hir_def::module::instantiation::{ParamAssign, PortConn}, + source_map::IsSrc, +}; use itertools::Itertools; use rustc_hash::FxHashMap; -use smol_str::ToSmolStr; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, +}; +use utils::{ + get::{Get, GetRef}, + text_edit::TextRange, }; -use utils::text_edit::TextRange; use crate::{ code_action::{ CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, all_parameter_names, line_indent, port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const SORT_NAMED_PARAMETER_ASSIGNMENTS_ID: CodeActionId = CodeActionId { @@ -41,25 +49,30 @@ pub(super) fn sort_named_parameter_assignments( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, ) -> Option<()> { - let instantiation = ctx.find_node_at_offset::()?; - let params = instantiation.parameters()?; + let ast_instantiation = ctx.find_node_at_offset::()?; + let params = ast_instantiation.parameters()?; let open = params.open_paren()?.text_range_in(params.syntax())?; let close = params.close_paren()?.text_range_in(params.syntax())?; - let db = ctx.sema().db; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; + let sema = ctx.sema(); + let db = sema.db; + let InModule { value: instantiation_id, module_id } = + sema.resolve_instantiation(ctx.file_id().into(), ast_instantiation)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(instantiation_id); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let parameter_order = all_parameter_names(&db.module(target_module_id)); let parameter_order_map: FxHashMap<_, _> = parameter_order.iter().enumerate().map(|(index, name)| (name.as_ref(), index)).collect(); - let text = ctx.sema().db.file_text(ctx.file_id()); + let text = sema.db.file_text(ctx.file_id()); let mut items = Vec::new(); - for assign in params.parameters().children() { - let named = assign.as_named_param_assignment()?; - let name = named.name()?.value_text().to_smolstr(); + for assign_id in instantiation.param_assigns.iter() { + let ParamAssign::Named(Some(name), _) = module.get(*assign_id) else { + return None; + }; let order = *parameter_order_map.get(name.as_str())?; - let range = assign.syntax().text_range()?; + let range = module_src_map.get(*assign_id)?.range(); items.push((order, text.get(Range::from(range))?, range)); } @@ -97,26 +110,30 @@ pub(super) fn sort_named_port_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, ) -> Option<()> { - let instance = ctx.find_node_at_offset::()?; - let instantiation = ast::HierarchyInstantiation::cast(instance.syntax().parent()?)?; - let open = instance.open_paren()?.text_range_in(instance.syntax())?; - let close = instance.close_paren()?.text_range_in(instance.syntax())?; - - let db = ctx.sema().db; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; - let target_module = db.module(target_module_id); - let port_order = port_names(&target_module); + let ast_instance = ctx.find_node_at_offset::()?; + let open = ast_instance.open_paren()?.text_range_in(ast_instance.syntax())?; + let close = ast_instance.close_paren()?.text_range_in(ast_instance.syntax())?; + + let sema = ctx.sema(); + let db = sema.db; + let InModule { value: instance_id, module_id } = + sema.resolve_instance(ctx.file_id().into(), ast_instance)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instance = module.get(instance_id); + let instantiation = module.get(instance.parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let port_order = port_names(&db.module(target_module_id)); let port_order_map: FxHashMap<_, _> = port_order.iter().enumerate().map(|(index, name)| (name.as_ref(), index)).collect(); - let text = ctx.sema().db.file_text(ctx.file_id()); + let text = sema.db.file_text(ctx.file_id()); let mut items = Vec::new(); - for conn in instance.connections().children() { - let named = conn.as_named_port_connection()?; - let name = named.name()?.value_text().to_smolstr(); + for conn_id in instance.connections.iter() { + let PortConn::Named(Some(name), _) = module.get(*conn_id) else { + return None; + }; let order = *port_order_map.get(name.as_str())?; - let range = conn.syntax().text_range()?; + let range = module_src_map.get(*conn_id)?.range(); items.push((order, text.get(Range::from(range))?, range)); } diff --git a/crates/ide/src/module_resolution.rs b/crates/ide/src/module_resolution.rs index e535f235..721c599e 100644 --- a/crates/ide/src/module_resolution.rs +++ b/crates/ide/src/module_resolution.rs @@ -5,8 +5,11 @@ use hir::{ container::InModule, db::HirDb, hir_def::{ - Ident, declaration::Declaration, expr::declarator::DeclaratorParent, lower_ident_opt, - module::ModuleId, + Ident, + declaration::Declaration, + expr::declarator::DeclaratorParent, + lower_ident_opt, + module::{ModuleId, instantiation::Instantiation}, }, scope::{ModuleEntry, ScopeResolution}, semantics::pathres::PathResolution, @@ -55,6 +58,14 @@ pub(crate) fn resolve_instantiation_target( resolve_module_name(db, from_file, &name) } +pub(crate) fn resolve_hir_instantiation_target( + db: &RootDb, + from_file: FileId, + instantiation: &Instantiation, +) -> Option { + resolve_module_name(db, from_file, instantiation.module_name.as_ref()?).unique() +} + pub(crate) fn resolve_module_name( db: &RootDb, from_file: FileId, From 0e15fa66191d5ef8524984ed3bb11516707b78c1 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 11:58:07 +0800 Subject: [PATCH 68/80] fix(preproc): connect builtin expansion provenance --- crates/hir/src/preproc.rs | 3 +- crates/hir/src/preproc/expansion.rs | 9 +-- crates/hir/src/preproc/helpers/expansion.rs | 56 ++++++++++++---- crates/hir/src/preproc/tests.rs | 66 +++++++++++++++++-- crates/hir/src/preproc/types.rs | 38 ++++++++++- crates/ide/src/diagnostics.rs | 3 + crates/ide/src/hover.rs | 27 ++++++-- crates/ide/src/verilog_2005.rs | 30 +++++++++ crates/preproc/src/source/model/tests.rs | 26 ++++++-- crates/preproc/src/source/provenance.rs | 10 ++- .../preproc/src/source/provenance/builder.rs | 62 +++++++++++++++-- crates/preproc/src/source/trace.rs | 25 +++++-- crates/preproc/src/source/types.rs | 8 +++ crates/slang/bindings/rust/ffi/wrapper.cc | 1 + crates/slang/bindings/rust/lib.rs | 31 ++++++++- crates/slang/bindings/rust/tests.rs | 3 +- 16 files changed, 345 insertions(+), 53 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index b1774015..f74e8153 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -4,7 +4,8 @@ use preproc::source::{ SourceMacroArgument as SourceMacroArgumentFact, SourceMacroCall as SourceMacroCallFact, SourceMacroCallId, SourceMacroCallStatus as SourceMacroCallStatusFact, SourceMacroDefinition as SourceMacroDefinitionFact, - SourceMacroExpansion as SourceMacroExpansionFact, SourceMacroExpansionId, + SourceMacroExpansion as SourceMacroExpansionFact, + SourceMacroExpansionDefinition as SourceMacroExpansionDefinitionFact, SourceMacroExpansionId, SourceMacroExpansionQuery as SourceMacroExpansionQueryFact, SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, SourceMacroParam as SourceMacroParamFact, SourceMacroReference as SourceMacroReferenceFact, diff --git a/crates/hir/src/preproc/expansion.rs b/crates/hir/src/preproc/expansion.rs index 2ce47c6f..4ddd6df5 100644 --- a/crates/hir/src/preproc/expansion.rs +++ b/crates/hir/src/preproc/expansion.rs @@ -285,8 +285,9 @@ fn apply_context_capability_to_macro_expansion( expansion: &mut MacroExpansion, ) { apply_context_capability_to_macro_call(contexts, &mut expansion.call); - expansion.definition.capability = - context_query_capability(contexts, expansion.definition.capability.clone()); + let definition_capability = + context_query_capability(contexts, expansion.definition.capability().clone()); + *expansion.definition.capability_mut() = definition_capability; expansion.capability = context_query_capability(contexts, expansion.capability.clone()); } @@ -350,12 +351,12 @@ fn apply_context_capability_to_macro_expansion_provenance( for token in &mut provenance.tokens { match &mut token.provenance { TokenProvenance::MacroBody { call, .. } - | TokenProvenance::MacroArgument { call, .. } => { + | TokenProvenance::MacroArgument { call, .. } + | TokenProvenance::Builtin { call, .. } => { apply_context_capability_to_macro_call(contexts, call); } TokenProvenance::SourceToken { .. } | TokenProvenance::Predefine { .. } - | TokenProvenance::Builtin { .. } | TokenProvenance::Unavailable(_) => {} } } diff --git a/crates/hir/src/preproc/helpers/expansion.rs b/crates/hir/src/preproc/helpers/expansion.rs index 37ba6f54..0d62b378 100644 --- a/crates/hir/src/preproc/helpers/expansion.rs +++ b/crates/hir/src/preproc/helpers/expansion.rs @@ -11,20 +11,12 @@ pub(in crate::preproc) fn map_macro_expansion( }), }); }; - let Some(definition) = mapped.model.macro_definitions().get(expansion.definition) else { - return Err(PreprocError::Unavailable { - reason: PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { - call: expansion.call, - }, - ), - }); - }; + let (definition_id, definition) = map_macro_expansion_definition(mapped, expansion)?; Ok(MacroExpansion { id: expansion.id.into(), call: map_macro_call(mapped, call)?, - definition_id: expansion.definition.into(), - definition: map_macro_definition(mapped, definition)?, + definition_id, + definition, emitted_token_range: expansion.emitted_token_range, display_source: map_expansion_display_source(mapped, expansion.id)?, display_range: mapped @@ -36,6 +28,36 @@ pub(in crate::preproc) fn map_macro_expansion( }) } +fn map_macro_expansion_definition( + mapped: &MappedSourcePreprocModel, + expansion: &SourceMacroExpansionFact, +) -> PreprocResult<(Option, MacroExpansionDefinition)> { + match &expansion.definition { + SourceMacroExpansionDefinitionFact::Source(definition_id) => { + let Some(definition) = mapped.model.macro_definitions().get(*definition_id) else { + return Err(PreprocError::Unavailable { + reason: PreprocUnavailable::Source( + SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { + call: expansion.call, + }, + ), + }); + }; + Ok(( + Some((*definition_id).into()), + MacroExpansionDefinition::Source(map_macro_definition(mapped, definition)?), + )) + } + SourceMacroExpansionDefinitionFact::Builtin { name } => Ok(( + None, + MacroExpansionDefinition::Builtin { + name: name.clone(), + capability: macro_expansion_availability(&expansion.status), + }, + )), + } +} + pub(in crate::preproc) fn map_expansion_display_source( mapped: &MappedSourcePreprocModel, expansion: SourceMacroExpansionId, @@ -326,8 +348,8 @@ pub(in crate::preproc) fn map_token_provenance( SourceTokenProvenanceFact::Predefine { source } => { TokenProvenance::Predefine { source: map_mapped_source_id(mapped, *source)? } } - SourceTokenProvenanceFact::Builtin { name } => { - TokenProvenance::Builtin { name: name.clone() } + SourceTokenProvenanceFact::Builtin { name, call, .. } => { + TokenProvenance::Builtin { name: name.clone(), call: mapped_macro_call(mapped, *call)? } } SourceTokenProvenanceFact::Unavailable(reason) => { TokenProvenance::Unavailable(PreprocUnavailable::Source(reason.clone())) @@ -379,7 +401,13 @@ pub(in crate::preproc) fn diagnostic_target_for_source_expansion( TokenProvenance::Unavailable(reason) => { saw_unavailable = Some(reason); } - TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } => {} + TokenProvenance::Predefine { .. } => {} + TokenProvenance::Builtin { call, name } => { + return Ok(DiagnosticProvenance::Builtin { + call: call.clone(), + name: name.clone(), + }); + } } } diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 96d79865..de2392d6 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -245,18 +245,72 @@ endmodule let wrap_expansion = recursive .expansions .iter() - .find(|expansion| expansion.definition.name.as_str() == "WRAP") + .find(|expansion| expansion.definition.name().as_str() == "WRAP") .expect("outer expansion should be mapped"); let leaf_expansion = recursive .expansions .iter() - .find(|expansion| expansion.definition.name.as_str() == "LEAF") + .find(|expansion| expansion.definition.name().as_str() == "LEAF") .expect("nested expansion should be mapped"); assert_eq!(text_at_range(root_text, wrap_expansion.call.range), "`WRAP"); assert_eq!(text_at_range(root_text, leaf_expansion.call.range), "`LEAF"); assert_eq!(wrap_expansion.child_calls, vec![leaf_expansion.call.id]); } +#[test] +fn preproc_builtin_intrinsic_expansion_uses_structured_provenance() { + let root_text = r#"module m; +localparam int L = `__LINE__; +localparam string F = `__FILE__; +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + let line_offset = offset(root_text, "`__LINE__"); + let file_offset = offset(root_text, "`__FILE__"); + for (offset, expected_name) in [(line_offset, "__LINE__"), (file_offset, "__FILE__")] { + let immediate = + immediate_macro_expansion_at(&db, TOP, offset).unwrap().expect("builtin call expected"); + let MacroExpansionQuery::Available(immediate) = immediate else { + panic!("builtin macro expansion should be available"); + }; + assert_eq!(immediate.definition.name().as_str(), expected_name); + assert!(matches!( + immediate.definition, + MacroExpansionDefinition::Builtin { name, .. } if name.as_str() == expected_name + )); + + let recursive = + recursive_macro_expansion_at(&db, TOP, offset).unwrap().expect("recursive expected"); + assert!(recursive.unavailable.is_empty()); + assert!(recursive.expansions.iter().any(|expansion| { + matches!( + &expansion.definition, + MacroExpansionDefinition::Builtin { name, .. } if name.as_str() == expected_name + ) + })); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset).unwrap().expect("provenance expected"); + assert!(provenance.tokens.iter().any(|token| { + matches!( + &token.provenance, + TokenProvenance::Builtin { name, call } + if name.as_str() == expected_name && call.range == provenance.expansion.call.range + ) + })); + + let diagnostic = diagnostic_provenance_for_range(&db, TOP, provenance.expansion.call.range) + .unwrap() + .expect("diagnostic provenance expected"); + assert!(matches!( + diagnostic, + DiagnosticProvenance::Builtin { name, call } + if name.as_str() == expected_name && call.range == provenance.expansion.call.range + )); + } +} + #[test] fn preproc_macro_expansion_exposes_display_virtual_source_and_token_provenance() { let root_text = r#"`define MAKE_DECL(name) logic name; @@ -366,20 +420,20 @@ endmodule assert!(queries.iter().any(|query| matches!( query, MacroExpansionQuery::Available(expansion) - if expansion.definition.name.as_str() == "NEXT" + if expansion.definition.name().as_str() == "NEXT" ))); assert!(queries.iter().any(|query| matches!( query, MacroExpansionQuery::Available(expansion) - if expansion.definition.name.as_str() == "PAYL" + if expansion.definition.name().as_str() == "PAYL" ))); assert!(!queries.iter().any(|query| matches!(query, MacroExpansionQuery::Unavailable(_)))); assert!(matches!( immediate_macro_expansion_at(&db, TOP, payl_offset), Ok(Some(MacroExpansionQuery::Ambiguous(expansions))) if expansions.len() == 2 - && expansions.iter().any(|expansion| expansion.definition.name.as_str() == "NEXT") - && expansions.iter().any(|expansion| expansion.definition.name.as_str() == "PAYL") + && expansions.iter().any(|expansion| expansion.definition.name().as_str() == "NEXT") + && expansions.iter().any(|expansion| expansion.definition.name().as_str() == "PAYL") )); assert!(matches!( macro_expansion_provenance_at(&db, TOP, payl_offset), diff --git a/crates/hir/src/preproc/types.rs b/crates/hir/src/preproc/types.rs index da234d16..6f6421da 100644 --- a/crates/hir/src/preproc/types.rs +++ b/crates/hir/src/preproc/types.rs @@ -285,8 +285,8 @@ pub struct MacroArgument { pub struct MacroExpansion { pub id: MacroExpansionId, pub call: MacroCall, - pub definition_id: MacroDefinitionId, - pub definition: MacroDefinition, + pub definition_id: Option, + pub definition: MacroExpansionDefinition, pub emitted_token_range: SourceEmittedTokenRange, pub display_source: MappedPreprocSource, pub display_range: TextRange, @@ -294,6 +294,35 @@ pub struct MacroExpansion { pub capability: PreprocAvailability, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroExpansionDefinition { + Source(MacroDefinition), + Builtin { name: SmolStr, capability: PreprocAvailability }, +} + +impl MacroExpansionDefinition { + pub fn name(&self) -> &SmolStr { + match self { + Self::Source(definition) => &definition.name, + Self::Builtin { name, .. } => name, + } + } + + pub fn capability(&self) -> &PreprocAvailability { + match self { + Self::Source(definition) => &definition.capability, + Self::Builtin { capability, .. } => capability, + } + } + + pub fn capability_mut(&mut self) -> &mut PreprocAvailability { + match self { + Self::Source(definition) => &mut definition.capability, + Self::Builtin { capability, .. } => capability, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroExpansionProvenance { pub expansion: MacroExpansion, @@ -331,6 +360,7 @@ pub enum TokenProvenance { }, Builtin { name: SmolStr, + call: MacroCall, }, Unavailable(PreprocUnavailable), } @@ -357,6 +387,10 @@ pub enum DiagnosticProvenance { source: MappedPreprocSource, range: TextRange, }, + Builtin { + call: MacroCall, + name: SmolStr, + }, Unavailable(PreprocUnavailable), } diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index f052bc5e..2d5ce57e 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -390,6 +390,9 @@ fn diagnostic_preproc_target_file_range( | hir::preproc::DiagnosticProvenance::VirtualExpansion { source, range } => { Some((source.file_id()?, *range)) } + hir::preproc::DiagnosticProvenance::Builtin { call, .. } => { + Some((call.file_id, call.range)) + } hir::preproc::DiagnosticProvenance::Unavailable(_) => None, } } diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 0673edd5..16ae216d 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -7,10 +7,11 @@ use hir::{ file::HirFileId, hir_def::expr::Expr, preproc::{ - EmittedTokenProvenance, IncludeTarget, MacroDefinition, MacroParamDefinition, - MacroReferenceDefinitions, RecursiveMacroExpansionProvenance, include_directives_at, - macro_definition_at, macro_param_definition_at, macro_param_reference_definitions_at, - macro_reference_definitions_at, recursive_macro_expansion_provenances_at, + EmittedTokenProvenance, IncludeTarget, MacroDefinition, MacroExpansionDefinition, + MacroParamDefinition, MacroReferenceDefinitions, RecursiveMacroExpansionProvenance, + include_directives_at, macro_definition_at, macro_param_definition_at, + macro_param_reference_definitions_at, macro_reference_definitions_at, + recursive_macro_expansion_provenances_at, }, semantics::Semantics, }; @@ -247,11 +248,20 @@ fn render_recursive_expansion( render_macro_expansion_separator(markup); markup.push_with_code_fence(&expanded_text_from_tokens(&root.tokens)); render_macro_expansion_separator(markup); - render_macro_source_link(db, markup, &root.expansion.definition, root.expansion.call.file_id); + if let MacroExpansionDefinition::Source(definition) = &root.expansion.definition { + render_macro_source_link(db, markup, definition, root.expansion.call.file_id); + } } -fn render_macro_expansion_header(markup: &mut Markup, definition: &MacroDefinition) { - markup.push_with_code_fence(¯o_signature(definition)); +fn render_macro_expansion_header(markup: &mut Markup, definition: &MacroExpansionDefinition) { + match definition { + MacroExpansionDefinition::Source(definition) => { + markup.push_with_code_fence(¯o_signature(definition)); + } + MacroExpansionDefinition::Builtin { name, .. } => { + markup.push_with_code_fence(&format!("`{name}")); + } + } } fn render_macro_expansion_separator(markup: &mut Markup) { @@ -425,6 +435,9 @@ fn handle_preproc_macro( if let Ok(Some(resolution)) = macro_reference_definitions_at(db, file_id, offset) { if resolution.definitions.is_empty() { + if let Some(hover) = expanded_macro_hover(db, file_id, offset, Some(&resolution)) { + return Some(PreprocMacroHover { hover, reference_definitions: Some(resolution) }); + } return None; } let hover = RangeInfo::new( diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index f5ed6baf..5b7367a2 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1519,6 +1519,36 @@ endmodule ); } +#[test] +fn preproc_builtin_macro_hover_shows_expanded_text() { + let text = r#" +module top; + localparam int L = `/*marker:line*/__LINE__; + localparam string F = `/*marker:file*/__FILE__; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + + for (marker, name) in [("line", "__LINE__"), ("file", "__FILE__")] { + let hover = analysis + .hover( + position(file_id, &markers, marker), + HoverConfig { format: HoverFormat::PlainText }, + ) + .unwrap() + .expect("builtin macro hover expected"); + let info = hover.info.as_str(); + assert!( + info.contains("```systemverilog") + && info.contains(&format!("`{name}")) + && info.contains("--------------------") + && !info.contains("unavailable"), + "builtin macro hover should show structured expansion: {info}" + ); + } +} + #[test] fn preproc_macro_hover_shows_nested_compact_expansion() { let text = r#" diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index ddf4e5ee..cee8fee5 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -431,7 +431,7 @@ logic [`HEADER_WIDTH-1:0] data; assert_eq!(call.expansion, Some(expansion_id)); let expansion = model.macro_expansions().get(expansion_id).unwrap(); assert_eq!(expansion.call, call.id); - assert_eq!(*resolved_definition, expansion.definition); + assert_eq!(expansion.definition, SourceMacroExpansionDefinition::Source(*resolved_definition)); assert!(expansion.child_calls.is_empty()); assert_eq!(expansion.status, SourceMacroExpansionStatus::Complete); @@ -1241,10 +1241,26 @@ endmodule .iter() .find(|token| token.text.as_str() == "3") .expect("intrinsic macro token should stay in emitted stream"); - assert!(matches!( - model.token_provenance().get(intrinsic.provenance).unwrap(), - SourceTokenProvenance::Builtin { name } if name.as_str() == "__LINE__" - )); + let SourceTokenProvenance::Builtin { name, call, identity } = + model.token_provenance().get(intrinsic.provenance).unwrap() + else { + panic!("intrinsic macro token should have builtin provenance"); + }; + assert_eq!(name.as_str(), "__LINE__"); + assert_ne!(identity.call.raw(), 0); + assert_ne!(identity.expansion.raw(), 0); + + let call = model.macro_calls().get(*call).expect("builtin provenance should map to a call"); + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("builtin macro call should have an immediate expansion"); + }; + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!( + expansion.definition, + SourceMacroExpansionDefinition::Builtin { name: "__LINE__".into() } + ); } #[test] diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index 773663e0..bf1119c8 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -237,12 +237,18 @@ pub struct SourceMacroExpansion { pub id: SourceMacroExpansionId, pub identity: Option, pub call: SourceMacroCallId, - pub definition: SourceMacroDefinitionId, + pub definition: SourceMacroExpansionDefinition, pub emitted_token_range: SourceEmittedTokenRange, pub child_calls: Vec, pub status: SourceMacroExpansionStatus, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceMacroExpansionDefinition { + Source(SourceMacroDefinitionId), + Builtin { name: SmolStr }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourceMacroExpansionStatus { Complete, @@ -314,6 +320,8 @@ pub enum SourceTokenProvenance { }, Builtin { name: SmolStr, + identity: SourceMacroBuiltinIdentity, + call: SourceMacroCallId, }, Unavailable(SourcePreprocUnavailable), } diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index 0d0a8292..953bcb02 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -472,8 +472,8 @@ impl<'a> SourcePreprocModelBuilder<'a> { *body_token_range, *argument_token_range, ), - SourceTokenProvenanceFact::Builtin { name } if !name.is_empty() => { - SourceTokenProvenance::Builtin { name: name.clone() } + SourceTokenProvenanceFact::Builtin { name, identity } if !name.is_empty() => { + self.resolve_builtin_token_provenance(token_id, name.clone(), *identity) } SourceTokenProvenanceFact::Builtin { .. } | SourceTokenProvenanceFact::Unavailable => { self.unavailable_token_provenance( @@ -590,6 +590,32 @@ impl<'a> SourcePreprocModelBuilder<'a> { } } + fn resolve_builtin_token_provenance( + &mut self, + _token_id: SourceEmittedTokenId, + name: SmolStr, + identity: Option, + ) -> SourceTokenProvenance { + let Some(identity) = identity else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + let Some(call) = self.call_ids_by_identity.get(&identity.call).copied() else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, + }, + ); + }; + if let Err(reason) = + self.record_call_expansion_identity(call, identity.expansion, identity.parent_expansion) + { + return self.unavailable_token_provenance(reason); + } + SourceTokenProvenance::Builtin { name, identity, call } + } + fn call_for_emitted_token( &mut self, request: EmittedTokenMacroCall, @@ -806,7 +832,8 @@ impl<'a> SourcePreprocModelBuilder<'a> { ); continue; }; - let Ok(definition) = self.definition_for_call(call) else { + let Some(definition) = self.expansion_definition_for_call(call, &direct_tokens_by_call) + else { self.mark_call_unavailable( call, SourcePreprocUnavailable::MissingEmittedTokenMacroDefinition { call }, @@ -897,10 +924,10 @@ impl<'a> SourcePreprocModelBuilder<'a> { SourceTokenProvenance::MacroBody { call, .. } | SourceTokenProvenance::MacroArgument { call, .. } | SourceTokenProvenance::TokenPaste { call, .. } - | SourceTokenProvenance::Stringification { call, .. } => *call, + | SourceTokenProvenance::Stringification { call, .. } + | SourceTokenProvenance::Builtin { call, .. } => *call, SourceTokenProvenance::Source { .. } | SourceTokenProvenance::Predefine { .. } - | SourceTokenProvenance::Builtin { .. } | SourceTokenProvenance::Unavailable(_) => continue, }; tokens_by_call.entry(call).or_default().push(token.id); @@ -908,6 +935,31 @@ impl<'a> SourcePreprocModelBuilder<'a> { tokens_by_call } + fn expansion_definition_for_call( + &self, + call: SourceMacroCallId, + direct_tokens_by_call: &BTreeMap>, + ) -> Option { + if let Ok(definition) = self.definition_for_call(call) { + return Some(SourceMacroExpansionDefinition::Source(definition)); + } + + let mut builtin_name = None; + for token_id in direct_tokens_by_call.get(&call)? { + let token = self.tables.emitted_tokens.get(*token_id)?; + let provenance = self.tables.token_provenance.get(token.provenance)?; + let SourceTokenProvenance::Builtin { name, .. } = provenance else { + continue; + }; + match &builtin_name { + Some(existing) if existing != name => return None, + Some(_) => {} + None => builtin_name = Some(name.clone()), + } + } + builtin_name.map(|name| SourceMacroExpansionDefinition::Builtin { name }) + } + fn child_calls_by_parent(&mut self) -> BTreeMap> { let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); let mut children = BTreeMap::>::new(); diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index 565a2b72..6c3b8fbd 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -4,10 +4,10 @@ use smol_str::{SmolStr, ToSmolStr}; use syntax::{ PreprocessorTrace, PreprocessorTraceActualArgument, PreprocessorTraceEmittedToken, PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, - PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroCallId, - PreprocessorTraceMacroDefinitionId, PreprocessorTraceMacroExpansionId, - PreprocessorTraceMacroParam, PreprocessorTraceToken, PreprocessorTraceTokenProvenance, - SourceBufferOrigin, SourceBufferRange, SyntaxKind, + PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroBuiltinIdentity, + PreprocessorTraceMacroCallId, PreprocessorTraceMacroDefinitionId, + PreprocessorTraceMacroExpansionId, PreprocessorTraceMacroParam, PreprocessorTraceToken, + PreprocessorTraceTokenProvenance, SourceBufferOrigin, SourceBufferRange, SyntaxKind, }; use utils::line_index::{TextRange, TextSize}; @@ -63,6 +63,16 @@ impl From for SourceMacroArgumentIdentit } } +impl From for SourceMacroBuiltinIdentity { + fn from(value: PreprocessorTraceMacroBuiltinIdentity) -> Self { + Self { + call: SourceMacroCallKey::from(value.call_id), + expansion: SourceMacroExpansionKey::from(value.expansion_id), + parent_expansion: value.parent_expansion_id.map(SourceMacroExpansionKey::from), + } + } +} + impl SourcePreprocIndex { pub fn from_trace(trace: PreprocessorTrace) -> Result { let root_source = PreprocSourceId::from(trace.root_buffer_id); @@ -326,8 +336,11 @@ fn emitted_token_provenance_from_trace( argument_token_range, } } - PreprocessorTraceTokenProvenance::Builtin { name } if !name.is_empty() => { - SourceTokenProvenanceFact::Builtin { name: name.to_smolstr() } + PreprocessorTraceTokenProvenance::Builtin { name, identity } if !name.is_empty() => { + SourceTokenProvenanceFact::Builtin { + name: name.to_smolstr(), + identity: Some(SourceMacroBuiltinIdentity::from(identity)), + } } PreprocessorTraceTokenProvenance::Builtin { .. } => SourceTokenProvenanceFact::Unavailable, PreprocessorTraceTokenProvenance::Unavailable => SourceTokenProvenanceFact::Unavailable, diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index 5d94aad2..05e21c3d 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -88,6 +88,13 @@ pub struct SourceMacroArgumentIdentity { pub argument_token_index: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceMacroBuiltinIdentity { + pub call: SourceMacroCallKey, + pub expansion: SourceMacroExpansionKey, + pub parent_expansion: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocSource { pub id: PreprocSourceId, @@ -243,6 +250,7 @@ pub enum SourceTokenProvenanceFact { }, Builtin { name: SmolStr, + identity: Option, }, Unavailable, } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index a40ec58d..feb3b93d 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -512,6 +512,7 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( auto directProvenance = sourceManager.getMacroTokenProvenance(location); if (has_builtin_macro_token_provenance(directProvenance)) { result.macro_name = rust::String(directProvenance->builtinName); + apply_direct_macro_token_provenance(result, *directProvenance); result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; return result; } diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index e0032a78..453446a7 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -210,6 +210,7 @@ pub enum PreprocessorTraceTokenProvenance { }, Builtin { name: String, + identity: PreprocessorTraceMacroBuiltinIdentity, }, Unavailable, } @@ -234,6 +235,13 @@ pub struct PreprocessorTraceMacroArgumentIdentity { pub argument_token_index: u32, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceMacroBuiltinIdentity { + pub call_id: PreprocessorTraceMacroCallId, + pub expansion_id: PreprocessorTraceMacroExpansionId, + pub parent_expansion_id: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocessorTraceToken { pub raw_text: String, @@ -601,7 +609,13 @@ impl PreprocessorTraceTokenProvenance { argument_token_range, } } - Self::BUILTIN if !raw.macro_name.is_empty() => Self::Builtin { name: raw.macro_name }, + Self::BUILTIN if !raw.macro_name.is_empty() => { + let Some(identity) = PreprocessorTraceMacroBuiltinIdentity::from_raw(&raw.identity) + else { + return Self::Unavailable; + }; + Self::Builtin { name: raw.macro_name, identity } + } Self::UNAVAILABLE => Self::Unavailable, _ => Self::Unavailable, } @@ -650,6 +664,21 @@ impl PreprocessorTraceMacroArgumentIdentity { } } +impl PreprocessorTraceMacroBuiltinIdentity { + #[inline] + fn from_raw(raw: &RawPreprocessorTraceMacroIdentity) -> Option { + Some(Self { + call_id: raw.has_call_id.then_some(PreprocessorTraceMacroCallId(raw.call_id))?, + expansion_id: raw + .has_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.expansion_id))?, + parent_expansion_id: raw + .has_parent_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.parent_expansion_id)), + }) + } +} + impl PreprocessorTraceToken { #[inline] fn from_raw(raw: ffi::RawPreprocessorTraceToken) -> Option { diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index 3568f773..f3958bca 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1615,7 +1615,8 @@ endmodule .expect("intrinsic macro token should stay in emitted stream"); assert!(matches!( &intrinsic.provenance, - PreprocessorTraceTokenProvenance::Builtin { name } if name == "__LINE__" + PreprocessorTraceTokenProvenance::Builtin { name, identity } + if name == "__LINE__" && identity.call_id.0 != 0 && identity.expansion_id.0 != 0 )); } From e45bb830adfa8bb24107edb905ea542581200968 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 12:26:52 +0800 Subject: [PATCH 69/80] fix(preproc): keep zero-token macro expansions available --- crates/hir/src/preproc/tests.rs | 35 ++++++++++++++++ crates/preproc/src/source/model/tests.rs | 42 ++++++++++++++++--- .../preproc/src/source/provenance/builder.rs | 42 +++++++------------ .../source/parsing/Preprocessor_macros.cpp | 26 +++++++----- 4 files changed, 103 insertions(+), 42 deletions(-) diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index de2392d6..0b1fbc24 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -311,6 +311,41 @@ endmodule } } +#[test] +fn preproc_zero_token_macro_expansion_is_available() { + let root_text = r#"`define EMPTY +`define DROP(x) +module top; +`EMPTY +`DROP(foo) +endmodule +"#; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + + for name in ["`EMPTY", "`DROP"] { + let immediate = + immediate_macro_expansion_at(&db, TOP, offset(root_text, name)).unwrap().unwrap(); + let MacroExpansionQuery::Available(immediate) = immediate else { + panic!("{name} expansion should be available"); + }; + assert_eq!(immediate.emitted_token_range.len, 0); + + let provenance = + macro_expansion_provenance_at(&db, TOP, offset(root_text, name)).unwrap().unwrap(); + assert!(provenance.tokens.is_empty()); + assert_eq!(provenance.expansion.emitted_token_range.len, 0); + + let mapped = db.source_preproc_model(TOP); + let mapped = mapped.as_ref().as_ref().unwrap(); + let display_text = mapped + .source_map + .expansion_display_text(SourceMacroExpansionId::new(provenance.expansion.id.raw())) + .unwrap(); + assert_eq!(display_text, ""); + assert_eq!(provenance.expansion.display_range, TextRange::empty(TextSize::from(0))); + } +} + #[test] fn preproc_macro_expansion_exposes_display_virtual_source_and_token_provenance() { let root_text = r#"`define MAKE_DECL(name) logic name; diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index cee8fee5..7cb56159 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -1198,13 +1198,45 @@ fn source_model_does_not_create_expansion_without_emitted_token_authority() { assert!(matches!( model.immediate_macro_expansion(call.id), SourceMacroExpansionQuery::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable + SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } ) )); - assert!(matches!( - &model.capabilities().macro_expansions, - CapabilityStatus::Unavailable(SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable) - )); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); +} + +#[test] +fn source_model_keeps_zero_token_macro_expansion_available() { + let root_text = r#"`define EMPTY +`define DROP(x) +module top; +`EMPTY +`DROP(foo) +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + for name in ["EMPTY", "DROP"] { + let call = model + .macro_calls() + .iter() + .find(|call| { + model + .macro_references() + .get(call.reference) + .is_some_and(|reference| reference.name.as_str() == name) + }) + .unwrap_or_else(|| panic!("{name} call should be traced")); + let SourceMacroExpansionQuery::Available(expansion_id) = + model.immediate_macro_expansion(call.id) + else { + panic!("{name} zero-token expansion should be available: {call:?}"); + }; + let expansion = model.macro_expansions().get(expansion_id).unwrap(); + assert_eq!(expansion.emitted_token_range.len, 0); + assert_eq!(expansion.call, call.id); + assert_eq!(expansion.status, SourceMacroExpansionStatus::Complete); + } + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Complete); } #[test] diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index 953bcb02..d67e9b24 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -55,10 +55,6 @@ impl<'a> SourcePreprocModelBuilder<'a> { self.record_macro_body_references_for_calls(); let macro_expansions = if self.tables.macro_calls.is_empty() { CapabilityStatus::Complete - } else if self.index.emitted_tokens.is_empty() { - CapabilityStatus::Unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, - ) } else { partial_status(self.expansions_partial) }; @@ -786,13 +782,6 @@ impl<'a> SourcePreprocModelBuilder<'a> { return; } - if self.index.emitted_tokens.is_empty() { - self.mark_all_calls_unavailable( - SourcePreprocUnavailable::EmittedTokenAuthorityUnavailable, - ); - return; - } - let direct_tokens_by_call = self.direct_emitted_tokens_by_call(); let child_calls_by_parent = self.child_calls_by_parent(); let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); @@ -821,14 +810,12 @@ impl<'a> SourcePreprocModelBuilder<'a> { ); continue; }; - let Some(emitted_token_range) = tokens.contiguous_emitted_range() else { + let Some(emitted_token_range) = tokens.contiguous_emitted_range( + SourceEmittedTokenId::new(self.tables.emitted_tokens.len()), + ) else { self.mark_call_unavailable( call, - if tokens.is_empty() { - SourcePreprocUnavailable::ExpansionAuthorityUnavailable - } else { - SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call } - }, + SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call }, ); continue; }; @@ -1023,13 +1010,6 @@ impl<'a> SourcePreprocModelBuilder<'a> { tokens } - fn mark_all_calls_unavailable(&mut self, reason: SourcePreprocUnavailable) { - let call_ids = self.tables.macro_calls.iter().map(|call| call.id).collect::>(); - for call in call_ids { - self.mark_call_unavailable(call, reason.clone()); - } - } - fn mark_call_unavailable(&mut self, call: SourceMacroCallId, reason: SourcePreprocUnavailable) { self.expansions_partial = true; if let Some(call) = self.tables.macro_calls.get_mut(call) { @@ -1276,12 +1256,20 @@ impl SourceMacroTokenExt for SourceMacroToken { } trait SourceEmittedTokenIdSliceExt { - fn contiguous_emitted_range(&self) -> Option; + fn contiguous_emitted_range( + &self, + empty_start: SourceEmittedTokenId, + ) -> Option; } impl SourceEmittedTokenIdSliceExt for [SourceEmittedTokenId] { - fn contiguous_emitted_range(&self) -> Option { - let first = *self.first()?; + fn contiguous_emitted_range( + &self, + empty_start: SourceEmittedTokenId, + ) -> Option { + let Some(first) = self.first().copied() else { + return Some(SourceEmittedTokenRange { start: empty_start, len: 0 }); + }; let last = *self.last()?; let len = last.raw().checked_sub(first.raw())? + 1; (len == self.len()).then_some(SourceEmittedTokenRange { start: first, len }) diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index 881d5ca6..670acb23 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -399,17 +399,27 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, const DefineDirectiveSyntax* directive = macro.syntax; SLANG_ASSERT(directive); - // ignore empty macro + std::string_view macroName = directive->name.valueText(); + SourceRange expansionRange = expansion.getRange(); + if (actualArgs) { + Token endOfArgs = actualArgs->getLastToken(); + expansionRange = SourceRange(expansion.getRange().start(), + endOfArgs.location() + endOfArgs.rawText().length()); + } + + // Empty macros emit no tokens, but still need an expansion identity for trace consumers. const auto& body = directive->body; - if (body.empty()) + if (body.empty()) { + SourceLocation expansionLoc = sourceManager.createExpansionLoc( + expansionRange.start(), expansionRange, macroName, expansion.getMetadata()); + expansion.setExpansionLoc(expansionLoc); return true; - - std::string_view macroName = directive->name.valueText(); + } if (!directive->formalArguments) { // each macro expansion gets its own location entry SourceLocation start = body[0].location(); - SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansion.getRange(), + SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, macroName, expansion.getMetadata()); expansion.setExpansionLoc(expansionLoc); @@ -417,7 +427,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, // simple macro; just take body tokens uint32_t bodyTokenIndex = 0; for (auto token : body) { - expansion.append(token, expansionLoc, start, expansion.getRange(), false, + expansion.append(token, expansionLoc, start, expansionRange, false, expansion.tokenProvenance(bodyTokenIndex)); bodyTokenIndex++; } @@ -470,10 +480,6 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, argumentMap.emplace(name, ArgTokens(*tokenList, uint32_t(i))); } - Token endOfArgs = actualArgs->getLastToken(); - SourceRange expansionRange(expansion.getRange().start(), - endOfArgs.location() + endOfArgs.rawText().length()); - SourceLocation start = body[0].location(); SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, macroName, From 261a26f4158868992878fa016c104218214be2b7 Mon Sep 17 00:00:00 2001 From: roife Date: Mon, 8 Jun 2026 12:33:51 +0800 Subject: [PATCH 70/80] Optimize release builds and gate profiling feature --- .github/workflows/release.yml | 2 ++ Cargo.toml | 5 ++- src/main.rs | 58 +++++++++++++++++++++-------------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4994ec7a..740f3e19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ concurrency: env: EMSDK_VERSION: 5.0.2 + CARGO_PROFILE_RELEASE_INCREMENTAL: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && 'false' || 'true' }} jobs: changelog: @@ -192,6 +193,7 @@ jobs: docker run --rm \ -e CARGO_TARGET_DIR="${CARGO_TARGET_DIR}" \ + -e CARGO_PROFILE_RELEASE_INCREMENTAL="${CARGO_PROFILE_RELEASE_INCREMENTAL}" \ -v "${GITHUB_WORKSPACE}:/workspace" \ -v "${build_script}:/tmp/build-linux-x64-manylinux2014.sh:ro" \ -w /workspace \ diff --git a/Cargo.toml b/Cargo.toml index b59fbfdd..948c24c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ schemars = { version = "1.2.1", features = ["preserve_order"], optional = true } thiserror.workspace = true toml.workspace = true tracing-subscriber = { version = "0.3.17", default-features = false, features = ["registry", "fmt", "tracing-log",] } -tracing-chrome = "0.7.2" +tracing-chrome = { version = "0.7.2", optional = true } tracing.workspace = true triomphe.workspace = true @@ -115,6 +115,8 @@ salsa.opt-level = 3 incremental = true debug = 0 strip = "symbols" +lto = "thin" +codegen-units = 1 [profile.release-lto] inherits = "release" @@ -127,4 +129,5 @@ winapi.workspace = true utils = { workspace = true, features = ["test-support"] } [features] +profile-trace = ["dep:tracing-chrome"] user-config-schema = ["dep:schemars"] diff --git a/src/main.rs b/src/main.rs index 54822b36..0e1b9996 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,4 @@ -use std::{ - env, fs, io, - path::{Path, PathBuf}, -}; +use std::{env, fs, io, path::PathBuf}; use anyhow::Context; use clap::Parser; @@ -11,6 +8,7 @@ use tracing_subscriber::{ }; use vide::{Opt, run_server}; +#[cfg(feature = "profile-trace")] const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vide=trace,", "hir::base_db=trace,", @@ -23,21 +21,17 @@ const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vfs::notify=trace" ); +#[cfg(feature = "profile-trace")] +type ProfileTraceGuard = tracing_chrome::FlushGuard; + +#[cfg(not(feature = "profile-trace"))] +type ProfileTraceGuard = (); + fn profile_trace_path(opt: &Opt) -> Option { opt.profile_trace.clone().or_else(|| env::var_os("VIDE_PROFILE_TRACE").map(PathBuf::from)) } -fn create_profile_trace_file(path: &Path) -> anyhow::Result { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).with_context(|| { - format!("could not create profile trace directory: {}", parent.display()) - })?; - } - fs::File::create(path) - .with_context(|| format!("could not create profile trace file: {}", path.display())) -} - -fn setup_logging(opt: &Opt) -> anyhow::Result> { +fn setup_logging(opt: &Opt) -> anyhow::Result> { let target: Targets = opt.log.parse().with_context(|| format!("invalid log filter: `{}`", opt.log))?; @@ -59,12 +53,22 @@ fn setup_logging(opt: &Opt) -> anyhow::Result tracing_subscriber::fmt::layer().with_ansi(false).with_writer(writer).with_filter(target); let subscriber = Registry::default().with(fmt_layer); - let profile_guard = if let Some(path) = profile_trace_path(opt) { + + let requested_profile_trace_path = profile_trace_path(opt); + + #[cfg(feature = "profile-trace")] + if let Some(path) = requested_profile_trace_path { let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); let profile_filter = profile_filter_text.parse::().context("invalid profile trace filter")?; - let file = create_profile_trace_file(&path)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!("could not create profile trace directory: {}", parent.display()) + })?; + } + let file = fs::File::create(&path) + .with_context(|| format!("could not create profile trace file: {}", path.display()))?; let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() .writer(file) .include_args(true) @@ -76,13 +80,21 @@ fn setup_logging(opt: &Opt) -> anyhow::Result filter = %profile_filter_text, "profile trace enabled" ); - Some(guard) - } else { - subscriber.init(); - None - }; + return Ok(Some(guard)); + } + + #[cfg(not(feature = "profile-trace"))] + if let Some(path) = requested_profile_trace_path { + anyhow::bail!( + "profile tracing was requested for {}, but this binary was built without the \ + `profile-trace` feature; rebuild with `cargo build --release --features \ + profile-trace` to enable --profile_trace and VIDE_PROFILE_TRACE", + path.display() + ); + } - Ok(profile_guard) + subscriber.init(); + Ok(None) } fn main() -> anyhow::Result<()> { From 7bd4afa7d36b77271f86ec2006f56e1e2bb17a33 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 12:42:14 +0800 Subject: [PATCH 71/80] test(hir): accept structured diagnostic provenance unavailability --- crates/hir/src/preproc/tests.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index 0b1fbc24..a53c6c4e 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -646,22 +646,18 @@ endmodule TextRange::new(offset(root_text, "`JOIN"), offset_after(root_text, "`JOIN(foo,bar)")); let provenance = diagnostic_provenance_for_range(&db, TOP, call_range).unwrap().unwrap(); - assert!(matches!(provenance, DiagnosticProvenance::Unavailable(PreprocUnavailable::Source(_)))); + assert!( + matches!(provenance, DiagnosticProvenance::Unavailable(_)), + "token paste diagnostic provenance should be unavailable, got {provenance:?}" + ); let stringification_range = TextRange::new(offset(root_text, "`STR"), offset_after(root_text, "`STR(foo)")); let provenance = diagnostic_provenance_for_range(&db, TOP, stringification_range).unwrap().unwrap(); assert!( - matches!( - &provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - | SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - | SourcePreprocUnavailable::ExpansionAuthorityUnavailable - )) - ), - "stringification should be unsupported or unavailable, got {provenance:?}" + matches!(provenance, DiagnosticProvenance::Unavailable(_)), + "stringification diagnostic provenance should be unavailable, got {provenance:?}" ); } @@ -691,13 +687,10 @@ endmodule let provenance = diagnostic_provenance_for_range(&db, TOP, instantiation_src.range()).unwrap().unwrap(); - assert!(matches!( - provenance, - DiagnosticProvenance::Unavailable(PreprocUnavailable::Source( - SourcePreprocUnavailable::MissingEmittedTokenMacroExpansionIdentity { .. } - | SourcePreprocUnavailable::ExpansionAuthorityUnavailable - )) - )); + assert!( + matches!(provenance, DiagnosticProvenance::Unavailable(_)), + "unbacked predefine diagnostic provenance should be unavailable, got {provenance:?}" + ); } #[test] From e34b02906dba8c299d1caa5d2aa440deed1e289c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 13:20:41 +0800 Subject: [PATCH 72/80] fix(preproc): track macro operation provenance --- crates/hir/src/preproc/expansion.rs | 4 +- crates/hir/src/preproc/helpers/expansion.rs | 15 ++- crates/hir/src/preproc/types.rs | 6 ++ crates/ide/src/source_tokens.rs | 2 + crates/ide/src/verilog_2005.rs | 7 +- crates/preproc/src/source/model/tests.rs | 102 ++++++++++++++---- crates/preproc/src/source/provenance.rs | 4 +- .../preproc/src/source/provenance/builder.rs | 59 +++++++++- crates/preproc/src/source/trace.rs | 26 ++++- crates/preproc/src/source/types.rs | 14 +++ crates/slang/bindings/rust/ffi/wrapper.cc | 21 ++++ crates/slang/bindings/rust/lib.rs | 50 +++++++++ crates/slang/bindings/rust/tests.rs | 40 ++++++- .../source/parsing/Preprocessor_macros.cpp | 30 +++++- 14 files changed, 340 insertions(+), 40 deletions(-) diff --git a/crates/hir/src/preproc/expansion.rs b/crates/hir/src/preproc/expansion.rs index 4ddd6df5..97176a7a 100644 --- a/crates/hir/src/preproc/expansion.rs +++ b/crates/hir/src/preproc/expansion.rs @@ -352,7 +352,9 @@ fn apply_context_capability_to_macro_expansion_provenance( match &mut token.provenance { TokenProvenance::MacroBody { call, .. } | TokenProvenance::MacroArgument { call, .. } - | TokenProvenance::Builtin { call, .. } => { + | TokenProvenance::Builtin { call, .. } + | TokenProvenance::TokenPaste { call } + | TokenProvenance::Stringification { call } => { apply_context_capability_to_macro_call(contexts, call); } TokenProvenance::SourceToken { .. } diff --git a/crates/hir/src/preproc/helpers/expansion.rs b/crates/hir/src/preproc/helpers/expansion.rs index 0d62b378..5d448ea3 100644 --- a/crates/hir/src/preproc/helpers/expansion.rs +++ b/crates/hir/src/preproc/helpers/expansion.rs @@ -341,10 +341,12 @@ pub(in crate::preproc) fn map_token_provenance( }; TokenProvenance::MacroArgument { call, argument_index: *argument_index, source, range } } - SourceTokenProvenanceFact::TokenPaste { .. } - | SourceTokenProvenanceFact::Stringification { .. } => TokenProvenance::Unavailable( - PreprocUnavailable::Source(SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance), - ), + SourceTokenProvenanceFact::TokenPaste { call, .. } => { + TokenProvenance::TokenPaste { call: mapped_macro_call(mapped, *call)? } + } + SourceTokenProvenanceFact::Stringification { call, .. } => { + TokenProvenance::Stringification { call: mapped_macro_call(mapped, *call)? } + } SourceTokenProvenanceFact::Predefine { source } => { TokenProvenance::Predefine { source: map_mapped_source_id(mapped, *source)? } } @@ -401,6 +403,11 @@ pub(in crate::preproc) fn diagnostic_target_for_source_expansion( TokenProvenance::Unavailable(reason) => { saw_unavailable = Some(reason); } + TokenProvenance::TokenPaste { .. } | TokenProvenance::Stringification { .. } => { + saw_unavailable = Some(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + )); + } TokenProvenance::Predefine { .. } => {} TokenProvenance::Builtin { call, name } => { return Ok(DiagnosticProvenance::Builtin { diff --git a/crates/hir/src/preproc/types.rs b/crates/hir/src/preproc/types.rs index 6f6421da..7aa87c91 100644 --- a/crates/hir/src/preproc/types.rs +++ b/crates/hir/src/preproc/types.rs @@ -362,6 +362,12 @@ pub enum TokenProvenance { name: SmolStr, call: MacroCall, }, + TokenPaste { + call: MacroCall, + }, + Stringification { + call: MacroCall, + }, Unavailable(PreprocUnavailable), } diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index 51ff4b72..67cd4517 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -325,6 +325,8 @@ fn preproc_hit_for_token( ), TokenProvenance::Predefine { .. } | TokenProvenance::Builtin { .. } + | TokenProvenance::TokenPaste { .. } + | TokenProvenance::Stringification { .. } | TokenProvenance::Unavailable(_) => return None, }; diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 5b7367a2..5e9c4387 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -1653,7 +1653,7 @@ endmodule } #[test] -fn preproc_macro_hover_falls_back_to_definition_body_for_unsupported_expansion() { +fn preproc_macro_hover_shows_token_paste_expansion() { let text = r#" `define JOIN(a,b) a``b module top; @@ -1670,10 +1670,11 @@ endmodule let info = hover.info.as_str(); assert!( info.contains("```systemverilog") - && info.contains("`define `JOIN(a, b) a `` b") + && info.contains("`JOIN(a, b)") + && info.contains("foobar") && info.contains("from [feature.v]") && !info.contains("unavailable"), - "unsupported expansion hover should show the macro definition display: {info}" + "token paste expansion hover should show the expanded display text: {info}" ); } diff --git a/crates/preproc/src/source/model/tests.rs b/crates/preproc/src/source/model/tests.rs index 7cb56159..60a88bc5 100644 --- a/crates/preproc/src/source/model/tests.rs +++ b/crates/preproc/src/source/model/tests.rs @@ -1073,7 +1073,7 @@ endmodule } #[test] -fn source_model_marks_unsupported_macro_ops_unavailable_without_dropping_tokens() { +fn source_model_records_macro_operation_tokens_without_dropping_tokens() { let root_text = r#"`define JOIN(a,b) a``b `define STR(x) `"x`" module m; @@ -1088,35 +1088,97 @@ endmodule .iter() .find(|token| token.text.as_str() == "foobar") .expect("token paste result should not be dropped"); - assert!(matches!( - model.token_provenance().get(pasted.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) - )); + let SourceTokenProvenance::TokenPaste { call: paste_call, identity: paste_identity } = + model.token_provenance().get(pasted.provenance).unwrap() + else { + panic!( + "token paste should carry macro operation provenance: {:?}", + model.token_provenance().get(pasted.provenance).unwrap() + ); + }; + assert_eq!( + Some(paste_identity.call), + model.macro_calls().get(*paste_call).and_then(|call| call.identity) + ); let stringified = model .emitted_tokens() .iter() .find(|token| token.text.as_str() == "\"foo\"") .expect("stringification result should not be dropped"); - assert!(matches!( - model.token_provenance().get(stringified.provenance).unwrap(), - SourceTokenProvenance::Unavailable( - SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance - ) - )); + let SourceTokenProvenance::Stringification { + call: stringification_call, + identity: stringification_identity, + } = model.token_provenance().get(stringified.provenance).unwrap() + else { + panic!("stringification should carry macro operation provenance"); + }; + assert_eq!( + Some(stringification_identity.call), + model.macro_calls().get(*stringification_call).and_then(|call| call.identity) + ); + assert_ne!(paste_call, stringification_call); assert_eq!(model.capabilities().emitted_tokens, CapabilityStatus::Complete); - assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Partial); - assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Partial); - for call in model.macro_calls().iter() { - assert!(matches!( - model.immediate_macro_expansion(call.id), - SourceMacroExpansionQuery::Unavailable(_) - )); + assert_eq!(model.capabilities().emitted_token_provenance, CapabilityStatus::Complete); + assert_eq!(model.capabilities().macro_expansions, CapabilityStatus::Complete); + for call in [*paste_call, *stringification_call] { + let SourceMacroExpansionQuery::Available(expansion) = model.immediate_macro_expansion(call) + else { + panic!("macro operation call should have an available expansion"); + }; + assert_ne!(model.macro_expansions().get(expansion).unwrap().emitted_token_range.len, 0); } } +#[test] +fn source_model_links_pasted_macro_usage_to_parent_call() { + let root_text = r#"`define FOOBAR 9 +`define CALL(a,b) `a``b +module m; +localparam int W = `CALL(FOO,BAR); +endmodule +"#; + let (model, _root_source) = source_model_from_root(root_text, SyntaxTreeOptions::default()); + + let parent_call = model + .macro_calls() + .iter() + .find(|call| { + model + .macro_references() + .get(call.reference) + .is_some_and(|reference| reference.name.as_str() == "CALL") + }) + .expect("CALL invocation should be recorded"); + let child_call = model + .macro_calls() + .iter() + .find(|call| { + model + .macro_references() + .get(call.reference) + .is_some_and(|reference| reference.name.as_str() == "FOOBAR") + }) + .expect("pasted macro usage should be expanded as a child call"); + assert_eq!(child_call.parent_expansion_identity, parent_call.expansion_identity); + + let SourceMacroExpansionQuery::Available(parent_expansion) = + model.immediate_macro_expansion(parent_call.id) + else { + panic!("CALL invocation should have an immediate expansion"); + }; + let SourceMacroExpansionQuery::Available(child_expansion) = + model.immediate_macro_expansion(child_call.id) + else { + panic!("pasted macro usage should have an immediate expansion"); + }; + + let recursive = model.recursive_macro_expansion(parent_call.id); + assert!(recursive.unavailable.is_empty()); + assert_eq!(recursive.expansions, vec![parent_expansion, child_expansion]); + assert!(model.emitted_tokens().iter().any(|token| token.text.as_str() == "9")); +} + #[test] fn source_model_does_not_create_expansion_without_emitted_token_authority() { let root_text = "`define A 1\nmodule m; localparam int W = `A; endmodule\n"; diff --git a/crates/preproc/src/source/provenance.rs b/crates/preproc/src/source/provenance.rs index bf1119c8..1c2343a5 100644 --- a/crates/preproc/src/source/provenance.rs +++ b/crates/preproc/src/source/provenance.rs @@ -308,12 +308,12 @@ pub enum SourceTokenProvenance { argument_token_range: SourceRange, }, TokenPaste { + identity: SourceMacroOperationIdentity, call: SourceMacroCallId, - parts: Vec, }, Stringification { + identity: SourceMacroOperationIdentity, call: SourceMacroCallId, - argument_index: usize, }, Predefine { source: PreprocSourceId, diff --git a/crates/preproc/src/source/provenance/builder.rs b/crates/preproc/src/source/provenance/builder.rs index d67e9b24..c42985b9 100644 --- a/crates/preproc/src/source/provenance/builder.rs +++ b/crates/preproc/src/source/provenance/builder.rs @@ -20,6 +20,12 @@ pub struct SourcePreprocModelBuilder<'a> { expansions_partial: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MacroOperationProvenanceKind { + TokenPaste, + Stringification, +} + impl<'a> SourcePreprocModelBuilder<'a> { pub fn new(index: &'a SourcePreprocIndex) -> Self { Self { @@ -471,6 +477,16 @@ impl<'a> SourcePreprocModelBuilder<'a> { SourceTokenProvenanceFact::Builtin { name, identity } if !name.is_empty() => { self.resolve_builtin_token_provenance(token_id, name.clone(), *identity) } + SourceTokenProvenanceFact::TokenPaste { identity } => self + .resolve_macro_operation_token_provenance( + *identity, + MacroOperationProvenanceKind::TokenPaste, + ), + SourceTokenProvenanceFact::Stringification { identity } => self + .resolve_macro_operation_token_provenance( + *identity, + MacroOperationProvenanceKind::Stringification, + ), SourceTokenProvenanceFact::Builtin { .. } | SourceTokenProvenanceFact::Unavailable => { self.unavailable_token_provenance( SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, @@ -604,14 +620,55 @@ impl<'a> SourcePreprocModelBuilder<'a> { }, ); }; + let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); if let Err(reason) = - self.record_call_expansion_identity(call, identity.expansion, identity.parent_expansion) + self.record_call_expansion_identity(call, call_expansion_identity, None) { return self.unavailable_token_provenance(reason); } SourceTokenProvenance::Builtin { name, identity, call } } + fn resolve_macro_operation_token_provenance( + &mut self, + identity: Option, + kind: MacroOperationProvenanceKind, + ) -> SourceTokenProvenance { + let Some(identity) = identity else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::MissingEmittedTokenMacroCallIdentity, + ); + }; + if self.definition_for_identity(identity.definition).is_err() { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroDefinitionIdentity { + identity: identity.definition, + }, + ); + }; + let Some(call) = self.call_ids_by_identity.get(&identity.call).copied() else { + return self.unavailable_token_provenance( + SourcePreprocUnavailable::UnknownEmittedTokenMacroCallIdentity { + identity: identity.call, + }, + ); + }; + let call_expansion_identity = identity.parent_expansion.unwrap_or(identity.expansion); + if let Err(reason) = + self.record_call_expansion_identity(call, call_expansion_identity, None) + { + return self.unavailable_token_provenance(reason); + } + match kind { + MacroOperationProvenanceKind::TokenPaste => { + SourceTokenProvenance::TokenPaste { identity, call } + } + MacroOperationProvenanceKind::Stringification => { + SourceTokenProvenance::Stringification { identity, call } + } + } + } + fn call_for_emitted_token( &mut self, request: EmittedTokenMacroCall, diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index 6c3b8fbd..1f35d31e 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -6,8 +6,9 @@ use syntax::{ PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroBuiltinIdentity, PreprocessorTraceMacroCallId, PreprocessorTraceMacroDefinitionId, - PreprocessorTraceMacroExpansionId, PreprocessorTraceMacroParam, PreprocessorTraceToken, - PreprocessorTraceTokenProvenance, SourceBufferOrigin, SourceBufferRange, SyntaxKind, + PreprocessorTraceMacroExpansionId, PreprocessorTraceMacroOperationIdentity, + PreprocessorTraceMacroParam, PreprocessorTraceToken, PreprocessorTraceTokenProvenance, + SourceBufferOrigin, SourceBufferRange, SyntaxKind, }; use utils::line_index::{TextRange, TextSize}; @@ -73,6 +74,17 @@ impl From for SourceMacroBuiltinIdentity } } +impl From for SourceMacroOperationIdentity { + fn from(value: PreprocessorTraceMacroOperationIdentity) -> Self { + Self { + call: SourceMacroCallKey::from(value.call_id), + definition: SourceMacroDefinitionKey::from(value.definition_id), + expansion: SourceMacroExpansionKey::from(value.expansion_id), + parent_expansion: value.parent_expansion_id.map(SourceMacroExpansionKey::from), + } + } +} + impl SourcePreprocIndex { pub fn from_trace(trace: PreprocessorTrace) -> Result { let root_source = PreprocSourceId::from(trace.root_buffer_id); @@ -342,6 +354,16 @@ fn emitted_token_provenance_from_trace( identity: Some(SourceMacroBuiltinIdentity::from(identity)), } } + PreprocessorTraceTokenProvenance::TokenPaste { identity } => { + SourceTokenProvenanceFact::TokenPaste { + identity: Some(SourceMacroOperationIdentity::from(identity)), + } + } + PreprocessorTraceTokenProvenance::Stringification { identity } => { + SourceTokenProvenanceFact::Stringification { + identity: Some(SourceMacroOperationIdentity::from(identity)), + } + } PreprocessorTraceTokenProvenance::Builtin { .. } => SourceTokenProvenanceFact::Unavailable, PreprocessorTraceTokenProvenance::Unavailable => SourceTokenProvenanceFact::Unavailable, } diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index 05e21c3d..a52d1c90 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -95,6 +95,14 @@ pub struct SourceMacroBuiltinIdentity { pub parent_expansion: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceMacroOperationIdentity { + pub call: SourceMacroCallKey, + pub definition: SourceMacroDefinitionKey, + pub expansion: SourceMacroExpansionKey, + pub parent_expansion: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocSource { pub id: PreprocSourceId, @@ -252,6 +260,12 @@ pub enum SourceTokenProvenanceFact { name: SmolStr, identity: Option, }, + TokenPaste { + identity: Option, + }, + Stringification { + identity: Option, + }, Unavailable, } diff --git a/crates/slang/bindings/rust/ffi/wrapper.cc b/crates/slang/bindings/rust/ffi/wrapper.cc index feb3b93d..9eecd5ec 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -168,6 +168,8 @@ constexpr uint8_t TRACE_TOKEN_PROVENANCE_SOURCE = 1; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_BODY = 2; constexpr uint8_t TRACE_TOKEN_PROVENANCE_MACRO_ARGUMENT = 3; constexpr uint8_t TRACE_TOKEN_PROVENANCE_BUILTIN = 4; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_TOKEN_PASTE = 5; +constexpr uint8_t TRACE_TOKEN_PROVENANCE_STRINGIFICATION = 6; ::RawPreprocessorTraceEmittedToken empty_preprocessor_trace_emitted_token() { ::RawPreprocessorTraceEmittedToken token; @@ -332,6 +334,18 @@ void apply_direct_macro_token_provenance( provenance.argumentTokenIndex != slang::SourceManager::MacroTokenProvenance::InvalidIndex; } +bool apply_macro_operation_token_provenance( + ::RawPreprocessorTraceEmittedToken& result, + const std::optional& provenance, + uint8_t provenanceKind) { + if (!has_direct_macro_token_provenance(provenance)) + return false; + + apply_direct_macro_token_provenance(result, *provenance); + result.provenance_kind = provenanceKind; + return true; +} + bool apply_original_macro_loc_provenance_for_nested_argument( ::RawPreprocessorTraceEmittedToken& result, slang::parsing::Token token, @@ -500,7 +514,14 @@ ::RawPreprocessorTraceEmittedToken to_rust_preprocessor_trace_emitted_token( if (sourceManager.isMacroLoc(location)) { switch (sourceManager.getMacroExpansionKind(location)) { case slang::SourceManager::MacroExpansionKind::TokenPaste: + apply_macro_operation_token_provenance( + result, sourceManager.getMacroTokenProvenance(location), + TRACE_TOKEN_PROVENANCE_TOKEN_PASTE); + return result; case slang::SourceManager::MacroExpansionKind::Stringification: + apply_macro_operation_token_provenance( + result, sourceManager.getMacroTokenProvenance(location), + TRACE_TOKEN_PROVENANCE_STRINGIFICATION); return result; case slang::SourceManager::MacroExpansionKind::Body: case slang::SourceManager::MacroExpansionKind::Argument: diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 453446a7..a2130e91 100644 --- a/crates/slang/bindings/rust/lib.rs +++ b/crates/slang/bindings/rust/lib.rs @@ -212,6 +212,12 @@ pub enum PreprocessorTraceTokenProvenance { name: String, identity: PreprocessorTraceMacroBuiltinIdentity, }, + TokenPaste { + identity: PreprocessorTraceMacroOperationIdentity, + }, + Stringification { + identity: PreprocessorTraceMacroOperationIdentity, + }, Unavailable, } @@ -242,6 +248,14 @@ pub struct PreprocessorTraceMacroBuiltinIdentity { pub parent_expansion_id: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocessorTraceMacroOperationIdentity { + pub call_id: PreprocessorTraceMacroCallId, + pub definition_id: PreprocessorTraceMacroDefinitionId, + pub expansion_id: PreprocessorTraceMacroExpansionId, + pub parent_expansion_id: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PreprocessorTraceToken { pub raw_text: String, @@ -556,6 +570,8 @@ impl PreprocessorTraceTokenProvenance { const MACRO_ARGUMENT: u8 = 3; const MACRO_BODY: u8 = 2; const SOURCE: u8 = 1; + const STRINGIFICATION: u8 = 6; + const TOKEN_PASTE: u8 = 5; const UNAVAILABLE: u8 = 0; #[inline] @@ -616,6 +632,22 @@ impl PreprocessorTraceTokenProvenance { }; Self::Builtin { name: raw.macro_name, identity } } + Self::TOKEN_PASTE => { + let Some(identity) = + PreprocessorTraceMacroOperationIdentity::from_raw(&raw.identity) + else { + return Self::Unavailable; + }; + Self::TokenPaste { identity } + } + Self::STRINGIFICATION => { + let Some(identity) = + PreprocessorTraceMacroOperationIdentity::from_raw(&raw.identity) + else { + return Self::Unavailable; + }; + Self::Stringification { identity } + } Self::UNAVAILABLE => Self::Unavailable, _ => Self::Unavailable, } @@ -679,6 +711,24 @@ impl PreprocessorTraceMacroBuiltinIdentity { } } +impl PreprocessorTraceMacroOperationIdentity { + #[inline] + fn from_raw(raw: &RawPreprocessorTraceMacroIdentity) -> Option { + Some(Self { + call_id: raw.has_call_id.then_some(PreprocessorTraceMacroCallId(raw.call_id))?, + definition_id: raw + .has_definition_id + .then_some(PreprocessorTraceMacroDefinitionId(raw.definition_id))?, + expansion_id: raw + .has_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.expansion_id))?, + parent_expansion_id: raw + .has_parent_expansion_id + .then_some(PreprocessorTraceMacroExpansionId(raw.parent_expansion_id)), + }) + } +} + impl PreprocessorTraceToken { #[inline] fn from_raw(raw: ffi::RawPreprocessorTraceToken) -> Option { diff --git a/crates/slang/bindings/rust/tests.rs b/crates/slang/bindings/rust/tests.rs index f3958bca..42fcc9cb 100644 --- a/crates/slang/bindings/rust/tests.rs +++ b/crates/slang/bindings/rust/tests.rs @@ -1519,7 +1519,7 @@ fn preprocessor_trace_reports_escaped_identifier_macro_body_identity() { } #[test] -fn preprocessor_trace_keeps_unsupported_macro_ops_as_unavailable_tokens() { +fn preprocessor_trace_reports_macro_operation_token_provenance() { let source = r#"`define JOIN(a,b) a``b `define STR(x) `"x`" module m; @@ -1529,20 +1529,54 @@ endmodule "#; let trace = preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + let join_expansion_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.value_text == "`JOIN") + }) + .and_then(|event| event.macro_expansion_id) + .expect("JOIN usage should carry an expansion identity"); + let str_expansion_id = trace + .events + .iter() + .find(|event| { + event.kind == SyntaxKind::MACRO_USAGE + && event.name.as_ref().is_some_and(|name| name.value_text == "`STR") + }) + .and_then(|event| event.macro_expansion_id) + .expect("STR usage should carry an expansion identity"); let pasted = trace .emitted_tokens .iter() .find(|token| token.raw_text == "foobar") .expect("token paste result should stay in emitted stream"); - assert!(matches!(pasted.provenance, PreprocessorTraceTokenProvenance::Unavailable)); + let PreprocessorTraceTokenProvenance::TokenPaste { identity: pasted_identity } = + &pasted.provenance + else { + panic!("token paste should carry macro operation provenance: {pasted:?}"); + }; + assert!(pasted_identity.call_id.0 != 0); + assert!(pasted_identity.definition_id.0 != 0); + assert!(pasted_identity.expansion_id.0 != 0); + assert_eq!(pasted_identity.parent_expansion_id, Some(join_expansion_id)); let stringified = trace .emitted_tokens .iter() .find(|token| token.raw_text == "\"foo\"") .expect("stringification result should stay in emitted stream"); - assert!(matches!(stringified.provenance, PreprocessorTraceTokenProvenance::Unavailable)); + let PreprocessorTraceTokenProvenance::Stringification { identity: stringified_identity } = + &stringified.provenance + else { + panic!("stringification should carry macro operation provenance: {stringified:?}"); + }; + assert!(stringified_identity.call_id.0 != 0); + assert!(stringified_identity.definition_id.0 != 0); + assert!(stringified_identity.expansion_id.0 != 0); + assert_eq!(stringified_identity.parent_expansion_id, Some(str_expansion_id)); } #[test] diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index 670acb23..2911a0f2 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -155,6 +155,7 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< return opToken; auto loc = opToken.location(); + auto provenance = sourceManager.getMacroTokenProvenance(loc); auto originalLoc = loc; auto expansionRange = opToken.range(); if (sourceManager.isMacroLoc(loc)) { @@ -162,7 +163,17 @@ bool Preprocessor::applyMacroOps(std::span tokens, SmallVectorBase< expansionRange = sourceManager.getExpansionRange(loc); } - auto opLoc = sourceManager.createExpansionLoc(originalLoc, expansionRange, kind); + SourceManager::MacroExpansionMetadata metadata; + if (provenance) { + metadata.callId = provenance->callId; + metadata.definitionId = provenance->definitionId; + metadata.parentExpansionId = provenance->parentExpansionId != 0 + ? provenance->parentExpansionId + : provenance->expansionId; + } + auto opLoc = sourceManager.createExpansionLoc(originalLoc, expansionRange, kind, metadata); + if (provenance) + sourceManager.setMacroTokenProvenance(opLoc, *provenance); return opToken.withLocation(alloc, opLoc); }; @@ -779,10 +790,21 @@ bool Preprocessor::expandReplacementList( SourceManager::MacroExpansionMetadata metadata; metadata.callId = allocateMacroCallId(); metadata.definitionId = macro.definitionId; - if (sourceManager.isMacroLoc(token.location())) - metadata.parentExpansionId = token.location().buffer().getId(); - else + if (sourceManager.isMacroLoc(token.location())) { + auto provenance = sourceManager.getMacroTokenProvenance(token.location()); + auto expansionKind = sourceManager.getMacroExpansionKind(token.location()); + if ((expansionKind == SourceManager::MacroExpansionKind::TokenPaste || + expansionKind == SourceManager::MacroExpansionKind::Stringification) && + provenance && provenance->parentExpansionId != 0) { + metadata.parentExpansionId = provenance->parentExpansionId; + } + else { + metadata.parentExpansionId = token.location().buffer().getId(); + } + } + else { metadata.parentExpansionId = parentExpansionId; + } MacroExpansion expansion{sourceManager, alloc, expansionBuffer, token, false, metadata}; if (!expandMacro(macro, expansion, actualArgs)) From a1792e5fb79d5344edd106d05464d7d5bf0c8d4f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 13:43:40 +0800 Subject: [PATCH 73/80] feat(ide): add macro argument inlay hints --- crates/hir/src/preproc/expansion.rs | 71 +++++++ crates/hir/src/preproc/tests.rs | 38 ++++ crates/hir/src/preproc/types.rs | 6 + crates/ide/src/inlay_hint.rs | 185 +++++++++++++++++- editors/vscode/package.json | 5 + editors/vscode/package.nls.json | 1 + editors/vscode/package.nls.zh-cn.json | 1 + editors/vscode/src/generated/configuration.ts | 9 + schemas/v1/user-config.schema.json | 25 +++ src/config/user_config.rs | 25 +++ src/lsp_ext/to_proto.rs | 4 +- 11 files changed, 366 insertions(+), 4 deletions(-) diff --git a/crates/hir/src/preproc/expansion.rs b/crates/hir/src/preproc/expansion.rs index 97176a7a..10be0cb4 100644 --- a/crates/hir/src/preproc/expansion.rs +++ b/crates/hir/src/preproc/expansion.rs @@ -60,6 +60,77 @@ pub fn macro_expansion_queries_at( Ok(Vec::new()) } +pub fn macro_call_resolutions_in_range( + db: &dyn SourceRootDb, + file_id: FileId, + range: TextRange, +) -> PreprocResult> { + let mut resolutions = UniqVec::::default(); + let mut first_error = None; + let contexts = source_preproc_single_query_contexts(db, file_id); + + for model_file_id in contexts.model_file_ids.iter().copied() { + let mapped = db.source_preproc_model(model_file_id); + let mapped = match mapped_result(mapped.as_ref()) { + Ok(mapped) => mapped, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + + for call_fact in source_macro_calls_intersecting_range(mapped, file_id, range) { + let SourceMacroResolutionFact::Resolved { definition, .. } = &call_fact.callee else { + if let SourceMacroResolutionFact::Unavailable(reason) = &call_fact.callee { + record_first_error(&mut first_error, unavailable_error(reason.clone())); + } + continue; + }; + let Some(definition_fact) = mapped.model.macro_definitions().get(*definition) else { + let event_id = mapped + .model + .macro_references() + .get(call_fact.reference) + .map(|reference| reference.event_id.raw()) + .unwrap_or_default(); + record_first_error( + &mut first_error, + PreprocError::SourceQuery(SourcePreprocQueryError::Model( + SourcePreprocError::MissingEvent { event_id }, + )), + ); + continue; + }; + + let mut call = match map_macro_call(mapped, call_fact) { + Ok(call) => call, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + let mut definition = match map_macro_definition(mapped, definition_fact) { + Ok(definition) => definition, + Err(error) => { + record_first_error(&mut first_error, error); + continue; + } + }; + call.capability = context_query_capability(&contexts, call.capability); + definition.capability = context_query_capability(&contexts, definition.capability); + resolutions.push_unique_eq(MacroCallResolution { call, definition }); + } + } + + if resolutions.is_empty() + && let Err(error) = finish_empty_single_query(&contexts, first_error) + { + return Err(error); + } + + Ok(resolutions.into_vec()) +} + pub fn recursive_macro_expansion_at( db: &dyn SourceRootDb, file_id: FileId, diff --git a/crates/hir/src/preproc/tests.rs b/crates/hir/src/preproc/tests.rs index a53c6c4e..bad0b508 100644 --- a/crates/hir/src/preproc/tests.rs +++ b/crates/hir/src/preproc/tests.rs @@ -257,6 +257,44 @@ endmodule assert_eq!(wrap_expansion.child_calls, vec![leaf_expansion.call.id]); } +#[test] +fn preproc_macro_call_resolutions_in_range_map_formal_params() { + let root_text = "\ +`define MAKE(width, expr) logic [width-1:0] expr +module top; `MAKE(8, data_q) endmodule +"; + let db = db_with_entries(&[(TOP, "rtl/top.v", root_text)]); + let start = offset(root_text, "`MAKE"); + let end = offset_after(root_text, "data_q"); + + let resolutions = + macro_call_resolutions_in_range(&db, TOP, TextRange::new(start, end)).unwrap(); + + assert_eq!(resolutions.len(), 1); + let resolution = &resolutions[0]; + assert_eq!(text_at_range(root_text, resolution.call.range), "`MAKE(8, data_q)"); + assert_eq!( + resolution + .definition + .params + .as_ref() + .unwrap() + .iter() + .filter_map(|param| param.name.as_deref()) + .collect::>(), + vec!["width", "expr"] + ); + assert_eq!( + resolution + .call + .arguments + .iter() + .filter_map(|argument| argument.range.map(|range| text_at_range(root_text, range))) + .collect::>(), + vec!["8", "data_q"] + ); +} + #[test] fn preproc_builtin_intrinsic_expansion_uses_structured_provenance() { let root_text = r#"module m; diff --git a/crates/hir/src/preproc/types.rs b/crates/hir/src/preproc/types.rs index 7aa87c91..968c86aa 100644 --- a/crates/hir/src/preproc/types.rs +++ b/crates/hir/src/preproc/types.rs @@ -273,6 +273,12 @@ pub struct MacroCall { pub expansion: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroCallResolution { + pub call: MacroCall, + pub definition: MacroDefinition, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MacroArgument { pub argument_index: usize, diff --git a/crates/ide/src/inlay_hint.rs b/crates/ide/src/inlay_hint.rs index 5fb9d921..9a57edf1 100644 --- a/crates/ide/src/inlay_hint.rs +++ b/crates/ide/src/inlay_hint.rs @@ -15,6 +15,7 @@ use hir::{ port::{NonAnsiPortId, PortDeclId, PortDirection, Ports}, }, }, + preproc::{MacroCallResolution, macro_call_resolutions_in_range}, scope::{AnsiPortEntry, ModuleEntry, ModuleScope, NonAnsiPortEntry}, source_map::{IsNamedSrc, IsSrc}, }; @@ -32,6 +33,7 @@ use crate::{db::root_db::RootDb, markup::Markup, module_resolution::resolve_modu pub struct InlayHintConfig { pub port_connection: bool, pub parameter_assignment: bool, + pub macro_argument: bool, pub end_structure: bool, } @@ -45,6 +47,7 @@ impl InlayHintConfig { pub enum InlayKind { ParamAssign, Port, + MacroArgument, EndStructure, } @@ -103,6 +106,16 @@ impl HintAnchor { padding_right: false, } } + + fn macro_argument(range: TextRange) -> Self { + Self { + range, + position: range.start(), + kind: InlayKind::MacroArgument, + padding_left: false, + padding_right: true, + } + } } struct InlayHintCollector { @@ -159,6 +172,29 @@ impl InlayHintCollector { } } + fn collect_range_hint( + &mut self, + anchor: HintAnchor, + target_location: Option>, + label: String, + ) { + if !self.intersect(anchor.range) { + return; + } + + let tooltip = target_location.as_ref().map(|_| Markup::new()); + self.hints.push(InlayHint { + label, + tooltip, + target_location, + padding_left: anchor.padding_left, + padding_right: anchor.padding_right, + position: anchor.position, + kind: anchor.kind, + text_edit: None, + }); + } + fn collect_module_end_hint(&mut self, module_src: ModuleSrc, name: &str) { if let Some(end_range) = module_src.end_range() { self.collect_hint( @@ -191,6 +227,10 @@ pub(crate) fn inlay_hint( let mut collector = InlayHintCollector::new(range, config); + if collector.config.macro_argument { + collect_macro_argument_hints(db, file_id.file_id(), range, &mut collector); + } + for &item in src_map.items.iter() { #[allow(clippy::single_match)] match item { @@ -211,6 +251,49 @@ pub(crate) fn inlay_hint( collector.into_hints() } +fn collect_macro_argument_hints( + db: &RootDb, + file_id: FileId, + range: TextRange, + collector: &mut InlayHintCollector, +) { + let Ok(resolutions) = macro_call_resolutions_in_range(db, file_id, range) else { + return; + }; + + for resolution in resolutions { + collect_macro_argument_hints_for_call(resolution, collector); + } +} + +fn collect_macro_argument_hints_for_call( + resolution: MacroCallResolution, + collector: &mut InlayHintCollector, +) -> Option<()> { + let params = resolution.definition.params.as_ref()?; + for argument in &resolution.call.arguments { + let Some(argument_range) = argument.range else { + continue; + }; + let Some(param) = params.get(argument.argument_index) else { + continue; + }; + let Some(param_name) = ¶m.name else { + continue; + }; + let Some(param_range) = param.range else { + continue; + }; + collector.collect_range_hint( + HintAnchor::macro_argument(argument_range), + Some(InFile::new(HirFileId(resolution.definition.file_id), param_range)), + format!("{param_name}:"), + ); + } + + Some(()) +} + fn collect_module_items( db: &RootDb, module_id: ModuleId, @@ -507,15 +590,39 @@ mod tests { } fn port_config() -> InlayHintConfig { - InlayHintConfig { port_connection: true, parameter_assignment: false, end_structure: false } + InlayHintConfig { + port_connection: true, + parameter_assignment: false, + macro_argument: false, + end_structure: false, + } } fn parameter_config() -> InlayHintConfig { - InlayHintConfig { port_connection: false, parameter_assignment: true, end_structure: false } + InlayHintConfig { + port_connection: false, + parameter_assignment: true, + macro_argument: false, + end_structure: false, + } + } + + fn macro_argument_config() -> InlayHintConfig { + InlayHintConfig { + port_connection: false, + parameter_assignment: false, + macro_argument: true, + end_structure: false, + } } fn end_structure_config() -> InlayHintConfig { - InlayHintConfig { port_connection: false, parameter_assignment: false, end_structure: true } + InlayHintConfig { + port_connection: false, + parameter_assignment: false, + macro_argument: false, + end_structure: true, + } } fn port_hint_labels(text: &str) -> Vec { @@ -556,6 +663,34 @@ mod tests { .collect() } + fn macro_argument_hint_labels(text: &str) -> Vec { + let (db, file_id) = db_with_file(text); + let range = TextRange::new(TextSize::from(0), TextSize::of(text)); + inlay_hint(&db, file_id, range, macro_argument_config()) + .into_iter() + .filter(|hint| matches!(hint.kind, InlayKind::MacroArgument)) + .map(|hint| hint.label) + .collect() + } + + fn macro_argument_hint_labels_in_range(text: &str, range: TextRange) -> Vec { + let (db, file_id) = db_with_file(text); + inlay_hint(&db, file_id, range, macro_argument_config()) + .into_iter() + .filter(|hint| matches!(hint.kind, InlayKind::MacroArgument)) + .map(|hint| hint.label) + .collect() + } + + fn macro_argument_hints(text: &str) -> Vec { + let (db, file_id) = db_with_file(text); + let range = TextRange::new(TextSize::from(0), TextSize::of(text)); + inlay_hint(&db, file_id, range, macro_argument_config()) + .into_iter() + .filter(|hint| matches!(hint.kind, InlayKind::MacroArgument)) + .collect() + } + #[test] fn comment_only_range_skips_module_end_hint() { let text = "\ @@ -734,4 +869,48 @@ endmodule assert_eq!(param_hint_labels(text), vec!["P:"]); } + + #[test] + fn macro_argument_hints_show_function_like_macro_params() { + let text = "`define MAKE(width, expr) logic [width-1:0] expr\n\ + module top; `MAKE(8, data_q) endmodule\n"; + + assert_eq!(macro_argument_hint_labels(text), vec!["width:", "expr:"]); + } + + #[test] + fn macro_argument_hint_range_skips_previous_arguments() { + let text = "`define MAKE(width, expr) logic [width-1:0] expr\n\ + module top; `MAKE(8, data_q) endmodule\n"; + let start = TextSize::from(text.find("data_q").expect("second argument") as u32); + let end = start + TextSize::of("data_q"); + + assert_eq!( + macro_argument_hint_labels_in_range(text, TextRange::new(start, end)), + vec!["expr:"] + ); + } + + #[test] + fn macro_argument_hint_targets_formal_parameter() { + let text = "`define MAKE(width, expr) logic [width-1:0] expr\n\ + module top; `MAKE(8, data_q) endmodule\n"; + + let hints = macro_argument_hints(text); + + assert_eq!( + hints.iter().map(|hint| hint.label.as_str()).collect::>(), + vec!["width:", "expr:"] + ); + assert_eq!( + hints[0].target_location.as_ref().map(|target| target.value), + Some(TextRange::new( + TextSize::from(text.find("width").expect("formal parameter") as u32), + TextSize::from( + (text.find("width").expect("formal parameter") + "width".len()) as u32 + ), + )) + ); + assert!(hints.iter().all(|hint| hint.text_edit.is_none())); + } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 151aab95..c85bb069 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -260,6 +260,11 @@ "default": true, "description": "%configuration.inlayHints.parameter.assignment.enable.description%" }, + "vide.inlayHints.macro.argument.enable": { + "type": "boolean", + "default": true, + "description": "%configuration.inlayHints.macro.argument.enable.description%" + }, "vide.inlayHints.end.structure.enable": { "type": "boolean", "default": true, diff --git a/editors/vscode/package.nls.json b/editors/vscode/package.nls.json index 86889774..ec773245 100644 --- a/editors/vscode/package.nls.json +++ b/editors/vscode/package.nls.json @@ -26,6 +26,7 @@ "configuration.formatting.indent.width.description": "Fallback indentation width used when editor formatting options are unavailable.", "configuration.inlayHints.port.connection.enable.description": "Show inlay hints for port connections.", "configuration.inlayHints.parameter.assignment.enable.description": "Show inlay hints for parameter assignments.", + "configuration.inlayHints.macro.argument.enable.description": "Show inlay hints for macro arguments.", "configuration.inlayHints.end.structure.enable.description": "Show inlay hints for ending structure names.", "configuration.lens.instantiations.enable.description": "Show code lenses for module instantiations.", "configuration.semantic.tokens.port.clk.rst.enable.description": "Highlight clock and reset ports with dedicated semantic token modifiers.", diff --git a/editors/vscode/package.nls.zh-cn.json b/editors/vscode/package.nls.zh-cn.json index c8ce1a0d..748c2f4f 100644 --- a/editors/vscode/package.nls.zh-cn.json +++ b/editors/vscode/package.nls.zh-cn.json @@ -26,6 +26,7 @@ "configuration.formatting.indent.width.description": "编辑器格式化选项不可用时使用的后备缩进宽度。", "configuration.inlayHints.port.connection.enable.description": "显示端口连接的内联提示。", "configuration.inlayHints.parameter.assignment.enable.description": "显示参数赋值的内联提示。", + "configuration.inlayHints.macro.argument.enable.description": "显示宏实参的内联提示。", "configuration.inlayHints.end.structure.enable.description": "显示结束结构名称的内联提示。", "configuration.lens.instantiations.enable.description": "显示模块实例化的 Code Lens。", "configuration.semantic.tokens.port.clk.rst.enable.description": "使用专用语义标记修饰符高亮时钟和复位端口。", diff --git a/editors/vscode/src/generated/configuration.ts b/editors/vscode/src/generated/configuration.ts index 6dbb394a..4a96e856 100644 --- a/editors/vscode/src/generated/configuration.ts +++ b/editors/vscode/src/generated/configuration.ts @@ -164,6 +164,15 @@ export const USER_CONFIG_SETTINGS = [ markdownDescriptionKey: null, defaultValue: true, }, + { + path: ["inlayHints","macro","argument","enable"], + vscodeKey: "vide.inlayHints.macro.argument.enable", + vscodeSection: "inlayHints.macro.argument.enable", + docsGroup: "Annotations", + descriptionKey: "configuration.inlayHints.macro.argument.enable.description", + markdownDescriptionKey: null, + defaultValue: true, + }, { path: ["inlayHints","end","structure","enable"], vscodeKey: "vide.inlayHints.end.structure.enable", diff --git a/schemas/v1/user-config.schema.json b/schemas/v1/user-config.schema.json index cc17dd19..f0956b77 100644 --- a/schemas/v1/user-config.schema.json +++ b/schemas/v1/user-config.schema.json @@ -68,6 +68,11 @@ "enable": true } }, + "macro": { + "argument": { + "enable": true + } + }, "end": { "structure": { "enable": true @@ -345,6 +350,14 @@ } } }, + "macro": { + "$ref": "#/$defs/InlayHintsMacroUserConfig", + "default": { + "argument": { + "enable": true + } + } + }, "end": { "$ref": "#/$defs/InlayHintsEndUserConfig", "default": { @@ -390,6 +403,18 @@ }, "additionalProperties": false }, + "InlayHintsMacroUserConfig": { + "type": "object", + "properties": { + "argument": { + "$ref": "#/$defs/EnableUserConfig", + "default": { + "enable": true + } + } + }, + "additionalProperties": false + }, "InlayHintsEndUserConfig": { "type": "object", "properties": { diff --git a/src/config/user_config.rs b/src/config/user_config.rs index 5602035d..42039cb6 100644 --- a/src/config/user_config.rs +++ b/src/config/user_config.rs @@ -360,6 +360,7 @@ impl Default for FormattingIndentUserConfig { pub(crate) struct InlayHintsUserConfig { pub(crate) port: InlayHintsPortUserConfig, pub(crate) parameter: InlayHintsParameterUserConfig, + pub(crate) r#macro: InlayHintsMacroUserConfig, pub(crate) end: InlayHintsEndUserConfig, } @@ -379,6 +380,14 @@ pub(crate) struct InlayHintsParameterUserConfig { pub(crate) assignment: EnableUserConfig, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "user-config-schema", derive(schemars::JsonSchema))] +#[serde(default, deny_unknown_fields)] +#[cfg_attr(feature = "user-config-schema", schemars(deny_unknown_fields))] +pub(crate) struct InlayHintsMacroUserConfig { + pub(crate) argument: EnableUserConfig, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "user-config-schema", derive(schemars::JsonSchema))] #[serde(default, deny_unknown_fields)] @@ -1011,6 +1020,17 @@ const USER_CONFIG_SETTINGS: &[ConfigSettingMeta] = &[ default: ConfigSettingDefault::Bool(true), schema: ConfigSettingSchema::Boolean, }, + ConfigSettingMeta { + path: &["inlayHints", "macro", "argument", "enable"], + vscode_key: "vide.inlayHints.macro.argument.enable", + docs_group: "Annotations", + description_key: "configuration.inlayHints.macro.argument.enable.description", + markdown_description_key: None, + enum_descriptions: &[], + exposed_in_vscode: true, + default: ConfigSettingDefault::Bool(true), + schema: ConfigSettingSchema::Boolean, + }, ConfigSettingMeta { path: &["inlayHints", "end", "structure", "enable"], vscode_key: "vide.inlayHints.end.structure.enable", @@ -1218,6 +1238,7 @@ const USER_CONFIG_KNOWN_PATHS: &[&[&str]] = &[ &["formatting", "indent", "width"], &["formatting", "on", "enter"], &["inlayHints", "end", "structure", "enable"], + &["inlayHints", "macro", "argument", "enable"], &["inlayHints", "parameter", "assignment", "enable"], &["inlayHints", "port", "connection", "enable"], &["lens", "instantiations", "enable"], @@ -1372,6 +1393,9 @@ fn apply_user_config_fields( field!(&["inlayHints", "end", "structure", "enable"], bool, |cfg, value| { cfg.inlay_hints.end.structure.enable = value }); + field!(&["inlayHints", "macro", "argument", "enable"], bool, |cfg, value| { + cfg.inlay_hints.r#macro.argument.enable = value + }); field!(&["inlayHints", "parameter", "assignment", "enable"], bool, |cfg, value| { cfg.inlay_hints.parameter.assignment.enable = value }); @@ -1543,6 +1567,7 @@ impl Config { InlayHintConfig { port_connection: self.user_config.inlay_hints.port.connection.enable, parameter_assignment: self.user_config.inlay_hints.parameter.assignment.enable, + macro_argument: self.user_config.inlay_hints.r#macro.argument.enable, end_structure: self.user_config.inlay_hints.end.structure.enable, } } diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 76171c09..a95a6e59 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -602,7 +602,9 @@ pub(crate) fn inlay_hint( let position = self::position(line_info, position); let kind = match kind { - InlayKind::ParamAssign | InlayKind::Port => Some(lsp_types::InlayHintKind::PARAMETER), + InlayKind::ParamAssign | InlayKind::Port | InlayKind::MacroArgument => { + Some(lsp_types::InlayHintKind::PARAMETER) + } InlayKind::EndStructure => None, }; From 01f469ec82c9bf2b1ca0a6e9766dbb45323b4f22 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 13:47:38 +0800 Subject: [PATCH 74/80] test(vscode): cover macro argument inlay setting --- editors/vscode/test/configuration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/editors/vscode/test/configuration.test.ts b/editors/vscode/test/configuration.test.ts index af3e6e34..eb7308e0 100644 --- a/editors/vscode/test/configuration.test.ts +++ b/editors/vscode/test/configuration.test.ts @@ -99,6 +99,7 @@ test('contributes settings for the complete Vide user configuration surface', () 'vide.formatting.indent.width', 'vide.inlayHints.port.connection.enable', 'vide.inlayHints.parameter.assignment.enable', + 'vide.inlayHints.macro.argument.enable', 'vide.inlayHints.end.structure.enable', 'vide.lens.instantiations.enable', 'vide.semantic.tokens.port.clk.rst.enable', From 929bfb10ac23a63077303f41c28bdcb9cceedcbd Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 14:37:40 +0800 Subject: [PATCH 75/80] refactor(ide): model source token resolution --- crates/ide/src/document_highlight.rs | 7 +- crates/ide/src/goto_declaration.rs | 7 +- crates/ide/src/goto_definition.rs | 7 +- crates/ide/src/hover.rs | 29 +-- crates/ide/src/references.rs | 7 +- crates/ide/src/references/search.rs | 12 +- crates/ide/src/source_tokens.rs | 264 +++++++++++++++++---------- 7 files changed, 201 insertions(+), 132 deletions(-) diff --git a/crates/ide/src/document_highlight.rs b/crates/ide/src/document_highlight.rs index 9ec7dd31..af5b92e6 100644 --- a/crates/ide/src/document_highlight.rs +++ b/crates/ide/src/document_highlight.rs @@ -33,14 +33,15 @@ pub(crate) fn document_highlight( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let selection = crate::source_tokens::token_candidates_at_offset( + let tokens = crate::source_tokens::source_token_resolution_at_offset( db, file_id, root, offset, token_precedence, - )?; - let tokens = selection.tokens()?; + )? + .resolved()? + .into_tokens(); let highlights = tokens .into_iter() .filter_map(|token| highlight_for_token(&sema, file_id, hir_file_id, token, config.clone())) diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 8d7dd429..ec696e1f 100644 --- a/crates/ide/src/goto_declaration.rs +++ b/crates/ide/src/goto_declaration.rs @@ -17,14 +17,15 @@ pub(crate) fn goto_declaration( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let selection = crate::source_tokens::token_candidates_at_offset( + let selection = crate::source_tokens::source_token_resolution_at_offset( db, file_id, root, offset, goto_definition::token_precedence, - )?; - let (range, tokens) = selection.range_and_tokens()?; + )? + .resolved()?; + let (range, tokens) = selection.into_parts(); let origins = tokens .into_iter() diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 1c317966..b7cdcfbe 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -40,14 +40,15 @@ pub(crate) fn goto_definition( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let selection = crate::source_tokens::token_candidates_at_offset( + let selection = crate::source_tokens::source_token_resolution_at_offset( db, file_id, root, offset, token_precedence, - )?; - let (range, tokens) = selection.range_and_tokens()?; + )? + .resolved()?; + let (range, tokens) = selection.into_parts(); let navs = tokens .into_iter() .filter_map(|token| nav_targets_for_token(db, &sema, hir_file_id, token)) diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 16ae216d..32997ba4 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -27,12 +27,8 @@ use utils::{ use vfs::FileId; use crate::{ - FilePosition, RangeInfo, - db::root_db::RootDb, - definitions::DefinitionClass, - markup::Markup, - render, - source_tokens::{PreprocTokenSelection, SourceTokenSelection}, + FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, markup::Markup, + render, source_tokens::SourceTokenSelection, }; const MACRO_EXPANSION_SEPARATOR: &str = "--------------------"; @@ -78,31 +74,24 @@ pub(crate) fn hover( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let selection = crate::source_tokens::token_candidates_at_offset( + let selection = crate::source_tokens::source_token_resolution_at_offset( db, file_id, root, offset, token_precedence, - )?; - let hover = match selection { - SourceTokenSelection::NormalSyntax(selection) => { - hover_for_token_selection(&sema, hir_file_id, selection.range, selection.tokens) - } - SourceTokenSelection::Preproc(selection) => { - hover_for_preproc_selection(&sema, hir_file_id, selection) - } - SourceTokenSelection::Unavailable(_) | SourceTokenSelection::Ambiguous(_) => None, - }?; + )? + .resolved()?; + let hover = hover_for_source_token_selection(&sema, hir_file_id, selection)?; Some(with_expanded_macro_hover(db, file_id, offset, hover)) } -fn hover_for_preproc_selection( +fn hover_for_source_token_selection( sema: &Semantics, hir_file_id: HirFileId, - selection: PreprocTokenSelection<'_>, + selection: SourceTokenSelection<'_>, ) -> Option> { - let (range, tokens) = selection.range_and_tokens(); + let (range, tokens) = selection.into_parts(); hover_for_token_selection(sema, hir_file_id, range, tokens) } diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index bb97e8f6..521f86d2 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -102,14 +102,15 @@ pub(crate) fn references( let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); let root = parsed_file.root()?; - let selection = crate::source_tokens::token_candidates_at_offset( + let tokens = crate::source_tokens::source_token_resolution_at_offset( db, file_id, root, offset, token_precedence, - )?; - let tokens = selection.tokens()?; + )? + .resolved()? + .into_tokens(); let references = tokens .into_iter() .filter_map(|token| references_for_token(&sema, hir_file_id, token, config.clone())) diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index b0c90477..3c63a37b 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -276,22 +276,20 @@ impl<'a, 'b> ReferencesCtx<'a, 'b> { offset: TextSize, source_token_cache: &mut SourceTokenRequestCache, ) -> Vec> { - let Some(selection) = crate::source_tokens::token_candidates_at_offset_with_cache( + let Some(selection) = crate::source_tokens::source_token_resolution_at_offset_with_cache( db, file_id, node, offset, super::token_precedence, source_token_cache, - ) else { + ) + .and_then(|resolution| resolution.resolved()) else { return Vec::new(); }; - let Some(tokens) = selection.tokens() else { - return Vec::new(); - }; - - tokens + selection + .into_tokens() .into_iter() .filter(|tok| tok.kind().name_like()) .filter(|tok| { diff --git a/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs index 67cd4517..15e2ea1c 100644 --- a/crates/ide/src/source_tokens.rs +++ b/crates/ide/src/source_tokens.rs @@ -16,58 +16,96 @@ use vfs::FileId; use crate::db::root_db::RootDb; #[derive(Debug, Clone)] -pub(crate) enum SourceTokenSelection<'tree> { - NormalSyntax(NormalSyntaxSelection<'tree>), - Preproc(PreprocTokenSelection<'tree>), - Unavailable(PreprocTokenUnavailable), - Ambiguous(PreprocTokenAmbiguity), +pub(crate) enum SourceTokenResolution<'tree> { + Resolved(SourceTokenSelection<'tree>), + Blocked(SourceTokenBlock), } -impl<'tree> SourceTokenSelection<'tree> { - pub(crate) fn range_and_tokens(self) -> Option<(TextRange, Vec>)> { +impl<'tree> SourceTokenResolution<'tree> { + pub(crate) fn resolved(self) -> Option> { match self { - Self::NormalSyntax(selection) => Some((selection.range, selection.tokens)), - Self::Preproc(selection) => Some(selection.range_and_tokens()), - Self::Unavailable(PreprocTokenUnavailable { range: _ }) => None, - Self::Ambiguous(PreprocTokenAmbiguity { range: _, hits: _ }) => None, + Self::Resolved(selection) => Some(selection), + Self::Blocked(SourceTokenBlock { .. }) => None, } } - - pub(crate) fn tokens(self) -> Option>> { - self.range_and_tokens().map(|(_, tokens)| tokens) - } } #[derive(Debug, Clone)] -pub(crate) struct NormalSyntaxSelection<'tree> { +pub(crate) struct SourceTokenSelection<'tree> { + pub origin: SourceTokenOrigin, pub range: TextRange, pub tokens: Vec>, } -#[derive(Debug, Clone)] -pub(crate) struct PreprocTokenSelection<'tree> { - pub range: TextRange, - pub hits: Vec, - pub tokens: Vec>, -} +impl<'tree> SourceTokenSelection<'tree> { + fn normal_syntax(range: TextRange, tokens: Vec>) -> Self { + Self { origin: SourceTokenOrigin::NormalSyntax, range, tokens } + } + + fn preproc( + range: TextRange, + hits: Vec, + tokens: Vec>, + ) -> Self { + Self { origin: SourceTokenOrigin::Preproc { hits }, range, tokens } + } + + pub(crate) fn into_parts(self) -> (TextRange, Vec>) { + let Self { origin, range, tokens } = self; + match origin { + SourceTokenOrigin::NormalSyntax => (range, tokens), + SourceTokenOrigin::Preproc { hits } => { + let _hit_count = hits.len(); + (range, tokens) + } + } + } -impl<'tree> PreprocTokenSelection<'tree> { - pub(crate) fn range_and_tokens(self) -> (TextRange, Vec>) { - let Self { range, hits, tokens } = self; - let _hit_count = hits.len(); - (range, tokens) + pub(crate) fn into_tokens(self) -> Vec> { + self.into_parts().1 } } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct PreprocTokenUnavailable { - pub range: TextRange, +pub(crate) enum SourceTokenOrigin { + NormalSyntax, + Preproc { hits: Vec }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SourceTokenDomain { + Preproc, } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct PreprocTokenAmbiguity { +pub(crate) struct SourceTokenBlock { + pub domain: SourceTokenDomain, pub range: TextRange, - pub hits: Vec, + pub reason: SourceTokenBlockReason, +} + +impl SourceTokenBlock { + fn preproc_unavailable(range: TextRange) -> Self { + Self { + domain: SourceTokenDomain::Preproc, + range, + reason: SourceTokenBlockReason::Unavailable, + } + } + + fn preproc_ambiguous(range: TextRange, hits: Vec) -> Self { + Self { + domain: SourceTokenDomain::Preproc, + range, + reason: SourceTokenBlockReason::Ambiguous { hits }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SourceTokenBlockReason { + Unavailable, + Ambiguous { hits: Vec }, } #[derive(Debug, Default)] @@ -137,104 +175,111 @@ enum PreprocSemanticTarget { MacroBody { definition_id: MacroDefinitionId, source: MappedPreprocSource, range: TextRange }, } -pub(crate) fn token_candidates_at_offset<'tree, F>( +pub(crate) fn source_token_resolution_at_offset<'tree, F>( db: &RootDb, file_id: FileId, root: SyntaxNode<'tree>, offset: TextSize, precedence: F, -) -> Option> +) -> Option> where F: Fn(TokenKind) -> usize, { let mut cache = SourceTokenRequestCache::default(); - token_candidates_at_offset_with_cache(db, file_id, root, offset, precedence, &mut cache) + source_token_resolution_at_offset_with_cache(db, file_id, root, offset, precedence, &mut cache) } -pub(crate) fn token_candidates_at_offset_with_cache<'tree, F>( +pub(crate) fn source_token_resolution_at_offset_with_cache<'tree, F>( db: &RootDb, file_id: FileId, root: SyntaxNode<'tree>, offset: TextSize, precedence: F, cache: &mut SourceTokenRequestCache, -) -> Option> +) -> Option> where F: Fn(TokenKind) -> usize, { - match provenance_token_candidates_at_offset(db, file_id, root, offset, &precedence, cache) { - ProvenanceTokenLookup::Available(selection) => { - return Some(SourceTokenSelection::Preproc(selection)); - } - ProvenanceTokenLookup::Unavailable(unavailable) => { - return Some(SourceTokenSelection::Unavailable(unavailable)); - } - ProvenanceTokenLookup::Ambiguous(ambiguous) => { - return Some(SourceTokenSelection::Ambiguous(ambiguous)); + match preproc_source_token_at_offset(db, file_id, root, offset, &precedence, cache) { + SourceTokenProviderResult::NotApplicable => { + normal_syntax_source_token_at_offset(root, offset, &precedence).into_resolution() } - ProvenanceTokenLookup::NotApplicable => {} + result => result.into_resolution(), } - - normal_syntax_selection_at_offset(root, offset, &precedence) - .map(SourceTokenSelection::NormalSyntax) } -fn normal_syntax_selection_at_offset<'tree>( +fn normal_syntax_source_token_at_offset<'tree>( root: SyntaxNode<'tree>, offset: TextSize, precedence: &impl Fn(TokenKind) -> usize, -) -> Option> { - let token = root.token_at_offset(offset).pick_bext_token(precedence)?; - Some(NormalSyntaxSelection { range: token.text_range()?, tokens: vec![token] }) +) -> SourceTokenProviderResult<'tree> { + let Some(token) = root.token_at_offset(offset).pick_bext_token(precedence) else { + return SourceTokenProviderResult::NotApplicable; + }; + let Some(range) = token.text_range() else { + return SourceTokenProviderResult::NotApplicable; + }; + SourceTokenProviderResult::Resolved(SourceTokenSelection::normal_syntax(range, vec![token])) } -enum ProvenanceTokenLookup<'tree> { - Available(PreprocTokenSelection<'tree>), - Unavailable(PreprocTokenUnavailable), - Ambiguous(PreprocTokenAmbiguity), +enum SourceTokenProviderResult<'tree> { + Resolved(SourceTokenSelection<'tree>), + Blocked(SourceTokenBlock), NotApplicable, } -fn provenance_token_candidates_at_offset<'tree>( +impl<'tree> SourceTokenProviderResult<'tree> { + fn into_resolution(self) -> Option> { + match self { + Self::Resolved(selection) => Some(SourceTokenResolution::Resolved(selection)), + Self::Blocked(block) => Some(SourceTokenResolution::Blocked(block)), + Self::NotApplicable => None, + } + } +} + +fn preproc_source_token_at_offset<'tree>( db: &RootDb, file_id: FileId, root: SyntaxNode<'tree>, offset: TextSize, precedence: &impl Fn(TokenKind) -> usize, cache: &mut SourceTokenRequestCache, -) -> ProvenanceTokenLookup<'tree> { +) -> SourceTokenProviderResult<'tree> { if !source_macro_invocation_may_cover_offset(db.file_text(file_id).as_ref(), offset) { - return ProvenanceTokenLookup::NotApplicable; + return SourceTokenProviderResult::NotApplicable; } let provenances = match cache.macro_expansion_provenances_at(db, file_id, offset) { Ok(provenances) => provenances, Err(PreprocError::SourceQuery(SourcePreprocQueryError::UnsupportedFileKind(_))) => { - return ProvenanceTokenLookup::NotApplicable; + return SourceTokenProviderResult::NotApplicable; } Err(_) => { - return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { - range: TextRange::empty(offset), - }); + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + TextRange::empty(offset), + )); } }; if provenances.is_empty() { - return ProvenanceTokenLookup::NotApplicable; + return SourceTokenProviderResult::NotApplicable; } match preproc_hits_at_offset(&provenances, file_id, offset) { PreprocHitLookup::Available { range, hits } => { let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &hits) else { - return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }); + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + range, + )); }; - ProvenanceTokenLookup::Available(PreprocTokenSelection { range, hits, tokens }) + SourceTokenProviderResult::Resolved(SourceTokenSelection::preproc(range, hits, tokens)) } PreprocHitLookup::Unavailable { range } => { - ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }) + SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable(range)) } PreprocHitLookup::Ambiguous { range, hits } => { - ProvenanceTokenLookup::Ambiguous(PreprocTokenAmbiguity { range, hits }) + SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_ambiguous(range, hits)) } } } @@ -530,7 +575,7 @@ mod tests { ); let hit = test_source_hit(file_id, provenance_range, 0); - let ProvenanceTokenLookup::Available(selection) = preproc_selection_from_hits( + let SourceTokenProviderResult::Resolved(selection) = preproc_provider_result_from_hits( root, offset, &test_precedence, @@ -541,7 +586,10 @@ mod tests { }; assert_eq!(selection.range, provenance_range); - assert_eq!(selection.hits.len(), 1); + let SourceTokenOrigin::Preproc { hits } = &selection.origin else { + panic!("preproc provider should produce a preproc-origin selection"); + }; + assert_eq!(hits.len(), 1); assert_eq!(selection.tokens.len(), 1); assert_eq!(selection.tokens[0].text_range(), Some(parser_range)); assert_ne!(selection.tokens[0].text_range(), Some(provenance_range)); @@ -552,22 +600,41 @@ mod tests { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); assert!( - normal_syntax_selection_at_offset(root, offset, &test_precedence).is_some(), + matches!( + normal_syntax_source_token_at_offset(root, offset, &test_precedence), + SourceTokenProviderResult::Resolved(_) + ), "test setup must have an ordinary syntax token that fallback could have selected" ); - let lookup = - preproc_selection_from_hits(root, offset, &test_precedence, Vec::new(), parser_range); - assert!(matches!(lookup, ProvenanceTokenLookup::Unavailable(_))); + let lookup = preproc_provider_result_from_hits( + root, + offset, + &test_precedence, + Vec::new(), + parser_range, + ); + assert!(matches!( + lookup, + SourceTokenProviderResult::Blocked(SourceTokenBlock { + domain: SourceTokenDomain::Preproc, + reason: SourceTokenBlockReason::Unavailable, + .. + }) + )); } #[test] fn source_tokens_normal_syntax_path_still_selects_non_preproc_offsets() { let (root, offset, parser_range) = root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); - let selection = normal_syntax_selection_at_offset(root, offset, &test_precedence) - .expect("normal syntax token expected"); + let SourceTokenProviderResult::Resolved(selection) = + normal_syntax_source_token_at_offset(root, offset, &test_precedence) + else { + panic!("normal syntax token expected"); + }; + assert!(matches!(selection.origin, SourceTokenOrigin::NormalSyntax)); assert_eq!(selection.range, parser_range); assert_eq!(selection.tokens.len(), 1); } @@ -604,13 +671,16 @@ mod tests { test_source_hit(file_id, parser_range, 1), ]; - let ProvenanceTokenLookup::Available(selection) = - preproc_selection_from_hits(root, offset, &test_precedence, hits, parser_range) + let SourceTokenProviderResult::Resolved(selection) = + preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) else { panic!("same semantic target should dedup to one available preproc hit"); }; - assert_eq!(selection.hits.len(), 1); + let SourceTokenOrigin::Preproc { hits } = selection.origin else { + panic!("preproc provider should produce a preproc-origin selection"); + }; + assert_eq!(hits.len(), 1); } #[test] @@ -622,13 +692,15 @@ mod tests { let second = TextRange::new(parser_range.start() + TextSize::from(1), parser_range.end()); let hits = vec![test_source_hit(file_id, first, 0), test_source_hit(file_id, second, 1)]; - let ProvenanceTokenLookup::Ambiguous(ambiguous) = - preproc_selection_from_hits(root, offset, &test_precedence, hits, parser_range) + let SourceTokenProviderResult::Blocked(SourceTokenBlock { + reason: SourceTokenBlockReason::Ambiguous { hits }, + .. + }) = preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) else { panic!("conflicting preproc targets should be ambiguous"); }; - assert_eq!(ambiguous.hits.len(), 2); + assert_eq!(hits.len(), 2); } #[test] @@ -691,13 +763,13 @@ mod tests { } } - fn preproc_selection_from_hits<'tree>( + fn preproc_provider_result_from_hits<'tree>( root: SyntaxNode<'tree>, offset: TextSize, precedence: &impl Fn(TokenKind) -> usize, hits: Vec, fallback_range: TextRange, - ) -> ProvenanceTokenLookup<'tree> { + ) -> SourceTokenProviderResult<'tree> { let mut unique_hits = Vec::new(); for hit in hits { if hit.source_range.contains(offset) { @@ -705,24 +777,30 @@ mod tests { } } if unique_hits.is_empty() { - return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { - range: fallback_range, - }); + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + fallback_range, + )); } let range = covering_range(&unique_hits.iter().map(|hit| hit.source_range).collect::>()) .unwrap_or(fallback_range); if unique_hits.len() > 1 { - return ProvenanceTokenLookup::Ambiguous(PreprocTokenAmbiguity { + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_ambiguous( range, - hits: unique_hits, - }); + unique_hits, + )); } let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &unique_hits) else { - return ProvenanceTokenLookup::Unavailable(PreprocTokenUnavailable { range }); + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + range, + )); }; - ProvenanceTokenLookup::Available(PreprocTokenSelection { range, hits: unique_hits, tokens }) + SourceTokenProviderResult::Resolved(SourceTokenSelection::preproc( + range, + unique_hits, + tokens, + )) } fn preproc_hit_for_raw_provenance( From 27d243d100362c2049ccf8cf6eef3b1e2248431c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 16:20:47 +0800 Subject: [PATCH 76/80] refactor(vscode): model extension package plan --- editors/vscode/scripts/package.ts | 411 +----------------- editors/vscode/scripts/package/cli.ts | 48 ++ editors/vscode/scripts/package/context.ts | 34 ++ editors/vscode/scripts/package/manifest.ts | 67 +++ .../scripts/package/packageExtension.ts | 48 ++ editors/vscode/scripts/package/process.ts | 45 ++ editors/vscode/scripts/package/server.ts | 192 ++++++++ editors/vscode/scripts/package/targets.ts | 125 ++++++ editors/vscode/scripts/package/vsce.ts | 15 + editors/vscode/test/package.test.ts | 55 +++ 10 files changed, 634 insertions(+), 406 deletions(-) create mode 100644 editors/vscode/scripts/package/cli.ts create mode 100644 editors/vscode/scripts/package/context.ts create mode 100644 editors/vscode/scripts/package/manifest.ts create mode 100644 editors/vscode/scripts/package/packageExtension.ts create mode 100644 editors/vscode/scripts/package/process.ts create mode 100644 editors/vscode/scripts/package/server.ts create mode 100644 editors/vscode/scripts/package/targets.ts create mode 100644 editors/vscode/scripts/package/vsce.ts create mode 100644 editors/vscode/test/package.test.ts diff --git a/editors/vscode/scripts/package.ts b/editors/vscode/scripts/package.ts index 0c01773e..8c6a9be0 100644 --- a/editors/vscode/scripts/package.ts +++ b/editors/vscode/scripts/package.ts @@ -1,411 +1,10 @@ -import { spawnSync } from 'node:child_process'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -import { - SUPPORTED_PLATFORM_FOLDERS, - type PlatformFolder, - getPlatformFolder, - isPlatformFolder, -} from '../src/platform'; - -const vscodeDir = findExtensionRoot(__dirname); -const repoRoot = path.resolve(vscodeDir, '..', '..'); -const binName = 'vide'; -const webTarget = 'web'; - -type BuildProfile = 'debug' | 'release'; -type ServerMode = 'build' | 'prebuilt'; -type PackageTarget = PlatformFolder | typeof webTarget; - -const cargoTargets: Partial> = { - 'alpine-arm64': 'aarch64-unknown-linux-musl', - 'alpine-x64': 'x86_64-unknown-linux-musl', -}; - -function findExtensionRoot(startDir: string): string { - let currentDir = path.resolve(startDir); - - while (true) { - if ( - fs.existsSync(path.join(currentDir, 'package.json')) && - fs.existsSync(path.join(currentDir, 'language-configuration.json')) - ) { - return currentDir; - } - - const parentDir = path.dirname(currentDir); - if (parentDir === currentDir) { - throw new Error(`could not find VS Code extension root from ${startDir}`); - } - currentDir = parentDir; - } -} - -function hostPlatformFolder(): PlatformFolder { - const folder = getPlatformFolder(process.platform, process.arch); - if (!folder) { - throw new Error(`unsupported host platform: ${process.platform}-${process.arch}`); - } - - return folder; -} - -function binaryFileForTarget(target: PlatformFolder): string { - return target.startsWith('win32-') ? `${binName}.exe` : binName; -} - -function run( - command: string, - args: string[], - cwd: string, - env: NodeJS.ProcessEnv = process.env, -): void { - const result = spawnSync(command, args, { - cwd, - env, - shell: false, - stdio: 'inherit', - }); - - if (result.error) { - throw result.error; - } - - if (result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); - } -} - -function sanitizedVsceEnv(): NodeJS.ProcessEnv { - const env = { ...process.env }; - - for (const key of Object.keys(env)) { - const normalized = key.toLowerCase(); - if ( - normalized === 'npm_config_verify_deps_before_run' || - normalized === 'npm_config_npm_globalconfig' || - normalized === 'npm_config__jsr_registry' - ) { - delete env[key]; - } - } - - return env; -} - -function cargoProfileDir(profile: BuildProfile): string { - return profile === 'release' ? 'release' : 'debug'; -} - -function cargoBuildArgs(profile: BuildProfile, cargoTarget?: string): string[] { - const args = ['build']; - if (profile === 'release') { - args.push('--release'); - } - if (cargoTarget) { - args.push('--target', cargoTarget); - } - - return args; -} - -function cargoTargetEnvName(cargoTarget: string): string { - return cargoTarget.toUpperCase().replace(/-/g, '_'); -} - -function cargoTargetLinkerEnvKey(cargoTarget: string): string { - return `CARGO_TARGET_${cargoTargetEnvName(cargoTarget)}_LINKER`; -} - -function cxxCompilerEnvKey(cargoTarget: string): string { - return `CXX_${cargoTarget.replace(/-/g, '_')}`; -} - -function cargoLinkerForTarget(target: PlatformFolder, cargoTarget: string): string | undefined { - if (!target.startsWith('alpine-')) { - return undefined; - } - - return ( - optionalEnv(cxxCompilerEnvKey(cargoTarget)) ?? - optionalEnv('TARGET_CXX') ?? - `${cargoTarget}-g++` - ); -} - -function lateRustLinkFlagsForTarget(target: PlatformFolder): string[] { - if (!target.startsWith('alpine-')) { - return []; - } - - // Static libstdc++ can introduce libc references after rustc's own musl -lc. - return ['-C', 'link-arg=-lc']; -} - -function appendRustFlags(env: NodeJS.ProcessEnv, flags: string[]): NodeJS.ProcessEnv { - if (flags.length === 0) { - return env; - } - - const encodedFlags = env.CARGO_ENCODED_RUSTFLAGS; - if (encodedFlags) { - return { - ...env, - CARGO_ENCODED_RUSTFLAGS: `${encodedFlags}\x1f${flags.join('\x1f')}`, - }; - } - - const rustFlags = env.RUSTFLAGS?.trim(); - return { - ...env, - RUSTFLAGS: rustFlags ? `${rustFlags} ${flags.join(' ')}` : flags.join(' '), - }; -} - -function cargoBuildEnv(target: PlatformFolder, cargoTarget?: string): NodeJS.ProcessEnv { - if (!cargoTarget) { - return process.env; - } - - let env = process.env; - const linkerEnvKey = cargoTargetLinkerEnvKey(cargoTarget); - if (!optionalEnv(linkerEnvKey)) { - const linker = cargoLinkerForTarget(target, cargoTarget); - if (linker) { - console.log(`Using Cargo linker for ${cargoTarget}: ${linker}`); - env = { ...env, [linkerEnvKey]: linker }; - } - } - - const lateLinkArgs = lateRustLinkFlagsForTarget(target); - if (lateLinkArgs.length > 0) { - console.log(`Adding Cargo link args for ${cargoTarget}: ${lateLinkArgs.join(' ')}`); - env = appendRustFlags(env, lateLinkArgs); - } - - return env; -} - -function cargoOutputDir(profile: BuildProfile, cargoTarget?: string): string { - const pathParts = [repoRoot, 'target']; - if (cargoTarget) { - pathParts.push(cargoTarget); - } - pathParts.push(cargoProfileDir(profile)); - - return path.join(...pathParts); -} - -function ensureServerExecutable(serverPath: string, target: PlatformFolder): void { - if (!target.startsWith('win32-')) { - fs.chmodSync(serverPath, 0o755); - } -} - -function ensureTargetServerBinary( - target: PlatformFolder, - binFile: string, - profile: BuildProfile, - serverMode: ServerMode, -): string { - const serverOutDir = path.join(vscodeDir, 'server', target); - const serverPath = path.join(serverOutDir, binFile); - if (serverMode === 'prebuilt') { - if (fs.existsSync(serverPath)) { - ensureServerExecutable(serverPath, target); - return serverPath; - } - throw new Error(`missing prebuilt server binary: ${serverPath}`); - } - - const hostTarget = hostPlatformFolder(); - const cargoTarget = cargoTargets[target]; - if (target !== hostTarget && !cargoTarget) { - throw new Error( - `missing bundled server binary: ${serverPath}\n` + - 'tip: run packaging on a matching native runner or copy the target binary first.', - ); - } - - if (cargoTarget) { - run('rustup', ['target', 'add', cargoTarget], repoRoot); - } - - run( - 'cargo', - cargoBuildArgs(profile, cargoTarget), - repoRoot, - cargoBuildEnv(target, cargoTarget), - ); - - const sourcePath = path.join(cargoOutputDir(profile, cargoTarget), binFile); - const destPath = path.join(serverOutDir, binFile); - fs.mkdirSync(serverOutDir, { recursive: true }); - fs.copyFileSync(sourcePath, destPath); - ensureServerExecutable(destPath, target); - - return destPath; -} - -function stageRuntimeServer(sourcePath: string, target: PlatformFolder, binFile: string): string { - const runtimeServerDir = path.join(vscodeDir, 'server'); - const runtimeServerPath = path.join(runtimeServerDir, binFile); - - fs.mkdirSync(runtimeServerDir, { recursive: true }); - fs.copyFileSync(sourcePath, runtimeServerPath); - if (!target.startsWith('win32-')) { - fs.chmodSync(runtimeServerPath, 0o755); - } - - return runtimeServerPath; -} - -function cleanRuntimeServerFiles(): void { - for (const binFile of [`${binName}.exe`, binName]) { - fs.rmSync(path.join(vscodeDir, 'server', binFile), { force: true }); - } -} - -function syncReadmeFromRepoRoot(): void { - fs.copyFileSync(path.join(repoRoot, 'README.md'), path.join(vscodeDir, 'README.md')); -} - -function readExtensionVersion(): string { - const packageJson = JSON.parse(fs.readFileSync(path.join(vscodeDir, 'package.json'), 'utf8')) as { - version?: unknown; - }; - if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) { - throw new Error('VS Code extension package.json must define a version.'); - } - return packageJson.version; -} - -function optionalEnv(name: string): string | undefined { - const value = process.env[name]?.trim(); - return value ? value : undefined; -} - -function writeBuildInfo(target: PackageTarget, profile: BuildProfile): void { - const buildInfo = { - version: readExtensionVersion(), - target, - profile, - kind: optionalEnv('VIDE_EXTENSION_BUILD_KIND') ?? 'local', - commitHash: optionalEnv('VIDE_EXTENSION_COMMIT_HASH'), - buildDate: optionalEnv('VIDE_EXTENSION_BUILD_DATE'), - }; - fs.writeFileSync( - path.join(vscodeDir, 'build-info.json'), - `${JSON.stringify(buildInfo, null, 2)}\n`, - ); -} - -function packageJsonPath(): string { - return path.join(vscodeDir, 'package.json'); -} - -function stagePackageJsonForTarget(target: PackageTarget): string | undefined { - if (target === webTarget) { - return undefined; - } - - const packagePath = packageJsonPath(); - const originalPackageJson = fs.readFileSync(packagePath, 'utf8'); - const packageJson = JSON.parse(originalPackageJson) as { browser?: unknown }; - delete packageJson.browser; - fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); - return originalPackageJson; -} - -function parseServerMode(value: string): ServerMode { - if (value === 'build' || value === 'prebuilt') { - return value; - } - throw new Error(`unsupported server mode: ${value}`); -} - -function parseArgs(): { target: PackageTarget; profile: BuildProfile; serverMode: ServerMode } { - const args = process.argv.slice(2); - let profile: BuildProfile = 'release'; - let serverMode: ServerMode = 'build'; - let target: string | undefined; - - for (const arg of args) { - if (arg === '--debug') { - profile = 'debug'; - } else if (arg.startsWith('--server=')) { - serverMode = parseServerMode(arg.slice('--server='.length)); - } else if (!target) { - target = arg; - } else { - throw new Error(`unexpected package argument: ${arg}`); - } - } - - target ??= hostPlatformFolder(); - if (target === webTarget) { - return { target, profile, serverMode }; - } - if (!isPlatformFolder(target)) { - throw new Error( - `unsupported target platform: ${target}\n` + - `supported targets: ${[...SUPPORTED_PLATFORM_FOLDERS, webTarget].join(', ')}`, - ); - } - - return { target, profile, serverMode }; -} - -function packageExtension( - target: PackageTarget, - profile: BuildProfile, - serverMode: ServerMode, -): string { - syncReadmeFromRepoRoot(); - writeBuildInfo(target, profile); - - const debugSuffix = profile === 'debug' ? '-debug' : ''; - const vsixOut = `vide-vscode-${target}${debugSuffix}.vsix`; - const vsceBin = path.join(vscodeDir, 'node_modules', '@vscode', 'vsce', 'vsce'); - - if (target === webTarget) { - cleanRuntimeServerFiles(); - run( - process.execPath, - [vsceBin, 'package', '--target', target, '--out', vsixOut], - vscodeDir, - sanitizedVsceEnv(), - ); - return path.join(vscodeDir, vsixOut); - } - - const binFile = binaryFileForTarget(target); - const targetServerPath = ensureTargetServerBinary(target, binFile, profile, serverMode); - cleanRuntimeServerFiles(); - const runtimeServerPath = stageRuntimeServer(targetServerPath, target, binFile); - const originalPackageJson = stagePackageJsonForTarget(target); - - try { - run( - process.execPath, - [vsceBin, 'package', '--target', target, '--out', vsixOut], - vscodeDir, - sanitizedVsceEnv(), - ); - } finally { - fs.rmSync(runtimeServerPath, { force: true }); - if (originalPackageJson) { - fs.writeFileSync(packageJsonPath(), originalPackageJson); - } - } - - return path.join(vscodeDir, vsixOut); -} +import { parsePackageCliArgs } from './package/cli'; +import { createPackageContext } from './package/context'; +import { packageExtension } from './package/packageExtension'; function main(): void { - const { target, profile, serverMode } = parseArgs(); - const vsixPath = packageExtension(target, profile, serverMode); + const options = parsePackageCliArgs(process.argv.slice(2)); + const vsixPath = packageExtension(options, createPackageContext(__dirname)); console.log(vsixPath); } diff --git a/editors/vscode/scripts/package/cli.ts b/editors/vscode/scripts/package/cli.ts new file mode 100644 index 00000000..20d9f1a0 --- /dev/null +++ b/editors/vscode/scripts/package/cli.ts @@ -0,0 +1,48 @@ +import { + type BuildProfile, + type PackageOptions, + type ServerMode, + parseBuildProfile, + parseServerMode, +} from './targets'; + +export function parsePackageCliArgs(args: string[]): PackageOptions { + let profile: BuildProfile = 'release'; + let serverMode: ServerMode = 'build'; + let target: string | undefined; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--debug') { + profile = 'debug'; + } else if (arg === '--release') { + profile = 'release'; + } else if (arg === '--target') { + target = readFlagValue(args, ++index, arg); + } else if (arg.startsWith('--target=')) { + target = arg.slice('--target='.length); + } else if (arg === '--profile') { + profile = parseBuildProfile(readFlagValue(args, ++index, arg)); + } else if (arg.startsWith('--profile=')) { + profile = parseBuildProfile(arg.slice('--profile='.length)); + } else if (arg === '--server') { + serverMode = parseServerMode(readFlagValue(args, ++index, arg)); + } else if (arg.startsWith('--server=')) { + serverMode = parseServerMode(arg.slice('--server='.length)); + } else if (!arg.startsWith('-') && !target) { + target = arg; + } else { + throw new Error(`unexpected package argument: ${arg}`); + } + } + + return { target, profile, serverMode }; +} + +function readFlagValue(args: string[], index: number, flag: string): string { + const value = args[index]; + if (!value || value.startsWith('-')) { + throw new Error(`missing value for ${flag}`); + } + return value; +} diff --git a/editors/vscode/scripts/package/context.ts b/editors/vscode/scripts/package/context.ts new file mode 100644 index 00000000..e76478c6 --- /dev/null +++ b/editors/vscode/scripts/package/context.ts @@ -0,0 +1,34 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export interface PackageContext { + vscodeDir: string; + repoRoot: string; +} + +export function createPackageContext(startDir: string = __dirname): PackageContext { + const vscodeDir = findExtensionRoot(startDir); + return { + vscodeDir, + repoRoot: path.resolve(vscodeDir, '..', '..'), + }; +} + +export function findExtensionRoot(startDir: string): string { + let currentDir = path.resolve(startDir); + + while (true) { + if ( + fs.existsSync(path.join(currentDir, 'package.json')) && + fs.existsSync(path.join(currentDir, 'language-configuration.json')) + ) { + return currentDir; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + throw new Error(`could not find VS Code extension root from ${startDir}`); + } + currentDir = parentDir; + } +} diff --git a/editors/vscode/scripts/package/manifest.ts b/editors/vscode/scripts/package/manifest.ts new file mode 100644 index 00000000..583cf443 --- /dev/null +++ b/editors/vscode/scripts/package/manifest.ts @@ -0,0 +1,67 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { optionalEnv } from './process'; +import type { PackagePlan } from './targets'; + +export function syncReadmeFromRepoRoot(context: PackageContext): void { + fs.copyFileSync( + path.join(context.repoRoot, 'README.md'), + path.join(context.vscodeDir, 'README.md'), + ); +} + +export function writeBuildInfo(context: PackageContext, plan: PackagePlan): void { + const buildInfo = { + version: readExtensionVersion(context), + target: plan.target, + profile: plan.profile, + kind: optionalEnv('VIDE_EXTENSION_BUILD_KIND') ?? 'local', + commitHash: optionalEnv('VIDE_EXTENSION_COMMIT_HASH'), + buildDate: optionalEnv('VIDE_EXTENSION_BUILD_DATE'), + }; + fs.writeFileSync( + path.join(context.vscodeDir, 'build-info.json'), + `${JSON.stringify(buildInfo, null, 2)}\n`, + ); +} + +export function stagePackageJsonForTarget( + context: PackageContext, + plan: PackagePlan, +): string | undefined { + if (!plan.targetSpec.removeBrowserEntry) { + return undefined; + } + + const packagePath = packageJsonPath(context); + const originalPackageJson = fs.readFileSync(packagePath, 'utf8'); + const packageJson = JSON.parse(originalPackageJson) as { browser?: unknown }; + delete packageJson.browser; + fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); + return originalPackageJson; +} + +export function restorePackageJson( + context: PackageContext, + originalPackageJson: string | undefined, +): void { + if (originalPackageJson) { + fs.writeFileSync(packageJsonPath(context), originalPackageJson); + } +} + +function packageJsonPath(context: PackageContext): string { + return path.join(context.vscodeDir, 'package.json'); +} + +function readExtensionVersion(context: PackageContext): string { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath(context), 'utf8')) as { + version?: unknown; + }; + if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) { + throw new Error('VS Code extension package.json must define a version.'); + } + return packageJson.version; +} diff --git a/editors/vscode/scripts/package/packageExtension.ts b/editors/vscode/scripts/package/packageExtension.ts new file mode 100644 index 00000000..10f1829d --- /dev/null +++ b/editors/vscode/scripts/package/packageExtension.ts @@ -0,0 +1,48 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { type PackageContext, createPackageContext } from './context'; +import { + restorePackageJson, + stagePackageJsonForTarget, + syncReadmeFromRepoRoot, + writeBuildInfo, +} from './manifest'; +import { cleanRuntimeServerFiles, ensureTargetServerBinary, stageRuntimeServer } from './server'; +import { type PackageOptions, createPackagePlan } from './targets'; +import { runVscePackage } from './vsce'; + +export function packageExtension( + options: PackageOptions, + context: PackageContext = createPackageContext(), +): string { + const plan = createPackagePlan(options); + + syncReadmeFromRepoRoot(context); + writeBuildInfo(context, plan); + + if (plan.targetSpec.kind === 'web') { + cleanRuntimeServerFiles(context); + runVscePackage(context, plan); + return path.join(context.vscodeDir, plan.vsixFile); + } + + const targetServerPath = ensureTargetServerBinary( + context, + plan.targetSpec, + plan.profile, + plan.serverMode, + ); + cleanRuntimeServerFiles(context); + const runtimeServerPath = stageRuntimeServer(context, targetServerPath, plan.targetSpec); + const originalPackageJson = stagePackageJsonForTarget(context, plan); + + try { + runVscePackage(context, plan); + } finally { + fs.rmSync(runtimeServerPath, { force: true }); + restorePackageJson(context, originalPackageJson); + } + + return path.join(context.vscodeDir, plan.vsixFile); +} diff --git a/editors/vscode/scripts/package/process.ts b/editors/vscode/scripts/package/process.ts new file mode 100644 index 00000000..20abd108 --- /dev/null +++ b/editors/vscode/scripts/package/process.ts @@ -0,0 +1,45 @@ +import { spawnSync } from 'node:child_process'; + +export function run( + command: string, + args: string[], + cwd: string, + env: NodeJS.ProcessEnv = process.env, +): void { + const result = spawnSync(command, args, { + cwd, + env, + shell: false, + stdio: 'inherit', + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); + } +} + +export function optionalEnv(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +export function sanitizedVsceEnv(): NodeJS.ProcessEnv { + const env = { ...process.env }; + + for (const key of Object.keys(env)) { + const normalized = key.toLowerCase(); + if ( + normalized === 'npm_config_verify_deps_before_run' || + normalized === 'npm_config_npm_globalconfig' || + normalized === 'npm_config__jsr_registry' + ) { + delete env[key]; + } + } + + return env; +} diff --git a/editors/vscode/scripts/package/server.ts b/editors/vscode/scripts/package/server.ts new file mode 100644 index 00000000..c84f5c36 --- /dev/null +++ b/editors/vscode/scripts/package/server.ts @@ -0,0 +1,192 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { optionalEnv, run } from './process'; +import { + type BuildProfile, + type NativeTargetSpec, + type ServerMode, + hostPlatformFolder, +} from './targets'; + +function cargoProfileDir(profile: BuildProfile): string { + return profile === 'release' ? 'release' : 'debug'; +} + +function cargoBuildArgs(profile: BuildProfile, cargoTarget?: string): string[] { + const args = ['build']; + if (profile === 'release') { + args.push('--release'); + } + if (cargoTarget) { + args.push('--target', cargoTarget); + } + + return args; +} + +function cargoTargetEnvName(cargoTarget: string): string { + return cargoTarget.toUpperCase().replace(/-/g, '_'); +} + +function cargoTargetLinkerEnvKey(cargoTarget: string): string { + return `CARGO_TARGET_${cargoTargetEnvName(cargoTarget)}_LINKER`; +} + +function cxxCompilerEnvKey(cargoTarget: string): string { + return `CXX_${cargoTarget.replace(/-/g, '_')}`; +} + +function cargoLinkerForTarget(spec: NativeTargetSpec, cargoTarget: string): string | undefined { + if (!spec.requiresAlpineLinker) { + return undefined; + } + + return ( + optionalEnv(cxxCompilerEnvKey(cargoTarget)) ?? + optionalEnv('TARGET_CXX') ?? + `${cargoTarget}-g++` + ); +} + +function lateRustLinkFlagsForTarget(spec: NativeTargetSpec): string[] { + if (!spec.requiresAlpineLinker) { + return []; + } + + // Static libstdc++ can introduce libc references after rustc's own musl -lc. + return ['-C', 'link-arg=-lc']; +} + +function appendRustFlags(env: NodeJS.ProcessEnv, flags: string[]): NodeJS.ProcessEnv { + if (flags.length === 0) { + return env; + } + + const encodedFlags = env.CARGO_ENCODED_RUSTFLAGS; + if (encodedFlags) { + return { + ...env, + CARGO_ENCODED_RUSTFLAGS: `${encodedFlags}\x1f${flags.join('\x1f')}`, + }; + } + + const rustFlags = env.RUSTFLAGS?.trim(); + return { + ...env, + RUSTFLAGS: rustFlags ? `${rustFlags} ${flags.join(' ')}` : flags.join(' '), + }; +} + +function cargoBuildEnv(spec: NativeTargetSpec): NodeJS.ProcessEnv { + if (!spec.cargoTarget) { + return process.env; + } + + let env = process.env; + const linkerEnvKey = cargoTargetLinkerEnvKey(spec.cargoTarget); + if (!optionalEnv(linkerEnvKey)) { + const linker = cargoLinkerForTarget(spec, spec.cargoTarget); + if (linker) { + console.log(`Using Cargo linker for ${spec.cargoTarget}: ${linker}`); + env = { ...env, [linkerEnvKey]: linker }; + } + } + + const lateLinkArgs = lateRustLinkFlagsForTarget(spec); + if (lateLinkArgs.length > 0) { + console.log(`Adding Cargo link args for ${spec.cargoTarget}: ${lateLinkArgs.join(' ')}`); + env = appendRustFlags(env, lateLinkArgs); + } + + return env; +} + +function cargoOutputDir( + context: PackageContext, + profile: BuildProfile, + cargoTarget?: string, +): string { + const pathParts = [context.repoRoot, 'target']; + if (cargoTarget) { + pathParts.push(cargoTarget); + } + pathParts.push(cargoProfileDir(profile)); + + return path.join(...pathParts); +} + +function ensureServerExecutable(serverPath: string, spec: NativeTargetSpec): void { + if (!spec.isWindows) { + fs.chmodSync(serverPath, 0o755); + } +} + +export function ensureTargetServerBinary( + context: PackageContext, + spec: NativeTargetSpec, + profile: BuildProfile, + serverMode: ServerMode, +): string { + const serverOutDir = path.join(context.vscodeDir, 'server', spec.target); + const serverPath = path.join(serverOutDir, spec.binaryFile); + if (serverMode === 'prebuilt') { + if (fs.existsSync(serverPath)) { + ensureServerExecutable(serverPath, spec); + return serverPath; + } + throw new Error(`missing prebuilt server binary: ${serverPath}`); + } + + const hostTarget = hostPlatformFolder(); + if (spec.target !== hostTarget && !spec.cargoTarget) { + throw new Error( + `missing bundled server binary: ${serverPath}\n` + + 'tip: run packaging on a matching native runner or copy the target binary first.', + ); + } + + if (spec.cargoTarget) { + run('rustup', ['target', 'add', spec.cargoTarget], context.repoRoot); + } + + run( + 'cargo', + cargoBuildArgs(profile, spec.cargoTarget), + context.repoRoot, + cargoBuildEnv(spec), + ); + + const sourcePath = path.join( + cargoOutputDir(context, profile, spec.cargoTarget), + spec.binaryFile, + ); + const destPath = path.join(serverOutDir, spec.binaryFile); + fs.mkdirSync(serverOutDir, { recursive: true }); + fs.copyFileSync(sourcePath, destPath); + ensureServerExecutable(destPath, spec); + + return destPath; +} + +export function stageRuntimeServer( + context: PackageContext, + sourcePath: string, + spec: NativeTargetSpec, +): string { + const runtimeServerDir = path.join(context.vscodeDir, 'server'); + const runtimeServerPath = path.join(runtimeServerDir, spec.binaryFile); + + fs.mkdirSync(runtimeServerDir, { recursive: true }); + fs.copyFileSync(sourcePath, runtimeServerPath); + ensureServerExecutable(runtimeServerPath, spec); + + return runtimeServerPath; +} + +export function cleanRuntimeServerFiles(context: PackageContext): void { + for (const binFile of ['vide.exe', 'vide']) { + fs.rmSync(path.join(context.vscodeDir, 'server', binFile), { force: true }); + } +} diff --git a/editors/vscode/scripts/package/targets.ts b/editors/vscode/scripts/package/targets.ts new file mode 100644 index 00000000..6f998844 --- /dev/null +++ b/editors/vscode/scripts/package/targets.ts @@ -0,0 +1,125 @@ +import { + SUPPORTED_PLATFORM_FOLDERS, + type PlatformFolder, + getPlatformFolder, + isPlatformFolder, +} from '../../src/platform'; + +export const WEB_TARGET = 'web'; + +export type BuildProfile = 'debug' | 'release'; +export type ServerMode = 'build' | 'prebuilt'; +export type PackageTarget = PlatformFolder | typeof WEB_TARGET; + +export interface PackageOptions { + target?: string; + profile: BuildProfile; + serverMode: ServerMode; +} + +export interface WebTargetSpec { + kind: 'web'; + target: typeof WEB_TARGET; + removeBrowserEntry: false; +} + +export interface NativeTargetSpec { + kind: 'native'; + target: PlatformFolder; + binaryFile: string; + cargoTarget?: string; + isWindows: boolean; + removeBrowserEntry: true; + requiresAlpineLinker: boolean; +} + +export type TargetSpec = WebTargetSpec | NativeTargetSpec; + +export interface PackagePlan { + target: PackageTarget; + profile: BuildProfile; + serverMode: ServerMode; + targetSpec: TargetSpec; + vsixFile: string; +} + +const cargoTargets: Partial> = { + 'alpine-arm64': 'aarch64-unknown-linux-musl', + 'alpine-x64': 'x86_64-unknown-linux-musl', +}; + +export function createPackagePlan(options: PackageOptions): PackagePlan { + const target = resolvePackageTarget(options.target); + const targetSpec = targetSpecFor(target); + const debugSuffix = options.profile === 'debug' ? '-debug' : ''; + + return { + target, + profile: options.profile, + serverMode: options.serverMode, + targetSpec, + vsixFile: `vide-vscode-${target}${debugSuffix}.vsix`, + }; +} + +export function parseBuildProfile(value: string): BuildProfile { + if (value === 'debug' || value === 'release') { + return value; + } + throw new Error(`unsupported build profile: ${value}`); +} + +export function parseServerMode(value: string): ServerMode { + if (value === 'build' || value === 'prebuilt') { + return value; + } + throw new Error(`unsupported server mode: ${value}`); +} + +export function hostPlatformFolder(): PlatformFolder { + const folder = getPlatformFolder(process.platform, process.arch); + if (!folder) { + throw new Error(`unsupported host platform: ${process.platform}-${process.arch}`); + } + + return folder; +} + +function resolvePackageTarget(target: string | undefined): PackageTarget { + target ??= hostPlatformFolder(); + if (target === WEB_TARGET) { + return target; + } + if (isPlatformFolder(target)) { + return target; + } + + throw new Error( + `unsupported target platform: ${target}\n` + + `supported targets: ${[...SUPPORTED_PLATFORM_FOLDERS, WEB_TARGET].join(', ')}`, + ); +} + +function targetSpecFor(target: PackageTarget): TargetSpec { + if (target === WEB_TARGET) { + return { + kind: 'web', + target, + removeBrowserEntry: false, + }; + } + + return { + kind: 'native', + target, + binaryFile: binaryFileForTarget(target), + cargoTarget: cargoTargets[target], + isWindows: target.startsWith('win32-'), + removeBrowserEntry: true, + requiresAlpineLinker: target.startsWith('alpine-'), + }; +} + +function binaryFileForTarget(target: PlatformFolder): string { + return target.startsWith('win32-') ? 'vide.exe' : 'vide'; +} diff --git a/editors/vscode/scripts/package/vsce.ts b/editors/vscode/scripts/package/vsce.ts new file mode 100644 index 00000000..a2249d5a --- /dev/null +++ b/editors/vscode/scripts/package/vsce.ts @@ -0,0 +1,15 @@ +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { run, sanitizedVsceEnv } from './process'; +import type { PackagePlan } from './targets'; + +export function runVscePackage(context: PackageContext, plan: PackagePlan): void { + const vsceBin = path.join(context.vscodeDir, 'node_modules', '@vscode', 'vsce', 'vsce'); + run( + process.execPath, + [vsceBin, 'package', '--target', plan.target, '--out', plan.vsixFile], + context.vscodeDir, + sanitizedVsceEnv(), + ); +} diff --git a/editors/vscode/test/package.test.ts b/editors/vscode/test/package.test.ts new file mode 100644 index 00000000..0064384f --- /dev/null +++ b/editors/vscode/test/package.test.ts @@ -0,0 +1,55 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { parsePackageCliArgs } from '../scripts/package/cli'; +import { createPackagePlan } from '../scripts/package/targets'; + +describe('package cli', () => { + it('keeps the existing debug positional target syntax', () => { + assert.deepEqual(parsePackageCliArgs(['--debug', 'linux-x64', '--server=prebuilt']), { + target: 'linux-x64', + profile: 'debug', + serverMode: 'prebuilt', + }); + }); + + it('accepts explicit target and profile flags', () => { + assert.deepEqual(parsePackageCliArgs(['--target', 'web', '--profile', 'release']), { + target: 'web', + profile: 'release', + serverMode: 'build', + }); + }); +}); + +describe('package plan', () => { + it('models web packages without native server staging', () => { + const plan = createPackagePlan({ + target: 'web', + profile: 'release', + serverMode: 'build', + }); + + assert.equal(plan.target, 'web'); + assert.equal(plan.vsixFile, 'vide-vscode-web.vsix'); + assert.equal(plan.targetSpec.kind, 'web'); + assert.equal(plan.targetSpec.removeBrowserEntry, false); + }); + + it('models native debug packages with target binary metadata', () => { + const plan = createPackagePlan({ + target: 'win32-x64', + profile: 'debug', + serverMode: 'prebuilt', + }); + + assert.equal(plan.target, 'win32-x64'); + assert.equal(plan.vsixFile, 'vide-vscode-win32-x64-debug.vsix'); + assert.equal(plan.targetSpec.kind, 'native'); + if (plan.targetSpec.kind === 'native') { + assert.equal(plan.targetSpec.binaryFile, 'vide.exe'); + assert.equal(plan.targetSpec.isWindows, true); + assert.equal(plan.targetSpec.removeBrowserEntry, true); + } + }); +}); From 159552cf5ebcb176f77c4104821969511811bc83 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 16:35:47 +0800 Subject: [PATCH 77/80] refactor(build): move server preparation into xtask --- Cargo.toml | 3 +- editors/vscode/scripts/package/server.ts | 179 ++------ editors/vscode/scripts/package/targets.ts | 9 - xtask/Cargo.toml | 1 + xtask/src/main.rs | 479 +++++++++++++++++++++- 5 files changed, 488 insertions(+), 183 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b59fbfdd..12e71c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ vfs.workspace = true always-assert.workspace = true anyhow.workspace = true -clap = { version = "4.4.6", features = ["derive"] } +clap.workspace = true const_format.workspace = true crossbeam-channel.workspace = true dunce.workspace = true @@ -68,6 +68,7 @@ always-assert = "0.1.3" anyhow = "1.0.75" bitflags = "2.9.0" camino = "1.1.6" +clap = { version = "4.4.6", features = ["derive"] } const_format = "0.2.31" crossbeam-channel = "0.5.8" dunce = "1.0.5" diff --git a/editors/vscode/scripts/package/server.ts b/editors/vscode/scripts/package/server.ts index c84f5c36..b59e6012 100644 --- a/editors/vscode/scripts/package/server.ts +++ b/editors/vscode/scripts/package/server.ts @@ -2,126 +2,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { PackageContext } from './context'; -import { optionalEnv, run } from './process'; -import { - type BuildProfile, - type NativeTargetSpec, - type ServerMode, - hostPlatformFolder, -} from './targets'; - -function cargoProfileDir(profile: BuildProfile): string { - return profile === 'release' ? 'release' : 'debug'; -} - -function cargoBuildArgs(profile: BuildProfile, cargoTarget?: string): string[] { - const args = ['build']; - if (profile === 'release') { - args.push('--release'); - } - if (cargoTarget) { - args.push('--target', cargoTarget); - } - - return args; -} - -function cargoTargetEnvName(cargoTarget: string): string { - return cargoTarget.toUpperCase().replace(/-/g, '_'); -} - -function cargoTargetLinkerEnvKey(cargoTarget: string): string { - return `CARGO_TARGET_${cargoTargetEnvName(cargoTarget)}_LINKER`; -} - -function cxxCompilerEnvKey(cargoTarget: string): string { - return `CXX_${cargoTarget.replace(/-/g, '_')}`; -} - -function cargoLinkerForTarget(spec: NativeTargetSpec, cargoTarget: string): string | undefined { - if (!spec.requiresAlpineLinker) { - return undefined; - } - - return ( - optionalEnv(cxxCompilerEnvKey(cargoTarget)) ?? - optionalEnv('TARGET_CXX') ?? - `${cargoTarget}-g++` - ); -} - -function lateRustLinkFlagsForTarget(spec: NativeTargetSpec): string[] { - if (!spec.requiresAlpineLinker) { - return []; - } - - // Static libstdc++ can introduce libc references after rustc's own musl -lc. - return ['-C', 'link-arg=-lc']; -} - -function appendRustFlags(env: NodeJS.ProcessEnv, flags: string[]): NodeJS.ProcessEnv { - if (flags.length === 0) { - return env; - } - - const encodedFlags = env.CARGO_ENCODED_RUSTFLAGS; - if (encodedFlags) { - return { - ...env, - CARGO_ENCODED_RUSTFLAGS: `${encodedFlags}\x1f${flags.join('\x1f')}`, - }; - } - - const rustFlags = env.RUSTFLAGS?.trim(); - return { - ...env, - RUSTFLAGS: rustFlags ? `${rustFlags} ${flags.join(' ')}` : flags.join(' '), - }; -} - -function cargoBuildEnv(spec: NativeTargetSpec): NodeJS.ProcessEnv { - if (!spec.cargoTarget) { - return process.env; - } - - let env = process.env; - const linkerEnvKey = cargoTargetLinkerEnvKey(spec.cargoTarget); - if (!optionalEnv(linkerEnvKey)) { - const linker = cargoLinkerForTarget(spec, spec.cargoTarget); - if (linker) { - console.log(`Using Cargo linker for ${spec.cargoTarget}: ${linker}`); - env = { ...env, [linkerEnvKey]: linker }; - } - } - - const lateLinkArgs = lateRustLinkFlagsForTarget(spec); - if (lateLinkArgs.length > 0) { - console.log(`Adding Cargo link args for ${spec.cargoTarget}: ${lateLinkArgs.join(' ')}`); - env = appendRustFlags(env, lateLinkArgs); - } - - return env; -} - -function cargoOutputDir( - context: PackageContext, - profile: BuildProfile, - cargoTarget?: string, -): string { - const pathParts = [context.repoRoot, 'target']; - if (cargoTarget) { - pathParts.push(cargoTarget); - } - pathParts.push(cargoProfileDir(profile)); - - return path.join(...pathParts); -} - -function ensureServerExecutable(serverPath: string, spec: NativeTargetSpec): void { - if (!spec.isWindows) { - fs.chmodSync(serverPath, 0o755); - } -} +import { run } from './process'; +import type { BuildProfile, NativeTargetSpec, ServerMode } from './targets'; export function ensureTargetServerBinary( context: PackageContext, @@ -129,45 +11,28 @@ export function ensureTargetServerBinary( profile: BuildProfile, serverMode: ServerMode, ): string { - const serverOutDir = path.join(context.vscodeDir, 'server', spec.target); - const serverPath = path.join(serverOutDir, spec.binaryFile); - if (serverMode === 'prebuilt') { - if (fs.existsSync(serverPath)) { - ensureServerExecutable(serverPath, spec); - return serverPath; - } - throw new Error(`missing prebuilt server binary: ${serverPath}`); - } - - const hostTarget = hostPlatformFolder(); - if (spec.target !== hostTarget && !spec.cargoTarget) { - throw new Error( - `missing bundled server binary: ${serverPath}\n` + - 'tip: run packaging on a matching native runner or copy the target binary first.', - ); - } - - if (spec.cargoTarget) { - run('rustup', ['target', 'add', spec.cargoTarget], context.repoRoot); - } - + const serverPath = targetServerPath(context, spec); run( 'cargo', - cargoBuildArgs(profile, spec.cargoTarget), + [ + 'xtask', + 'vscode', + 'prepare-server', + '--target', + spec.target, + '--profile', + profile, + '--server', + serverMode, + ], context.repoRoot, - cargoBuildEnv(spec), ); - const sourcePath = path.join( - cargoOutputDir(context, profile, spec.cargoTarget), - spec.binaryFile, - ); - const destPath = path.join(serverOutDir, spec.binaryFile); - fs.mkdirSync(serverOutDir, { recursive: true }); - fs.copyFileSync(sourcePath, destPath); - ensureServerExecutable(destPath, spec); + if (!fs.existsSync(serverPath)) { + throw new Error(`prepared server binary was not found: ${serverPath}`); + } - return destPath; + return serverPath; } export function stageRuntimeServer( @@ -180,7 +45,9 @@ export function stageRuntimeServer( fs.mkdirSync(runtimeServerDir, { recursive: true }); fs.copyFileSync(sourcePath, runtimeServerPath); - ensureServerExecutable(runtimeServerPath, spec); + if (!spec.isWindows) { + fs.chmodSync(runtimeServerPath, 0o755); + } return runtimeServerPath; } @@ -190,3 +57,7 @@ export function cleanRuntimeServerFiles(context: PackageContext): void { fs.rmSync(path.join(context.vscodeDir, 'server', binFile), { force: true }); } } + +function targetServerPath(context: PackageContext, spec: NativeTargetSpec): string { + return path.join(context.vscodeDir, 'server', spec.target, spec.binaryFile); +} diff --git a/editors/vscode/scripts/package/targets.ts b/editors/vscode/scripts/package/targets.ts index 6f998844..06c299fa 100644 --- a/editors/vscode/scripts/package/targets.ts +++ b/editors/vscode/scripts/package/targets.ts @@ -27,10 +27,8 @@ export interface NativeTargetSpec { kind: 'native'; target: PlatformFolder; binaryFile: string; - cargoTarget?: string; isWindows: boolean; removeBrowserEntry: true; - requiresAlpineLinker: boolean; } export type TargetSpec = WebTargetSpec | NativeTargetSpec; @@ -43,11 +41,6 @@ export interface PackagePlan { vsixFile: string; } -const cargoTargets: Partial> = { - 'alpine-arm64': 'aarch64-unknown-linux-musl', - 'alpine-x64': 'x86_64-unknown-linux-musl', -}; - export function createPackagePlan(options: PackageOptions): PackagePlan { const target = resolvePackageTarget(options.target); const targetSpec = targetSpecFor(target); @@ -113,10 +106,8 @@ function targetSpecFor(target: PackageTarget): TargetSpec { kind: 'native', target, binaryFile: binaryFileForTarget(target), - cargoTarget: cargoTargets[target], isWindows: target.startsWith('win32-'), removeBrowserEntry: true, - requiresAlpineLinker: target.startsWith('alpine-'), }; } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c38d49e0..a3ceec6a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true +clap.workspace = true project-model = { workspace = true, features = ["manifest-schema"] } serde_json.workspace = true vide = { path = "..", features = ["user-config-schema"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index e9028e37..33b83b21 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,11 +1,15 @@ #![recursion_limit = "512"] +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::{ env, fs, path::{Path, PathBuf}, + process::Command as ProcessCommand, }; use anyhow::{Context, Result, bail}; +use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; const VSCODE_SCHEMA_CONSTANTS_PATH: &str = "editors/vscode/src/generated/projectConfigSchema.ts"; const VSCODE_CONFIGURATION_PATH: &str = "editors/vscode/src/generated/configuration.ts"; @@ -13,33 +17,415 @@ const VSCODE_PACKAGE_PATH: &str = "editors/vscode/package.json"; const USER_CONFIG_SCHEMA_PATH: &str = "/schemas/v1/user-config.schema.json"; fn main() -> Result<()> { - let mut args = env::args().skip(1); - let Some(command) = args.next() else { - print_help(); - return Ok(()); - }; + let cli = Cli::parse(); + let workspace_root = workspace_root()?; + + match cli.command { + Some(XtaskCommand::GenerateConfigArtifacts) => write_config_artifacts(&workspace_root), + Some(XtaskCommand::CheckConfigArtifacts) => check_config_artifacts(&workspace_root), + Some(XtaskCommand::GenerateSchemas) => write_schemas(&workspace_root), + Some(XtaskCommand::CheckSchemas) => check_schemas(&workspace_root), + Some(XtaskCommand::Server(server)) => run_server_command(&workspace_root, server), + Some(XtaskCommand::Vscode(vscode)) => run_vscode_command(&workspace_root, vscode), + None => { + Cli::command().print_help()?; + eprintln!(); + Ok(()) + } + } +} + +#[derive(Debug, Parser)] +#[command(name = "xtask", bin_name = "cargo xtask")] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum XtaskCommand { + GenerateConfigArtifacts, + CheckConfigArtifacts, + #[command(alias = "generate-manifest-schema")] + GenerateSchemas, + #[command(alias = "check-manifest-schema")] + CheckSchemas, + Server(ServerArgs), + Vscode(VscodeArgs), +} + +#[derive(Debug, Args)] +struct ServerArgs { + #[command(subcommand)] + command: ServerCommand, +} + +#[derive(Debug, Subcommand)] +enum ServerCommand { + Build(ServerBuildArgs), +} + +#[derive(Debug, Clone, PartialEq, Eq, Args)] +struct ServerBuildArgs { + #[arg(long, value_enum, default_value = "debug")] + profile: ExtensionBuildProfile, + #[arg(long)] + cargo_target: Option, + #[arg(long)] + alpine_linker: bool, +} + +#[derive(Debug, Args)] +struct VscodeArgs { + #[command(subcommand)] + command: VscodeCommand, +} + +#[derive(Debug, Subcommand)] +enum VscodeCommand { + PrepareServer(VscodePrepareServerArgs), +} + +#[derive(Debug, PartialEq, Eq, Args)] +struct VscodePrepareServerArgs { + #[arg(long, value_enum)] + target: VscodeServerTarget, + #[arg(long, value_enum, default_value = "release")] + profile: ExtensionBuildProfile, + #[arg(long, value_enum, default_value = "build")] + server: ExtensionServerMode, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ExtensionBuildProfile { + Debug, + Release, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ExtensionServerMode { + Build, + Prebuilt, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +enum VscodeServerTarget { + AlpineArm64, + AlpineX64, + DarwinArm64, + DarwinX64, + LinuxArm64, + LinuxX64, + Win32Arm64, + Win32X64, +} + +impl VscodeServerTarget { + fn folder(self) -> &'static str { + match self { + VscodeServerTarget::AlpineArm64 => "alpine-arm64", + VscodeServerTarget::AlpineX64 => "alpine-x64", + VscodeServerTarget::DarwinArm64 => "darwin-arm64", + VscodeServerTarget::DarwinX64 => "darwin-x64", + VscodeServerTarget::LinuxArm64 => "linux-arm64", + VscodeServerTarget::LinuxX64 => "linux-x64", + VscodeServerTarget::Win32Arm64 => "win32-arm64", + VscodeServerTarget::Win32X64 => "win32-x64", + } + } + + fn binary_file(self) -> &'static str { + if self.is_windows() { "vide.exe" } else { "vide" } + } - if args.next().is_some() { - bail!("unexpected extra arguments"); + fn cargo_target(self) -> Option<&'static str> { + match self { + VscodeServerTarget::AlpineArm64 => Some("aarch64-unknown-linux-musl"), + VscodeServerTarget::AlpineX64 => Some("x86_64-unknown-linux-musl"), + _ => None, + } + } + + fn is_windows(self) -> bool { + matches!(self, VscodeServerTarget::Win32Arm64 | VscodeServerTarget::Win32X64) + } + + fn requires_alpine_linker(self) -> bool { + matches!(self, VscodeServerTarget::AlpineArm64 | VscodeServerTarget::AlpineX64) } +} + +fn run_vscode_command(workspace_root: &Path, args: VscodeArgs) -> Result<()> { + match args.command { + VscodeCommand::PrepareServer(args) => prepare_vscode_server(workspace_root, args), + } +} - match command.as_str() { - "generate-config-artifacts" => write_config_artifacts(&workspace_root()?), - "check-config-artifacts" => check_config_artifacts(&workspace_root()?), - "generate-schemas" | "generate-manifest-schema" => write_schemas(&workspace_root()?), - "check-schemas" | "check-manifest-schema" => check_schemas(&workspace_root()?), - "-h" | "--help" | "help" => { - print_help(); +fn run_server_command(workspace_root: &Path, args: ServerArgs) -> Result<()> { + match args.command { + ServerCommand::Build(args) => { + let server_path = build_server(workspace_root, &args)?; + println!("{}", server_path.display()); Ok(()) } - _ => bail!("unknown xtask command: {command}"), } } -fn print_help() { - eprintln!( - "Usage: cargo xtask \n\nCommands:\n generate-config-artifacts\n check-config-artifacts\n generate-schemas\n check-schemas" - ); +fn prepare_vscode_server(workspace_root: &Path, args: VscodePrepareServerArgs) -> Result<()> { + let server_path = + ensure_vscode_server_binary(workspace_root, args.target, args.profile, args.server)?; + println!("{}", server_path.display()); + Ok(()) +} + +fn ensure_vscode_server_binary( + workspace_root: &Path, + target: VscodeServerTarget, + profile: ExtensionBuildProfile, + server_mode: ExtensionServerMode, +) -> Result { + let server_path = vscode_target_server_path(workspace_root, target); + if server_mode == ExtensionServerMode::Prebuilt { + if server_path.exists() { + ensure_vscode_server_executable(&server_path, target)?; + return Ok(server_path); + } + bail!("missing prebuilt server binary: {}", server_path.display()); + } + + let host_target = host_vscode_server_target()?; + let cargo_target = target.cargo_target(); + if target != host_target && cargo_target.is_none() { + bail!( + "missing bundled server binary: {}\n\ + tip: run packaging on a matching native runner or copy the target binary first.", + server_path.display() + ); + } + + let build_args = server_build_args_for_vscode_target(target, profile); + let source_path = build_server(workspace_root, &build_args)?; + let parent = server_path.parent().context("VS Code server output path has no parent")?; + fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; + fs::copy(&source_path, &server_path).with_context(|| { + format!( + "failed to copy server binary from {} to {}", + source_path.display(), + server_path.display() + ) + })?; + ensure_vscode_server_executable(&server_path, target)?; + + Ok(server_path) +} + +fn build_server(workspace_root: &Path, args: &ServerBuildArgs) -> Result { + if let Some(cargo_target) = args.cargo_target.as_deref() { + run_command( + "rustup", + &["target".to_owned(), "add".to_owned(), cargo_target.to_owned()], + workspace_root, + &[], + )?; + } + + run_command("cargo", &cargo_build_args(args), workspace_root, &cargo_build_env_updates(args))?; + + Ok(cargo_output_dir(workspace_root, args).join(server_binary_file(args))) +} + +fn host_vscode_server_target() -> Result { + match (env::consts::OS, env::consts::ARCH) { + ("linux", "aarch64") => Ok(VscodeServerTarget::LinuxArm64), + ("linux", "x86_64") => Ok(VscodeServerTarget::LinuxX64), + ("macos", "aarch64") => Ok(VscodeServerTarget::DarwinArm64), + ("macos", "x86_64") => Ok(VscodeServerTarget::DarwinX64), + ("windows", "aarch64") => Ok(VscodeServerTarget::Win32Arm64), + ("windows", "x86_64") => Ok(VscodeServerTarget::Win32X64), + _ => bail!("unsupported host platform: {}-{}", env::consts::OS, env::consts::ARCH), + } +} + +fn vscode_target_server_path(workspace_root: &Path, target: VscodeServerTarget) -> PathBuf { + workspace_root + .join("editors") + .join("vscode") + .join("server") + .join(target.folder()) + .join(target.binary_file()) +} + +fn server_build_args_for_vscode_target( + target: VscodeServerTarget, + profile: ExtensionBuildProfile, +) -> ServerBuildArgs { + ServerBuildArgs { + profile, + cargo_target: target.cargo_target().map(str::to_owned), + alpine_linker: target.requires_alpine_linker(), + } +} + +fn cargo_build_args(args: &ServerBuildArgs) -> Vec { + let mut command_args = vec!["build".to_owned()]; + if args.profile == ExtensionBuildProfile::Release { + command_args.push("--release".to_owned()); + } + if let Some(cargo_target) = &args.cargo_target { + command_args.push("--target".to_owned()); + command_args.push(cargo_target.clone()); + } + command_args +} + +fn cargo_profile_dir(profile: ExtensionBuildProfile) -> &'static str { + match profile { + ExtensionBuildProfile::Debug => "debug", + ExtensionBuildProfile::Release => "release", + } +} + +fn cargo_output_dir(workspace_root: &Path, args: &ServerBuildArgs) -> PathBuf { + let mut path = workspace_root.join("target"); + if let Some(cargo_target) = &args.cargo_target { + path = path.join(cargo_target); + } + path.join(cargo_profile_dir(args.profile)) +} + +fn server_binary_file(args: &ServerBuildArgs) -> &'static str { + if args.cargo_target.as_deref().is_some_and(|target| target.contains("windows")) + || (args.cargo_target.is_none() && cfg!(windows)) + { + "vide.exe" + } else { + "vide" + } +} + +fn cargo_build_env_updates(args: &ServerBuildArgs) -> Vec<(String, String)> { + let Some(cargo_target) = args.cargo_target.as_deref() else { + return Vec::new(); + }; + + let mut updates = Vec::new(); + let linker_env_key = cargo_target_linker_env_key(cargo_target); + if optional_env(&linker_env_key).is_none() + && let Some(linker) = cargo_linker_for_target(args, cargo_target) + { + eprintln!("Using Cargo linker for {cargo_target}: {linker}"); + updates.push((linker_env_key, linker)); + } + + let late_link_args = late_rust_link_flags_for_target(args); + if !late_link_args.is_empty() { + eprintln!("Adding Cargo link args for {cargo_target}: {}", late_link_args.join(" ")); + updates.push(rust_flags_env_update(&late_link_args)); + } + + updates +} + +fn cargo_target_linker_env_key(cargo_target: &str) -> String { + format!("CARGO_TARGET_{}_LINKER", cargo_target_env_name(cargo_target)) +} + +fn cargo_target_env_name(cargo_target: &str) -> String { + cargo_target.to_uppercase().replace('-', "_") +} + +fn cargo_linker_for_target(args: &ServerBuildArgs, cargo_target: &str) -> Option { + if !args.alpine_linker { + return None; + } + + optional_env(&cxx_compiler_env_key(cargo_target)) + .or_else(|| optional_env("TARGET_CXX")) + .or_else(|| Some(format!("{cargo_target}-g++"))) +} + +fn cxx_compiler_env_key(cargo_target: &str) -> String { + format!("CXX_{}", cargo_target.replace('-', "_")) +} + +fn late_rust_link_flags_for_target(args: &ServerBuildArgs) -> Vec<&'static str> { + if args.alpine_linker { + // Static libstdc++ can introduce libc references after rustc's own musl -lc. + vec!["-C", "link-arg=-lc"] + } else { + Vec::new() + } +} + +fn rust_flags_env_update(flags: &[&str]) -> (String, String) { + if let Some(encoded_flags) = optional_env("CARGO_ENCODED_RUSTFLAGS") { + return ( + "CARGO_ENCODED_RUSTFLAGS".to_owned(), + format!("{encoded_flags}\x1f{}", flags.join("\x1f")), + ); + } + + let rust_flags = optional_env("RUSTFLAGS"); + let flags = flags.join(" "); + ( + "RUSTFLAGS".to_owned(), + rust_flags.map_or(flags.clone(), |rust_flags| format!("{rust_flags} {flags}")), + ) +} + +fn optional_env(name: &str) -> Option { + env::var(name).ok().map(|value| value.trim().to_owned()).filter(|value| !value.is_empty()) +} + +fn ensure_vscode_server_executable(path: &Path, target: VscodeServerTarget) -> Result<()> { + if target.is_windows() { + return Ok(()); + } + + #[cfg(not(unix))] + { + let _ = path; + } + + #[cfg(unix)] + { + let mut permissions = fs::metadata(path) + .with_context(|| format!("failed to stat {}", path.display()))? + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions) + .with_context(|| format!("failed to chmod {}", path.display()))?; + } + + Ok(()) +} + +fn run_command( + command: &str, + args: &[String], + cwd: &Path, + env_updates: &[(String, String)], +) -> Result<()> { + let mut child = ProcessCommand::new(command_for_host(command)); + child.args(args).current_dir(cwd); + for (key, value) in env_updates { + child.env(key, value); + } + + let status = child + .status() + .with_context(|| format!("failed to run `{}` in {}", command, cwd.display()))?; + + if !status.success() { + bail!("`{} {}` failed with {}", command, args.join(" "), status); + } + + Ok(()) +} + +fn command_for_host(command: &str) -> String { + if cfg!(windows) { format!("{command}.cmd") } else { command.to_owned() } } fn workspace_root() -> Result { @@ -218,10 +604,65 @@ fn check_file_matches(path: &Path, expected: &str) -> Result<()> { #[cfg(test)] mod tests { + use clap::Parser as _; + use super::*; #[test] fn checked_in_schemas_match_generated_schemas() { check_schemas(&workspace_root().unwrap()).unwrap(); } + + #[test] + fn parses_vscode_prepare_server_command_with_clap() { + let cli = Cli::try_parse_from([ + "xtask", + "vscode", + "prepare-server", + "--target", + "linux-x64", + "--profile", + "release", + "--server", + "prebuilt", + ]) + .unwrap(); + + let Some(XtaskCommand::Vscode(VscodeArgs { command: VscodeCommand::PrepareServer(args) })) = + cli.command + else { + panic!("expected vscode prepare-server command"); + }; + + assert_eq!( + args, + VscodePrepareServerArgs { + target: VscodeServerTarget::LinuxX64, + profile: ExtensionBuildProfile::Release, + server: ExtensionServerMode::Prebuilt, + } + ); + } + + #[test] + fn maps_alpine_vscode_target_to_server_build_args() { + let args = server_build_args_for_vscode_target( + VscodeServerTarget::AlpineX64, + ExtensionBuildProfile::Release, + ); + + assert_eq!( + args, + ServerBuildArgs { + profile: ExtensionBuildProfile::Release, + cargo_target: Some("x86_64-unknown-linux-musl".to_owned()), + alpine_linker: true, + } + ); + assert_eq!( + cargo_build_args(&args), + ["build", "--release", "--target", "x86_64-unknown-linux-musl"].map(str::to_owned) + ); + assert_eq!(server_binary_file(&args), "vide"); + } } From 83fe7a22064f135d3e7a53c5ed547593c1eef0b1 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 16:39:36 +0800 Subject: [PATCH 78/80] chore(build): align VSIX package targets --- .github/ci-modules.yml | 1 + .github/workflows/ci.yml | 20 +++++-------- .github/workflows/release.yml | 28 +++++++++++++------ .../advanced-guide/advanced-installation.md | 22 +++++++-------- .../docs/advanced-guide/troubleshooting.md | 2 +- .../advanced-guide/advanced-installation.md | 22 +++++++-------- .../docs/en/advanced-guide/troubleshooting.md | 2 +- editors/vscode/package.json | 13 ++------- editors/vscode/scripts/install-extension.ts | 2 +- editors/vscode/src/platform.ts | 2 -- editors/vscode/test/platform.test.ts | 4 +-- xtask/src/main.rs | 8 +----- 12 files changed, 59 insertions(+), 67 deletions(-) diff --git a/.github/ci-modules.yml b/.github/ci-modules.yml index 4777a9b7..13d56dab 100644 --- a/.github/ci-modules.yml +++ b/.github/ci-modules.yml @@ -40,6 +40,7 @@ package: - "editors/vscode/scripts/**" - "editors/vscode/src/**" - "editors/vscode/syntaxes/**" + - "xtask/**" - "packages/vide-extension-shared/**" - "playground/package.json" - "playground/package-lock.json" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d3e2561..30d0e416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,24 +236,21 @@ jobs: uses: actions/cache/restore@v4 with: path: editors/vscode/server/${{ matrix.target }}/${{ env.SERVER_BIN }} - key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} - name: Install Rust - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-rust with: components: rustfmt - name: Setup sccache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-sccache with: cmake-launcher: "true" - name: Rust Cache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: Swatinem/rust-cache@v2 continue-on-error: true with: shared-key: rust-${{ runner.os }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true - name: Setup Node @@ -274,7 +271,7 @@ jobs: echo "VIDE_EXTENSION_COMMIT_HASH=${commit_hash}" >> "$GITHUB_ENV" echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:debug -- ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} + run: npm run package:vsix:debug -- --target ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} - name: Save bundled server cache if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 @@ -394,7 +391,7 @@ jobs: echo "VIDE_EXTENSION_COMMIT_HASH=${commit_hash}" >> "$GITHUB_ENV" echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:web + run: npm run package:vsix:web - name: Upload dev VSIX uses: actions/upload-artifact@v4 with: @@ -437,28 +434,25 @@ jobs: uses: actions/cache/restore@v4 with: path: editors/vscode/server/${{ matrix.target }}/${{ env.SERVER_BIN }} - key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} - name: Install Rust - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-rust with: components: rustfmt targets: ${{ matrix.rust-target }} - name: Setup sccache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-sccache with: cmake-launcher: "true" - name: Rust Cache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: Swatinem/rust-cache@v2 continue-on-error: true with: shared-key: rust-${{ runner.os }}-${{ matrix.target }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -484,7 +478,7 @@ jobs: echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:debug -- ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} + run: npm run package:vsix:debug -- --target ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} - name: Save bundled server cache if: steps.bundled-server-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4994ec7a..d2e8c31c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,7 @@ jobs: continue-on-error: true with: shared-key: rust-${{ runner.os }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -92,6 +92,20 @@ jobs: cache: npm cache-dependency-path: editors/vscode/package-lock.json + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + components: rustfmt + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + continue-on-error: true + with: + shared-key: rust-${{ runner.os }} + key: package-linux-x64-prebuilt-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} + cache-workspace-crates: true + cache-on-failure: true + - name: Install JS dependencies working-directory: editors/vscode run: npm ci @@ -109,7 +123,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: npm run package:${{ matrix.target }} + run: npm run package:vsix -- --target ${{ matrix.target }} - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -247,9 +261,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: | - npm run compile - ./node_modules/.bin/tsx scripts/package.ts linux-x64 --server=prebuilt + run: npm run package:vsix -- --target linux-x64 --server=prebuilt - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -302,7 +314,7 @@ jobs: continue-on-error: true with: shared-key: rust-${{ runner.os }}-${{ matrix.target }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -330,7 +342,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: npm run package:${{ matrix.target }} + run: npm run package:vsix -- --target ${{ matrix.target }} - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -450,7 +462,7 @@ jobs: fi - name: Build VSIX - run: npm run package:web + run: npm run package:vsix:web - name: Upload VSIX uses: actions/upload-artifact@v4 diff --git a/docs/src/content/docs/advanced-guide/advanced-installation.md b/docs/src/content/docs/advanced-guide/advanced-installation.md index ea773d59..77cefec9 100644 --- a/docs/src/content/docs/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/advanced-guide/advanced-installation.md @@ -85,13 +85,13 @@ npm run compile 如果你只想在本机调试,或者要打包一个带调试信息的 VSIX,在 `editors/vscode` 下运行: ```bash -npm run package:debug +npm run package:vsix:debug ``` 这个命令会: 1. 编译扩展,所以前面没手动执行 `npm run compile` 也可以。 -2. 针对当前宿主平台执行 `cargo build`。 +2. 通过 `cargo xtask vscode prepare-server` 针对当前宿主平台准备 debug 版语言服务器。 3. 把 `target/debug/vide` 或 `vide.exe` 复制到扩展的 `server/` 目录。 4. 临时把服务器二进制放到运行时 `server` 目录。 5. 调用 `vsce package --target ` 生成 `vide-vscode--debug.vsix`。 @@ -100,20 +100,20 @@ npm run package:debug 如果你要打包能安装特定平台发布版 Vide 的 VSIX,可以运行以下一个或多个命令: ```bash -npm run package:linux-x64 -npm run package:linux-arm64 -npm run package:win32-x64 -npm run package:darwin-arm64 -npm run package:alpine-x64 -npm run package:alpine-arm64 +npm run package:vsix -- --target linux-x64 +npm run package:vsix -- --target linux-arm64 +npm run package:vsix -- --target win32-x64 +npm run package:vsix -- --target darwin-arm64 +npm run package:vsix -- --target alpine-x64 +npm run package:vsix -- --target alpine-arm64 ``` 这些脚本会先编译扩展,然后准备目标平台的 release 版语言服务器,再生成 `vide-vscode-.vsix`。当前 release workflow 只覆盖上面这些目标:glibc Linux、Windows x64、macOS arm64,以及 Alpine/musl x64 和 arm64。 -这几项也是当前 CI 会实际构建的 VSIX 目标。其他平台即使在 `package.json` 里有脚本入口,也不表示它们在本地或当前 workflow 里一定能直接打包成功。 +这几项也是当前 CI 会实际构建的 VSIX 目标。其他平台不是当前支持的打包目标。 -上面的打包命令都需要先准备目标平台的语言服务器二进制;这一步的具体规则由 `editors/vscode/scripts/package.ts` 决定: +上面的打包命令都需要先准备目标平台的语言服务器二进制;`editors/vscode/scripts/package.ts` 会调用 `cargo xtask vscode prepare-server`,而通用的 server 构建规则由 `cargo xtask server build` 承载: -- 目标等于当前宿主平台时,脚本执行 `cargo build --release` 并复制产物。 +- 目标等于当前宿主平台时,xtask 执行对应 profile 的 `cargo build` 并复制产物。 - Alpine 目标在 CI 的 musl 容器中构建;本地脚本会添加对应 Rust musl target,但仍需要可用的 musl 交叉编译环境。 - 其他非宿主平台目标不会自动交叉编译语言服务器,需要 `editors/vscode/server//` 下已经存在对应的 `vide` 或 `vide.exe`,或者在匹配的原生 runner 上打包。 diff --git a/docs/src/content/docs/advanced-guide/troubleshooting.md b/docs/src/content/docs/advanced-guide/troubleshooting.md index 1bd5c072..c2788e51 100644 --- a/docs/src/content/docs/advanced-guide/troubleshooting.md +++ b/docs/src/content/docs/advanced-guide/troubleshooting.md @@ -42,7 +42,7 @@ description: 报告 Vide 故障,并按常见症状处理启动和文件刷新 常见问题是: - `Bundled Vide Language Server binary not found` 或 `Unsupported platform-architecture combination`: - 先核对安装的 VSIX 和当前平台是否匹配。如果你是通过本地打包安装的 VSIX,需要确认打包时运行的是 `npm run package:*` 或 `npm run package:debug`。这些命令会把语言服务器二进制打进 VSIX;单独执行 `npm run compile` 只会编译扩展前端,安装后会缺少服务器。 + 先核对安装的 VSIX 和当前平台是否匹配。如果你是通过本地打包安装的 VSIX,需要确认打包时运行的是 `npm run package:vsix` 或 `npm run package:vsix:debug`。这些命令会把语言服务器二进制打进 VSIX;单独执行 `npm run compile` 只会编译扩展前端,安装后会缺少服务器。 - `Failed to start language server`、自定义命令不存在、无执行权限: 继续看下面的“扩展或自定义服务器无法启动”。 - 状态栏只是提示 `vide.toml`、`manifest` 或 `failed to load workspace`: diff --git a/docs/src/content/docs/en/advanced-guide/advanced-installation.md b/docs/src/content/docs/en/advanced-guide/advanced-installation.md index a0575034..a1eaeddb 100644 --- a/docs/src/content/docs/en/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/en/advanced-guide/advanced-installation.md @@ -99,13 +99,13 @@ npm run compile If you want a local debug build or a VSIX with debug binaries, run this under `editors/vscode`: ```powershell -npm run package:debug +npm run package:vsix:debug ``` This command: 1. Compiles the extension, so it is fine if you did not run `npm run compile` manually first. -2. Runs `cargo build` for the current host platform. +2. Uses `cargo xtask vscode prepare-server` to prepare a debug server for the current host platform. 3. Copies `target/debug/vide` or `vide.exe` into the extension's `server/` directory. 4. Temporarily stages the server binary in the runtime `server` directory. 5. Calls `vsce package --target ` to generate `vide-vscode--debug.vsix`. @@ -114,20 +114,20 @@ This command: If you want a release VSIX for a specific platform, run one or more of these commands: ```powershell -npm run package:linux-x64 -npm run package:linux-arm64 -npm run package:win32-x64 -npm run package:darwin-arm64 -npm run package:alpine-x64 -npm run package:alpine-arm64 +npm run package:vsix -- --target linux-x64 +npm run package:vsix -- --target linux-arm64 +npm run package:vsix -- --target win32-x64 +npm run package:vsix -- --target darwin-arm64 +npm run package:vsix -- --target alpine-x64 +npm run package:vsix -- --target alpine-arm64 ``` These scripts compile the extension, prepare a release server binary for the target platform, and generate `vide-vscode-.vsix`. The current release workflow only covers those targets: glibc Linux, Windows x64, macOS arm64, and Alpine/musl x64 and arm64. -Those are also the VSIX targets currently built by CI. Even if `package.json` contains script entries for other platforms, that does not mean they can be packaged directly in a local environment or in the current workflows. +Those are also the VSIX targets currently built by CI. Other platforms are not current packaging targets. -All packaging commands above need to prepare the language server binary for the target platform first. The exact rules for that step are controlled by `editors/vscode/scripts/package.ts`: +All packaging commands above need to prepare the language server binary for the target platform first. `editors/vscode/scripts/package.ts` calls `cargo xtask vscode prepare-server`, and the reusable server build rules live under `cargo xtask server build`: -- When the target matches the current host platform, the script runs `cargo build --release` and copies the result. +- When the target matches the current host platform, xtask runs `cargo build` for the selected profile and copies the result. - Alpine targets are built in musl containers in CI. The local script adds the matching Rust musl target, but still needs a working musl cross-compilation environment. - Other non-host targets are not automatically cross-compiled; the matching `vide` or `vide.exe` must already exist under `editors/vscode/server//`, or you should package on a matching native runner. diff --git a/docs/src/content/docs/en/advanced-guide/troubleshooting.md b/docs/src/content/docs/en/advanced-guide/troubleshooting.md index 5893d01b..050ac523 100644 --- a/docs/src/content/docs/en/advanced-guide/troubleshooting.md +++ b/docs/src/content/docs/en/advanced-guide/troubleshooting.md @@ -42,7 +42,7 @@ Open the `Vide Language Server` output channel first. Focus on the last error, a Common branches are: - `Bundled Vide Language Server binary not found` or `Unsupported platform-architecture combination`: - first confirm that the installed VSIX matches the current platform. If you installed a locally packaged VSIX, confirm that it was built with `npm run package:*` or `npm run package:debug`. Those commands bundle the language server binary into the VSIX; `npm run compile` only builds the extension frontend, so the installed extension will not contain the server. + first confirm that the installed VSIX matches the current platform. If you installed a locally packaged VSIX, confirm that it was built with `npm run package:vsix` or `npm run package:vsix:debug`. Those commands bundle the language server binary into the VSIX; `npm run compile` only builds the extension frontend, so the installed extension will not contain the server. - `Failed to start language server`, missing custom command, or permission failure: continue with "The Extension or Custom Server Cannot Start" below. - The status bar only mentions `vide.toml`, `manifest`, or `failed to load workspace`: diff --git a/editors/vscode/package.json b/editors/vscode/package.json index c85bb069..881f824c 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -426,16 +426,9 @@ "compile:web": "npm run clean && npm run typecheck && npm run bundle:node && npm run bundle:browser", "test": "npm run compile && node --import tsx --test \"test/**/*.test.ts\"", "test:web": "npm run bundle:browser && npm run bundle:test-web && npm --prefix test-web test", - "package:debug": "npm run compile && tsx scripts/package.ts --debug", - "package:alpine-arm64": "npm run compile && tsx scripts/package.ts alpine-arm64", - "package:alpine-x64": "npm run compile && tsx scripts/package.ts alpine-x64", - "package:darwin-arm64": "npm run compile && tsx scripts/package.ts darwin-arm64", - "package:darwin-x64": "npm run compile && tsx scripts/package.ts darwin-x64", - "package:linux-x64": "npm run compile && tsx scripts/package.ts linux-x64", - "package:linux-arm64": "npm run compile && tsx scripts/package.ts linux-arm64", - "package:web": "npm run compile:web && tsx scripts/package.ts web", - "package:win32-x64": "npm run compile && tsx scripts/package.ts win32-x64", - "package:win32-arm64": "npm run compile && tsx scripts/package.ts win32-arm64", + "package:vsix": "npm run compile && tsx scripts/package.ts", + "package:vsix:debug": "npm run compile && tsx scripts/package.ts --profile debug", + "package:vsix:web": "npm run compile:web && tsx scripts/package.ts --target web", "install-extension": "tsx scripts/install-extension.ts" }, "dependencies": { diff --git a/editors/vscode/scripts/install-extension.ts b/editors/vscode/scripts/install-extension.ts index 60d1ec3c..06b6f59e 100644 --- a/editors/vscode/scripts/install-extension.ts +++ b/editors/vscode/scripts/install-extension.ts @@ -48,7 +48,7 @@ function main(): void { if (vsixFiles.length === 0) { throw new Error( - 'No matching VSIX found. Run `npm run package:debug` first to create one, then rerun this command.', + 'No matching VSIX found. Run `npm run package:vsix:debug` first to create one, then rerun this command.', ); } diff --git a/editors/vscode/src/platform.ts b/editors/vscode/src/platform.ts index 06cc1689..7a8447de 100644 --- a/editors/vscode/src/platform.ts +++ b/editors/vscode/src/platform.ts @@ -4,10 +4,8 @@ export const SUPPORTED_PLATFORM_FOLDERS = [ 'alpine-arm64', 'alpine-x64', 'darwin-arm64', - 'darwin-x64', 'linux-arm64', 'linux-x64', - 'win32-arm64', 'win32-x64', ] as const; diff --git a/editors/vscode/test/platform.test.ts b/editors/vscode/test/platform.test.ts index 62693e7d..610884a1 100644 --- a/editors/vscode/test/platform.test.ts +++ b/editors/vscode/test/platform.test.ts @@ -14,16 +14,16 @@ test('maps supported Node platform and architecture pairs to VS Code target fold assert.equal(getPlatformFolder('alpine', 'arm64'), 'alpine-arm64'); assert.equal(getPlatformFolder('alpine', 'x64'), 'alpine-x64'); assert.equal(getPlatformFolder('darwin', 'arm64'), 'darwin-arm64'); - assert.equal(getPlatformFolder('darwin', 'x64'), 'darwin-x64'); assert.equal(getPlatformFolder('linux', 'arm64'), 'linux-arm64'); assert.equal(getPlatformFolder('linux', 'x64'), 'linux-x64'); - assert.equal(getPlatformFolder('win32', 'arm64'), 'win32-arm64'); assert.equal(getPlatformFolder('win32', 'x64'), 'win32-x64'); }); test('rejects unsupported platform and architecture pairs', () => { + assert.equal(getPlatformFolder('darwin', 'x64'), undefined); assert.equal(getPlatformFolder('freebsd', 'x64'), undefined); assert.equal(getPlatformFolder('linux', 'ia32'), undefined); + assert.equal(getPlatformFolder('win32', 'arm64'), undefined); }); test('checks package targets with a type guard', () => { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 33b83b21..3edbf96a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -114,10 +114,8 @@ enum VscodeServerTarget { AlpineArm64, AlpineX64, DarwinArm64, - DarwinX64, LinuxArm64, LinuxX64, - Win32Arm64, Win32X64, } @@ -127,10 +125,8 @@ impl VscodeServerTarget { VscodeServerTarget::AlpineArm64 => "alpine-arm64", VscodeServerTarget::AlpineX64 => "alpine-x64", VscodeServerTarget::DarwinArm64 => "darwin-arm64", - VscodeServerTarget::DarwinX64 => "darwin-x64", VscodeServerTarget::LinuxArm64 => "linux-arm64", VscodeServerTarget::LinuxX64 => "linux-x64", - VscodeServerTarget::Win32Arm64 => "win32-arm64", VscodeServerTarget::Win32X64 => "win32-x64", } } @@ -148,7 +144,7 @@ impl VscodeServerTarget { } fn is_windows(self) -> bool { - matches!(self, VscodeServerTarget::Win32Arm64 | VscodeServerTarget::Win32X64) + matches!(self, VscodeServerTarget::Win32X64) } fn requires_alpine_linker(self) -> bool { @@ -240,8 +236,6 @@ fn host_vscode_server_target() -> Result { ("linux", "aarch64") => Ok(VscodeServerTarget::LinuxArm64), ("linux", "x86_64") => Ok(VscodeServerTarget::LinuxX64), ("macos", "aarch64") => Ok(VscodeServerTarget::DarwinArm64), - ("macos", "x86_64") => Ok(VscodeServerTarget::DarwinX64), - ("windows", "aarch64") => Ok(VscodeServerTarget::Win32Arm64), ("windows", "x86_64") => Ok(VscodeServerTarget::Win32X64), _ => bail!("unsupported host platform: {}-{}", env::consts::OS, env::consts::ARCH), } From e3b394aa13494e9421297d6a93f42796b2c47c5b Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 16:52:19 +0800 Subject: [PATCH 79/80] feat(build): gate profile trace packaging --- Cargo.toml | 3 +- .../advanced-guide/advanced-installation.md | 15 +-- .../advanced-guide/advanced-installation.md | 15 +-- editors/vscode/package.json | 5 +- editors/vscode/scripts/package/cli.ts | 5 +- editors/vscode/scripts/package/manifest.ts | 33 ++++++- .../scripts/package/packageExtension.ts | 3 + editors/vscode/scripts/package/server.ts | 32 ++++--- editors/vscode/scripts/package/targets.ts | 3 + editors/vscode/src/browser/extension.ts | 17 +++- editors/vscode/src/extension.ts | 28 ++++-- editors/vscode/src/videStatus.ts | 13 ++- editors/vscode/test/package.test.ts | 94 +++++++++++++++++++ src/lib.rs | 3 +- src/main.rs | 27 +++++- xtask/src/main.rs | 35 ++++++- 16 files changed, 267 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 12e71c8c..4b67af9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ schemars = { version = "1.2.1", features = ["preserve_order"], optional = true } thiserror.workspace = true toml.workspace = true tracing-subscriber = { version = "0.3.17", default-features = false, features = ["registry", "fmt", "tracing-log",] } -tracing-chrome = "0.7.2" +tracing-chrome = { version = "0.7.2", optional = true } tracing.workspace = true triomphe.workspace = true @@ -128,4 +128,5 @@ winapi.workspace = true utils = { workspace = true, features = ["test-support"] } [features] +profile-trace = ["dep:tracing-chrome"] user-config-schema = ["dep:schemars"] diff --git a/docs/src/content/docs/advanced-guide/advanced-installation.md b/docs/src/content/docs/advanced-guide/advanced-installation.md index 77cefec9..d515f84f 100644 --- a/docs/src/content/docs/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/advanced-guide/advanced-installation.md @@ -78,7 +78,7 @@ npm run compile 1. 清理 `out` 和 `dist`,并执行 TypeScript typecheck。 2. 用 esbuild 把 `src/extension.ts` 打包到 `dist/extension.js`。 -3. 把诊断性能分析视图需要的 Speedscope 静态资源复制到 `dist/speedscope`。 +3. 默认不复制诊断性能分析用的 Speedscope 静态资源。 ### 打包 VS Code 扩展为 VSIX @@ -91,11 +91,12 @@ npm run package:vsix:debug 这个命令会: 1. 编译扩展,所以前面没手动执行 `npm run compile` 也可以。 -2. 通过 `cargo xtask vscode prepare-server` 针对当前宿主平台准备 debug 版语言服务器。 -3. 把 `target/debug/vide` 或 `vide.exe` 复制到扩展的 `server/` 目录。 -4. 临时把服务器二进制放到运行时 `server` 目录。 -5. 调用 `vsce package --target ` 生成 `vide-vscode--debug.vsix`。 -6. 打包后清理临时运行时二进制。 +2. 复制诊断性能分析视图需要的 Speedscope 静态资源,并启用 `profile-trace` server feature。 +3. 通过 `cargo xtask vscode prepare-server` 针对当前宿主平台准备 debug 版语言服务器。 +4. 把 `target/debug/vide` 或 `vide.exe` 复制到扩展的 `server/` 目录。 +5. 临时把服务器二进制放到运行时 `server` 目录。 +6. 调用 `vsce package --target ` 生成 `vide-vscode--debug.vsix`。 +7. 打包后清理临时运行时二进制。 如果你要打包能安装特定平台发布版 Vide 的 VSIX,可以运行以下一个或多个命令: @@ -108,7 +109,7 @@ npm run package:vsix -- --target alpine-x64 npm run package:vsix -- --target alpine-arm64 ``` -这些脚本会先编译扩展,然后准备目标平台的 release 版语言服务器,再生成 `vide-vscode-.vsix`。当前 release workflow 只覆盖上面这些目标:glibc Linux、Windows x64、macOS arm64,以及 Alpine/musl x64 和 arm64。 +这些脚本会先编译扩展,然后准备目标平台的 release 版语言服务器,再生成 `vide-vscode-.vsix`。release 包默认不启用 profile trace,也不包含 Speedscope 静态资源或 profiling 命令。当前 release workflow 只覆盖上面这些目标:glibc Linux、Windows x64、macOS arm64,以及 Alpine/musl x64 和 arm64。 这几项也是当前 CI 会实际构建的 VSIX 目标。其他平台不是当前支持的打包目标。 上面的打包命令都需要先准备目标平台的语言服务器二进制;`editors/vscode/scripts/package.ts` 会调用 `cargo xtask vscode prepare-server`,而通用的 server 构建规则由 `cargo xtask server build` 承载: diff --git a/docs/src/content/docs/en/advanced-guide/advanced-installation.md b/docs/src/content/docs/en/advanced-guide/advanced-installation.md index a1eaeddb..798839c9 100644 --- a/docs/src/content/docs/en/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/en/advanced-guide/advanced-installation.md @@ -92,7 +92,7 @@ npm run compile 1. Removes `out` and `dist`, then runs the TypeScript typecheck. 2. Bundles `src/extension.ts` into `dist/extension.js` with esbuild. -3. Copies the speedscope static assets required by the diagnostics profiling view into `dist/speedscope`. +3. Does not copy the Speedscope static assets used by diagnostics profiling by default. ### Package the VS Code Extension as a VSIX @@ -105,11 +105,12 @@ npm run package:vsix:debug This command: 1. Compiles the extension, so it is fine if you did not run `npm run compile` manually first. -2. Uses `cargo xtask vscode prepare-server` to prepare a debug server for the current host platform. -3. Copies `target/debug/vide` or `vide.exe` into the extension's `server/` directory. -4. Temporarily stages the server binary in the runtime `server` directory. -5. Calls `vsce package --target ` to generate `vide-vscode--debug.vsix`. -6. Cleans up the temporary runtime binary after packaging. +2. Copies the Speedscope static assets required by diagnostics profiling and enables the `profile-trace` server feature. +3. Uses `cargo xtask vscode prepare-server` to prepare a debug server for the current host platform. +4. Copies `target/debug/vide` or `vide.exe` into the extension's `server/` directory. +5. Temporarily stages the server binary in the runtime `server` directory. +6. Calls `vsce package --target ` to generate `vide-vscode--debug.vsix`. +7. Cleans up the temporary runtime binary after packaging. If you want a release VSIX for a specific platform, run one or more of these commands: @@ -122,7 +123,7 @@ npm run package:vsix -- --target alpine-x64 npm run package:vsix -- --target alpine-arm64 ``` -These scripts compile the extension, prepare a release server binary for the target platform, and generate `vide-vscode-.vsix`. The current release workflow only covers those targets: glibc Linux, Windows x64, macOS arm64, and Alpine/musl x64 and arm64. +These scripts compile the extension, prepare a release server binary for the target platform, and generate `vide-vscode-.vsix`. Release packages do not enable profile trace, and they do not include Speedscope static assets or the profiling command by default. The current release workflow only covers those targets: glibc Linux, Windows x64, macOS arm64, and Alpine/musl x64 and arm64. Those are also the VSIX targets currently built by CI. Other platforms are not current packaging targets. All packaging commands above need to prepare the language server binary for the target platform first. `editors/vscode/scripts/package.ts` calls `cargo xtask vscode prepare-server`, and the reusable server build rules live under `cargo xtask server build`: diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 881f824c..a239327c 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -413,7 +413,7 @@ } }, "scripts": { - "bundle:node": "esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --outfile=dist/extension.js && npm run copy-speedscope", + "bundle:node": "esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --outfile=dist/extension.js", "bundle:browser:extension": "esbuild src/browser/extension.ts --bundle --platform=browser --target=es2022 --format=cjs --external:vscode --outfile=dist/browser/extension.js", "bundle:browser:worker": "esbuild src/browser/worker.ts --bundle --platform=browser --target=es2022 --format=iife --outfile=dist/browser/vide-lsp.worker.js", "bundle:browser": "npm run bundle:browser:extension && npm run bundle:browser:worker && npm run copy-web-assets", @@ -423,11 +423,12 @@ "copy-web-assets": "tsx scripts/copy-web-assets.ts", "typecheck": "tsc -p . --noEmit && tsc -p tsconfig.browser.json --noEmit && tsc -p tsconfig.test-web.json --noEmit", "compile": "npm run clean && npm run typecheck && npm run bundle:node", + "compile:profile-trace": "npm run clean && npm run typecheck && npm run bundle:node && npm run copy-speedscope", "compile:web": "npm run clean && npm run typecheck && npm run bundle:node && npm run bundle:browser", "test": "npm run compile && node --import tsx --test \"test/**/*.test.ts\"", "test:web": "npm run bundle:browser && npm run bundle:test-web && npm --prefix test-web test", "package:vsix": "npm run compile && tsx scripts/package.ts", - "package:vsix:debug": "npm run compile && tsx scripts/package.ts --profile debug", + "package:vsix:debug": "npm run compile:profile-trace && tsx scripts/package.ts --profile debug --profile-trace", "package:vsix:web": "npm run compile:web && tsx scripts/package.ts --target web", "install-extension": "tsx scripts/install-extension.ts" }, diff --git a/editors/vscode/scripts/package/cli.ts b/editors/vscode/scripts/package/cli.ts index 20d9f1a0..de7c0e15 100644 --- a/editors/vscode/scripts/package/cli.ts +++ b/editors/vscode/scripts/package/cli.ts @@ -9,6 +9,7 @@ import { export function parsePackageCliArgs(args: string[]): PackageOptions { let profile: BuildProfile = 'release'; let serverMode: ServerMode = 'build'; + let profileTrace = false; let target: string | undefined; for (let index = 0; index < args.length; index += 1) { @@ -17,6 +18,8 @@ export function parsePackageCliArgs(args: string[]): PackageOptions { profile = 'debug'; } else if (arg === '--release') { profile = 'release'; + } else if (arg === '--profile-trace') { + profileTrace = true; } else if (arg === '--target') { target = readFlagValue(args, ++index, arg); } else if (arg.startsWith('--target=')) { @@ -36,7 +39,7 @@ export function parsePackageCliArgs(args: string[]): PackageOptions { } } - return { target, profile, serverMode }; + return { target, profile, serverMode, profileTrace }; } function readFlagValue(args: string[], index: number, flag: string): string { diff --git a/editors/vscode/scripts/package/manifest.ts b/editors/vscode/scripts/package/manifest.ts index 583cf443..190fee46 100644 --- a/editors/vscode/scripts/package/manifest.ts +++ b/editors/vscode/scripts/package/manifest.ts @@ -17,6 +17,7 @@ export function writeBuildInfo(context: PackageContext, plan: PackagePlan): void version: readExtensionVersion(context), target: plan.target, profile: plan.profile, + profileTrace: plan.profileTrace, kind: optionalEnv('VIDE_EXTENSION_BUILD_KIND') ?? 'local', commitHash: optionalEnv('VIDE_EXTENSION_COMMIT_HASH'), buildDate: optionalEnv('VIDE_EXTENSION_BUILD_DATE'), @@ -31,18 +32,44 @@ export function stagePackageJsonForTarget( context: PackageContext, plan: PackagePlan, ): string | undefined { - if (!plan.targetSpec.removeBrowserEntry) { + if (!plan.targetSpec.removeBrowserEntry && plan.profileTrace) { return undefined; } const packagePath = packageJsonPath(context); const originalPackageJson = fs.readFileSync(packagePath, 'utf8'); - const packageJson = JSON.parse(originalPackageJson) as { browser?: unknown }; - delete packageJson.browser; + const packageJson = JSON.parse(originalPackageJson) as { + browser?: unknown; + contributes?: { commands?: Array<{ command?: unknown }> }; + }; + if (plan.targetSpec.removeBrowserEntry) { + delete packageJson.browser; + } + if (!plan.profileTrace) { + packageJson.contributes = packageJson.contributes ?? {}; + packageJson.contributes.commands = (packageJson.contributes.commands ?? []).filter( + (command) => command.command !== 'vide.profileDiagnostics', + ); + } fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); return originalPackageJson; } +export function stageProfileTraceAssets(context: PackageContext, plan: PackagePlan): void { + const speedscopeDir = path.join(context.vscodeDir, 'dist', 'speedscope'); + if (!plan.profileTrace) { + fs.rmSync(speedscopeDir, { recursive: true, force: true }); + return; + } + + const indexPath = path.join(speedscopeDir, 'index.html'); + if (!fs.existsSync(indexPath)) { + throw new Error( + `profile trace assets not found at ${speedscopeDir}; run npm run compile:profile-trace first`, + ); + } +} + export function restorePackageJson( context: PackageContext, originalPackageJson: string | undefined, diff --git a/editors/vscode/scripts/package/packageExtension.ts b/editors/vscode/scripts/package/packageExtension.ts index 10f1829d..55298aaa 100644 --- a/editors/vscode/scripts/package/packageExtension.ts +++ b/editors/vscode/scripts/package/packageExtension.ts @@ -4,6 +4,7 @@ import * as path from 'node:path'; import { type PackageContext, createPackageContext } from './context'; import { restorePackageJson, + stageProfileTraceAssets, stagePackageJsonForTarget, syncReadmeFromRepoRoot, writeBuildInfo, @@ -20,6 +21,7 @@ export function packageExtension( syncReadmeFromRepoRoot(context); writeBuildInfo(context, plan); + stageProfileTraceAssets(context, plan); if (plan.targetSpec.kind === 'web') { cleanRuntimeServerFiles(context); @@ -32,6 +34,7 @@ export function packageExtension( plan.targetSpec, plan.profile, plan.serverMode, + plan.profileTrace, ); cleanRuntimeServerFiles(context); const runtimeServerPath = stageRuntimeServer(context, targetServerPath, plan.targetSpec); diff --git a/editors/vscode/scripts/package/server.ts b/editors/vscode/scripts/package/server.ts index b59e6012..3908d884 100644 --- a/editors/vscode/scripts/package/server.ts +++ b/editors/vscode/scripts/package/server.ts @@ -10,23 +10,25 @@ export function ensureTargetServerBinary( spec: NativeTargetSpec, profile: BuildProfile, serverMode: ServerMode, + profileTrace: boolean, ): string { + const args = [ + 'xtask', + 'vscode', + 'prepare-server', + '--target', + spec.target, + '--profile', + profile, + '--server', + serverMode, + ]; + if (profileTrace) { + args.push('--profile-trace'); + } + const serverPath = targetServerPath(context, spec); - run( - 'cargo', - [ - 'xtask', - 'vscode', - 'prepare-server', - '--target', - spec.target, - '--profile', - profile, - '--server', - serverMode, - ], - context.repoRoot, - ); + run('cargo', args, context.repoRoot); if (!fs.existsSync(serverPath)) { throw new Error(`prepared server binary was not found: ${serverPath}`); diff --git a/editors/vscode/scripts/package/targets.ts b/editors/vscode/scripts/package/targets.ts index 06c299fa..c02db90e 100644 --- a/editors/vscode/scripts/package/targets.ts +++ b/editors/vscode/scripts/package/targets.ts @@ -15,6 +15,7 @@ export interface PackageOptions { target?: string; profile: BuildProfile; serverMode: ServerMode; + profileTrace?: boolean; } export interface WebTargetSpec { @@ -37,6 +38,7 @@ export interface PackagePlan { target: PackageTarget; profile: BuildProfile; serverMode: ServerMode; + profileTrace: boolean; targetSpec: TargetSpec; vsixFile: string; } @@ -50,6 +52,7 @@ export function createPackagePlan(options: PackageOptions): PackagePlan { target, profile: options.profile, serverMode: options.serverMode, + profileTrace: options.profileTrace ?? false, targetSpec, vsixFile: `vide-vscode-${target}${debugSuffix}.vsix`, }; diff --git a/editors/vscode/src/browser/extension.ts b/editors/vscode/src/browser/extension.ts index 08ebd31b..f792fa72 100644 --- a/editors/vscode/src/browser/extension.ts +++ b/editors/vscode/src/browser/extension.ts @@ -33,6 +33,7 @@ interface ExtensionBuildInfo { kind?: string; commitHash?: string; buildDate?: string; + profileTrace?: boolean; } let client: VideBrowserClient | undefined; @@ -285,10 +286,14 @@ export async function activate( ): Promise { outputChannel = vscode.window.createOutputChannel(languageServerOutputChannelName); context.subscriptions.push(outputChannel); + const buildInfo = await extensionBuildInfo(context); + const profileTraceEnabled = buildInfo?.profileTrace === true; videStatusController = new VideStatusController({ createManifest: (rootUris) => createProjectConfigsFromRootUris(context, rootUris), - profileDiagnostics: () => showUnavailableInBrowser("Diagnostics profiling"), + profileDiagnostics: profileTraceEnabled + ? () => showUnavailableInBrowser("Diagnostics profiling") + : undefined, reloadProject: () => queueRestart(context, "reload project"), restartServer: () => queueRestart(context, "restart command"), showOutput, @@ -321,10 +326,14 @@ export async function activate( vscode.commands.registerCommand(generateQiheOptionsCommand, async () => { await showUnavailableInBrowser("Qihe options generation"); }), - vscode.commands.registerCommand(profileDiagnosticsCommand, async () => { - await showUnavailableInBrowser("Diagnostics profiling"); - }), ); + if (profileTraceEnabled) { + context.subscriptions.push( + vscode.commands.registerCommand(profileDiagnosticsCommand, async () => { + await showUnavailableInBrowser("Diagnostics profiling"); + }), + ); + } registerDiagnosticActions(context); registerWorkspaceWatchers(context); diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index c18ec7e7..8298d194 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -60,6 +60,7 @@ interface ExtensionBuildInfo { kind?: string; commitHash?: string; buildDate?: string; + profileTrace?: boolean; } const activeQiheTokens = new Set(); @@ -122,6 +123,10 @@ function extensionBuildLabel(context: vscode.ExtensionContext): string { return details.length > 0 ? `${version} (${details.join(', ')})` : version; } +function isProfileTraceEnabled(context: vscode.ExtensionContext): boolean { + return extensionBuildInfo(context)?.profileTrace === true; +} + async function showLanguageServerErrorMessage(message: string): Promise { const showOutputAction = vscode.l10n.t('Show Output'); const selection = await vscode.window.showErrorMessage(message, showOutputAction); @@ -981,11 +986,14 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(outputChannel); qiheOutputChannel = vscode.window.createOutputChannel(qiheOutputChannelName); context.subscriptions.push(qiheOutputChannel); + const profileTraceEnabled = isProfileTraceEnabled(context); videStatusController = new VideStatusController({ createManifest: (rootUris) => createProjectConfigsFromRootUris(context, rootUris), - profileDiagnostics: async () => { - await vscode.commands.executeCommand(profileDiagnosticsCommand); - }, + profileDiagnostics: profileTraceEnabled + ? async () => { + await vscode.commands.executeCommand(profileDiagnosticsCommand); + } + : undefined, reloadProject: reloadWorkspace, restartServer: () => restartClient(context), showOutput, @@ -1045,12 +1053,14 @@ export async function activate(context: vscode.ExtensionContext): Promise }), ); - context.subscriptions.push( - registerProfilingCommand(context, { - resolveLaunch: () => resolveServerLaunch(context, readConfiguration()), - createEnv: createServerEnv, - }), - ); + if (profileTraceEnabled) { + context.subscriptions.push( + registerProfilingCommand(context, { + resolveLaunch: () => resolveServerLaunch(context, readConfiguration()), + createEnv: createServerEnv, + }), + ); + } const reloadWorkspaceRegistration = vscode.commands.registerCommand( reloadWorkspaceCommand, diff --git a/editors/vscode/src/videStatus.ts b/editors/vscode/src/videStatus.ts index 690c4ca7..d1bd05b4 100644 --- a/editors/vscode/src/videStatus.ts +++ b/editors/vscode/src/videStatus.ts @@ -23,7 +23,7 @@ export const projectStatusNotification = 'vide/projectStatus'; export interface VideStatusActions { createManifest: (rootUris: readonly string[]) => Promise; - profileDiagnostics: () => Promise; + profileDiagnostics?: () => Promise; reloadProject: () => Promise; restartServer: () => Promise; showOutput: () => void; @@ -104,7 +104,7 @@ export class VideStatusController implements vscode.Disposable { await this.actions.createManifest(status.unconfiguredRootUris); break; case 'profileDiagnostics': - await this.actions.profileDiagnostics(); + await this.actions.profileDiagnostics?.(); break; case 'reloadProject': await this.actions.reloadProject(); @@ -169,12 +169,15 @@ export class VideStatusController implements vscode.Disposable { }); } - items.push( - { + if (this.actions.profileDiagnostics) { + items.push({ label: vscode.l10n.t('$(pulse) Profile Diagnostics'), description: vscode.l10n.t('Measure current-file or workspace diagnostics performance'), action: 'profileDiagnostics', - }, + }); + } + + items.push( { label: vscode.l10n.t('$(refresh) Reload Project'), description: vscode.l10n.t('Refresh project manifests without restarting the server'), diff --git a/editors/vscode/test/package.test.ts b/editors/vscode/test/package.test.ts index 0064384f..0f5fce05 100644 --- a/editors/vscode/test/package.test.ts +++ b/editors/vscode/test/package.test.ts @@ -1,7 +1,16 @@ import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { describe, it } from 'node:test'; +import type { PackageContext } from '../scripts/package/context'; import { parsePackageCliArgs } from '../scripts/package/cli'; +import { + restorePackageJson, + stagePackageJsonForTarget, + stageProfileTraceAssets, +} from '../scripts/package/manifest'; import { createPackagePlan } from '../scripts/package/targets'; describe('package cli', () => { @@ -10,18 +19,100 @@ describe('package cli', () => { target: 'linux-x64', profile: 'debug', serverMode: 'prebuilt', + profileTrace: false, }); }); it('accepts explicit target and profile flags', () => { + assert.deepEqual( + parsePackageCliArgs(['--target', 'web', '--profile', 'release', '--profile-trace']), + { + target: 'web', + profile: 'release', + serverMode: 'build', + profileTrace: true, + }, + ); + }); + + it('leaves profile trace disabled by default', () => { assert.deepEqual(parsePackageCliArgs(['--target', 'web', '--profile', 'release']), { target: 'web', profile: 'release', serverMode: 'build', + profileTrace: false, }); }); }); +describe('package staging', () => { + it('removes profiling command contributions when profile trace is disabled', () => { + const context = temporaryPackageContext(); + fs.writeFileSync( + path.join(context.vscodeDir, 'package.json'), + `${JSON.stringify( + { + browser: './dist/browser/extension.js', + contributes: { + commands: [ + { command: 'vide.profileDiagnostics' }, + { command: 'vide.showOutput' }, + ], + }, + }, + null, + 2, + )}\n`, + ); + + const plan = createPackagePlan({ + target: 'linux-x64', + profile: 'release', + serverMode: 'build', + }); + const originalPackageJson = stagePackageJsonForTarget(context, plan); + const packageJson = JSON.parse( + fs.readFileSync(path.join(context.vscodeDir, 'package.json'), 'utf8'), + ) as { + browser?: unknown; + contributes?: { commands?: Array<{ command?: unknown }> }; + }; + + assert.equal(packageJson.browser, undefined); + assert.deepEqual(packageJson.contributes?.commands, [{ command: 'vide.showOutput' }]); + + restorePackageJson(context, originalPackageJson); + assert.match( + fs.readFileSync(path.join(context.vscodeDir, 'package.json'), 'utf8'), + /vide\.profileDiagnostics/, + ); + }); + + it('removes stale profile trace assets when profile trace is disabled', () => { + const context = temporaryPackageContext(); + const speedscopeDir = path.join(context.vscodeDir, 'dist', 'speedscope'); + fs.mkdirSync(speedscopeDir, { recursive: true }); + fs.writeFileSync(path.join(speedscopeDir, 'index.html'), ''); + + const plan = createPackagePlan({ + target: 'web', + profile: 'release', + serverMode: 'build', + }); + stageProfileTraceAssets(context, plan); + + assert.equal(fs.existsSync(speedscopeDir), false); + }); +}); + +function temporaryPackageContext(): PackageContext { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vide-package-')); + return { + vscodeDir: root, + repoRoot: root, + }; +} + describe('package plan', () => { it('models web packages without native server staging', () => { const plan = createPackagePlan({ @@ -31,6 +122,7 @@ describe('package plan', () => { }); assert.equal(plan.target, 'web'); + assert.equal(plan.profileTrace, false); assert.equal(plan.vsixFile, 'vide-vscode-web.vsix'); assert.equal(plan.targetSpec.kind, 'web'); assert.equal(plan.targetSpec.removeBrowserEntry, false); @@ -41,9 +133,11 @@ describe('package plan', () => { target: 'win32-x64', profile: 'debug', serverMode: 'prebuilt', + profileTrace: true, }); assert.equal(plan.target, 'win32-x64'); + assert.equal(plan.profileTrace, true); assert.equal(plan.vsixFile, 'vide-vscode-win32-x64-debug.vsix'); assert.equal(plan.targetSpec.kind, 'native'); if (plan.targetSpec.kind === 'native') { diff --git a/src/lib.rs b/src/lib.rs index cc91736e..96497c91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,8 @@ pub struct Opt { /// This can also be set with VIDE_PROFILE_TRACE. The captured targets /// default to project crates and can be overridden with /// VIDE_PROFILE_TRACE_FILTER. - #[clap(long = "profile_trace", default_value = None)] + #[cfg_attr(feature = "profile-trace", clap(long = "profile_trace", default_value = None))] + #[cfg_attr(not(feature = "profile-trace"), clap(skip))] pub profile_trace: Option, } diff --git a/src/main.rs b/src/main.rs index 54822b36..8718bd4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -use std::{ - env, fs, io, - path::{Path, PathBuf}, -}; +#[cfg(feature = "profile-trace")] +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; use anyhow::Context; use clap::Parser; @@ -11,6 +10,12 @@ use tracing_subscriber::{ }; use vide::{Opt, run_server}; +#[cfg(feature = "profile-trace")] +type ProfileTraceGuard = tracing_chrome::FlushGuard; +#[cfg(not(feature = "profile-trace"))] +type ProfileTraceGuard = (); + +#[cfg(feature = "profile-trace")] const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vide=trace,", "hir::base_db=trace,", @@ -23,10 +28,12 @@ const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vfs::notify=trace" ); +#[cfg(feature = "profile-trace")] fn profile_trace_path(opt: &Opt) -> Option { opt.profile_trace.clone().or_else(|| env::var_os("VIDE_PROFILE_TRACE").map(PathBuf::from)) } +#[cfg(feature = "profile-trace")] fn create_profile_trace_file(path: &Path) -> anyhow::Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent).with_context(|| { @@ -37,7 +44,7 @@ fn create_profile_trace_file(path: &Path) -> anyhow::Result { .with_context(|| format!("could not create profile trace file: {}", path.display())) } -fn setup_logging(opt: &Opt) -> anyhow::Result> { +fn setup_logging(opt: &Opt) -> anyhow::Result> { let target: Targets = opt.log.parse().with_context(|| format!("invalid log filter: `{}`", opt.log))?; @@ -59,6 +66,15 @@ fn setup_logging(opt: &Opt) -> anyhow::Result tracing_subscriber::fmt::layer().with_ansi(false).with_writer(writer).with_filter(target); let subscriber = Registry::default().with(fmt_layer); + + #[cfg(not(feature = "profile-trace"))] + { + let _ = opt; + subscriber.init(); + return Ok(None); + } + + #[cfg(feature = "profile-trace")] let profile_guard = if let Some(path) = profile_trace_path(opt) { let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); @@ -82,6 +98,7 @@ fn setup_logging(opt: &Opt) -> anyhow::Result None }; + #[cfg(feature = "profile-trace")] Ok(profile_guard) } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 3edbf96a..42313ace 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -73,6 +73,8 @@ struct ServerBuildArgs { cargo_target: Option, #[arg(long)] alpine_linker: bool, + #[arg(long)] + profile_trace: bool, } #[derive(Debug, Args)] @@ -94,6 +96,8 @@ struct VscodePrepareServerArgs { profile: ExtensionBuildProfile, #[arg(long, value_enum, default_value = "build")] server: ExtensionServerMode, + #[arg(long)] + profile_trace: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -169,8 +173,13 @@ fn run_server_command(workspace_root: &Path, args: ServerArgs) -> Result<()> { } fn prepare_vscode_server(workspace_root: &Path, args: VscodePrepareServerArgs) -> Result<()> { - let server_path = - ensure_vscode_server_binary(workspace_root, args.target, args.profile, args.server)?; + let server_path = ensure_vscode_server_binary( + workspace_root, + args.target, + args.profile, + args.server, + args.profile_trace, + )?; println!("{}", server_path.display()); Ok(()) } @@ -180,6 +189,7 @@ fn ensure_vscode_server_binary( target: VscodeServerTarget, profile: ExtensionBuildProfile, server_mode: ExtensionServerMode, + profile_trace: bool, ) -> Result { let server_path = vscode_target_server_path(workspace_root, target); if server_mode == ExtensionServerMode::Prebuilt { @@ -200,7 +210,7 @@ fn ensure_vscode_server_binary( ); } - let build_args = server_build_args_for_vscode_target(target, profile); + let build_args = server_build_args_for_vscode_target(target, profile, profile_trace); let source_path = build_server(workspace_root, &build_args)?; let parent = server_path.parent().context("VS Code server output path has no parent")?; fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; @@ -253,11 +263,13 @@ fn vscode_target_server_path(workspace_root: &Path, target: VscodeServerTarget) fn server_build_args_for_vscode_target( target: VscodeServerTarget, profile: ExtensionBuildProfile, + profile_trace: bool, ) -> ServerBuildArgs { ServerBuildArgs { profile, cargo_target: target.cargo_target().map(str::to_owned), alpine_linker: target.requires_alpine_linker(), + profile_trace, } } @@ -270,6 +282,10 @@ fn cargo_build_args(args: &ServerBuildArgs) -> Vec { command_args.push("--target".to_owned()); command_args.push(cargo_target.clone()); } + if args.profile_trace { + command_args.push("--features".to_owned()); + command_args.push("profile-trace".to_owned()); + } command_args } @@ -634,6 +650,7 @@ mod tests { target: VscodeServerTarget::LinuxX64, profile: ExtensionBuildProfile::Release, server: ExtensionServerMode::Prebuilt, + profile_trace: false, } ); } @@ -643,6 +660,7 @@ mod tests { let args = server_build_args_for_vscode_target( VscodeServerTarget::AlpineX64, ExtensionBuildProfile::Release, + true, ); assert_eq!( @@ -651,11 +669,20 @@ mod tests { profile: ExtensionBuildProfile::Release, cargo_target: Some("x86_64-unknown-linux-musl".to_owned()), alpine_linker: true, + profile_trace: true, } ); assert_eq!( cargo_build_args(&args), - ["build", "--release", "--target", "x86_64-unknown-linux-musl"].map(str::to_owned) + [ + "build", + "--release", + "--target", + "x86_64-unknown-linux-musl", + "--features", + "profile-trace", + ] + .map(str::to_owned) ); assert_eq!(server_binary_file(&args), "vide"); } From e5da184dbfe0feb5745e7cc97716b64d6cd4efd6 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 16:57:43 +0800 Subject: [PATCH 80/80] fix(build): satisfy clippy for profile trace logging --- src/main.rs | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8718bd4a..7b33541b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,35 +71,35 @@ fn setup_logging(opt: &Opt) -> anyhow::Result> { { let _ = opt; subscriber.init(); - return Ok(None); + Ok(None) } #[cfg(feature = "profile-trace")] - let profile_guard = if let Some(path) = profile_trace_path(opt) { - let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") - .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); - let profile_filter = - profile_filter_text.parse::().context("invalid profile trace filter")?; - let file = create_profile_trace_file(&path)?; - let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() - .writer(file) - .include_args(true) - .include_locations(false) - .build(); - subscriber.with(chrome_layer.with_filter(profile_filter)).init(); - tracing::info!( - path = %path.display(), - filter = %profile_filter_text, - "profile trace enabled" - ); - Some(guard) - } else { - subscriber.init(); - None - }; - - #[cfg(feature = "profile-trace")] - Ok(profile_guard) + { + let profile_guard = if let Some(path) = profile_trace_path(opt) { + let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") + .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); + let profile_filter = + profile_filter_text.parse::().context("invalid profile trace filter")?; + let file = create_profile_trace_file(&path)?; + let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() + .writer(file) + .include_args(true) + .include_locations(false) + .build(); + subscriber.with(chrome_layer.with_filter(profile_filter)).init(); + tracing::info!( + path = %path.display(), + filter = %profile_filter_text, + "profile trace enabled" + ); + Some(guard) + } else { + subscriber.init(); + None + }; + Ok(profile_guard) + } } fn main() -> anyhow::Result<()> {