From e75211659de28646876bb5e7b265f8a26f328016 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Sat, 6 Jun 2026 18:01:06 +0800 Subject: [PATCH 01/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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 70cf6e047ae1af5fea46120d7185e1c4c201d70c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:29:06 +0800 Subject: [PATCH 46/67] 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 b3973011f4dff97c15c2745e1b5a87fdf41294eb Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:33:16 +0800 Subject: [PATCH 47/67] 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 48/67] 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 9dea282a45d68c5642d36ddc3fdf2fe870432398 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 02:37:42 +0800 Subject: [PATCH 49/67] 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 50/67] 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 51/67] 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 52/67] 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 53/67] 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 54/67] 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 55/67] 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 56/67] 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 57/67] 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 58/67] 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 59/67] 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 60/67] 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 61/67] 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 0e15fa66191d5ef8524984ed3bb11516707b78c1 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 11:58:07 +0800 Subject: [PATCH 62/67] 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 63/67] 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 7bd4afa7d36b77271f86ec2006f56e1e2bb17a33 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Mon, 8 Jun 2026 12:42:14 +0800 Subject: [PATCH 64/67] 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 65/67] 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 66/67] 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 67/67] 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',