diff --git a/.github/ci-modules.yml b/.github/ci-modules.yml index 4777a9b7..13d56dab 100644 --- a/.github/ci-modules.yml +++ b/.github/ci-modules.yml @@ -40,6 +40,7 @@ package: - "editors/vscode/scripts/**" - "editors/vscode/src/**" - "editors/vscode/syntaxes/**" + - "xtask/**" - "packages/vide-extension-shared/**" - "playground/package.json" - "playground/package-lock.json" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d3e2561..30d0e416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,24 +236,21 @@ jobs: uses: actions/cache/restore@v4 with: path: editors/vscode/server/${{ matrix.target }}/${{ env.SERVER_BIN }} - key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} - name: Install Rust - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-rust with: components: rustfmt - name: Setup sccache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-sccache with: cmake-launcher: "true" - name: Rust Cache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: Swatinem/rust-cache@v2 continue-on-error: true with: shared-key: rust-${{ runner.os }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true - name: Setup Node @@ -274,7 +271,7 @@ jobs: echo "VIDE_EXTENSION_COMMIT_HASH=${commit_hash}" >> "$GITHUB_ENV" echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:debug -- ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} + run: npm run package:vsix:debug -- --target ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} - name: Save bundled server cache if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 @@ -394,7 +391,7 @@ jobs: echo "VIDE_EXTENSION_COMMIT_HASH=${commit_hash}" >> "$GITHUB_ENV" echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:web + run: npm run package:vsix:web - name: Upload dev VSIX uses: actions/upload-artifact@v4 with: @@ -437,28 +434,25 @@ jobs: uses: actions/cache/restore@v4 with: path: editors/vscode/server/${{ matrix.target }}/${{ env.SERVER_BIN }} - key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: bundled-server-${{ matrix.target }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} - name: Install Rust - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-rust with: components: rustfmt targets: ${{ matrix.rust-target }} - name: Setup sccache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-sccache with: cmake-launcher: "true" - name: Rust Cache - if: steps.bundled-server-cache.outputs.cache-hit != 'true' uses: Swatinem/rust-cache@v2 continue-on-error: true with: shared-key: rust-${{ runner.os }}-${{ matrix.target }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -484,7 +478,7 @@ jobs: echo "VIDE_EXTENSION_BUILD_DATE=$(date -u +'%Y%m%dT%H%M%SZ')" >> "$GITHUB_ENV" - name: Package extension - run: npm run package:debug -- ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} + run: npm run package:vsix:debug -- --target ${{ matrix.target }} ${{ steps.bundled-server-cache.outputs.cache-hit == 'true' && '--server=prebuilt' || '--server=build' }} - name: Save bundled server cache if: steps.bundled-server-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4994ec7a..a5e04515 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ concurrency: env: EMSDK_VERSION: 5.0.2 + CARGO_PROFILE_RELEASE_INCREMENTAL: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && 'false' || 'true' }} jobs: changelog: @@ -81,7 +82,7 @@ jobs: continue-on-error: true with: shared-key: rust-${{ runner.os }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -92,6 +93,20 @@ jobs: cache: npm cache-dependency-path: editors/vscode/package-lock.json + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + components: rustfmt + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + continue-on-error: true + with: + shared-key: rust-${{ runner.os }} + key: package-linux-x64-prebuilt-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} + cache-workspace-crates: true + cache-on-failure: true + - name: Install JS dependencies working-directory: editors/vscode run: npm ci @@ -109,7 +124,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: npm run package:${{ matrix.target }} + run: npm run package:vsix -- --target ${{ matrix.target }} - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -192,6 +207,7 @@ jobs: docker run --rm \ -e CARGO_TARGET_DIR="${CARGO_TARGET_DIR}" \ + -e CARGO_PROFILE_RELEASE_INCREMENTAL="${CARGO_PROFILE_RELEASE_INCREMENTAL}" \ -v "${GITHUB_WORKSPACE}:/workspace" \ -v "${build_script}:/tmp/build-linux-x64-manylinux2014.sh:ro" \ -w /workspace \ @@ -247,9 +263,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: | - npm run compile - ./node_modules/.bin/tsx scripts/package.ts linux-x64 --server=prebuilt + run: npm run package:vsix -- --target linux-x64 --server=prebuilt - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -302,7 +316,7 @@ jobs: continue-on-error: true with: shared-key: rust-${{ runner.os }}-${{ matrix.target }} - key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**') }} + key: package-${{ hashFiles('Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'src/**', 'crates/**', 'xtask/**') }} cache-workspace-crates: true cache-on-failure: true @@ -330,7 +344,7 @@ jobs: - name: Build VSIX working-directory: editors/vscode - run: npm run package:${{ matrix.target }} + run: npm run package:vsix -- --target ${{ matrix.target }} - name: Upload VSIX uses: actions/upload-artifact@v4 @@ -450,7 +464,7 @@ jobs: fi - name: Build VSIX - run: npm run package:web + run: npm run package:vsix:web - name: Upload VSIX uses: actions/upload-artifact@v4 diff --git a/Cargo.toml b/Cargo.toml index b59fbfdd..a72b76a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ vfs.workspace = true always-assert.workspace = true anyhow.workspace = true -clap = { version = "4.4.6", features = ["derive"] } +clap.workspace = true const_format.workspace = true crossbeam-channel.workspace = true dunce.workspace = true @@ -46,7 +46,7 @@ schemars = { version = "1.2.1", features = ["preserve_order"], optional = true } thiserror.workspace = true toml.workspace = true tracing-subscriber = { version = "0.3.17", default-features = false, features = ["registry", "fmt", "tracing-log",] } -tracing-chrome = "0.7.2" +tracing-chrome = { version = "0.7.2", optional = true } tracing.workspace = true triomphe.workspace = true @@ -68,6 +68,7 @@ always-assert = "0.1.3" anyhow = "1.0.75" bitflags = "2.9.0" camino = "1.1.6" +clap = { version = "4.4.6", features = ["derive"] } const_format = "0.2.31" crossbeam-channel = "0.5.8" dunce = "1.0.5" @@ -115,6 +116,8 @@ salsa.opt-level = 3 incremental = true debug = 0 strip = "symbols" +lto = "thin" +codegen-units = 1 [profile.release-lto] inherits = "release" @@ -127,4 +130,5 @@ winapi.workspace = true utils = { workspace = true, features = ["test-support"] } [features] +profile-trace = ["dep:tracing-chrome"] user-config-schema = ["dep:schemars"] 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/compilation_plan.rs b/crates/hir/src/base_db/compilation_plan.rs index 9a0b62e5..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, + } } } @@ -140,17 +161,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 { @@ -216,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 { @@ -243,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; }; @@ -256,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(); @@ -277,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/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 ee9e5a68..128feb32 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,8 +1,7 @@ -use preproc::source::{PreprocSourceId, SourcePreprocError, SourcePreprocModel}; use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{ - Compilation, ParserExpectedSyntax, PreprocessorTrace, SourceBufferOrigin, SyntaxDiagnostic, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeBufferIds, + Compilation, ParserExpectedSyntax, PreprocessorTrace, SyntaxDiagnostic, SyntaxTree, + SyntaxTreeBuffer, SyntaxTreeBufferIds, }; use triomphe::Arc; use utils::{line_index::TextSize, path_identity::PathIdentityIndex}; @@ -15,6 +14,24 @@ use crate::base_db::{ 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, 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_contexts_for_file, + source_preproc_model, +}; + pub trait FileLoader { fn resolve_path(&self, path: AnchoredPath<'_>) -> Option; } @@ -92,7 +109,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() }; @@ -125,17 +142,9 @@ pub struct CompilationDiagnostic { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct MappedSourcePreprocModel { - pub model: SourcePreprocModel, - pub source_file_ids: FxHashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SourcePreprocQueryError { - UnsupportedFileKind(SourceFileKind), - TraceUnavailable, - Model(SourcePreprocError), - UnmappedSource { buffer_id: u32, path: String }, +pub struct ParsedCompilationUnit { + pub syntax_tree: SyntaxTree, + pub preprocessor_trace: Option, } fn source_file_identity(db: &dyn SourceDb, file_id: FileId) -> SourceFileIdentity { @@ -171,39 +180,6 @@ fn insert_buffer_file_ids( } } -fn source_preproc_file_ids( - db: &dyn SourceRootDb, - file_id: FileId, - trace: &PreprocessorTrace, -) -> Result, SourcePreprocQueryError> { - let mut source_file_ids = FxHashMap::default(); - let path_file_ids = path_file_ids(db); - source_file_ids.insert(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); - 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_file_ids.insert(source_id, mapped_file_id); - } - SourceBufferOrigin::Predefine => {} - } - } - - Ok(source_file_ids) -} - fn syntax_tree_options_for_file( db: &dyn SourceRootDb, file_id: FileId, @@ -213,7 +189,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() @@ -228,14 +204,14 @@ 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() } } -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 = @@ -255,15 +231,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, @@ -373,10 +366,19 @@ 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, + ) -> Arc; fn macro_reference_index_for_profile( &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, @@ -439,36 +441,6 @@ fn include_buffers_for_profile( Arc::new(compilation_plan::include_buffers_for_plan(db, &plan)) } -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 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 source_file_ids = match source_preproc_file_ids(db, file_id, &trace) { - Ok(source_file_ids) => source_file_ids, - 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))), - }; - - Arc::new(Ok(MappedSourcePreprocModel { model, source_file_ids })) -} - fn macro_reference_index_for_profile( db: &dyn SourceRootDb, profile_id: Option, @@ -689,15 +661,25 @@ 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}; - 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::salsa::{self, Durability}; + use crate::base_db::{ + project::{CompilationProfile, Predefine, PredefineSource}, + salsa::{self, Durability}, + }; const TOP: FileId = FileId(0); + const MANIFEST: FileId = FileId(1); const ROOT: SourceRootId = SourceRootId(0); #[salsa::database(SourceDbStorage, SourceRootDbStorage)] @@ -731,9 +713,49 @@ 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); + 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 + } + + 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 } @@ -742,6 +764,14 @@ mod tests { 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 = @@ -767,6 +797,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(); @@ -775,25 +862,407 @@ 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, }, ], events: Vec::new(), include_edges: Vec::new(), + emitted_tokens: Vec::new(), }; + let options = SyntaxTreeOptions::default(); + let preprocess = PreprocessConfig::default(); + let source_map = + source_preproc_file_ids(&db, TOP, None, &trace, &options, &preprocess).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_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(), + 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, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let options = SyntaxTreeOptions { + 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, &preprocess).unwrap(); + let first = PreprocSourceId::from(9); + let second = PreprocSourceId::from(2); + let extra = PreprocSourceId::from(4); + let expected_path = preproc_virtual_predefines_path(None); + + let Some(PreprocSourceMapping::VirtualDisplay { path, origin }) = source_map.get(first) + else { + 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::VirtualDisplay { + path: expected_path, + 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 { .. }) + )); + + 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_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(); + 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(); + 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(), + text: None, + buffer_id: 1, + origin: SourceBufferOrigin::Source, + }, + SourceBufferId { + path: external_path.clone(), + text: None, + buffer_id: 4, + origin: SourceBufferOrigin::Source, + }, + ], + events: Vec::new(), + include_edges: Vec::new(), + emitted_tokens: Vec::new(), + }; + let options = SyntaxTreeOptions { + include_buffers: vec![SyntaxTreeBuffer { + path: external_path, + text: "`define FROM_BUFFER 1\n".to_owned(), + }], + ..SyntaxTreeOptions::default() + }; + + 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::VirtualDisplay { path, origin }) = source_map.get(source) + else { + panic!("external include buffer should map to display-only virtual source"); + }; + + 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/base_db/source_db/preproc.rs b/crates/hir/src/base_db/source_db/preproc.rs new file mode 100644 index 00000000..8421db00 --- /dev/null +++ b/crates/hir/src/base_db/source_db/preproc.rs @@ -0,0 +1,870 @@ +use ::preproc::source::{ + PreprocSourceId, SourceEmittedTokenId, SourceEmittedTokenRange, SourceMacroCallId, + SourceMacroExpansionId, SourceMacroReferenceId, SourcePosition, SourcePreprocError, + SourcePreprocModel, SourcePreprocUnavailable, SourceRange, SourceTokenProvenance, +}; +use rustc_hash::{FxHashMap, FxHashSet}; +use smol_str::SmolStr; +use syntax::{PreprocessorTrace, SourceBufferOrigin, 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, 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, + 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)] +pub struct PreprocSourceMap { + entries: FxHashMap, + expansion_entries: FxHashMap, + predefine_sources: FxHashMap, + text_lengths: FxHashMap, + range_offsets: FxHashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourcePreprocRelevantContexts { + pub model_file_ids: Vec, + pub status: SourcePreprocContextStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SourcePreprocContextIndex { + contexts_by_file: FxHashMap>, + status: SourcePreprocContextStatus, +} + +impl SourcePreprocContextIndex { + fn contexts_for_file(&self, file_id: FileId) -> SourcePreprocRelevantContexts { + SourcePreprocRelevantContexts { + model_file_ids: self.contexts_by_file.get(&file_id).cloned().unwrap_or_default(), + status: self.status, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SourcePreprocContextStatus { + #[default] + Complete, + Partial { + skipped_models: usize, + }, +} + +#[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, + }) + } +} + +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 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) = db.parsed_compilation_unit(file_id).preprocessor_trace.clone() 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); + let range_index = PreprocRangeIndex::from_model(&model, &source_map); + + Arc::new(Ok(MappedSourcePreprocModel { model, source_map, range_index })) +} + +pub(super) fn source_preproc_context_index_for_profile( + db: &dyn SourceRootDb, + profile_id: Option, +) -> Arc { + let plan = db.compilation_plan_for_profile(profile_id); + let mut contexts_by_file = FxHashMap::>::default(); + let mut skipped_models = 0usize; + + for model_file_id in plan.roots.iter().copied() { + if !matches!( + db.file_kind(model_file_id), + SourceFileKind::SystemVerilog | SourceFileKind::IncludeHeader + ) { + continue; + } + let mapped = db.source_preproc_model(model_file_id); + match mapped.as_ref() { + Ok(mapped) => { + 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(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)) +} 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..17b92415 --- /dev/null +++ b/crates/hir/src/base_db/source_db/preproc/source_mapping.rs @@ -0,0 +1,484 @@ +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 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(); + 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) = config_indexes_by_text.get_mut(source_text).and_then(Vec::pop) + 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) = decode_manifest_predefine_source(raw_source) else { + return false; + }; + source_definition.as_str() == predefine.as_str() + && predefine_definition_name(source_definition.as_str()) + == predefine_definition_name(predefine.as_str()) +} + +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 { + 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/display.rs b/crates/hir/src/display.rs index df0d0c43..34b5a52a 100644 --- a/crates/hir/src/display.rs +++ b/crates/hir/src/display.rs @@ -106,9 +106,6 @@ impl HirDisplay for InContainer { match self.value { DataTy::Builtin(ty_id) => match ty_id.lookup(f.db) { BuiltinDataTy::Int { kind, signing } => { - if signing { - f.write_str("signed ")?; - } match kind { IntKind::Byte => f.write_str("byte"), IntKind::ShortInt => f.write_str("shortint"), @@ -116,27 +113,45 @@ impl HirDisplay for InContainer { IntKind::LongInt => f.write_str("longint"), IntKind::Integer => f.write_str("integer"), IntKind::Time => f.write_str("time"), + }?; + if signing { + f.write_str(" signed")?; } + Ok(()) } BuiltinDataTy::Vector { kind, signing, dimensions } => { - if signing { - f.write_str("signed ")?; - } + let mut wrote_head = false; match kind { VecKind::Bit => { if !f.simplified_ty { - f.write_str("bit")? + f.write_str("bit")?; + wrote_head = true; } } VecKind::Logic => { if !f.simplified_ty { - f.write_str("logic")? + f.write_str("logic")?; + wrote_head = true; } } - VecKind::Reg => f.write_str("reg")?, + VecKind::Reg => { + f.write_str("reg")?; + wrote_head = true; + } + } + if signing { + if wrote_head { + f.write_str(" ")?; + } + f.write_str("signed")?; + wrote_head = true; } for dim in dimensions.iter().flatten() { + if wrote_head { + f.write_str(" ")?; + } self.with_value(*dim).hir_fmt(f)?; + wrote_head = true; } Ok(()) } diff --git a/crates/hir/src/hir_def/aggregate.rs b/crates/hir/src/hir_def/aggregate.rs index 8b4b9250..f12acfec 100644 --- a/crates/hir/src/hir_def/aggregate.rs +++ b/crates/hir/src/hir_def/aggregate.rs @@ -75,6 +75,7 @@ pub(crate) fn lower_struct_def( #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct StructSrc { pub node: SyntaxNodePtr, + pub name: Option, } impl IsSrc for StructSrc { @@ -89,6 +90,18 @@ impl IsSrc for StructSrc { } } +impl IsNamedSrc for StructSrc { + #[inline] + fn name_kind(&self) -> Option { + self.name.map(|name| name.kind()) + } + + #[inline] + fn name_range(&self) -> Option { + self.name.map(|name| name.range()) + } +} + impl<'a> ToAstNode<'a, ast::StructUnionType<'a>> for StructSrc { fn to_node(&self, tree: &'a syntax::SyntaxTree) -> Option> { let mut node = self.node.to_node(tree)?; @@ -101,16 +114,28 @@ impl<'a> ToAstNode<'a, ast::StructUnionType<'a>> for StructSrc { impl From> for StructSrc { fn from(node: ast::StructUnionType<'_>) -> Self { - StructSrc { node: AstNodeExt::to_ptr(&node) } + let syntax = node.syntax(); + let name = struct_name_token(node).map(|name| SyntaxTokenPtr::from_token_in(syntax, name)); + StructSrc { node: AstNodeExt::to_ptr(&node), name } } } impl<'a> FromSourceAst<'a, ast::StructUnionType<'a>> for StructSrc { fn from_source_ast(node: SourceAst>) -> Self { - StructSrc { node: AstNodeExt::to_ptr(&node.into_inner()) } + let node = node.into_inner(); + let syntax = node.syntax(); + let name = struct_name_token(node) + .and_then(|name| root_token_in(syntax, name).map(SyntaxTokenPtr::from_token)); + StructSrc { node: AstNodeExt::to_ptr(&node), name } } } +fn struct_name_token(node: ast::StructUnionType<'_>) -> Option> { + let data_type = ast::DataType::StructUnionType(node); + let typedef = data_type.syntax().parent().and_then(ast::TypedefDeclaration::cast)?; + typedef.name() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ClassMemberKind { Property, diff --git a/crates/hir/src/hir_def/expr/data_ty.rs b/crates/hir/src/hir_def/expr/data_ty.rs index e4ce7250..9d420b67 100644 --- a/crates/hir/src/hir_def/expr/data_ty.rs +++ b/crates/hir/src/hir_def/expr/data_ty.rs @@ -140,8 +140,7 @@ impl LowerExprCtx<'_> { LogicType(_) => Either::Right(VecKind::Logic), }; - let signing = Self::lower_signing(ty.signing()) - .unwrap_or(matches!(kind, Either::Left(IntKind::Time) | Either::Right(_))); + let signing = Self::lower_signing(ty.signing()).unwrap_or(matches!(kind, Either::Left(_))); let dimensions = ty.dimensions().children().map(|dim| self.lower_dimension(dim)).collect(); match kind { diff --git a/crates/hir/src/hir_def/subroutine.rs b/crates/hir/src/hir_def/subroutine.rs index e833b25c..6c1c2e7a 100644 --- a/crates/hir/src/hir_def/subroutine.rs +++ b/crates/hir/src/hir_def/subroutine.rs @@ -24,7 +24,7 @@ use super::{ impl_lower_expr, timing_control::{EventExpr, EventExprSrc}, }, - lower_ident, lower_ident_opt, + lower_ident_opt, module::{ModuleId, generate::GenerateBlockId}, stmt::{LowerStmt, Stmt, StmtId, StmtSrc, impl_lower_stmt}, typedef::{Typedef, TypedefId, TypedefSrc, lower_typedef_data_ty}, @@ -236,10 +236,10 @@ where fn lower_name(name: ast::Name) -> Option { if let Some(id) = name.as_identifier_name().and_then(|n| n.identifier()) { - return lower_ident(Some(id)); + return lower_ident_opt(Some(id)); } if let Some(select) = name.as_identifier_select_name() { - return select.identifier().and_then(|tok| lower_ident(Some(tok))); + return select.identifier().and_then(|tok| lower_ident_opt(Some(tok))); } if let Some(scoped) = name.as_scoped_name() { return lower_name(scoped.right()); diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index fcc70848..f74e8153 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -1,1050 +1,49 @@ -use std::collections::BTreeMap; - use preproc::source::{ - MacroIncludeTarget, SourceIncludeChainEntry, SourceMacroBinding, SourcePosition, - SourcePreprocError, SourcePreprocProvenance, SourceRange, + CapabilityStatus, MacroIncludeTarget, PreprocSourceId, SourceEmittedTokenId, + SourceEmittedTokenRange, SourceIncludeChainEntry, SourceIncludeStatus, + SourceMacroArgument as SourceMacroArgumentFact, SourceMacroCall as SourceMacroCallFact, + SourceMacroCallId, SourceMacroCallStatus as SourceMacroCallStatusFact, + SourceMacroDefinition as SourceMacroDefinitionFact, + SourceMacroExpansion as SourceMacroExpansionFact, + SourceMacroExpansionDefinition as SourceMacroExpansionDefinitionFact, SourceMacroExpansionId, + SourceMacroExpansionQuery as SourceMacroExpansionQueryFact, + SourceMacroExpansionStatus as SourceMacroExpansionStatusFact, + SourceMacroParam as SourceMacroParamFact, SourceMacroReference as SourceMacroReferenceFact, + 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}, - path_identity::PathIdentityIndex, - paths::{AbsPathBuf, Utf8Path}, uniq_vec::UniqVec, }; use vfs::FileId; use crate::base_db::{ - project::CompilationProfileId, - source_db::{MappedSourcePreprocModel, SourceFileKind, SourcePreprocQueryError, SourceRootDb}, + project::{CompilationProfileId, Predefine}, + source_db::{ + 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 }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDefinition { - pub file_id: FileId, - pub name: SmolStr, - pub define_index: usize, - pub event_id: u32, - pub event_range: TextRange, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroUsage { - pub file_id: FileId, - pub name: SmolStr, - pub usage_index: usize, - pub range: TextRange, -} - -#[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 event_id: u32, - pub file_id: FileId, - pub range: TextRange, - pub name_range: Option, -} - -#[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 file_id: FileId, - pub name: SmolStr, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReferenceResolution { - pub reference: MacroReference, - pub definition: MacroDefinition, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroReferenceDefinitions { - pub reference: MacroReference, - pub definitions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -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.range.start(), - range_end: definition.range.end(), - name: definition.name.clone(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -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, Default)] -pub struct MacroReferenceIndex { - references_by_definition: BTreeMap>, - definitions_by_reference: BTreeMap>, -} - -impl MacroReferenceIndex { - pub fn references_for(&self, definition: &MacroDefinition) -> Vec { - self.references_by_definition - .get(&MacroDefinitionKey::from_definition(definition)) - .cloned() - .unwrap_or_default() - } - - pub fn definitions_for_reference( - &self, - reference: &MacroReference, - ) -> Option<&[MacroDefinition]> { - self.definitions_by_reference - .get(&MacroReferenceKey::from_reference(reference)) - .map(Vec::as_slice) - } - - 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()); - - let reference_key = MacroReferenceKey::from_reference(&reference); - let definitions = self.definitions_by_reference.entry(reference_key).or_default(); - push_unique_macro_definition(definitions, definition); - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IncludeDirective { - pub file_id: FileId, - pub include_index: usize, - pub range: TextRange, - pub target: IncludeTarget, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InactiveBranch { - pub file_id: FileId, - pub range: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IncludeTarget { - Literal { path: SmolStr, resolved_file: Option }, - Token { raw: SmolStr }, -} - -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 mapped = db.source_preproc_model(file_id); - let mapped = mapped_result(mapped.as_ref())?; - let position = root_position(mapped, offset)?; - - mapped - .model - .visible_macros_at(position) - .into_iter() - .filter_map(|binding| map_binding_definition(mapped, binding).transpose()) - .collect() -} - -pub fn visible_macro_names_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 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 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) { - names.push_unique(name); - } - } - - for predefine in &db.file_preprocess_config(file_id).predefines { - if let Some(name) = predefine_macro_name(predefine) { - 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 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())?; - - 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) { - 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(), - event_range, - range, - })); - } - } - - Ok(None) -} - -pub fn macro_usage_resolution_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())?; - - 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) { - 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 definition_provenance = - map_definition_provenance(mapped, &source_resolution.definition_provenance)?; - let include_chain = map_include_chain(mapped, &source_resolution.definition_include_chain)?; - - return Ok(Some(MacroUsageResolution { - usage: MacroUsage { file_id: usage_file_id, name, usage_index, range }, - definition, - definition_provenance, - include_chain, - })); - } - - Ok(None) -} - -pub fn macro_reference_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())?; - - 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, - })); - } - } - - Ok(None) -} - -pub fn macro_reference_resolution_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - let Some(resolution) = macro_reference_definitions_at(db, file_id, offset)? else { - return Ok(None); - }; - let Some(definition) = resolution.definitions.into_iter().next() else { - return Ok(None); - }; - Ok(Some(MacroReferenceResolution { reference: resolution.reference, definition })) -} - -pub fn macro_reference_definitions_at( - db: &dyn SourceRootDb, - file_id: FileId, - offset: TextSize, -) -> PreprocResult> { - 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 })) -} - -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(index.references_for(definition)) -} - -pub(crate) fn build_macro_reference_index( - db: &dyn SourceRootDb, - profile_id: Option, -) -> MacroReferenceIndex { - let mut index = MacroReferenceIndex::default(); - - 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; - }; - 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", - ); - } - } - - index -} - -fn collect_macro_references_in_model( - mapped: &MappedSourcePreprocModel, - index: &mut MacroReferenceIndex, -) -> PreprocResult<()> { - for resolved in mapped.model.resolved_macro_references()? { - let Some(definition) = map_binding_definition(mapped, resolved.definition)? else { - 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(()) -} - -pub fn include_directive_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())?; - for (include_index, include) in mapped.model.includes().iter().enumerate() { - let (include_file_id, range) = map_source_range(mapped, include.range)?; - if include_file_id != file_id || !range_contains_offset(range, offset) { - continue; - } - let target = match &include.target { - MacroIncludeTarget::Literal { path, .. } => IncludeTarget::Literal { - path: path.clone(), - resolved_file: resolve_literal_include(db, include_file_id, path), - }, - MacroIncludeTarget::Token { raw } => IncludeTarget::Token { raw: raw.clone() }, - }; - return Ok(Some(IncludeDirective { - file_id: include_file_id, - include_index, - range, - target, - })); - } - - Ok(None) -} - -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(); - - for source_range in mapped.model.inactive_ranges() { - let (branch_file_id, range) = map_source_range(mapped, *source_range)?; - if branch_file_id == file_id { - branches.push(InactiveBranch { file_id: branch_file_id, range }); - } - } - - Ok(branches) -} - -fn mapped_result( - result: &Result, -) -> PreprocResult<&MappedSourcePreprocModel> { - 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 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)) -} - -fn map_source_id( - mapped: &MappedSourcePreprocModel, - source: preproc::source::PreprocSourceId, -) -> PreprocResult { - mapped - .source_file_ids - .get(&source) - .copied() - .ok_or_else(|| PreprocError::UnmappedSource { buffer_id: source.raw() }) -} - -fn map_definition_provenance( - mapped: &MappedSourcePreprocModel, - 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()?; - Ok(MacroDefinitionProvenance { - event_id: provenance.event_id.raw(), - file_id, - 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 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); - }; - Ok(Some(MacroDefinition { - file_id, - name, - define_index: binding.define_index, - event_id: binding.event_id.raw(), - event_range, - range, - })) -} - -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_definition( - definitions: &mut Vec, - definition: MacroDefinition, -) { - if definitions.iter().any(|existing| { - existing.file_id == definition.file_id - && existing.range == definition.range - && existing.name == definition.name - }) { - return; - } - definitions.push(definition); -} - -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 -} - -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() -} +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::{ - line_index::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, - }, - source_root::{SourceRoot, SourceRootId}, - }; - - const TOP: FileId = FileId(0); - const HEADER: FileId = FileId(1); - const LEAF: FileId = FileId(2); - const ROOT: SourceRootId = SourceRootId(0); - const PROFILE: CompilationProfileId = CompilationProfileId(0); - - #[salsa::database(SourceDbStorage, SourceRootDbStorage)] - #[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 { - 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 text_at_range(text: &str, range: TextRange) -> &str { - &text[usize::from(range.start())..usize::from(range.end())] - } - - #[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!(text_at_range(header_text, resolution.definition.range).contains("HEADER_WIDTH")); - - let include = - include_directive_at(&db, TOP, offset(root_text, "defs.vh")).unwrap().unwrap(); - let IncludeTarget::Literal { resolved_file, .. } = include.target else { - panic!("literal include expected"); - }; - assert_eq!(resolved_file, Some(HEADER)); - } - - #[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_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(); - - let refs = macro_references(&db, HEADER, &definition).unwrap(); - - 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" - })); - - let definitions = - 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!(definitions.definitions.iter().any(|indexed| { - indexed.file_id == HEADER - && indexed.range == definition.range - && indexed.name == definition.name - })); - } - - #[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_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"); - - let refs = macro_references(&db, HEADER, definition).unwrap(); - 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_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" - })); - } -} +mod tests; diff --git a/crates/hir/src/preproc/conditionals.rs b/crates/hir/src/preproc/conditionals.rs new file mode 100644 index 00000000..e42c131c --- /dev/null +++ b/crates/hir/src/preproc/conditionals.rs @@ -0,0 +1,50 @@ +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 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); + } + } + } + + 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..2aca2ba1 --- /dev/null +++ b/crates/hir/src/preproc/definitions.rs @@ -0,0 +1,252 @@ +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(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), + } + } + } + } + + 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 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)); + } + } + } + + 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(mut 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) + { + param_definition.macro_definition.capability = context_query_capability( + &contexts, + param_definition.macro_definition.capability, + ); + 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(mut param_definition) = + map_macro_param_definition(mapped, definition, param_index, param)? + else { + continue; + }; + 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); + 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: 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 new file mode 100644 index 00000000..10be0cb4 --- /dev/null +++ b/crates/hir/src/preproc/expansion.rs @@ -0,0 +1,502 @@ +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 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); + } + } + + if !queries.is_empty() { + return Ok(queries.into_vec()); + } + finish_empty_single_query(&contexts, first_error)?; + + 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, + 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 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); + } + } + + 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 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); + } + } + + 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) => { + 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), + } + } + } + + 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) => { + 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), + }, + 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 }, + }) +} + +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); + 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()); +} + +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, .. } + | TokenProvenance::Builtin { call, .. } + | TokenProvenance::TokenPaste { call } + | TokenProvenance::Stringification { call } => { + apply_context_capability_to_macro_call(contexts, call); + } + TokenProvenance::SourceToken { .. } + | TokenProvenance::Predefine { .. } + | TokenProvenance::Unavailable(_) => {} + } + } +} + +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..ae7e3a72 --- /dev/null +++ b/crates/hir/src/preproc/helpers/context.rs @@ -0,0 +1,119 @@ +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 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, +) -> 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 if !is_include_only => true, + SourceFileKind::SystemVerilog | 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() { + 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..5d448ea3 --- /dev/null +++ b/crates/hir/src/preproc/helpers/expansion.rs @@ -0,0 +1,439 @@ +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 (definition_id, definition) = map_macro_expansion_definition(mapped, expansion)?; + Ok(MacroExpansion { + id: expansion.id.into(), + call: map_macro_call(mapped, call)?, + definition_id, + 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_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, +) -> 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 + .macro_call_ids_at(file_id, offset) + .into_iter() + .filter_map(|call| mapped.model.macro_calls().get(call)) + .collect() +} + +pub(in crate::preproc) fn source_macro_calls_intersecting_range( + mapped: &MappedSourcePreprocModel, + file_id: FileId, + source_range: TextRange, +) -> Vec<&SourceMacroCallFact> { + mapped + .macro_call_ids_intersecting_range(file_id, source_range) + .into_iter() + .filter_map(|call| mapped.model.macro_calls().get(call)) + .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 { 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)? } + } + SourceTokenProvenanceFact::Builtin { name, call, .. } => { + TokenProvenance::Builtin { name: name.clone(), call: mapped_macro_call(mapped, *call)? } + } + 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::TokenPaste { .. } | TokenProvenance::Stringification { .. } => { + saw_unavailable = Some(PreprocUnavailable::Source( + SourcePreprocUnavailable::UnsupportedEmittedTokenProvenance, + )); + } + TokenProvenance::Predefine { .. } => {} + TokenProvenance::Builtin { call, name } => { + return Ok(DiagnosticProvenance::Builtin { + call: call.clone(), + name: name.clone(), + }); + } + } + } + + 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..3e6d5895 --- /dev/null +++ b/crates/hir/src/preproc/helpers/source.rs @@ -0,0 +1,77 @@ +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 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..27efd7ec --- /dev/null +++ b/crates/hir/src/preproc/includes.rs @@ -0,0 +1,81 @@ +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: context_query_capability( + &contexts, + 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..7f38b418 --- /dev/null +++ b/crates/hir/src/preproc/predefines.rs @@ -0,0 +1,124 @@ +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(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(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); + } + } + } + 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..fa8428ee --- /dev/null +++ b/crates/hir/src/preproc/reference_queries.rs @@ -0,0 +1,280 @@ +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_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; + }; + + 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)?; + 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, + 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_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_id in mapped.macro_reference_ids_intersecting_range(file_id, range) { + let Some(reference) = mapped.model.macro_references().get(reference_id) else { + continue; + }; + + match map_macro_reference(mapped, 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), + } + } + } + + 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, + 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_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, + 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 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 { + 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: 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 new file mode 100644 index 00000000..bad0b508 --- /dev/null +++ b/crates/hir/src/preproc/tests.rs @@ -0,0 +1,1374 @@ +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_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; +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_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; +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"); + 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(); + 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::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), + 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), + 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(_)), + "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(_)), + "stringification diagnostic provenance should be 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(_)), + "unbacked predefine diagnostic provenance should be unavailable, got {provenance:?}" + ); +} + +#[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_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 +`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 { + 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_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 +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_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 +`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_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"; + 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..968c86aa --- /dev/null +++ b/crates/hir/src/preproc/types.rs @@ -0,0 +1,654 @@ +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 capability: PreprocAvailability, + 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 MacroCallResolution { + pub call: MacroCall, + pub definition: MacroDefinition, +} + +#[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: Option, + pub definition: MacroExpansionDefinition, + 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 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, + 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, + call: MacroCall, + }, + TokenPaste { + call: MacroCall, + }, + Stringification { + call: MacroCall, + }, + 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, + }, + Builtin { + call: MacroCall, + name: SmolStr, + }, + 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/ide/src/code_action/context.rs b/crates/ide/src/code_action/context.rs index 24ac41e6..e72ebe37 100644 --- a/crates/ide/src/code_action/context.rs +++ b/crates/ide/src/code_action/context.rs @@ -26,6 +26,7 @@ impl<'a> CodeActionCtx<'a> { ) -> Option { let parsed_file = sema.parse_file(file_id); parsed_file.compilation_unit()?; + Some(Self { sema, file_id, range, diagnostics, parsed_file }) } diff --git a/crates/ide/src/code_action/handlers.rs b/crates/ide/src/code_action/handlers.rs index 3289147b..33390b07 100644 --- a/crates/ide/src/code_action/handlers.rs +++ b/crates/ide/src/code_action/handlers.rs @@ -8,13 +8,21 @@ mod add_instance_parens; mod add_missing_connections; mod add_missing_parameters; mod apply_de_morgan; +mod convert_always_block; mod convert_literal_base; +mod convert_named_port_connections; mod convert_ordered_connections; +mod convert_port_declarations; mod expand_compound_assignment; mod expand_postfix_inc_dec; +mod extract_variable; mod insert_expected_token; mod invert_if_else; +mod merge_nested_if; +mod pull_assignment_up; +mod reformat_number_literal; mod remove_empty_port_connections; +mod remove_parentheses; mod sort_named_instantiation_items; mod split_declaration_declarators; mod wrap_statement_in_begin_end; @@ -22,22 +30,31 @@ mod wrap_statement_in_begin_end; pub(crate) fn all() -> &'static [Handler] { &[ convert_literal_base::convert_literal_base, + reformat_number_literal::reformat_number_literal, add_missing_connections::add_missing_connections, add_missing_parameters::add_missing_parameters, convert_ordered_connections::convert_ordered_ports, convert_ordered_connections::convert_ordered_params, + convert_named_port_connections::convert_named_port_connection_shorthand, remove_empty_port_connections::remove_empty_port_connections, add_implicit_named_port_parens::add_implicit_named_port_parens, add_instance_parens::add_instance_parens, + convert_always_block::convert_always_block, + convert_port_declarations::convert_port_declarations, split_declaration_declarators::split_declaration_declarators, sort_named_instantiation_items::sort_named_parameter_assignments, sort_named_instantiation_items::sort_named_port_connections, add_default_case_item::add_default_case_item, invert_if_else::invert_if_else, + merge_nested_if::merge_nested_if, wrap_statement_in_begin_end::unwrap_single_statement_block, wrap_statement_in_begin_end::wrap_statement_in_begin_end, + remove_parentheses::remove_parentheses, expand_postfix_inc_dec::expand_postfix_inc_dec, expand_compound_assignment::expand_compound_assignment, + extract_variable::extract_variable, + pull_assignment_up::pull_assignment_up, + pull_assignment_up::pull_assignment_down, apply_de_morgan::apply_de_morgan, insert_expected_token::insert_expected_token, ] diff --git a/crates/ide/src/code_action/handlers/add_default_case_item.rs b/crates/ide/src/code_action/handlers/add_default_case_item.rs index 3e635346..928063ef 100644 --- a/crates/ide/src/code_action/handlers/add_default_case_item.rs +++ b/crates/ide/src/code_action/handlers/add_default_case_item.rs @@ -12,6 +12,18 @@ const ID: CodeActionId = CodeActionId { name: "add_default_case_item", kind: CodeActionKind::Generate, repair: None }; const LABEL: &str = "Add default case item"; +// Assist: add_default_case_item +// +// This adds a `default` item to a case statement that does not already have +// one. +// +// ``` +// module top; always_comb case$0 (sel) 1'b0: y = a; endcase endmodule +// ``` +// -> +// ``` +// module top; always_comb case (sel) 1'b0: y = a; default: ; endcase endmodule +// ``` pub(super) fn add_default_case_item( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs index 31ec47aa..660ccae2 100644 --- a/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs +++ b/crates/ide/src/code_action/handlers/add_implicit_named_port_parens.rs @@ -14,6 +14,18 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Add explicit empty port connection"; +// Assist: add_implicit_named_port_parens +// +// This makes an implicit named port connection explicit by adding empty +// parentheses. +// +// ``` +// child u(.ready$0); +// ``` +// -> +// ``` +// child u(.ready()); +// ``` pub(super) fn add_implicit_named_port_parens( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_instance_parens.rs b/crates/ide/src/code_action/handlers/add_instance_parens.rs index a25aeee8..56de01d1 100644 --- a/crates/ide/src/code_action/handlers/add_instance_parens.rs +++ b/crates/ide/src/code_action/handlers/add_instance_parens.rs @@ -11,6 +11,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Add empty instance port list"; +// Assist: add_instance_parens +// +// This adds an empty port list to an instance that is missing one. +// +// ``` +// child u$0; +// ``` +// -> +// ``` +// child u(); +// ``` pub(super) fn add_instance_parens( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/add_missing_connections.rs b/crates/ide/src/code_action/handlers/add_missing_connections.rs index 4bc703f4..607db3f6 100644 --- a/crates/ide/src/code_action/handlers/add_missing_connections.rs +++ b/crates/ide/src/code_action/handlers/add_missing_connections.rs @@ -1,13 +1,13 @@ use hir::{ base_db::source_db::SourceDb, container::InModule, db::HirDb, - hir_def::module::instantiation::PortConn, + hir_def::module::instantiation::PortConn, source_map::IsSrc, }; use rustc_hash::FxHashSet; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, }; -use utils::get::GetRef; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ @@ -15,7 +15,7 @@ use crate::{ apply_missing_list_edit, missing_member_entry_text, port_names, remaining_ordered_port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const ID: CodeActionId = CodeActionId { @@ -25,6 +25,18 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Fill connections"; +// Assist: add_missing_connections +// +// This fills the missing port connections for an instance from the target +// module definition. +// +// ``` +// child u($0.a(a)); +// ``` +// -> +// ``` +// child u(.a(a), .b('0)); +// ``` pub(super) fn add_missing_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -36,14 +48,13 @@ pub(super) fn add_missing_connections( let ast_instance = ctx.find_node_at_offset::()?; let InModule { value: instance_id, module_id } = sema.resolve_instance(file_id, ast_instance)?; - let module = db.module(module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); let instance = module.get(instance_id); let open_paren = ast_instance.open_paren()?.text_range_in(ast_instance.syntax())?; let close_paren = ast_instance.close_paren()?.text_range_in(ast_instance.syntax())?; - let instantiation = ast::HierarchyInstantiation::cast(ast_instance.syntax().parent()?)?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; + let instantiation = module.get(instance.parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let is_ordered = instance @@ -83,8 +94,8 @@ pub(super) fn add_missing_connections( .collect(); let text = sema.db.file_text(ctx.file_id()); - let item_ranges = ast_instance.connections().children().filter_map(|conn| { - let range = conn.syntax().text_range()?; + let item_ranges = instance.connections.iter().filter_map(|conn_id| { + let range = module_src_map.get(*conn_id)?.range(); (!range.is_empty()).then_some(range) }); apply_missing_list_edit(builder, &text, open_paren, close_paren, item_ranges, entries); diff --git a/crates/ide/src/code_action/handlers/add_missing_parameters.rs b/crates/ide/src/code_action/handlers/add_missing_parameters.rs index f0dee54f..b9f466d7 100644 --- a/crates/ide/src/code_action/handlers/add_missing_parameters.rs +++ b/crates/ide/src/code_action/handlers/add_missing_parameters.rs @@ -1,13 +1,13 @@ use hir::{ base_db::source_db::SourceDb, container::InModule, db::HirDb, - hir_def::module::instantiation::ParamAssign, + hir_def::module::instantiation::ParamAssign, source_map::IsSrc, }; use rustc_hash::FxHashSet; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, }; -use utils::get::GetRef; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ @@ -15,7 +15,7 @@ use crate::{ all_parameter_names, apply_missing_list_edit, leading_parameter_names, missing_member_entry_text, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const ID: CodeActionId = CodeActionId { @@ -25,6 +25,18 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Fill parameters"; +// Assist: add_missing_parameters +// +// This fills the missing parameter assignments for an instantiation from the +// target module definition. +// +// ``` +// child #($0.A(1)) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(0)) u(); +// ``` pub(super) fn add_missing_parameters( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -36,15 +48,14 @@ pub(super) fn add_missing_parameters( let ast_instantiation = ctx.find_node_at_offset::()?; let InModule { value: instantiation_id, module_id } = sema.resolve_instantiation(file_id, ast_instantiation)?; - let module = db.module(module_id); + let (module, module_src_map) = db.module_with_source_map(module_id); let instantiation = module.get(instantiation_id); let params_node = ast_instantiation.parameters()?; let open_paren = params_node.open_paren()?.text_range_in(params_node.syntax())?; let close_paren = params_node.close_paren()?.text_range_in(params_node.syntax())?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), ast_instantiation).unique()?; + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let is_ordered = instantiation @@ -87,8 +98,8 @@ pub(super) fn add_missing_parameters( .collect(); let text = sema.db.file_text(ctx.file_id()); - let item_ranges = params_node.parameters().children().filter_map(|assign| { - let range = assign.syntax().text_range()?; + let item_ranges = instantiation.param_assigns.iter().filter_map(|assign_id| { + let range = module_src_map.get(*assign_id)?.range(); (!range.is_empty()).then_some(range) }); apply_missing_list_edit(builder, &text, open_paren, close_paren, item_ranges, entries); diff --git a/crates/ide/src/code_action/handlers/apply_de_morgan.rs b/crates/ide/src/code_action/handlers/apply_de_morgan.rs index 465edb75..5f5d8ff3 100644 --- a/crates/ide/src/code_action/handlers/apply_de_morgan.rs +++ b/crates/ide/src/code_action/handlers/apply_de_morgan.rs @@ -16,6 +16,18 @@ const FACTOR_ID: CodeActionId = CodeActionId { name: "factor_de_morgan", kind: CodeActionKind::RefactorRewrite, repair: None }; const FACTOR_LABEL: &str = "Factor De Morgan's law"; +// Assist: apply_de_morgan +// +// This applies or factors De Morgan's law for boolean expressions and if +// conditions. +// +// ``` +// assign y = $0!(a && b); +// ``` +// -> +// ``` +// assign y = !a || !b; +// ``` pub(super) fn apply_de_morgan( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_always_block.rs b/crates/ide/src/code_action/handlers/convert_always_block.rs new file mode 100644 index 00000000..36a713c4 --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_always_block.rs @@ -0,0 +1,153 @@ +use std::ops::Range; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ALWAYS_TO_COMB_ID: CodeActionId = CodeActionId { + name: "convert_always_to_always_comb", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_TO_COMB_LABEL: &str = "Convert to always_comb"; + +const ALWAYS_TO_FF_ID: CodeActionId = CodeActionId { + name: "convert_always_to_always_ff", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_TO_FF_LABEL: &str = "Convert to always_ff"; + +const ALWAYS_COMB_TO_ALWAYS_ID: CodeActionId = CodeActionId { + name: "convert_always_comb_to_always", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_COMB_TO_ALWAYS_LABEL: &str = "Convert to always @(*)"; + +const ALWAYS_FF_TO_ALWAYS_ID: CodeActionId = CodeActionId { + name: "convert_always_ff_to_always", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ALWAYS_FF_TO_ALWAYS_LABEL: &str = "Convert to always @(...)"; + +// Assist: convert_always_block +// +// This converts compatible procedural blocks between `always`, `always_comb`, +// and `always_ff`. +// +// ``` +// always$0 @(*) begin y = a; end +// ``` +// -> +// ``` +// always_comb begin y = a; end +// ``` +pub(super) fn convert_always_block( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let proc = ctx.find_node_at_offset::()?; + let keyword = proc.keyword()?.text_range_in(proc.syntax())?; + let target = proc.syntax().text_range()?; + let mut allowed_ranges = vec![keyword]; + + match proc { + ast::ProceduralBlock::AlwaysBlock(_) => { + let timing_stmt = proc.statement().as_timing_control_statement()?; + let timing = timing_stmt.timing_control(); + allowed_ranges.push(timing.syntax().text_range()?); + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + if timing.as_implicit_event_control().is_some() { + let stmt_range = timing_stmt.statement().syntax().text_range()?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let stmt_text = text.get(Range::from(stmt_range))?; + collector.add(ALWAYS_TO_COMB_ID, ALWAYS_TO_COMB_LABEL, target, |builder| { + builder.replace(keyword, "always_comb"); + builder + .replace(timing_stmt.syntax().text_range().unwrap(), stmt_text.to_owned()); + }); + } + + if edge_sensitive_timing_control(timing) { + collector.add(ALWAYS_TO_FF_ID, ALWAYS_TO_FF_LABEL, target, |builder| { + builder.replace(keyword, "always_ff"); + }); + } + + Some(()) + } + ast::ProceduralBlock::AlwaysCombBlock(_) => { + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + collector.add( + ALWAYS_COMB_TO_ALWAYS_ID, + ALWAYS_COMB_TO_ALWAYS_LABEL, + target, + |builder| { + builder.replace(keyword, "always"); + builder.insert(keyword.end(), " @(*)"); + }, + ) + } + ast::ProceduralBlock::AlwaysFFBlock(_) => { + let timing_stmt = proc.statement().as_timing_control_statement()?; + allowed_ranges.push(timing_stmt.timing_control().syntax().text_range()?); + if !range_intersects_any(ctx.range(), &allowed_ranges) { + return None; + } + + if !edge_sensitive_timing_control(timing_stmt.timing_control()) { + return None; + } + + collector.add(ALWAYS_FF_TO_ALWAYS_ID, ALWAYS_FF_TO_ALWAYS_LABEL, target, |builder| { + builder.replace(keyword, "always"); + }) + } + _ => None, + } +} + +fn range_intersects_any( + range: utils::text_edit::TextRange, + allowed_ranges: &[utils::text_edit::TextRange], +) -> bool { + allowed_ranges.iter().any(|allowed| range_intersects(range, *allowed)) +} + +fn range_intersects(lhs: utils::text_edit::TextRange, rhs: utils::text_edit::TextRange) -> bool { + if lhs.is_empty() { + rhs.contains(lhs.start()) + } else { + lhs.start() < rhs.end() && rhs.start() < lhs.end() + } +} + +fn edge_sensitive_timing_control(timing: ast::TimingControl<'_>) -> bool { + timing + .as_event_control_with_expression() + .is_some_and(|control| edge_sensitive_event_expr(control.expr())) +} + +fn edge_sensitive_event_expr(expr: ast::EventExpression<'_>) -> bool { + match expr { + ast::EventExpression::ParenthesizedEventExpression(expr) => { + edge_sensitive_event_expr(expr.expr()) + } + ast::EventExpression::BinaryEventExpression(expr) => { + edge_sensitive_event_expr(expr.left()) && edge_sensitive_event_expr(expr.right()) + } + ast::EventExpression::SignalEventExpression(expr) => expr.edge().is_some(), + } +} diff --git a/crates/ide/src/code_action/handlers/convert_literal_base.rs b/crates/ide/src/code_action/handlers/convert_literal_base.rs index 55cc9420..51b442bf 100644 --- a/crates/ide/src/code_action/handlers/convert_literal_base.rs +++ b/crates/ide/src/code_action/handlers/convert_literal_base.rs @@ -13,6 +13,18 @@ const ACTION_ID: CodeActionId = CodeActionId { repair: None, }; +// Assist: convert_literal_base +// +// This converts an integer literal between binary, octal, decimal, and +// hexadecimal notation. +// +// ``` +// localparam int value = 8'h0f$0; +// ``` +// -> +// ``` +// localparam int value = 8'b1111; +// ``` pub(super) fn convert_literal_base( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/convert_named_port_connections.rs b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs new file mode 100644 index 00000000..18435ae3 --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_named_port_connections.rs @@ -0,0 +1,131 @@ +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const EXPAND_ID: CodeActionId = CodeActionId { + name: "expand_named_port_connection_shorthand", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const EXPAND_LABEL: &str = "Expand named port shorthand"; + +const COLLAPSE_ID: CodeActionId = CodeActionId { + name: "collapse_named_port_connection_shorthand", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const COLLAPSE_LABEL: &str = "Collapse named port to shorthand"; + +// Assist: convert_named_port_connection_shorthand +// +// This expands named port connection shorthand, or collapses same-name +// connections to shorthand. +// +// ``` +// child u(.ready$0); +// ``` +// -> +// ``` +// child u(.ready(ready)); +// ``` +pub(super) fn convert_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + expand_named_port_connection_shorthand(collector, ctx) + .or(collapse_named_port_connection_shorthand(collector, ctx)) +} + +fn expand_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + ctx.find_node_at_offset::()?; + let instance = ctx.find_node_at_offset::()?; + let conns = named_port_connections(instance)?; + let edits = conns + .iter() + .filter(|conn| conn.open_paren().is_none()) + .map(|conn| { + let name = conn.name()?; + Some((name.text_range_in(conn.syntax())?.end(), name.value_text().to_string())) + }) + .collect::>>()?; + if edits.is_empty() { + return None; + } + + let target = instance.syntax().text_range()?; + + collector.add(EXPAND_ID, EXPAND_LABEL, target, |builder| { + for (insert_offset, name) in edits { + builder.insert(insert_offset, format!("({name})")); + } + }) +} + +fn collapse_named_port_connection_shorthand( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + ctx.find_node_at_offset::()?; + let instance = ctx.find_node_at_offset::()?; + let conns = named_port_connections(instance)?; + let edits = conns + .iter() + .filter_map(|conn| collapsible_named_port_connection_range(*conn)) + .collect::>(); + if edits.is_empty() { + return None; + } + + let target = instance.syntax().text_range()?; + collector.add(COLLAPSE_ID, COLLAPSE_LABEL, target, |builder| { + for remove_range in edits { + builder.delete(remove_range); + } + }) +} + +fn named_port_connections( + instance: ast::HierarchicalInstance<'_>, +) -> Option>> { + let conns = instance + .connections() + .children() + .map(|conn| conn.as_named_port_connection()) + .collect::>>()?; + (!conns.is_empty()).then_some(conns) +} + +fn collapsible_named_port_connection_range( + conn: ast::NamedPortConnection<'_>, +) -> Option { + let conn_name = conn.name()?; + let port_name = conn_name.value_text().to_string(); + + let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); + + use ast::{Expression, Name}; + let actual = match expr { + Expression::Name(Name::IdentifierName(ident)) => ident.identifier()?, + Expression::Name(Name::IdentifierSelectName(ident)) + if ident.selectors().children().next().is_none() => + { + ident.identifier()? + } + _ => return None, + }; + if actual.value_text().to_string() != port_name { + return None; + } + + Some(TextRange::new( + conn_name.text_range_in(conn.syntax())?.end(), + conn.close_paren()?.text_range_in(conn.syntax())?.end(), + )) +} diff --git a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs index df2fbb29..52eb0ae3 100644 --- a/crates/ide/src/code_action/handlers/convert_ordered_connections.rs +++ b/crates/ide/src/code_action/handlers/convert_ordered_connections.rs @@ -1,18 +1,22 @@ use std::ops::Range; -use hir::{base_db::source_db::SourceDb, db::HirDb}; -use itertools::Itertools; -use syntax::{ - ast::{self, AstNode}, - has_text_range::HasTextRange, +use hir::{ + base_db::source_db::SourceDb, + container::InModule, + db::HirDb, + hir_def::module::instantiation::{ParamAssign, PortConn}, + source_map::IsSrc, }; +use itertools::Itertools; +use syntax::ast; +use utils::get::{Get, GetRef}; use crate::{ code_action::{ CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, RepairKind, leading_parameter_names, port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const PORTS_ID: CodeActionId = CodeActionId { @@ -29,6 +33,18 @@ const PARAMS_ID: CodeActionId = CodeActionId { }; const PARAMS_LABEL: &str = "Convert ordered parameter assignments to named assignments"; +// Assist: convert_ordered_ports +// +// This converts ordered port connections to named port connections using the +// target module's port order. +// +// ``` +// child u($0a, b); +// ``` +// -> +// ``` +// child u(.a(a), .b(b)); +// ``` pub(super) fn convert_ordered_ports( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -37,21 +53,25 @@ pub(super) fn convert_ordered_ports( let db = sema.db; let text = db.file_text(ctx.file_id()); let ast_instance = ctx.find_node_at_offset::()?; - let instantiation = ast::HierarchyInstantiation::cast(ast_instance.syntax().parent()?)?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; - let target_module = db.module(target_module_id); - let port_names = port_names(&target_module); + let InModule { value: instance_id, module_id } = + sema.resolve_instance(ctx.file_id().into(), ast_instance)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(module.get(instance_id).parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let port_names = port_names(&db.module(target_module_id)); - let replacements = ast_instance - .connections() - .children() + let replacements = module + .get(instance_id) + .connections + .iter() .enumerate() - .filter_map(|(idx, conn)| { - let ordered = conn.as_ordered_port_connection()?; + .filter_map(|(idx, conn_id)| { + let PortConn::Ordered(expr_id) = module.get(*conn_id) else { + return None; + }; let name = port_names.get(idx)?; - let expr = ordered.expr().syntax().text_range()?; - let range = ordered.syntax().text_range()?; + let expr = module_src_map.get(*expr_id)?.range(); + let range = module_src_map.get(*conn_id)?.range(); Some((range, format!(".{name}({})", text.get(Range::from(expr))?))) }) .collect_vec(); @@ -69,6 +89,18 @@ pub(super) fn convert_ordered_ports( Some(()) } +// Assist: convert_ordered_params +// +// This converts ordered parameter assignments to named parameter assignments +// using the target module's parameter order. +// +// ``` +// child #($01, 2) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(2)) u(); +// ``` pub(super) fn convert_ordered_params( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -77,21 +109,25 @@ pub(super) fn convert_ordered_params( let db = sema.db; let text = db.file_text(ctx.file_id()); let ast_instantiation = ctx.find_node_at_offset::()?; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), ast_instantiation).unique()?; + let InModule { value: instantiation_id, module_id } = + sema.resolve_instantiation(ctx.file_id().into(), ast_instantiation)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(instantiation_id); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let target_module = db.module(target_module_id); let param_names = leading_parameter_names(&target_module); - let replacements = ast_instantiation - .parameters()? - .parameters() - .children() + let replacements = instantiation + .param_assigns + .iter() .enumerate() - .filter_map(|(idx, assign)| { - let ordered = assign.as_ordered_param_assignment()?; + .filter_map(|(idx, assign_id)| { + let ParamAssign::Ordered(expr_id) = module.get(*assign_id) else { + return None; + }; let name = param_names.get(idx)?; - let expr = ordered.expr().syntax().text_range()?; - let range = ordered.syntax().text_range()?; + let expr = module_src_map.get(*expr_id)?.range(); + let range = module_src_map.get(*assign_id)?.range(); Some((range, format!(".{name}({})", text.get(Range::from(expr))?))) }) .collect_vec(); diff --git a/crates/ide/src/code_action/handlers/convert_port_declarations.rs b/crates/ide/src/code_action/handlers/convert_port_declarations.rs new file mode 100644 index 00000000..a159330d --- /dev/null +++ b/crates/ide/src/code_action/handlers/convert_port_declarations.rs @@ -0,0 +1,409 @@ +use std::ops::Range; + +use hir::{ + base_db::source_db::SourceDb, + container::{InContainer, InModule}, + db::HirDb, + display::HirDisplay, + hir_def::{ + Ident, + declaration::DeclarationSrc, + expr::declarator::{DeclId, DeclaratorParent}, + module::{ + Module, ModuleId, ModuleSourceMap, + port::{PortDecl, PortDeclSrc, Ports}, + }, + }, + scope::{ModuleEntry, ModuleScope, NonAnsiPortEntry}, + source_map::IsSrc, +}; +use itertools::Itertools; +use syntax::{ + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; +use utils::{ + get::{Get, GetRef}, + text_edit::TextRange, +}; + +use crate::code_action::{ + CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +}; + +const ANSI_TO_NON_ANSI_ID: CodeActionId = CodeActionId { + name: "convert_ansi_ports_to_non_ansi", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const ANSI_TO_NON_ANSI_LABEL: &str = "Convert ANSI port declarations to non-ANSI"; + +const NON_ANSI_TO_ANSI_ID: CodeActionId = CodeActionId { + name: "convert_non_ansi_ports_to_ansi", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const NON_ANSI_TO_ANSI_LABEL: &str = "Convert non-ANSI port declarations to ANSI"; + +// Assist: convert_port_declarations +// +// This converts module ports between ANSI declarations and non-ANSI +// declarations. +// +// ``` +// module top($0input a, output logic b); endmodule +// ``` +// -> +// ``` +// module top(a, b); input a; output logic b; endmodule +// ``` +pub(super) fn convert_port_declarations( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + convert_ansi_ports_to_non_ansi(collector, ctx) + .or(convert_non_ansi_ports_to_ansi(collector, ctx)) +} + +fn convert_ansi_ports_to_non_ansi( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let ast_module = ctx.find_node_at_offset::()?; + let port_list = ast_module.header().ports()?.as_ansi_port_list()?; + + let module_id = ctx.sema().module_to_def(ctx.file_id().into(), ast_module)?; + let (module, module_src_map) = ctx.sema().db.module_with_source_map(module_id); + let Ports::Ansi(port_decls) = &module.ports else { + return None; + }; + + let mut port_names = Vec::with_capacity(port_decls.len()); + let mut port_items = Vec::with_capacity(port_decls.len()); + for (port_id, port_decl) in port_decls.iter() { + let src = module_src_map.port_srcs.get(port_id)?; + let PortDeclSrc::ImplicitAnsiPort(_) = src else { + return None; + }; + + let name = port_decl_declared_name(&module, port_decl)?; + port_names.push(name); + port_items.push((port_decl, src)); + } + + if port_names.is_empty() { + return None; + } + + let open_paren = port_list.open_paren()?.text_range_in(port_list.syntax())?; + let close_paren = port_list.close_paren()?.text_range_in(port_list.syntax())?; + if !port_list_trigger_range(open_paren, close_paren)?.contains_range(ctx.range()) { + return None; + } + + let body_range = module_body_range(ast_module)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let generated_members = port_items + .iter() + .map(|(port_decl, src)| { + render_ansi_port_declaration(ctx, module_id, port_decl, *src, &text) + }) + .collect::>>()?; + let port_list_replacement = render_port_list(&text, open_paren, close_paren, &port_names)?; + let body_replacement = + render_module_body(&text, ast_module, body_range, &generated_members, &[])?; + let target = port_list.syntax().text_range()?; + + collector.add(ANSI_TO_NON_ANSI_ID, ANSI_TO_NON_ANSI_LABEL, target, |builder| { + builder.replace(target, port_list_replacement); + builder.replace(body_range, body_replacement); + }) +} + +fn convert_non_ansi_ports_to_ansi( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let ast_module = ctx.find_node_at_offset::()?; + let port_list = ast_module.header().ports()?.as_non_ansi_port_list()?; + + let module_id = ctx.sema().module_to_def(ctx.file_id().into(), ast_module)?; + let (module, module_src_map) = ctx.sema().db.module_with_source_map(module_id); + let Ports::NonAnsi { ports, refs, .. } = &module.ports else { + return None; + }; + + let mut port_names = Vec::new(); + for (_, port) in ports.iter() { + let mut ref_ids = port.refs.clone()?; + let ref_id = ref_ids.next()?; + if ref_ids.next().is_some() { + return None; + } + + let port_ref = &refs[ref_id]; + if port_ref.select.is_some() { + return None; + } + + let ident = port_ref.ident.as_ref()?; + if port.label.as_ref() != Some(ident) { + return None; + } + port_names.push(ident.clone()); + } + if port_names.is_empty() { + return None; + } + + let open_paren = port_list.open_paren()?.text_range_in(port_list.syntax())?; + let close_paren = port_list.close_paren()?.text_range_in(port_list.syntax())?; + if !port_list_trigger_range(open_paren, close_paren)?.contains_range(ctx.range()) { + return None; + } + + let body_range = module_body_range(ast_module)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let module_scope = ctx.sema().db.module_scope(module_id); + let port_replacements = port_names + .iter() + .map(|name| { + non_ansi_port_replacement(ctx, &module, &module_src_map, &module_scope, name, &text) + }) + .collect::>>()?; + let ansi_items = port_replacements + .iter() + .map(|replacement| replacement.ansi_item.clone()) + .collect::>(); + let removed_ranges = port_replacements + .into_iter() + .flat_map(|replacement| replacement.remove_ranges) + .collect::>(); + let port_list_replacement = render_port_list(&text, open_paren, close_paren, &ansi_items)?; + let body_replacement = render_module_body(&text, ast_module, body_range, &[], &removed_ranges)?; + let target = port_list.syntax().text_range()?; + + collector.add(NON_ANSI_TO_ANSI_ID, NON_ANSI_TO_ANSI_LABEL, target, |builder| { + builder.replace(target, port_list_replacement); + builder.replace(body_range, body_replacement); + }) +} + +fn port_list_trigger_range(open: TextRange, close: TextRange) -> Option { + (open.end() <= close.start()).then(|| TextRange::new(open.end(), close.start())) +} + +fn port_decl_declared_name(module: &Module, port_decl: &PortDecl) -> Option { + let decl_id = single_port_decl_id(port_decl)?; + Some(module.get(decl_id).name.as_ref()?.to_string()) +} + +fn single_port_decl_id(port_decl: &PortDecl) -> Option { + let mut decls = port_decl.decls.clone(); + let decl_id = decls.next()?; + if decls.next().is_some() { + return None; + } + Some(decl_id) +} + +struct NonAnsiPortReplacement { + ansi_item: String, + remove_ranges: Vec, +} + +fn non_ansi_port_replacement( + ctx: &CodeActionCtx, + module: &Module, + module_src_map: &ModuleSourceMap, + module_scope: &ModuleScope, + name: &Ident, + text: &str, +) -> Option { + let ModuleEntry::NonAnsiPortEntry(NonAnsiPortEntry { + port_decl: Some(port_decl), + data_decl, + .. + }) = module_scope.get(name)? + else { + return None; + }; + let DeclaratorParent::PortDeclId(port_decl_id) = module.get(port_decl).parent else { + return None; + }; + let port_decl = module.get(port_decl_id); + if port_decl_declared_name(module, port_decl).as_deref() != Some(name.as_str()) { + return None; + } + + let port_src = module_src_map.port_srcs.get(port_decl_id)?; + let PortDeclSrc::PortDeclaration(_) = port_src else { + return None; + }; + let port_range = port_src.range(); + + if let Some(data_decl) = data_decl { + let data_range = data_decl_range_for_name(module, module_src_map, data_decl, name)?; + let direction = port_decl.header.dir().display_source(ctx.sema().db).ok()?; + let data_decl = declaration_text_without_semicolon(text, data_range)?; + return Some(NonAnsiPortReplacement { + ansi_item: format!("{direction} {data_decl}"), + remove_ranges: vec![port_range, data_range], + }); + } + + Some(NonAnsiPortReplacement { + ansi_item: declaration_text_without_semicolon(text, port_range)?, + remove_ranges: vec![port_range], + }) +} + +fn data_decl_range_for_name( + module: &Module, + module_src_map: &ModuleSourceMap, + decl_id: DeclId, + name: &Ident, +) -> Option { + let decl = module.get(decl_id); + if decl.name.as_ref() != Some(name) { + return None; + } + + let DeclaratorParent::DeclarationId(declaration_id) = decl.parent else { + return None; + }; + let declaration = module.get(declaration_id); + let mut decls = declaration.decls(); + let single_decl_id = decls.next()?; + if single_decl_id != decl_id || decls.next().is_some() { + return None; + } + + let src = module_src_map.declaration_srcs.get(declaration_id)?; + match src { + DeclarationSrc::DataDeclaration(_) | DeclarationSrc::NetDeclaration(_) => Some(src.range()), + _ => None, + } +} + +fn render_ansi_port_declaration( + ctx: &CodeActionCtx, + module_id: ModuleId, + port_decl: &PortDecl, + src: PortDeclSrc, + text: &str, +) -> Option { + let source = text.get(Range::from(src.range()))?; + if source + .split_ascii_whitespace() + .next() + .is_some_and(|word| matches!(word, "input" | "output" | "inout" | "ref")) + { + return Some(format!("{source};")); + } + + let decl_id = single_port_decl_id(port_decl)?; + let header = InModule::new(module_id, port_decl.header).display_source(ctx.sema().db).ok()?; + let decl = InContainer::new(module_id.into(), decl_id).display_signature(ctx.sema().db).ok()?; + + if header.is_empty() { Some(format!("{decl};")) } else { Some(format!("{header} {decl};")) } +} + +fn declaration_text_without_semicolon(text: &str, range: TextRange) -> Option { + Some(text.get(Range::from(range))?.strip_suffix(';')?.to_owned()) +} + +fn module_body_range(module: ast::ModuleDeclaration<'_>) -> Option { + let header = module.header(); + Some(TextRange::new( + header.semi()?.text_range_in(header.syntax())?.end(), + module.endmodule()?.text_range_in(module.syntax())?.start(), + )) +} + +fn render_port_list( + text: &str, + open: TextRange, + close: TextRange, + items: &[String], +) -> Option { + let content = text.get(usize::from(open.end())..usize::from(close.start()))?; + if content.contains('\n') { + let close_indent = line_indent(text, close.start()); + let item_indent = format!("{close_indent} "); + let rendered = items + .iter() + .enumerate() + .map(|(idx, item)| { + let suffix = if idx + 1 == items.len() { "" } else { "," }; + format!("{item_indent}{item}{suffix}") + }) + .collect::>() + .join("\n"); + Some(format!("(\n{rendered}\n{close_indent})")) + } else { + Some(format!("({})", items.join(", "))) + } +} + +fn render_module_body( + text: &str, + module: ast::ModuleDeclaration<'_>, + body_range: TextRange, + prefix_items: &[String], + remove_ranges: &[TextRange], +) -> Option { + let mut items = prefix_items.to_vec(); + let mut body = text.get(Range::from(body_range))?.to_owned(); + remove_ranges_from_body(&mut body, body_range, remove_ranges)?; + let body = body.trim(); + if !body.is_empty() { + items.push(body.to_owned()); + } + + let endmodule = module.endmodule()?.text_range_in(module.syntax())?; + let module_indent = line_indent(text, endmodule.start()); + if items.is_empty() { + return Some(format!("\n{module_indent}")); + } + + let item_indent = format!("{module_indent} "); + let rendered = items + .into_iter() + .map(|item| indent_block(&item, &item_indent)) + .collect::>() + .join("\n"); + Some(format!("\n{rendered}\n{module_indent}")) +} + +fn remove_ranges_from_body( + body: &mut String, + body_range: TextRange, + remove_ranges: &[TextRange], +) -> Option<()> { + let body_start = usize::from(body_range.start()); + let body_end = usize::from(body_range.end()); + let mut ranges = remove_ranges + .iter() + .filter(|range| body_range.contains_range(**range)) + .map(|range| { + Some(( + usize::from(range.start()).checked_sub(body_start)?, + usize::from(range.end()).checked_sub(body_start)?, + )) + }) + .collect::>>()?; + + ranges.sort_by_key(|(start, _)| *start); + for (start, end) in ranges.into_iter().rev() { + if start > end || body_start + end > body_end { + return None; + } + body.replace_range(start..end, ""); + } + Some(()) +} + +fn indent_block(text: &str, indent: &str) -> String { + text.lines().map(|line| format!("{indent}{line}")).join("\n") +} diff --git a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs index e2275ee8..61e2feae 100644 --- a/crates/ide/src/code_action/handlers/expand_compound_assignment.rs +++ b/crates/ide/src/code_action/handlers/expand_compound_assignment.rs @@ -21,6 +21,18 @@ const COLLAPSE_ID: CodeActionId = CodeActionId { }; const COLLAPSE_LABEL: &str = "Collapse compound assignment"; +// Assist: expand_compound_assignment +// +// This expands compound assignments, or collapses simple self-assignments into +// compound assignments. +// +// ``` +// always_comb a $0+= b; +// ``` +// -> +// ``` +// always_comb a = a + b; +// ``` pub(super) fn expand_compound_assignment( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs index 46619c77..307ee17a 100644 --- a/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs +++ b/crates/ide/src/code_action/handlers/expand_postfix_inc_dec.rs @@ -78,6 +78,18 @@ const ASSIGNMENT_TO_PREFIX_ID: CodeActionId = CodeActionId { }; const ASSIGNMENT_TO_PREFIX_LABEL: &str = "Convert assignment to prefix expression"; +// Assist: expand_postfix_inc_dec +// +// This converts between postfix, prefix, compound assignment, and expanded +// assignment forms of increment/decrement expressions. +// +// ``` +// always_ff @(posedge clk) count$0++; +// ``` +// -> +// ``` +// always_ff @(posedge clk) count = count + 1; +// ``` pub(super) fn expand_postfix_inc_dec( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/extract_variable.rs b/crates/ide/src/code_action/handlers/extract_variable.rs new file mode 100644 index 00000000..1b260627 --- /dev/null +++ b/crates/ide/src/code_action/handlers/extract_variable.rs @@ -0,0 +1,232 @@ +use std::ops::Range; + +use hir::{ + base_db::source_db::SourceDb, + container::InContainer, + display::HirDisplay, + type_infer::{BuiltinTy, Ty, type_of_expr, type_of_path_resolution}, +}; +use syntax::{ + SyntaxAncestors, SyntaxKind, TokenKind, WalkEvent, + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::{ + get::GetRef, + text_edit::{TextRange, TextSize}, +}; + +use crate::code_action::{ + CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, line_indent, +}; + +const ID: CodeActionId = + CodeActionId { name: "extract_variable", kind: CodeActionKind::RefactorExtract, repair: None }; + +// Assist: extract_variable +// +// This extracts a selected expression into a new local variable or continuous +// net declaration. +// +// ``` +// always_comb begin y = $0a + b$0; end +// ``` +// -> +// ``` +// always_comb begin logic value = a + b; +// y = value; end +// ``` +pub(super) fn extract_variable( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let text = ctx.sema().db.file_text(ctx.file_id()); + let expr = selected_expression(ctx, &text)?; + let expr_range = expr.syntax().text_range()?; + let target = extract_target(&text, expr)?; + let expr_text = text.get(Range::from(expr_range))?.trim().to_owned(); + let name = fresh_variable_name(&text, "value"); + + collector.add(ID, "Extract into variable", expr_range, |builder| { + let ty_text = extracted_variable_type(ctx, expr).unwrap_or_else(|| "logic".to_owned()); + let declaration = target.declaration(&ty_text, &name, &expr_text); + builder.insert(target.insert_offset, declaration); + builder.replace(expr_range, name); + }) +} + +struct ExtractTarget { + insert_offset: TextSize, + indent: String, + declaration_style: DeclarationStyle, +} + +impl ExtractTarget { + fn declaration(&self, ty_text: &str, name: &str, expr_text: &str) -> String { + match self.declaration_style { + DeclarationStyle::Local => { + format!("{}{ty_text} {name} = {expr_text};\n", self.indent) + } + DeclarationStyle::ContinuousNet => { + format!("{}wire {ty_text} {name} = {expr_text};\n", self.indent) + } + } + } +} + +enum DeclarationStyle { + Local, + ContinuousNet, +} + +fn extract_target(text: &str, expr: ast::Expression<'_>) -> Option { + if let Some(stmt) = + SyntaxAncestors::start_from(expr.syntax()).find_map(ast::ExpressionStatement::cast) + && stmt.syntax().parent().and_then(ast::BlockStatement::cast).is_some() + { + let stmt_range = stmt.syntax().text_range()?; + return Some(ExtractTarget { + insert_offset: stmt_range.start(), + indent: line_indent(text, stmt_range.start()), + declaration_style: DeclarationStyle::Local, + }); + } + + let assign = + SyntaxAncestors::start_from(expr.syntax()).find_map(ast::ContinuousAssign::cast)?; + expression_is_assignment_rhs(expr)?; + let assign_range = assign.syntax().text_range()?; + Some(ExtractTarget { + insert_offset: assign_range.start(), + indent: line_indent(text, assign_range.start()), + declaration_style: DeclarationStyle::ContinuousNet, + }) +} + +fn expression_is_assignment_rhs(expr: ast::Expression<'_>) -> Option<()> { + assignment_expression_containing_rhs(expr) + .filter(|binary| { + binary.operator_token().is_some_and(|token| token.kind() == TokenKind::EQUALS) + }) + .map(|_| ()) +} + +fn assignment_expression_containing_rhs( + expr: ast::Expression<'_>, +) -> Option> { + let expr_range = expr.syntax().text_range()?; + SyntaxAncestors::start_from(expr.syntax()).filter_map(ast::BinaryExpression::cast).find( + |binary| { + is_assignment_expression(binary.syntax().kind()) + && binary + .right() + .syntax() + .text_range() + .is_some_and(|range| range.contains_range(expr_range)) + }, + ) +} + +fn is_assignment_expression(kind: SyntaxKind) -> bool { + matches!( + kind, + SyntaxKind::ASSIGNMENT_EXPRESSION + | SyntaxKind::NONBLOCKING_ASSIGNMENT_EXPRESSION + | SyntaxKind::ADD_ASSIGNMENT_EXPRESSION + | SyntaxKind::SUBTRACT_ASSIGNMENT_EXPRESSION + | SyntaxKind::MULTIPLY_ASSIGNMENT_EXPRESSION + | SyntaxKind::DIVIDE_ASSIGNMENT_EXPRESSION + | SyntaxKind::MOD_ASSIGNMENT_EXPRESSION + | SyntaxKind::AND_ASSIGNMENT_EXPRESSION + | SyntaxKind::OR_ASSIGNMENT_EXPRESSION + | SyntaxKind::XOR_ASSIGNMENT_EXPRESSION + | SyntaxKind::LOGICAL_LEFT_SHIFT_ASSIGNMENT_EXPRESSION + | SyntaxKind::LOGICAL_RIGHT_SHIFT_ASSIGNMENT_EXPRESSION + | SyntaxKind::ARITHMETIC_LEFT_SHIFT_ASSIGNMENT_EXPRESSION + | SyntaxKind::ARITHMETIC_RIGHT_SHIFT_ASSIGNMENT_EXPRESSION + ) +} + +fn selected_expression<'a>(ctx: &'a CodeActionCtx<'_>, text: &str) -> Option> { + let range = trim_range(text, ctx.range())?; + if range.is_empty() { + return None; + } + + ctx.syntax().node_preorder().find_map(|event| match event { + WalkEvent::Enter(node) => { + let expr = ast::Expression::cast(node)?; + (expr.syntax().text_range()? == range).then_some(expr) + } + WalkEvent::Leave(_) => None, + }) +} + +fn trim_range(text: &str, range: TextRange) -> Option { + let selected = text.get(Range::::from(range))?; + let trimmed_start = selected.trim_start(); + let trimmed = trimmed_start.trim_end(); + + let leading = selected.len() - trimmed_start.len(); + let trailing = trimmed_start.len() - trimmed.len(); + Some(TextRange::new( + range.start() + TextSize::from(leading as u32), + range.end() - TextSize::from(trailing as u32), + )) +} + +fn extracted_variable_type(ctx: &CodeActionCtx<'_>, expr: ast::Expression<'_>) -> Option { + let ty = type_of_expr(ctx.sema().db, ctx.sema().resolve_expr(ctx.file_id().into(), expr)?).ty; + render_ty(ctx, &ty) + .or_else(|| expected_type_for_assignment_rhs(ctx, expr).and_then(|ty| render_ty(ctx, &ty))) +} + +fn expected_type_for_assignment_rhs( + ctx: &CodeActionCtx<'_>, + expr: ast::Expression<'_>, +) -> Option { + let assignment = assignment_expression_containing_rhs(expr)?; + let res = ctx + .sema() + .expr_to_def(ctx.sema().resolve_expr(ctx.file_id().into(), assignment.left())?)?; + Some(type_of_path_resolution(ctx.sema().db, res).ty) +} + +fn render_ty(ctx: &CodeActionCtx<'_>, ty: &Ty) -> Option { + match ty { + Ty::Builtin(BuiltinTy::Data { id, container }) => { + InContainer::new(*container, hir::hir_def::expr::data_ty::DataTy::Builtin(*id)) + .display_source(ctx.sema().db) + .ok() + } + Ty::Alias { typedef, .. } => { + let container = typedef.cont_id.to_container(ctx.sema().db); + container.get(typedef.value).name.as_ref().map(ToString::to_string) + } + Ty::Struct(struct_ref) => { + let container = struct_ref.cont_id.to_container(ctx.sema().db); + container.get(struct_ref.value).name.as_ref().map(ToString::to_string) + } + Ty::Unknown + | Ty::Error + | Ty::Void + | Ty::Module(_) + | Ty::GenerateBlock(_) + | Ty::Block(_) => None, + } +} + +fn fresh_variable_name(text: &str, base: &str) -> String { + if !text.contains(base) { + return base.to_owned(); + } + + let mut idx = 1usize; + loop { + let candidate = format!("{base}_{idx}"); + if !text.contains(&candidate) { + return candidate; + } + idx += 1; + } +} diff --git a/crates/ide/src/code_action/handlers/insert_expected_token.rs b/crates/ide/src/code_action/handlers/insert_expected_token.rs index 3d213b55..64be8952 100644 --- a/crates/ide/src/code_action/handlers/insert_expected_token.rs +++ b/crates/ide/src/code_action/handlers/insert_expected_token.rs @@ -11,6 +11,17 @@ const ID: CodeActionId = CodeActionId { repair: Some(RepairKind::InsertExpectedToken), }; +// Assist: insert_expected_token +// +// This inserts a token that the parser expected at the diagnostic location. +// +// ``` +// module top$0 endmodule +// ``` +// -> +// ``` +// module top; endmodule +// ``` pub(super) fn insert_expected_token( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/invert_if_else.rs b/crates/ide/src/code_action/handlers/invert_if_else.rs index 0dfb2873..de3876ea 100644 --- a/crates/ide/src/code_action/handlers/invert_if_else.rs +++ b/crates/ide/src/code_action/handlers/invert_if_else.rs @@ -12,6 +12,18 @@ const ID: CodeActionId = CodeActionId { name: "invert_if_else", kind: CodeActionKind::RefactorRewrite, repair: None }; const LABEL: &str = "Invert if/else"; +// Assist: invert_if_else +// +// This swaps the then and else branches of an if statement and negates the +// condition. +// +// ``` +// always_comb if$0 (ready) y = a; else y = b; +// ``` +// -> +// ``` +// always_comb if (!(ready)) y = b; else y = a; +// ``` pub(super) fn invert_if_else( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/merge_nested_if.rs b/crates/ide/src/code_action/handlers/merge_nested_if.rs new file mode 100644 index 00000000..061b8787 --- /dev/null +++ b/crates/ide/src/code_action/handlers/merge_nested_if.rs @@ -0,0 +1,138 @@ +use std::{borrow::Cow, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = + CodeActionId { name: "merge_nested_if", kind: CodeActionKind::RefactorRewrite, repair: None }; + +// Assist: merge_nested_if +// +// This merges nested if statements without else branches into one if statement +// with a combined condition. +// +// ``` +// always_comb if$0 (a) begin if (b) y = 1; end +// ``` +// -> +// ``` +// always_comb if (a && b) y = 1; +// ``` +pub(super) fn merge_nested_if( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let current_if = ctx.find_node_at_offset::()?; + if !in_if_head(current_if, ctx.range()) || current_if.else_clause().is_some() { + return None; + } + + let outer_if = outermost_mergeable_if(current_if); + let chain = nested_if_chain(outer_if); + if chain.len() < 2 { + return None; + } + + let innermost_if = *chain.last()?; + let innermost_body_stmt = single_statement_body(innermost_if.statement())?; + + let text = ctx.sema().db.file_text(ctx.file_id()); + let predicates = chain + .iter() + .map(|if_stmt| { + let range = if_stmt.predicate().syntax().text_range()?; + let predicate = text.get(Range::from(range))?.trim(); + if predicate.contains("||") || predicate.contains('?') { + Some(Cow::Owned(format!("({predicate})"))) + } else { + Some(Cow::Borrowed(predicate)) + } + }) + .collect::>>()?; + + let outer_pred_range = outer_if.predicate().syntax().text_range()?; + let outer_body_range = outer_if.statement().syntax().text_range()?; + + let innermost_body_range = innermost_body_stmt.syntax().text_range()?; + let innermost_body = text.get(Range::from(innermost_body_range))?.trim().to_owned(); + + collector.add(ID, "Merge nested if", outer_if.syntax().text_range()?, |builder| { + let merged_predicate = predicates.join(" && "); + builder.replace(outer_pred_range, merged_predicate); + builder.replace(outer_body_range, innermost_body); + }) +} + +fn in_if_head(if_stmt: ast::ConditionalStatement<'_>, range: TextRange) -> bool { + let Some(if_range) = if_stmt.syntax().text_range() else { return false }; + let Some(pred_range) = if_stmt.predicate().syntax().text_range() else { return false }; + TextRange::new(if_range.start(), pred_range.end()).contains_range(range) +} + +fn outermost_mergeable_if<'a>( + mut if_stmt: ast::ConditionalStatement<'a>, +) -> ast::ConditionalStatement<'a> { + while let Some(parent_if) = parent_conditional_statement(if_stmt) { + if parent_if.else_clause().is_some() { + break; + } + let Some(body) = single_statement_body(parent_if.statement()) else { break }; + let Some(body_stmt) = body.as_conditional_statement() else { break }; + if body_stmt.syntax() != if_stmt.syntax() { + break; + } + if_stmt = parent_if; + } + + if_stmt +} + +fn parent_conditional_statement<'a>( + if_stmt: ast::ConditionalStatement<'a>, +) -> Option> { + let mut parent = if_stmt.syntax().parent(); + while let Some(node) = parent { + if let Some(parent_if) = ast::ConditionalStatement::cast(node) { + return Some(parent_if); + } + parent = node.parent(); + } + None +} + +fn nested_if_chain<'a>( + outer_if: ast::ConditionalStatement<'a>, +) -> Vec> { + let mut chain = vec![outer_if]; + let mut current_if = outer_if; + while let Some(body) = single_statement_body(current_if.statement()) { + let Some(nested_if) = body.as_conditional_statement() else { + break; + }; + if nested_if.else_clause().is_some() { + break; + } + chain.push(nested_if); + current_if = nested_if; + } + chain +} + +fn single_statement_body(stmt: ast::Statement<'_>) -> Option> { + let Some(block) = stmt.as_block_statement() else { + return Some(stmt); + }; + + let mut items = block.items().children(); + let item = items.next()?; + if items.next().is_some() { + return None; + } + ast::Statement::cast(item.syntax()) +} diff --git a/crates/ide/src/code_action/handlers/pull_assignment_up.rs b/crates/ide/src/code_action/handlers/pull_assignment_up.rs new file mode 100644 index 00000000..feb203a5 --- /dev/null +++ b/crates/ide/src/code_action/handlers/pull_assignment_up.rs @@ -0,0 +1,152 @@ +use std::{borrow::Cow, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + TokenKind, + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "pull_assignment_up", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; +const DOWN_ID: CodeActionId = CodeActionId { + name: "pull_assignment_down", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; + +// Assist: pull_assignment_up +// +// This pulls matching assignments out of an if/else chain into a single ternary +// assignment. +// +// ``` +// always_comb if$0 (a) y = 1; else y = 0; +// ``` +// -> +// ``` +// always_comb y = a ? 1 : 0; +// ``` +pub(super) fn pull_assignment_up( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let mut conditional = ctx.find_node_at_offset::()?; + while let Some(parent_if) = conditional + .syntax() + .parent() + .and_then(|node| ast::ElseClause::cast(node)?.syntax().parent()) + .and_then(ast::ConditionalStatement::cast) + { + conditional = parent_if; + } + + let text = ctx.sema().db.file_text(ctx.file_id()); + let (lhs, expr) = conditional_assignment_expression(conditional, &text)?; + + collector.add(ID, "Pull assignment up", conditional.syntax().text_range()?, |builder| { + let replacement = format!("{} = {};", lhs.trim(), expr); + builder.replace(conditional.syntax().text_range().unwrap(), replacement); + }) +} + +// Assist: pull_assignment_down +// +// This expands a ternary assignment into an if/else assignment chain. +// +// ``` +// always_comb $0y = a ? 1 : 0; +// ``` +// -> +// ``` +// always_comb if (a) y = 1; else y = 0; +// ``` +pub(super) fn pull_assignment_down( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let assignment = ctx.find_node_at_offset::()?; + if assignment.operator_token()?.kind() != TokenKind::EQUALS { + return None; + } + + let conditional = assignment.right().as_conditional_expression()?; + let stmt = syntax::SyntaxAncestors::start_from(assignment.syntax()) + .find_map(ast::ExpressionStatement::cast)?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let lhs = text.get(Range::from(assignment.left().syntax().text_range()?))?.trim(); + let replacement = conditional_assignment_statement(conditional, lhs, &text)?; + + collector.add(DOWN_ID, "Pull assignment down", stmt.syntax().text_range()?, |builder| { + builder.replace(stmt.syntax().text_range().unwrap(), replacement); + }) +} + +fn conditional_assignment_expression<'a>( + conditional: ast::ConditionalStatement<'_>, + text: &'a str, +) -> Option<(&'a str, String)> { + let (lhs, then_rhs) = assignment_rhs_text(conditional.statement(), text)?; + + let else_syntax = conditional.else_clause()?.clause().syntax(); + let (else_lhs, else_expr) = if let Some(nested) = ast::ConditionalStatement::cast(else_syntax) { + conditional_assignment_expression(nested, text)? + } else { + let else_stmt = ast::Statement::cast(else_syntax)?; + let (lhs, expr) = assignment_rhs_text(else_stmt, text)?; + (lhs, expr.to_owned()) + }; + + if else_lhs != lhs { + return None; + } + + let predicate: Cow<'a, str> = { + let predicate = + text.get(Range::from(conditional.predicate().syntax().text_range()?))?.trim(); + + if predicate.contains('?') { format!("({predicate})").into() } else { predicate.into() } + }; + Some((lhs, format!("{predicate} ? {then_rhs} : {else_expr}"))) +} + +fn assignment_rhs_text<'a>(stmt: ast::Statement<'_>, text: &'a str) -> Option<(&'a str, &'a str)> { + if let Some(block) = stmt.as_block_statement() { + let item = block.items().only_children()?; + let stmt = ast::Statement::cast(item.syntax())?; + return assignment_rhs_text(stmt, text); + } + + let assignment = stmt.as_expression_statement()?.expr().as_binary_expression()?; + if assignment.operator_token()?.kind() != TokenKind::EQUALS { + return None; + } + + let lhs = text.get(Range::from(assignment.left().syntax().text_range()?))?.trim(); + let rhs = text.get(Range::from(assignment.right().syntax().text_range()?))?.trim(); + Some((lhs, rhs)) +} + +fn conditional_assignment_statement( + conditional: ast::ConditionalExpression<'_>, + lhs: &str, + text: &str, +) -> Option { + let predicate = text.get(Range::from(conditional.predicate().syntax().text_range()?))?.trim(); + let then_expr = expr_text(conditional.left(), text)?; + let else_expr = if let Some(nested) = conditional.right().as_conditional_expression() { + conditional_assignment_statement(nested, lhs, text)? + } else { + format!("{lhs} = {};", expr_text(conditional.right(), text)?) + }; + Some(format!("if ({predicate}) {lhs} = {then_expr}; else {else_expr}")) +} + +fn expr_text<'a>(expr: ast::Expression<'_>, text: &'a str) -> Option<&'a str> { + text.get(Range::from(expr.syntax().text_range()?)).map(str::trim) +} diff --git a/crates/ide/src/code_action/handlers/reformat_number_literal.rs b/crates/ide/src/code_action/handlers/reformat_number_literal.rs new file mode 100644 index 00000000..167477db --- /dev/null +++ b/crates/ide/src/code_action/handlers/reformat_number_literal.rs @@ -0,0 +1,108 @@ +use std::ops::Range; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + ast::{self, AstNode}, + has_text_range::HasTextRange, +}; +use utils::text_edit::TextRange; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "reformat_number_literal", + kind: CodeActionKind::RefactorInline, + repair: None, +}; +const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5; + +// Assist: reformat_number_literal +// +// This adds digit separators to long integer literals or removes existing digit +// separators. +// +// ``` +// localparam int value = 10000$0; +// ``` +// -> +// ``` +// localparam int value = 10_000; +// ``` +pub(super) fn reformat_number_literal( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let text = ctx.sema().db.file_text(ctx.file_id()); + let (raw, prefix, digits, group_size, range) = selected_integer_literal(ctx, &text)?; + + if digits.contains('_') { + let replacement = raw.replace('_', ""); + return collector.add(ID, "Remove digit separators", range, |builder| { + builder.replace(range, replacement); + }); + } + + if digits.chars().count() < MIN_NUMBER_OF_DIGITS_TO_FORMAT { + return None; + } + + let replacement = format!("{}{}", prefix, add_group_separators(digits, group_size)); + let label = format!("Convert {raw} to {replacement}"); + collector.add(ID, label, range, |builder| { + builder.replace(range, replacement); + }) +} + +fn selected_integer_literal<'a>( + ctx: &CodeActionCtx<'_>, + text: &'a str, +) -> Option<(&'a str, &'a str, &'a str, usize, TextRange)> { + if let Some(expr) = ctx.find_node_at_offset::() { + let range = expr.syntax().text_range()?; + let raw = text.get(Range::from(range))?; + return parse_based_literal(raw, range); + } + + let literal = ctx.find_node_at_offset::()?; + let ast::LiteralExpression::IntegerLiteralExpression(integer) = literal else { + return None; + }; + let range = integer.text_range()?; + let raw = text.get(Range::from(range))?; + Some((raw, "", raw, 3, range)) +} + +fn parse_based_literal( + raw: &str, + range: TextRange, +) -> Option<(&str, &str, &str, usize, TextRange)> { + let apostrophe = raw.find('\'')?; + let after_quote = raw.get(apostrophe + 1..)?; + let (sign_len, rest) = match after_quote.as_bytes().first().copied() { + Some(b's' | b'S') => (1usize, after_quote.get(1..)?), + _ => (0usize, after_quote), + }; + let base = rest.as_bytes().first().copied()?; + let group_size = match base.to_ascii_lowercase() { + b'b' => 4, + b'o' => 3, + b'd' => 3, + b'h' => 4, + _ => return None, + }; + let digits_start = apostrophe + 1 + sign_len + 1; + let digits = raw.get(digits_start..)?; + Some((raw, raw.get(..digits_start)?, digits, group_size, range)) +} + +fn add_group_separators(digits: &str, group_size: usize) -> String { + let clean: Vec = digits.chars().filter(|ch| *ch != '_').collect(); + let mut buf = String::with_capacity(clean.len() + clean.len() / group_size); + for (idx, ch) in clean.iter().rev().enumerate() { + if idx != 0 && idx % group_size == 0 { + buf.push('_'); + } + buf.push(*ch); + } + buf.chars().rev().collect() +} diff --git a/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs b/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs index 29a31812..82a2c5f0 100644 --- a/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs +++ b/crates/ide/src/code_action/handlers/remove_empty_port_connections.rs @@ -17,6 +17,17 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Remove empty port connections"; +// Assist: remove_empty_port_connections +// +// This removes empty ordered port connections from an instance port list. +// +// ``` +// child u(a, $0, b); +// ``` +// -> +// ``` +// child u(a, b); +// ``` pub(super) fn remove_empty_port_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/remove_parentheses.rs b/crates/ide/src/code_action/handlers/remove_parentheses.rs new file mode 100644 index 00000000..e6e431da --- /dev/null +++ b/crates/ide/src/code_action/handlers/remove_parentheses.rs @@ -0,0 +1,160 @@ +use std::{cmp::Ordering, ops::Range}; + +use hir::base_db::source_db::SourceDb; +use syntax::{ + SyntaxKind, TokenKind, + ast::{self, AstNode}, + has_text_range::{HasTextRange, HasTextRangeIn}, +}; + +use crate::code_action::{CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind}; + +const ID: CodeActionId = CodeActionId { + name: "remove_parentheses", + kind: CodeActionKind::RefactorRewrite, + repair: None, +}; + +// Assist: remove_parentheses +// +// This removes parentheses when they are redundant for the surrounding +// expression. +// +// ``` +// assign y = $0(a + b) + c; +// ``` +// -> +// ``` +// assign y = a + b + c; +// ``` +pub(super) fn remove_parentheses( + collector: &mut CodeActionCollector, + ctx: &CodeActionCtx, +) -> Option<()> { + let parens = ctx.find_node_at_offset::()?; + let range = parens.syntax().text_range()?; + let left = parens.open_paren()?.text_range_in(parens.syntax())?; + let right = parens.close_paren()?.text_range_in(parens.syntax())?; + if !left.contains_range(ctx.range()) && !right.contains_range(ctx.range()) { + return None; + } + + let expr = parens.expression(); + let parent = parens.syntax().parent()?; + if parentheses_are_required(parens, expr, parent) { + return None; + } + + let expr_range = expr.syntax().text_range()?; + let text = ctx.sema().db.file_text(ctx.file_id()); + let inner = text.get(Range::from(expr_range))?.to_owned(); + collector.add(ID, "Remove redundant parentheses", range, |builder| { + builder.replace(range, inner); + }) +} + +fn parentheses_are_required( + parens: ast::ParenthesizedExpression<'_>, + expr: ast::Expression<'_>, + parent: syntax::SyntaxNode<'_>, +) -> bool { + if ast::ParenthesizedExpression::cast(parent).is_some() { + return false; + } + + if matches!(parent.kind(), SyntaxKind::MEMBER_ACCESS_EXPRESSION | SyntaxKind::SCOPED_NAME) { + return true; + } + + let Some(parent_binary) = ast::BinaryExpression::cast(parent) else { + return ast::Expression::cast(parent).is_some_and(|_| { + expr.as_binary_expression().is_some() || expr.as_conditional_expression().is_some() + }); + }; + let Some(child_binary) = expr.as_binary_expression() else { + return false; + }; + + let (Some(parent_prec), Some(child_prec)) = + (binary_precedence(parent_binary), binary_precedence(child_binary)) + else { + return true; + }; + + match child_prec.cmp(&parent_prec) { + Ordering::Greater => false, + Ordering::Less => true, + Ordering::Equal => { + let same_associative_op = parent_binary + .operator_token() + .zip(child_binary.operator_token()) + .is_some_and(|(parent_op, child_op)| { + parent_op.kind() == child_op.kind() + && associative_binary_operator(parent_op.kind()) + }); + !(parent_binary.left().syntax() == parens.syntax() && same_associative_op) + } + } +} + +fn associative_binary_operator(kind: TokenKind) -> bool { + matches!( + kind, + TokenKind::PLUS + | TokenKind::STAR + | TokenKind::DOUBLE_AND + | TokenKind::DOUBLE_OR + | TokenKind::AND + | TokenKind::OR + | TokenKind::XOR + | TokenKind::TILDE_XOR + | TokenKind::XOR_TILDE + ) +} + +fn binary_precedence(expr: ast::BinaryExpression<'_>) -> Option { + let kind = expr.operator_token()?.kind(); + Some(match kind { + TokenKind::DOUBLE_STAR => 12, + TokenKind::STAR | TokenKind::SLASH | TokenKind::PERCENT => 11, + TokenKind::PLUS | TokenKind::MINUS => 10, + TokenKind::LEFT_SHIFT + | TokenKind::RIGHT_SHIFT + | TokenKind::TRIPLE_LEFT_SHIFT + | TokenKind::TRIPLE_RIGHT_SHIFT => 9, + TokenKind::LESS_THAN_EQUALS + if expr.syntax().kind() == SyntaxKind::NONBLOCKING_ASSIGNMENT_EXPRESSION => + { + 1 + } + TokenKind::GREATER_THAN + | TokenKind::GREATER_THAN_EQUALS + | TokenKind::LESS_THAN + | TokenKind::LESS_THAN_EQUALS => 8, + TokenKind::DOUBLE_EQUALS + | TokenKind::EXCLAMATION_EQUALS + | TokenKind::TRIPLE_EQUALS + | TokenKind::EXCLAMATION_DOUBLE_EQUALS + | TokenKind::DOUBLE_EQUALS_QUESTION + | TokenKind::EXCLAMATION_EQUALS_QUESTION => 7, + TokenKind::AND => 6, + TokenKind::XOR | TokenKind::TILDE_XOR | TokenKind::XOR_TILDE => 5, + TokenKind::OR => 4, + TokenKind::DOUBLE_AND => 3, + TokenKind::DOUBLE_OR => 2, + TokenKind::EQUALS + | TokenKind::PLUS_EQUAL + | TokenKind::MINUS_EQUAL + | TokenKind::STAR_EQUAL + | TokenKind::SLASH_EQUAL + | TokenKind::PERCENT_EQUAL + | TokenKind::AND_EQUAL + | TokenKind::OR_EQUAL + | TokenKind::XOR_EQUAL + | TokenKind::LEFT_SHIFT_EQUAL + | TokenKind::RIGHT_SHIFT_EQUAL + | TokenKind::TRIPLE_LEFT_SHIFT_EQUAL + | TokenKind::TRIPLE_RIGHT_SHIFT_EQUAL => 1, + _ => return None, + }) +} diff --git a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs index 917f7699..9ee0c222 100644 --- a/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs +++ b/crates/ide/src/code_action/handlers/sort_named_instantiation_items.rs @@ -1,21 +1,29 @@ use std::ops::Range; -use hir::{base_db::source_db::SourceDb, db::HirDb}; +use hir::{ + base_db::source_db::SourceDb, + container::InModule, + db::HirDb, + hir_def::module::instantiation::{ParamAssign, PortConn}, + source_map::IsSrc, +}; use itertools::Itertools; use rustc_hash::FxHashMap; -use smol_str::ToSmolStr; use syntax::{ ast::{self, AstNode}, - has_text_range::{HasTextRange, HasTextRangeIn}, + has_text_range::HasTextRangeIn, +}; +use utils::{ + get::{Get, GetRef}, + text_edit::TextRange, }; -use utils::text_edit::TextRange; use crate::{ code_action::{ CodeActionCollector, CodeActionCtx, CodeActionId, CodeActionKind, all_parameter_names, line_indent, port_names, }, - module_resolution::resolve_instantiation_target, + module_resolution::resolve_hir_instantiation_target, }; const SORT_NAMED_PARAMETER_ASSIGNMENTS_ID: CodeActionId = CodeActionId { @@ -25,29 +33,46 @@ const SORT_NAMED_PARAMETER_ASSIGNMENTS_ID: CodeActionId = CodeActionId { }; const SORT_NAMED_PARAMETER_ASSIGNMENTS_LABEL: &str = "Sort named parameter assignments"; +// Assist: sort_named_parameter_assignments +// +// This sorts named parameter assignments to match the target module's parameter +// declaration order. +// +// ``` +// child #(.B(2), $0.A(1)) u(); +// ``` +// -> +// ``` +// child #(.A(1), .B(2)) u(); +// ``` pub(super) fn sort_named_parameter_assignments( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, ) -> Option<()> { - let instantiation = ctx.find_node_at_offset::()?; - let params = instantiation.parameters()?; + let ast_instantiation = ctx.find_node_at_offset::()?; + let params = ast_instantiation.parameters()?; let open = params.open_paren()?.text_range_in(params.syntax())?; let close = params.close_paren()?.text_range_in(params.syntax())?; - let db = ctx.sema().db; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; + let sema = ctx.sema(); + let db = sema.db; + let InModule { value: instantiation_id, module_id } = + sema.resolve_instantiation(ctx.file_id().into(), ast_instantiation)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instantiation = module.get(instantiation_id); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; let parameter_order = all_parameter_names(&db.module(target_module_id)); let parameter_order_map: FxHashMap<_, _> = parameter_order.iter().enumerate().map(|(index, name)| (name.as_ref(), index)).collect(); - let text = ctx.sema().db.file_text(ctx.file_id()); + let text = sema.db.file_text(ctx.file_id()); let mut items = Vec::new(); - for assign in params.parameters().children() { - let named = assign.as_named_param_assignment()?; - let name = named.name()?.value_text().to_smolstr(); + for assign_id in instantiation.param_assigns.iter() { + let ParamAssign::Named(Some(name), _) = module.get(*assign_id) else { + return None; + }; let order = *parameter_order_map.get(name.as_str())?; - let range = assign.syntax().text_range()?; + let range = module_src_map.get(*assign_id)?.range(); items.push((order, text.get(Range::from(range))?, range)); } @@ -69,30 +94,46 @@ const SORT_NAMED_PORT_CONNECTIONS_ID: CodeActionId = CodeActionId { }; const SORT_NAMED_PORT_CONNECTIONS_LABEL: &str = "Sort named port connections"; +// Assist: sort_named_port_connections +// +// This sorts named port connections to match the target module's port +// declaration order. +// +// ``` +// child u(.b(b), $0.a(a)); +// ``` +// -> +// ``` +// child u(.a(a), .b(b)); +// ``` pub(super) fn sort_named_port_connections( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, ) -> Option<()> { - let instance = ctx.find_node_at_offset::()?; - let instantiation = ast::HierarchyInstantiation::cast(instance.syntax().parent()?)?; - let open = instance.open_paren()?.text_range_in(instance.syntax())?; - let close = instance.close_paren()?.text_range_in(instance.syntax())?; - - let db = ctx.sema().db; - let target_module_id = - resolve_instantiation_target(db, ctx.file_id(), instantiation).unique()?; - let target_module = db.module(target_module_id); - let port_order = port_names(&target_module); + let ast_instance = ctx.find_node_at_offset::()?; + let open = ast_instance.open_paren()?.text_range_in(ast_instance.syntax())?; + let close = ast_instance.close_paren()?.text_range_in(ast_instance.syntax())?; + + let sema = ctx.sema(); + let db = sema.db; + let InModule { value: instance_id, module_id } = + sema.resolve_instance(ctx.file_id().into(), ast_instance)?; + let (module, module_src_map) = db.module_with_source_map(module_id); + let instance = module.get(instance_id); + let instantiation = module.get(instance.parent); + let target_module_id = resolve_hir_instantiation_target(db, ctx.file_id(), instantiation)?; + let port_order = port_names(&db.module(target_module_id)); let port_order_map: FxHashMap<_, _> = port_order.iter().enumerate().map(|(index, name)| (name.as_ref(), index)).collect(); - let text = ctx.sema().db.file_text(ctx.file_id()); + let text = sema.db.file_text(ctx.file_id()); let mut items = Vec::new(); - for conn in instance.connections().children() { - let named = conn.as_named_port_connection()?; - let name = named.name()?.value_text().to_smolstr(); + for conn_id in instance.connections.iter() { + let PortConn::Named(Some(name), _) = module.get(*conn_id) else { + return None; + }; let order = *port_order_map.get(name.as_str())?; - let range = conn.syntax().text_range()?; + let range = module_src_map.get(*conn_id)?.range(); items.push((order, text.get(Range::from(range))?, range)); } diff --git a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs index 2963bc71..78fb4a10 100644 --- a/crates/ide/src/code_action/handlers/split_declaration_declarators.rs +++ b/crates/ide/src/code_action/handlers/split_declaration_declarators.rs @@ -19,6 +19,19 @@ const ID: CodeActionId = CodeActionId { }; const LABEL: &str = "Split declaration"; +// Assist: split_declaration_declarators +// +// This splits a declaration with multiple declarators into one declaration per +// declarator. +// +// ``` +// logic $0a, b; +// ``` +// -> +// ``` +// logic a; +// logic b; +// ``` pub(super) fn split_declaration_declarators( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs b/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs index 0c134a1b..4bfe2402 100644 --- a/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs +++ b/crates/ide/src/code_action/handlers/wrap_statement_in_begin_end.rs @@ -18,6 +18,17 @@ const WRAP_ID: CodeActionId = CodeActionId { }; const WRAP_LABEL: &str = "Wrap statement in begin/end"; +// Assist: wrap_statement_in_begin_end +// +// This wraps a control-flow body statement in a `begin`/`end` block. +// +// ``` +// always_comb if (a) $0y = 1; +// ``` +// -> +// ``` +// always_comb if (a) begin y = 1; end +// ``` pub(super) fn wrap_statement_in_begin_end( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, @@ -47,6 +58,17 @@ const UNWRAP_ID: CodeActionId = CodeActionId { }; const UNWRAP_LABEL: &str = "Unwrap single-statement begin/end"; +// Assist: unwrap_single_statement_block +// +// This unwraps a `begin`/`end` block that contains exactly one statement. +// +// ``` +// always_comb if (a) $0begin y = 1; end +// ``` +// -> +// ``` +// always_comb if (a) y = 1; +// ``` pub(super) fn unwrap_single_statement_block( collector: &mut CodeActionCollector, ctx: &CodeActionCtx, diff --git a/crates/ide/src/code_action/tests.rs b/crates/ide/src/code_action/tests.rs index 2f7161ad..1bd3f82b 100644 --- a/crates/ide/src/code_action/tests.rs +++ b/crates/ide/src/code_action/tests.rs @@ -13,6 +13,11 @@ fn db_with_file(text: &str) -> (RootDb, FileId, TextSize) { let marker = "/*caret*/"; let offset = text.find(marker).expect("missing caret marker"); let text = text.replace(marker, ""); + let (db, file_id) = db_with_text(&text); + (db, file_id, TextSize::from(offset as u32)) +} + +fn db_with_text(text: &str) -> (RootDb, FileId) { let file_id = FileId(0); let mut file_set = FileSet::default(); file_set.insert(file_id, VfsPath::new_virtual_path("/test.sv".to_owned())); @@ -21,12 +26,12 @@ fn db_with_file(text: &str) -> (RootDb, FileId, TextSize) { change.set_roots(vec![SourceRoot::new_local(file_set)]); change.add_changed_file(ChangedFile { file_id, - change_kind: ChangeKind::Create(Arc::from(text.as_str()), LineEnding::Unix), + change_kind: ChangeKind::Create(Arc::from(text), LineEnding::Unix), }); let mut db = RootDb::new(None); db.apply_change(change); - (db, file_id, TextSize::from(offset as u32)) + (db, file_id) } fn apply_action(text: &str, repair: RepairKind) -> Option { @@ -90,6 +95,57 @@ fn apply_action_without_diagnostics_by( Some(text) } +fn apply_action_without_diagnostics_with_selection( + text: &str, + action_name: &str, +) -> Option { + apply_action_without_diagnostics_with_selection_by(text, |action| action.id.name == action_name) +} + +fn apply_action_without_diagnostics_with_selection_by( + text: &str, + pred: impl Fn(&CodeAction) -> bool, +) -> Option { + let (mut text, range) = text_with_selection_range(text); + let (db, file_id) = db_with_text(&text); + let actions = code_action( + &db, + file_id, + range, + CodeActionDiagnostics::default(), + CodeActionResolveStrategy::All, + ); + let action = actions.into_iter().find(pred)?; + let edit = action.source_change?.text_edits.remove(&file_id)?; + edit.apply(&mut text); + Some(text) +} + +fn action_labels_without_diagnostics_with_selection(text: &str) -> Vec { + let (text, range) = text_with_selection_range(text); + let (db, file_id) = db_with_text(&text); + code_action( + &db, + file_id, + range, + CodeActionDiagnostics::default(), + CodeActionResolveStrategy::All, + ) + .into_iter() + .map(|action| action.label) + .collect() +} + +fn text_with_selection_range(text: &str) -> (String, TextRange) { + let marker = "/*selection*/"; + let start = text.find(marker).expect("missing selection start marker"); + let text = text.replacen(marker, "", 1); + let end = text.find(marker).expect("missing selection end marker"); + let text = text.replacen(marker, "", 1); + let range = TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32)); + (text, range) +} + fn diagnostic_for_repair(repair: RepairKind) -> CodeActionDiagnostic { match repair { RepairKind::MissingConnection => CodeActionDiagnostic { @@ -317,6 +373,53 @@ fn literal_base_is_not_available_for_string_literals() { assert!(!labels.iter().any(|label| label.starts_with("Convert literal to "))); } +#[test] +fn reformat_number_literal_adds_decimal_separators() { + let text = "module top; localparam int value = /*caret*/10000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Convert 10000 to 10_000", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 10_000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_removes_separators() { + let text = "module top; localparam int value = /*caret*/10_000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Remove digit separators", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 10000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_formats_hex_literals() { + let text = "module top; localparam int value = /*caret*/'hff0000; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_label( + text, + "reformat_number_literal", + "Convert 'hff0000 to 'hff_0000", + ) + .unwrap(); + + assert_eq!(fixed, "module top; localparam int value = 'hff_0000; endmodule\n"); +} + +#[test] +fn reformat_number_literal_requires_enough_digits() { + let labels = action_labels_without_diagnostics( + "module top; localparam int value = /*caret*/999; endmodule\n", + ); + assert!(!labels.iter().any(|label| label.starts_with("Convert 999 to "))); +} + #[test] fn missing_connection_repair_fills_named_connections() { let text = "module child(input a, input b); endmodule\nmodule top; child u(/*caret*/.a()); endmodule\n"; @@ -524,6 +627,139 @@ fn implicit_named_port_repair_is_available_without_diagnostics() { assert_eq!(fixed, "module child(input a); endmodule\nmodule top; child u(.a()); endmodule\n"); } +#[test] +fn named_port_shorthand_expands() { + let text = + "module child(input a); endmodule\nmodule top; logic a; child u(/*caret*/.a); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "expand_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a); endmodule\nmodule top; logic a; child u(.a(a)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_expands_all_named_connections_in_instance() { + let text = "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a, .b); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "expand_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(.a(a), .b(b)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses() { + let text = "module child(input a); endmodule\nmodule top; logic a; child u(/*caret*/.a(a)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a); endmodule\nmodule top; logic a; child u(.a); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses_all_named_connections_in_instance() { + let text = "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a(a), .b(b)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(.a, .b); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapses_matching_connections_in_instance() { + let text = "module child(input a, b, c); endmodule\nmodule top; logic sw1, b, gate_out; child u(/*caret*/.a(sw1), .c(c), .b(gate_out)); endmodule\n"; + let fixed = + apply_action_without_diagnostics(text, "collapse_named_port_connection_shorthand").unwrap(); + assert_eq!( + fixed, + "module child(input a, b, c); endmodule\nmodule top; logic sw1, b, gate_out; child u(.a(sw1), .c, .b(gate_out)); endmodule\n" + ); +} + +#[test] +fn named_port_shorthand_collapse_requires_at_least_one_same_name() { + let labels = action_labels_without_diagnostics( + "module child(input a); endmodule\nmodule top; logic b; child u(/*caret*/.a(b)); endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Collapse named port to shorthand")); +} + +#[test] +fn named_port_shorthand_requires_all_connections_named() { + let labels = action_labels_without_diagnostics( + "module child(input a, b); endmodule\nmodule top; logic a, b; child u(/*caret*/.a, b); endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Expand named port shorthand")); +} + +#[test] +fn convert_always_star_to_always_comb() { + let text = "module top; logic a, y; /*caret*/always @(*) begin y = a; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_comb").unwrap(); + assert_eq!(fixed, "module top; logic a, y; always_comb begin y = a; end endmodule\n"); +} + +#[test] +fn convert_always_comb_to_always_star() { + let text = "module top; logic a, y; /*caret*/always_comb begin y = a; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_comb_to_always").unwrap(); + assert_eq!(fixed, "module top; logic a, y; always @(*) begin y = a; end endmodule\n"); +} + +#[test] +fn convert_always_posedge_to_always_ff() { + let text = "module top; logic clk, d, q; /*caret*/always @(posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_ff").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always_ff @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_event_list_to_always_ff() { + let text = "module top; logic clk, d, q; always @(/*caret*/posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_to_always_ff").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always_ff @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_ff_to_plain_always() { + let text = "module top; logic clk, d, q; /*caret*/always_ff @(posedge clk) q <= d; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_always_ff_to_always").unwrap(); + assert_eq!(fixed, "module top; logic clk, d, q; always @(posedge clk) q <= d; endmodule\n"); +} + +#[test] +fn convert_always_block_requires_caret_on_keyword_or_event_list() { + let labels = action_labels_without_diagnostics( + "module top; logic a, y; always @(*) begin /*caret*/y = a; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always_comb")); + + let labels = action_labels_without_diagnostics( + "module top; logic a, y; always_comb begin /*caret*/y = a; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always @(*)")); + + let labels = action_labels_without_diagnostics( + "module top; logic clk, d, q; always_ff @(posedge clk) /*caret*/q <= d; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always @(...)")); +} + +#[test] +fn convert_always_to_always_ff_requires_edge_sensitivity() { + let labels = action_labels_without_diagnostics( + "module top; logic clk, d, q; /*caret*/always @(clk) q <= d; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert to always_ff")); +} + #[test] fn instance_missing_parens_repair_adds_port_list() { let text = "module child; endmodule\nmodule top; child u/*caret*/; endmodule\n"; @@ -538,6 +774,82 @@ fn instance_missing_parens_repair_requires_diagnostics() { assert!(!labels.iter().any(|label| label == "Add empty instance port list")); } +#[test] +fn convert_ansi_ports_to_non_ansi() { + let text = "module top(/*caret*/input a, output logic b);\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert_eq!( + fixed, + "module top(a, b);\n input a;\n output logic b;\n assign b = a;\nendmodule\n" + ); +} + +#[test] +fn convert_ansi_ports_to_non_ansi_uses_inherited_header() { + let text = "module top(/*caret*/input a, b);\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert_eq!( + fixed, + "module top(a, b);\n input a;\n input wire logic b;\n assign b = a;\nendmodule\n" + ); +} + +#[test] +fn convert_non_ansi_ports_to_ansi() { + let text = + "module top(/*caret*/a, b);\ninput wire a;\noutput logic b;\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert_eq!(fixed, "module top(input wire a, output logic b);\n assign b = a;\nendmodule\n"); +} + +#[test] +fn convert_non_ansi_ports_to_ansi_merges_data_declaration() { + let text = "module top (\n /*caret*/c,\n led0\n);\n input wire c;\n output led0;\n reg led0;\n\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert_eq!(fixed, "module top (\n input wire c,\n output reg led0\n);\nendmodule\n"); +} + +#[test] +fn convert_ansi_ports_to_non_ansi_preserves_body_comments() { + let text = + "module top(/*caret*/input a, output logic b);\n// keep this\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_ansi_ports_to_non_ansi").unwrap(); + assert!(fixed.contains("// keep this"), "{fixed}"); + assert!(fixed.contains("assign b = a;"), "{fixed}"); +} + +#[test] +fn convert_non_ansi_ports_to_ansi_preserves_body_comments() { + let text = "module top(/*caret*/a, b);\n// keep first\ninput wire a;\n// keep second\noutput logic b;\nassign b = a;\nendmodule\n"; + let fixed = apply_action_without_diagnostics(text, "convert_non_ansi_ports_to_ansi").unwrap(); + assert!(fixed.contains("// keep first"), "{fixed}"); + assert!(fixed.contains("// keep second"), "{fixed}"); + assert!(fixed.contains("assign b = a;"), "{fixed}"); +} + +#[test] +fn convert_port_declarations_requires_caret_in_port_list() { + let labels = action_labels_without_diagnostics( + "module /*caret*/top(input a, output logic b);\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert ANSI port declarations to non-ANSI")); + + let labels = action_labels_without_diagnostics( + "module top(input a, output logic b);\n/*caret*/assign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert ANSI port declarations to non-ANSI")); + + let labels = action_labels_without_diagnostics( + "module /*caret*/top(a, b);\ninput wire a;\noutput logic b;\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert non-ANSI port declarations to ANSI")); + + let labels = action_labels_without_diagnostics( + "module top(a, b);\ninput wire a;\n/*caret*/output logic b;\nassign b = a;\nendmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Convert non-ANSI port declarations to ANSI")); +} + #[test] fn split_declaration_declarators_splits_data_declaration() { let text = "module top; /*caret*/logic [3:0] a, b = 4'h0; endmodule\n"; @@ -620,6 +932,232 @@ fn invert_if_else_swaps_branches_and_negates_condition() { assert_eq!(fixed, "module top; always_comb if (!(a)) y = 0; else y = 1; endmodule\n"); } +#[test] +fn remove_parentheses_removes_redundant_binary_parens() { + let text = "module top; assign y = /*caret*/(a + b) + c; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "remove_parentheses").unwrap(); + assert_eq!(fixed, "module top; assign y = a + b + c; endmodule\n"); +} + +#[test] +fn remove_parentheses_keeps_required_parens() { + let labels = action_labels_without_diagnostics( + "module top; assign y = /*caret*/(a + b) * c; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Remove redundant parentheses")); +} + +#[test] +fn remove_parentheses_requires_cursor_on_paren() { + let labels = action_labels_without_diagnostics( + "module top; assign y = (a /*caret*/+ b) + c; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Remove redundant parentheses")); +} + +#[test] +fn merge_nested_if_merges_simple_nested_if() { + let text = "module top; always_comb if (/*caret*/a) begin if (b) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_wraps_or_conditions() { + let text = + "module top; always_comb if (/*caret*/a || b) begin if (c || d) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if ((a || b) && (c || d)) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_merges_multiple_nested_levels() { + let text = "module top; always_comb if (/*caret*/a) begin if (b) begin if (c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_triggers_from_middle_nested_level() { + let text = "module top; always_comb if (a) begin if (/*caret*/b) begin if (c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_triggers_from_innermost_nested_level() { + let text = "module top; always_comb if (a) begin if (b) begin if (/*caret*/c) y = 1; end end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_merges_mixed_block_and_unbraced_levels() { + let text = "module top; always_comb if (a) begin if (/*caret*/b) if (c) y = 1; end endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "merge_nested_if").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a && b && c) y = 1; endmodule\n"); +} + +#[test] +fn merge_nested_if_requires_no_else_branches() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (/*caret*/a) begin if (b) y = 1; else y = 0; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Merge nested if")); +} + +#[test] +fn merge_nested_if_rejects_block_with_declarations() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (/*caret*/a) begin logic tmp; if (b) y = tmp; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Merge nested if")); +} + +#[test] +fn extract_variable_inserts_local_before_statement() { + let text = "module top; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; always_comb begin logic value = a + b;\ny = value; end endmodule\n" + ); +} + +#[test] +fn extract_variable_allows_selection_padding() { + let text = + "module top; always_comb begin y =/*selection*/ a + b /*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; always_comb begin logic value = a + b;\ny = value ; end endmodule\n" + ); +} + +#[test] +fn extract_variable_uses_assignment_lhs_type() { + let text = "module top; logic [7:0] y, a, b; always_comb begin y = /*selection*/a + b/*selection*/; end endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; logic [7:0] y, a, b; always_comb begin logic [7:0] value = a + b;\ny = value; end endmodule\n" + ); +} + +#[test] +fn extract_variable_from_continuous_assign() { + let text = "module top; assign y = /*selection*/a + b/*selection*/; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!(fixed, "module top; wire logic value = a + b;\nassign y = value; endmodule\n"); +} + +#[test] +fn extract_variable_uses_continuous_assign_lhs_type() { + let text = + "module top; logic [7:0] y, a, b; assign y = /*selection*/a + b/*selection*/; endmodule\n"; + let fixed = apply_action_without_diagnostics_with_selection(text, "extract_variable").unwrap(); + assert_eq!( + fixed, + "module top; logic [7:0] y, a, b; wire logic [7:0] value = a + b;\nassign y = value; endmodule\n" + ); +} + +#[test] +fn extract_variable_requires_selection() { + let labels = action_labels_without_diagnostics( + "module top; always_comb begin y = a /*caret*/+ b; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_requires_complete_expression_selection() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; always_comb begin y = a /*selection*/+/*selection*/ b; end endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_rejects_continuous_assign_lhs() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; assign /*selection*/y/*selection*/ = a + b; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn extract_variable_requires_block_scope() { + let labels = action_labels_without_diagnostics_with_selection( + "module top; always_comb if (a) y = /*selection*/b + c/*selection*/; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Extract into variable")); +} + +#[test] +fn pull_assignment_up_converts_if_else_assignment_to_ternary() { + let text = "module top; always_comb /*caret*/if (a) y = 1; else y = 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : 0; endmodule\n"); +} + +#[test] +fn pull_assignment_up_converts_else_if_chain_to_nested_ternary() { + let text = + "module top; always_comb if (/*caret*/a) y = 1; else if (b) y = 2; else y = 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : b ? 2 : 3; endmodule\n"); +} + +#[test] +fn pull_assignment_up_triggers_from_else_if_chain_body() { + let text = + "module top; always_comb if (a) y = 1; else if (b) /*caret*/y = 2; else y = 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = a ? 1 : b ? 2 : 3; endmodule\n"); +} + +#[test] +fn pull_assignment_up_wraps_conditional_predicate() { + let text = "module top; always_comb if (a ? b : c) /*caret*/y = 1; else y = 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_up").unwrap(); + assert_eq!(fixed, "module top; always_comb y = (a ? b : c) ? 1 : 0; endmodule\n"); +} + +#[test] +fn pull_assignment_up_requires_single_assignment_branches() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (a) begin /*caret*/y = 1; z = 0; end else y = 2; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Pull assignment up")); +} + +#[test] +fn pull_assignment_up_rejects_block_with_declarations() { + let labels = action_labels_without_diagnostics( + "module top; always_comb if (a) begin logic tmp; /*caret*/y = tmp; end else y = 0; endmodule\n", + ); + assert!(!labels.iter().any(|label| label == "Pull assignment up")); +} + +#[test] +fn pull_assignment_down_converts_ternary_assignment_to_if_else() { + let text = "module top; always_comb /*caret*/y = a ? 1 : 0; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_down").unwrap(); + assert_eq!(fixed, "module top; always_comb if (a) y = 1; else y = 0; endmodule\n"); +} + +#[test] +fn pull_assignment_down_converts_nested_ternary_to_else_if_chain() { + let text = "module top; always_comb /*caret*/y = a ? 1 : b ? 2 : 3; endmodule\n"; + let fixed = apply_action_without_diagnostics(text, "pull_assignment_down").unwrap(); + assert_eq!( + fixed, + "module top; always_comb if (a) y = 1; else if (b) y = 2; else y = 3; endmodule\n" + ); +} + #[test] fn unwrap_single_statement_block_unwraps_single_statement() { let text = "module top; always_comb if (a) /*caret*/begin y = 1; end endmodule\n"; diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 7193e2a0..2d5ce57e 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -334,10 +334,24 @@ 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(); + 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) { ModuleResolution::Ambiguous { candidates, kind } => { @@ -348,7 +362,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 +380,23 @@ 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::Builtin { call, .. } => { + Some((call.file_id, call.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(); @@ -442,7 +473,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; @@ -456,7 +487,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; @@ -469,7 +501,7 @@ mod tests { files, SourceRootRole::Local, true, - PreprocessConfig { predefines, ..PreprocessConfig::default() }, + PreprocessConfig::with_predefine_strings(predefines, Vec::new()), ) } @@ -480,6 +512,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, @@ -597,6 +635,95 @@ 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 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( @@ -724,6 +851,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); @@ -779,7 +954,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); @@ -896,8 +1074,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/document_highlight.rs b/crates/ide/src/document_highlight.rs index 0f0e05f3..af5b92e6 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,21 @@ 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 tokens = crate::source_tokens::source_token_resolution_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )? + .resolved()? + .into_tokens(); + let highlights = 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 +73,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/document_symbols.rs b/crates/ide/src/document_symbols.rs index 3a251c54..92dfb220 100644 --- a/crates/ide/src/document_symbols.rs +++ b/crates/ide/src/document_symbols.rs @@ -7,6 +7,7 @@ use hir::{ file::HirFileId, hir_def::{ DEFAULT_NAME, + aggregate::{StructDef, StructId, StructKind, StructSrc}, block::{BlockId, BlockInfo, BlockItem, BlockSrc, LocalBlockId}, declaration::{Declaration, DeclarationId, DeclarationSrc}, expr::declarator::{DeclId, Declarator, DeclaratorSrc, DeclsRange}, @@ -224,9 +225,7 @@ pub(crate) fn document_symbols(db: &dyn HirDb, file_id: FileId) -> Vec { build_subroutine(&mut collector, subroutine_id, file, src_map) } - FileItem::StructId(_) => { - // TODO: implement document symbols for these items - } + FileItem::StructId(struct_id) => build_struct(&mut collector, struct_id, file, src_map), FileItem::ConfigDeclId(config_id) => { build_config_decl(&mut collector, config_id, file, src_map) } @@ -328,9 +327,7 @@ fn collect_module_items( ModuleItem::SubroutineId(subroutine_id) => { build_subroutine(collector, subroutine_id, module, src_map) } - ModuleItem::StructId(_) => { - // TODO: implement document symbols for these items - } + ModuleItem::StructId(struct_id) => build_struct(collector, struct_id, module, src_map), } } collector.pop(); @@ -365,9 +362,7 @@ fn collect_block_items( BlockItem::TypedefId(typedef_id) => { build_typedef(collector, typedef_id, block, src_map) } - BlockItem::StructId(_) => { - // TODO: implement document symbols for these items - } + BlockItem::StructId(struct_id) => build_struct(collector, struct_id, block, src_map), } } collector.pop(); @@ -489,6 +484,7 @@ fn build_generate_region( + GetRef + GetRef + GetRef + + GetRef + GetRef, SrcMap: Get> + Get> @@ -497,6 +493,7 @@ fn build_generate_region( + Get> + Get> + Get> + + Get> + Get>, { let hir = arena.get(generate_region_id); @@ -527,7 +524,9 @@ fn build_generate_region( let proc = arena.get(proc_id); build_stmt(db, collector, proc.stmt, arena, src_map); } - GenerateItem::StructId(_) => {} + GenerateItem::StructId(struct_id) => { + build_struct(collector, struct_id, arena, src_map); + } GenerateItem::SubroutineId(subroutine_id) => { build_subroutine(collector, subroutine_id, arena, src_map); } @@ -578,14 +577,43 @@ fn build_generate_block( } } } - GenerateBlockItem::ContAssignId(_) - | GenerateBlockItem::DefParamId(_) - | GenerateBlockItem::StructId(_) => {} + GenerateBlockItem::ContAssignId(_) | GenerateBlockItem::DefParamId(_) => {} + GenerateBlockItem::StructId(struct_id) => { + build_struct(collector, struct_id, generate_block, src_map); + } } } collector.pop(); } +#[inline] +fn build_struct( + collector: &mut SymbolCollecter, + struct_id: Idx, + arena: &Arn, + src_map: &SrcMap, +) where + Arn: GetRef, + SrcMap: Get>, +{ + let hir = arena.get(struct_id); + let Some(src) = src_map.get(struct_id) else { + return; + }; + + let name = hir.name.clone().or_else(|| Some(struct_kind_name(hir.kind))); + collector.push_symbol_with_kind(&name, src, SymbolKind::Struct); + collector.pop(); +} + +#[inline] +fn struct_kind_name(kind: StructKind) -> SmolStr { + match kind { + StructKind::Struct => SmolStr::new_static("struct"), + StructKind::Union => SmolStr::new_static("union"), + } +} + #[inline] fn build_specify_block( collector: &mut SymbolCollecter, @@ -666,7 +694,11 @@ fn build_typedef( let Some(src) = src_map.get(typedef_id) else { return; }; - collector.push_symbol_with_kind(&hir.name, src, SymbolKind::Typedef); + let kind = match hir.ty { + Some(hir::hir_def::expr::data_ty::DataTy::Struct(_)) => SymbolKind::Struct, + _ => SymbolKind::Typedef, + }; + collector.push_symbol_with_kind(&hir.name, src, kind); collector.pop(); } diff --git a/crates/ide/src/goto_declaration.rs b/crates/ide/src/goto_declaration.rs index 7bdaf711..ec696e1f 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, @@ -18,22 +17,36 @@ 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 selection = crate::source_tokens::source_token_resolution_at_offset( + db, + file_id, + root, + offset, + goto_definition::token_precedence, + )? + .resolved()?; + let (range, tokens) = selection.into_parts(); - let origins = match DefinitionClass::resolve(&sema, hir_file_id, token)? { - DefinitionClass::Definition(definition) => { - definition.declaration_origins().into_iter().collect_vec() - } - DefinitionClass::PortConnShorthand { port, .. } => { - port.declaration_origins().into_iter().collect_vec() - } - 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 ac32f760..b7cdcfbe 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -3,15 +3,15 @@ use hir::{ container::InFile, file::HirFileId, preproc::{ - IncludeTarget, MacroDefinition, include_directive_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, }; use itertools::Itertools; use syntax::{ - SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, - has_text_range::HasTextRange, + SyntaxTokenWithParent, TokenKind, token::{TokenKindExt, pair_token}, }; use utils::line_index::{TextRange, TextSize}; @@ -40,19 +40,43 @@ 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::source_token_resolution_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )? + .resolved()?; + let (range, tokens) = selection.into_parts(); + let navs = tokens + .into_iter() + .filter_map(|token| nav_targets_for_token(db, &sema, hir_file_id, token)) + .flatten() + .unique() + .collect_vec(); + if navs.is_empty() { + return None; + } + + Some(RangeInfo::new(range, navs)) +} - let navs = handle_ctrl_flow_kw(&sema, hir_file_id, token).or_else(|| { - DefinitionClass::resolve(&sema, hir_file_id, token)? +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( @@ -60,23 +84,54 @@ fn handle_preproc_macro( file_id: FileId, 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)])); + 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 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() { + return None; + } + return Some(RangeInfo::new(reference_range, targets)); } - 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(); - Some(RangeInfo::new(reference_range, targets)) + if let Ok(Some(definition)) = macro_definition_at(db, file_id, offset) { + return Some(RangeInfo::new(definition.name_range, vec![macro_nav_target(definition)])); + } + + 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)); + } + + None } -fn macro_nav_target(definition: MacroDefinition) -> NavTarget { +fn macro_param_nav_target(definition: MacroParamDefinition) -> NavTarget { NavTarget { - file_id: definition.file_id, + 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, + full_range: definition.name_range, + focus_range: Some(definition.name_range), + name: Some(definition.name), + kind: None, container_name: None, description: Some("macro definition".to_owned()), } @@ -87,24 +142,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 63d4042f..32997ba4 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -1,28 +1,48 @@ use hir::{ - base_db::source_db::SourceDb, + base_db::{ + source_db::{SourceDb, SourceRootDb}, + source_root::SourceRootRole, + }, container::InContainer, file::HirFileId, hir_def::expr::Expr, preproc::{ - IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, - macro_reference_definitions_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, }; use syntax::{ - SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, + SyntaxTokenWithParent, TokenKind, ast::{self, AstNode}, - has_text_range::HasTextRange, token::TokenKindExt, }; -use utils::{get::GetRef, line_index::TextSize}; +use utils::{ + get::GetRef, + line_index::{TextRange, TextSize}, +}; use vfs::FileId; use crate::{ FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, markup::Markup, - render, + render, source_tokens::SourceTokenSelection, }; +const MACRO_EXPANSION_SEPARATOR: &str = "--------------------"; + +struct MacroSourceLink { + label: String, + target: String, +} + +struct PreprocMacroHover { + hover: RangeInfo, + reference_definitions: Option, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HoverFormat { Markdown, @@ -40,7 +60,10 @@ 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( + 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) { @@ -51,11 +74,39 @@ 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 selection = crate::source_tokens::source_token_resolution_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )? + .resolved()?; + let hover = hover_for_source_token_selection(&sema, hir_file_id, selection)?; + Some(with_expanded_macro_hover(db, file_id, offset, hover)) +} - 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)) +fn hover_for_source_token_selection( + sema: &Semantics, + hir_file_id: HirFileId, + selection: SourceTokenSelection<'_>, +) -> Option> { + let (range, tokens) = selection.into_parts(); + hover_for_token_selection(sema, hir_file_id, range, 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)) + .collect::>(); + let res = merge_hover_results(markups)?; + Some(RangeInfo::new(range, res)) } pub(crate) fn token_precedence(kind: TokenKind) -> usize { @@ -85,54 +136,432 @@ fn handle_literal( render::render_literal(literal) } -fn handle_preproc_macro( +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 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, None) 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, + reference_definitions: Option<&MacroReferenceDefinitions>, ) -> Option> { - if let Some(definition) = macro_definition_at(db, file_id, offset).ok()? { - return Some(RangeInfo::new(definition.range, macro_definition_markup(&definition))); + 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; } - 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))) + 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; + } + + 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 macro_definition_markup(definition: &MacroDefinition) -> Markup { +fn expanded_macro_markup(db: &RootDb, expansions: &[RecursiveMacroExpansionProvenance]) -> Markup { let mut markup = Markup::new(); - markup.print("Macro"); + + for expansion in expansions { + 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 { + return; + }; + + if !markup.is_empty() { + markup.newline(); + } + render_macro_expansion_header(markup, &root.expansion.definition); + render_macro_expansion_separator(markup); + markup.push_with_code_fence(&expanded_text_from_tokens(&root.tokens)); + render_macro_expansion_separator(markup); + 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: &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) { + markup.newline(); + markup.print(MACRO_EXPANSION_SEPARATOR); markup.newline(); - markup.push_with_backticks(definition.name.as_str()); +} + +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_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 } => { + macro_file_source_link(db, *file_id, anchor_file_id) + } + hir::preproc::MappedPreprocSource::VirtualFile { .. } + | hir::preproc::MappedPreprocSource::VirtualDisplay { .. } => None, + } +} + +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) + { + 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; + } + + 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 { + 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 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, + offset: TextSize, +) -> Option { + if let Ok(Some(definition)) = macro_param_definition_at(db, file_id, offset) { + 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(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(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() { + 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( + resolution.range, + macro_definitions_markup(db, file_id, &resolution.definitions), + ); + return Some(PreprocMacroHover { hover, reference_definitions: Some(resolution) }); + } + + None +} + +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, + 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, + anchor_file_id: FileId, + definitions: &[MacroDefinition], +) -> Markup { + let mut markup = Markup::new(); + if definitions.len() == 1 { + render_macro_definition_display(db, &mut markup, anchor_file_id, &definitions[0]); + 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 +} + +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_with_strong("Macro"); + 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, 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( 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/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index cdac0865..00fcd829 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)] @@ -58,6 +59,7 @@ pub enum SymbolKind { Genvar, Specparam, Typedef, + Struct, Instance, Block, Stmt, diff --git a/crates/ide/src/markup.rs b/crates/ide/src/markup.rs index 3e46c345..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); } @@ -77,12 +83,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/module_resolution.rs b/crates/ide/src/module_resolution.rs index e535f235..721c599e 100644 --- a/crates/ide/src/module_resolution.rs +++ b/crates/ide/src/module_resolution.rs @@ -5,8 +5,11 @@ use hir::{ container::InModule, db::HirDb, hir_def::{ - Ident, declaration::Declaration, expr::declarator::DeclaratorParent, lower_ident_opt, - module::ModuleId, + Ident, + declaration::Declaration, + expr::declarator::DeclaratorParent, + lower_ident_opt, + module::{ModuleId, instantiation::Instantiation}, }, scope::{ModuleEntry, ScopeResolution}, semantics::pathres::PathResolution, @@ -55,6 +58,14 @@ pub(crate) fn resolve_instantiation_target( resolve_module_name(db, from_file, &name) } +pub(crate) fn resolve_hir_instantiation_target( + db: &RootDb, + from_file: FileId, + instantiation: &Instantiation, +) -> Option { + resolve_module_name(db, from_file, instantiation.module_name.as_ref()?).unique() +} + pub(crate) fn resolve_module_name( db: &RootDb, from_file: FileId, diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 929f5717..521f86d2 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, MacroReferenceIndexStatus, macro_definition_at, + macro_param_definition_at, macro_param_reference_definitions_at, macro_param_references, + macro_reference_definitions_at, macro_references, }, semantics::Semantics, }; @@ -9,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}, }; @@ -60,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( @@ -75,15 +102,36 @@ 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 tokens = crate::source_tokens::source_token_resolution_at_offset( + db, + file_id, + root, + offset, + token_precedence, + )? + .resolved()? + .into_tokens(); + let references = 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)]) }) } @@ -93,11 +141,18 @@ 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 { macro_reference_definitions_at(db, file_id, offset).ok()??.definitions }; + if definitions.is_empty() { + return None; + } definitions .into_iter() @@ -105,14 +160,74 @@ 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, + status: ReferencesStatus::Complete, + }) +} + fn macro_references_for_definition( db: &RootDb, file_id: FileId, 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| { config.search_scope.as_ref().is_none_or(|scope| { @@ -133,16 +248,38 @@ 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 macro_nav_target(definition: MacroDefinition) -> NavTarget { +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 { NavTarget { - file_id: definition.file_id, + 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, + full_range: definition.name_range, + focus_range: Some(definition.name_range), + name: Some(definition.name), + kind: None, container_name: None, description: Some("macro definition".to_owned()), } @@ -171,7 +308,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>( @@ -188,7 +329,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 { @@ -198,3 +339,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/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index 911c8d17..3c63a37b 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::{ @@ -27,6 +27,7 @@ use crate::{ ScopeVisibility, db::root_db::RootDb, definitions::{Definition, DefinitionClass}, + source_tokens::SourceTokenRequestCache, }; /// A search scope is a set of files and ranges within those files that should @@ -204,13 +205,24 @@ 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(); 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, + &mut source_token_cache, + ) }) .filter(|tp| self.classify_and_filter(sema, file_id.into(), tp)) .for_each(|token| { @@ -256,23 +268,40 @@ 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) - } + source_token_cache: &mut SourceTokenRequestCache, + ) -> Vec> { + let Some(selection) = crate::source_tokens::source_token_resolution_at_offset_with_cache( + db, + file_id, + node, + offset, + super::token_precedence, + source_token_cache, + ) + .and_then(|resolution| resolution.resolved()) else { + return Vec::new(); + }; + + selection + .into_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/rename.rs b/crates/ide/src/rename.rs index 04cef1b4..948700af 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -1,6 +1,4 @@ -use hir::{ - base_db::source_db::SourceDb, container::InFile, hir_def::lower_ident, semantics::Semantics, -}; +use hir::{base_db::source_db::SourceDb, container::InFile, semantics::Semantics}; use nohash_hasher::IntMap; use rustc_hash::FxHashMap; use smol_str::SmolStr; @@ -427,7 +425,7 @@ fn check_same_name_conn( DefinitionClass::PortConnShorthand { port, .. } => port, DefinitionClass::Ambiguous(_) => return None, }; - let port_name = lower_ident(Some(name_token))?; + let port_name = name_token.value_text().to_string(); let expr = conn.expr()?.as_simple_property_expr()?.expr().as_simple_sequence_expr()?.expr(); let actual_token = match expr { Expression::Name(Name::IdentifierName(ident)) => ident.identifier()?, @@ -438,7 +436,7 @@ fn check_same_name_conn( } _ => return None, }; - if lower_ident(Some(actual_token))?.as_str() != port_name.as_str() { + if actual_token.value_text().to_string() != port_name { return None; } let actual_token = SyntaxTokenWithParent { parent: expr.syntax(), tok: actual_token }; @@ -548,7 +546,7 @@ fn edits_from_refs( && conn_data_range(port_conn).is_some_and(|r| r == range) && let Some(port_name) = port_conn .name() - .filter(|n| lower_ident(Some(*n)).is_some_and(|name| name == new_name)) { + .filter(|n| n.value_text().to_string() == new_name) { // .new(data) => .new let Some(start) = port_name.text_range_in(port_conn.syntax()).map(|range| range.start()) else { diff --git a/crates/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/crates/ide/src/source_tokens.rs b/crates/ide/src/source_tokens.rs new file mode 100644 index 00000000..15e2ea1c --- /dev/null +++ b/crates/ide/src/source_tokens.rs @@ -0,0 +1,821 @@ +use hir::{ + base_db::source_db::{SourceDb, 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, +}; +use utils::line_index::{TextRange, TextSize}; +use vfs::FileId; + +use crate::db::root_db::RootDb; + +#[derive(Debug, Clone)] +pub(crate) enum SourceTokenResolution<'tree> { + Resolved(SourceTokenSelection<'tree>), + Blocked(SourceTokenBlock), +} + +impl<'tree> SourceTokenResolution<'tree> { + pub(crate) fn resolved(self) -> Option> { + match self { + Self::Resolved(selection) => Some(selection), + Self::Blocked(SourceTokenBlock { .. }) => None, + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct SourceTokenSelection<'tree> { + pub origin: SourceTokenOrigin, + pub range: TextRange, + pub tokens: Vec>, +} + +impl<'tree> SourceTokenSelection<'tree> { + fn normal_syntax(range: TextRange, tokens: Vec>) -> Self { + Self { origin: SourceTokenOrigin::NormalSyntax, range, tokens } + } + + fn preproc( + range: TextRange, + hits: Vec, + tokens: Vec>, + ) -> Self { + Self { origin: SourceTokenOrigin::Preproc { hits }, range, tokens } + } + + pub(crate) fn into_parts(self) -> (TextRange, Vec>) { + let Self { origin, range, tokens } = self; + match origin { + SourceTokenOrigin::NormalSyntax => (range, tokens), + SourceTokenOrigin::Preproc { hits } => { + let _hit_count = hits.len(); + (range, tokens) + } + } + } + + pub(crate) fn into_tokens(self) -> Vec> { + self.into_parts().1 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SourceTokenOrigin { + NormalSyntax, + Preproc { hits: Vec }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SourceTokenDomain { + Preproc, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SourceTokenBlock { + pub domain: SourceTokenDomain, + pub range: TextRange, + pub reason: SourceTokenBlockReason, +} + +impl SourceTokenBlock { + fn preproc_unavailable(range: TextRange) -> Self { + Self { + domain: SourceTokenDomain::Preproc, + range, + reason: SourceTokenBlockReason::Unavailable, + } + } + + fn preproc_ambiguous(range: TextRange, hits: Vec) -> Self { + Self { + domain: SourceTokenDomain::Preproc, + range, + reason: SourceTokenBlockReason::Ambiguous { hits }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SourceTokenBlockReason { + Unavailable, + Ambiguous { hits: Vec }, +} + +#[derive(Debug, Default)] +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, + pub call: usize, + pub emitted_token: usize, + pub display_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 source_token_resolution_at_offset<'tree, F>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: F, +) -> Option> +where + F: Fn(TokenKind) -> usize, +{ + let mut cache = SourceTokenRequestCache::default(); + source_token_resolution_at_offset_with_cache(db, file_id, root, offset, precedence, &mut cache) +} + +pub(crate) fn source_token_resolution_at_offset_with_cache<'tree, F>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: F, + cache: &mut SourceTokenRequestCache, +) -> Option> +where + F: Fn(TokenKind) -> usize, +{ + match preproc_source_token_at_offset(db, file_id, root, offset, &precedence, cache) { + SourceTokenProviderResult::NotApplicable => { + normal_syntax_source_token_at_offset(root, offset, &precedence).into_resolution() + } + result => result.into_resolution(), + } +} + +fn normal_syntax_source_token_at_offset<'tree>( + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, +) -> SourceTokenProviderResult<'tree> { + let Some(token) = root.token_at_offset(offset).pick_bext_token(precedence) else { + return SourceTokenProviderResult::NotApplicable; + }; + let Some(range) = token.text_range() else { + return SourceTokenProviderResult::NotApplicable; + }; + SourceTokenProviderResult::Resolved(SourceTokenSelection::normal_syntax(range, vec![token])) +} + +enum SourceTokenProviderResult<'tree> { + Resolved(SourceTokenSelection<'tree>), + Blocked(SourceTokenBlock), + NotApplicable, +} + +impl<'tree> SourceTokenProviderResult<'tree> { + fn into_resolution(self) -> Option> { + match self { + Self::Resolved(selection) => Some(SourceTokenResolution::Resolved(selection)), + Self::Blocked(block) => Some(SourceTokenResolution::Blocked(block)), + Self::NotApplicable => None, + } + } +} + +fn preproc_source_token_at_offset<'tree>( + db: &RootDb, + file_id: FileId, + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, + cache: &mut SourceTokenRequestCache, +) -> SourceTokenProviderResult<'tree> { + if !source_macro_invocation_may_cover_offset(db.file_text(file_id).as_ref(), offset) { + return SourceTokenProviderResult::NotApplicable; + } + + let provenances = match cache.macro_expansion_provenances_at(db, file_id, offset) { + Ok(provenances) => provenances, + Err(PreprocError::SourceQuery(SourcePreprocQueryError::UnsupportedFileKind(_))) => { + return SourceTokenProviderResult::NotApplicable; + } + Err(_) => { + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + TextRange::empty(offset), + )); + } + }; + if provenances.is_empty() { + return SourceTokenProviderResult::NotApplicable; + } + + match preproc_hits_at_offset(&provenances, file_id, offset) { + PreprocHitLookup::Available { range, hits } => { + let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &hits) + else { + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + range, + )); + }; + SourceTokenProviderResult::Resolved(SourceTokenSelection::preproc(range, hits, tokens)) + } + PreprocHitLookup::Unavailable { range } => { + SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable(range)) + } + PreprocHitLookup::Ambiguous { range, hits } => { + SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_ambiguous(range, hits)) + } + } +} + +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); + } + } + + 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(&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 preproc_hit_for_token( + expansion: &MacroExpansionProvenance, + token: &EmittedTokenProvenance, + file_id: FileId, + offset: TextSize, +) -> 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::TokenPaste { .. } + | TokenProvenance::Stringification { .. } + | TokenProvenance::Unavailable(_) => return None, + }; + + 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(), + display_range: token.display_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 syntax_tokens_for_preproc_hit<'tree>( + root: SyntaxNode<'tree>, + 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(token_range) = token.text_range() else { + continue; + }; + 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.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()?; + Some(TextRange::new(start, end)) +} + +#[cfg(test)] +mod tests { + use syntax::{SyntaxTree, token::TokenKindExt}; + + 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!( + 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 SourceTokenProviderResult::Resolved(selection) = preproc_provider_result_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); + let SourceTokenOrigin::Preproc { hits } = &selection.origin else { + panic!("preproc provider should produce a preproc-origin selection"); + }; + assert_eq!(hits.len(), 1); + assert_eq!(selection.tokens.len(), 1); + assert_eq!(selection.tokens[0].text_range(), Some(parser_range)); + assert_ne!(selection.tokens[0].text_range(), Some(provenance_range)); + } + + #[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!( + matches!( + normal_syntax_source_token_at_offset(root, offset, &test_precedence), + SourceTokenProviderResult::Resolved(_) + ), + "test setup must have an ordinary syntax token that fallback could have selected" + ); + + let lookup = preproc_provider_result_from_hits( + root, + offset, + &test_precedence, + Vec::new(), + parser_range, + ); + assert!(matches!( + lookup, + SourceTokenProviderResult::Blocked(SourceTokenBlock { + domain: SourceTokenDomain::Preproc, + reason: SourceTokenBlockReason::Unavailable, + .. + }) + )); + } + + #[test] + fn source_tokens_normal_syntax_path_still_selects_non_preproc_offsets() { + let (root, offset, parser_range) = + root_and_offset("module m; wire payload_i; endmodule\n", "payload_i", 0); + let SourceTokenProviderResult::Resolved(selection) = + normal_syntax_source_token_at_offset(root, offset, &test_precedence) + else { + panic!("normal syntax token expected"); + }; + + assert!(matches!(selection.origin, SourceTokenOrigin::NormalSyntax)); + assert_eq!(selection.range, parser_range); + assert_eq!(selection.tokens.len(), 1); + } + + #[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) = + 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 SourceTokenProviderResult::Resolved(selection) = + preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) + else { + panic!("same semantic target should dedup to one available preproc hit"); + }; + + let SourceTokenOrigin::Preproc { hits } = selection.origin else { + panic!("preproc provider should produce a preproc-origin selection"); + }; + assert_eq!(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 SourceTokenProviderResult::Blocked(SourceTokenBlock { + reason: SourceTokenBlockReason::Ambiguous { hits }, + .. + }) = preproc_provider_result_from_hits(root, offset, &test_precedence, hits, parser_range) + else { + panic!("conflicting preproc targets should be ambiguous"); + }; + + assert_eq!(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, + 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 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 { + expansion: 0, + call: 0, + emitted_token, + display_range: range, + source_range: range, + provenance: PreprocTokenProvenance::SourceToken { source: source.clone(), range }, + target: PreprocSemanticTarget::SourceToken { source, range }, + } + } + + fn preproc_provider_result_from_hits<'tree>( + root: SyntaxNode<'tree>, + offset: TextSize, + precedence: &impl Fn(TokenKind) -> usize, + hits: Vec, + fallback_range: TextRange, + ) -> SourceTokenProviderResult<'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 SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + fallback_range, + )); + } + let range = + covering_range(&unique_hits.iter().map(|hit| hit.source_range).collect::>()) + .unwrap_or(fallback_range); + if unique_hits.len() > 1 { + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_ambiguous( + range, + unique_hits, + )); + } + let Some(tokens) = syntax_tokens_for_preproc_hit(root, offset, precedence, &unique_hits) + else { + return SourceTokenProviderResult::Blocked(SourceTokenBlock::preproc_unavailable( + range, + )); + }; + SourceTokenProviderResult::Resolved(SourceTokenSelection::preproc( + range, + 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()) + } +} diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 909d1133..4ce60a83 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() }, }], ))); @@ -1234,8 +1316,366 @@ 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] +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_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() + ); + assert!( + !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() + ); + + 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_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("```systemverilog") + && 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("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}" + ); + + 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("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_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#" +`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("```systemverilog") + && 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("Context ") + && !info.contains("Expansion steps"), + "nested macro hover should show compact signature, result, and source: {info}" + ); +} + +#[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("```systemverilog") + && 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("Context ") + && !call_info.contains("Expansion steps"), + "outer macro hover should keep compact expansion facts: {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("```systemverilog") + && 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 nested macro expansion without unavailable text: {payl_info}" + ); +} + +#[test] +fn preproc_macro_hover_shows_token_paste_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("```systemverilog") + && info.contains("`JOIN(a, b)") + && info.contains("foobar") + && info.contains("from [feature.v]") + && !info.contains("unavailable"), + "token paste expansion hover should show the expanded display text: {info}" + ); } #[test] @@ -1270,8 +1710,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")) @@ -1394,8 +1837,19 @@ 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("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 should show project-relative macro source path: {}", + hover.info.as_str() + ); let completion_items = analysis .completions_with_trigger( @@ -2806,3 +3260,39 @@ fn verilog_2005_lsp_snapshots() { assert_snapshot!("verilog_2005_lsp_snapshots", report); } + +#[test] +fn document_symbols_include_typedef_structs_and_nested_generate_structs() { + let text = r#" +module top; + typedef struct packed { + logic ready; + } packet_t; + + generate + if (1) begin : g + typedef union packed { + logic [7:0] raw; + logic flag; + } state_t; + end + endgenerate +endmodule +"#; + let (host, file_id) = setup(text); + let analysis = host.make_analysis(); + + let symbols = analysis.document_symbol(file_id).unwrap(); + let mut lines = Vec::new(); + collect_symbol_lines(&symbols, 0, &mut lines); + let dump = lines.join("\n"); + + assert!( + dump.contains("packet_t Struct"), + "typedef struct should surface as a struct symbol: {dump}" + ); + assert!( + dump.contains("state_t Struct"), + "nested generate typedef union should surface as a struct symbol: {dump}" + ); +} diff --git a/crates/preproc/src/source.rs b/crates/preproc/src/source.rs index 15443b6e..dec845a7 100644 --- a/crates/preproc/src/source.rs +++ b/crates/preproc/src/source.rs @@ -1,7 +1,7 @@ mod model; -mod references; +mod provenance; mod trace; mod types; -pub use references::{SourceMacroReferenceResolution, SourceMacroReferenceSite}; +pub use provenance::*; pub use types::*; diff --git a/crates/preproc/src/source/model.rs b/crates/preproc/src/source/model.rs index 31ebcb5b..620abbbd 100644 --- a/crates/preproc/src/source/model.rs +++ b/crates/preproc/src/source/model.rs @@ -1,17 +1,16 @@ -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 { - SourcePreprocIndex::from_trace(trace).map(Self::new) + let index = SourcePreprocIndex::from_trace(trace)?; + Ok(Self::new(index)) } pub fn index(&self) -> &SourcePreprocIndex { @@ -22,6 +21,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 } @@ -51,7 +90,7 @@ impl SourcePreprocModel { } pub fn inactive_ranges(&self) -> &[SourceRange] { - &self.index.inactive_ranges + &self.tables.inactive_ranges } pub fn events(&self) -> impl Iterator> + '_ { @@ -62,18 +101,60 @@ impl SourcePreprocModel { .filter_map(|(source_order, directive)| self.event_from_record(source_order, directive)) } - 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); + pub fn visible_macros_at(&self, position: SourcePosition) -> Vec<&SourceMacroDefinition> { + self.tables + .state_timeline + .state_at_position(position) + .map(|state| self.definitions_for_state(state)) + .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 }, + ) + } } - environment } - pub fn visible_macros_at(&self, position: SourcePosition) -> Vec> { - let environment = self.macro_environment_at(position); - self.bindings_for_environment(&environment) + 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 { @@ -126,148 +207,50 @@ 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) - } - - 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 - .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()) - } - - 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); - } - None - } - - fn bindings_for_environment( - &self, - environment: &SourceMacroEnvironment, - ) -> Vec> { - environment + fn definitions_for_state(&self, state: &SourceMacroState) -> Vec<&SourceMacroDefinition> { + state .definitions - .iter() - .filter_map(|(name, define_index)| { - 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 }) - } - - fn apply_macro_state( + fn collect_recursive_macro_expansion( &self, - directive: &SourcePreprocEventRecord, - environment: &mut SourceMacroEnvironment, + call: SourceMacroCallId, + result: &mut SourceRecursiveMacroExpansion, + visiting: &mut Vec, ) { - 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); + 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; } - } - 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()); + 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 }); } - MacroEventKind::Include - | MacroEventKind::Conditional - | MacroEventKind::Branch - | MacroEventKind::Usage => {} } } @@ -335,380 +318,5 @@ 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::{ - PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, - PreprocessorTraceToken, SourceBufferId, SourceBufferOrigin, SourceBufferRange, SyntaxKind, - SyntaxTree, SyntaxTreeBuffer, SyntaxTreeOptions, - }; - 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 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())] - } - - #[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); - - let before_include = model.macro_environment_at(SourcePosition { - source: root_source, - offset: offset_before(root_text, "`include"), - }); - assert!(!before_include.contains("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 binding = 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") - .unwrap(); - assert_eq!(binding.define.name_range.unwrap().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 = 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)); - 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_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 = 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 binding = 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") - .unwrap(); - assert_eq!(binding.define.body[0].value.as_str(), "16"); - assert_eq!(binding.define.name_range.unwrap().source, root_source); - } - - #[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 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); - } - - #[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 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::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 - })); - } - - #[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 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::ConditionalToken { - 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 - })); - } - - #[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 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); - } - - #[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(), - buffer_id: 1, - origin: SourceBufferOrigin::Source, - }], - events: vec![PreprocessorTraceEvent { - event_id: PreprocessorTraceEventId(0), - kind: SyntaxKind::DEFINE_DIRECTIVE, - range: None, - directive: None, - name: Some(PreprocessorTraceToken { - raw_text: "WIDTH".to_owned(), - value_text: "WIDTH".to_owned(), - range: Some(SourceBufferRange { buffer_id: 1, range: 8..13 }), - }), - include_file_name: None, - params: Vec::new(), - body_tokens: Vec::new(), - expr_tokens: Vec::new(), - disabled_ranges: Vec::new(), - }], - include_edges: 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..60a88bc5 --- /dev/null +++ b/crates/preproc/src/source/model/tests.rs @@ -0,0 +1,1515 @@ +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 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, +) -> (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 = 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, "defs.vh"); + (model, root_source, header_source) +} + +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 = 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) +} + +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 = 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(); + + 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 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" +`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!(expansion.definition, SourceMacroExpansionDefinition::Source(*resolved_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"); + 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] +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_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_records_macro_operation_tokens_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"); + 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"); + 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::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"; + 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::MissingEmittedTokenMacroExpansionIdentity { .. } + ) + )); + 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] +fn source_model_maps_predefine_and_intrinsic_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 intrinsic = model + .emitted_tokens() + .iter() + .find(|token| token.text.as_str() == "3") + .expect("intrinsic macro token should stay in emitted stream"); + 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] +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 = 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"); + 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 new file mode 100644 index 00000000..1c2343a5 --- /dev/null +++ b/crates/preproc/src/source/provenance.rs @@ -0,0 +1,548 @@ +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 + } + } + }; +} + +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); +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 }, + MacroBodyToken { call: SourceMacroCallId, token_index: usize }, + ExpansionToken { emitted_token: SourceEmittedTokenId }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceMacroDefinition { + pub id: SourceMacroDefinitionId, + pub event_id: SourcePreprocEventId, + pub identity: Option, + 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, Default)] +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, Default)] +pub struct SourceMacroStateTimeline { + states: Vec, + checkpoints: Vec, + source_order_scopes: BTreeMap, + source_order_boundaries: BTreeMap>, + final_source_order: usize, +} + +#[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, Copy, PartialEq, Eq)] +struct SourceMacroStatePositionBoundary { + source_order: usize, + boundary: SourcePosition, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SourceMacroStateSourceScope { + end_order: usize, +} + +#[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, + 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 identity: Option, + pub call: SourceMacroCallId, + 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, + 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, + 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 SourceTokenProvenance { + Source { + 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 { + identity: SourceMacroOperationIdentity, + call: SourceMacroCallId, + }, + Stringification { + identity: SourceMacroOperationIdentity, + call: SourceMacroCallId, + }, + Predefine { + source: PreprocSourceId, + }, + Builtin { + name: SmolStr, + identity: SourceMacroBuiltinIdentity, + call: SourceMacroCallId, + }, + 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, + 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 inactive_ranges: Vec, + pub state_timeline: SourceMacroStateTimeline, + pub capabilities: SourcePreprocCapabilities, + pub issues: 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 { + 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 }, + DetachedSource { source: PreprocSourceId }, + MissingPredefineSourceText { source: PreprocSourceId }, + UnverifiedPredefineSource { source: PreprocSourceId }, + MacroCallAuthorityUnavailable, + EmittedTokenAuthorityUnavailable, + TokenProvenanceAuthorityUnavailable, + ExpansionAuthorityUnavailable, + MissingMacroCall { call: SourceMacroCallId }, + MissingMacroExpansion { call: SourceMacroCallId }, + MissingEmittedTokenMacroCall { source: PreprocSourceId }, + UnknownMacroUsageDefinitionIdentity { identity: SourceMacroDefinitionKey }, + MissingEmittedTokenMacroCallIdentity, + UnknownEmittedTokenMacroCallIdentity { identity: SourceMacroCallKey }, + MissingEmittedTokenMacroDefinitionIdentity, + UnknownEmittedTokenMacroDefinitionIdentity { identity: SourceMacroDefinitionKey }, + MissingEmittedTokenMacroExpansionIdentity { call: SourceMacroCallId }, + UnmappedParentMacroExpansionIdentity { identity: SourceMacroExpansionKey }, + MissingEmittedTokenMacroDefinition { call: SourceMacroCallId }, + MissingEmittedTokenMacroBody { call: SourceMacroCallId }, + MissingEmittedTokenMacroArgument { call: SourceMacroCallId }, + NonContiguousEmittedTokenRange { call: SourceMacroCallId }, + UnsupportedEmittedTokenProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourcePreprocFactIssue { + MissingDefinitionName { event_id: SourcePreprocEventId }, + MissingDefinitionNameRange { event_id: SourcePreprocEventId }, + MissingReferenceName { event_id: SourcePreprocEventId }, + MissingReferenceNameRange { event_id: SourcePreprocEventId }, + DetachedSource { source: PreprocSourceId }, +} + +impl SourcePreprocTables { + pub fn from_index(index: &SourcePreprocIndex) -> Self { + SourcePreprocModelBuilder::new(index).build() + } + + 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(), + inactive_ranges: Vec::new(), + state_timeline: SourceMacroStateTimeline::default(), + capabilities: SourcePreprocCapabilities::unavailable(), + issues: 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 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 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 source_end_order; + }; + let index = + boundaries.partition_point(|boundary| boundary.boundary.offset <= position.offset); + boundaries.get(index).map(|boundary| boundary.source_order).unwrap_or(source_end_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()) + } +} + +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_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); + +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..c42985b9 --- /dev/null +++ b/crates/preproc/src/source/provenance/builder.rs @@ -0,0 +1,1364 @@ +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, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MacroOperationProvenanceKind { + TokenPaste, + Stringification, +} + +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 { + 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(); + self.record_source_order_scopes(); + 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 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(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(); + + for edge in &self.index.include_edges { + 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(); + 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 => 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 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, 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, + ) + } + } + } + + 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 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, + }, + ); + }; + 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); + } + 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, + ) -> 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; + } + + 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(); + 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); + } + + 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( + SourceEmittedTokenId::new(self.tables.emitted_tokens.len()), + ) else { + self.mark_call_unavailable( + call, + SourcePreprocUnavailable::NonContiguousEmittedTokenRange { call }, + ); + continue; + }; + let Some(definition) = self.expansion_definition_for_call(call, &direct_tokens_by_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); + 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, + site, + name, + name_range, + definition.directive_range, + resolution, + ); + } + } + } + + fn macro_reference_exists( + &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 + }) + } + + 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, .. } + | SourceTokenProvenance::Builtin { call, .. } => *call, + SourceTokenProvenance::Source { .. } + | SourceTokenProvenance::Predefine { .. } + | SourceTokenProvenance::Unavailable(_) => continue, + }; + tokens_by_call.entry(call).or_default().push(token.id); + } + 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(); + 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>, + 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(); + } + + 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, + recursive_tokens_by_call, + visiting, + )); + } + } + visiting.pop(); + tokens.sort_by_key(|token| token.raw()); + tokens.dedup(); + recursive_tokens_by_call.insert(call, tokens.clone()); + tokens + } + + 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(source) => { + self.references_partial = true; + self.tables.issues.push(SourcePreprocFactIssue::DetachedSource { source }); + SourceMacroResolution::Unavailable(SourcePreprocUnavailable::DetachedSource { + source, + }) + } + } + } + + fn include_chain_for_source( + &self, + source: PreprocSourceId, + ) -> Result, PreprocSourceId> { + let mut chain = Vec::new(); + let mut current = source; + + loop { + 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(current); + } + PreprocSourceOrigin::Included { include_event_id } => { + let directive = self + .tables + .include_graph + .directives() + .iter() + .find(|directive| directive.event_id == include_event_id) + .expect("included source should point at an include directive"); + chain.push(SourceIncludeChainEntry { + 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 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 } +} + +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, + empty_start: SourceEmittedTokenId, + ) -> Option; +} + +impl SourceEmittedTokenIdSliceExt for [SourceEmittedTokenId] { + 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 }) + } +} + +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/references.rs b/crates/preproc/src/source/references.rs deleted file mode 100644 index c105fc83..00000000 --- a/crates/preproc/src/source/references.rs +++ /dev/null @@ -1,174 +0,0 @@ -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, - 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(name) = usage.name.as_ref() else { - return Ok(None); - }; - let Some(environment) = - self.macro_environment_before(SourcePreprocEntity::Usage(usage_index)) - else { - return Ok(None); - }; - let Some(define_index) = environment.define_index(name.as_str()) else { - return Ok(None); - }; - let Some(define) = self.index.defines.get(define_index) 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)?; - Ok(Some(SourceMacroResolution { - usage_index, - usage, - definition, - definition_provenance, - definition_include_chain, - })) - } - - pub fn definition_for_conditional_token( - &self, - 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()) - } - - pub fn resolved_macro_references( - &self, - ) -> 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 { - continue; - }; - let Some(name) = usage.name.clone() else { - continue; - }; - 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, - }); - } - - 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( - &self, - conditional_index: usize, - name: &str, - ) -> Option> { - let conditional = self.index.conditionals.get(conditional_index)?; - if conditional.kind != MacroConditionalKind::IfNDef { - return None; - } - - // 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 => {} - } - } - - None - } -} diff --git a/crates/preproc/src/source/trace.rs b/crates/preproc/src/source/trace.rs index 2812ea2a..1f35d31e 100644 --- a/crates/preproc/src/source/trace.rs +++ b/crates/preproc/src/source/trace.rs @@ -2,9 +2,13 @@ use std::collections::BTreeMap; use smol_str::{SmolStr, ToSmolStr}; use syntax::{ - PreprocessorTrace, PreprocessorTraceEvent, PreprocessorTraceEventId, - PreprocessorTraceMacroParam, PreprocessorTraceToken, SourceBufferOrigin, SourceBufferRange, - SyntaxKind, + PreprocessorTrace, PreprocessorTraceActualArgument, PreprocessorTraceEmittedToken, + PreprocessorTraceEvent, PreprocessorTraceEventId, PreprocessorTraceMacroArgumentIdentity, + PreprocessorTraceMacroBodyIdentity, PreprocessorTraceMacroBuiltinIdentity, + PreprocessorTraceMacroCallId, PreprocessorTraceMacroDefinitionId, + PreprocessorTraceMacroExpansionId, PreprocessorTraceMacroOperationIdentity, + PreprocessorTraceMacroParam, PreprocessorTraceToken, PreprocessorTraceTokenProvenance, + SourceBufferOrigin, SourceBufferRange, SyntaxKind, }; use utils::line_index::{TextRange, TextSize}; @@ -16,6 +20,71 @@ 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 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 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); @@ -31,6 +100,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,8 +128,7 @@ impl SourcePreprocIndex { for (source_order, directive) in trace.events.into_iter().enumerate() { collect_trace_event(&mut index, source_order, directive)?; } - - validate_include_edges(&index)?; + index.emitted_tokens = emitted_tokens.into_iter().map(emitted_token_from_trace).collect(); Ok(index) } @@ -86,35 +155,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, @@ -145,23 +185,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); @@ -180,8 +216,22 @@ fn collect_trace_event( let event_index = index.usages.len(); index.usages.push(SourceMacroUsage { event_id, - 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), + 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.macro_name(), + name_range: directive.name.source_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); @@ -198,8 +248,9 @@ fn collect_trace_define( ) -> SourceMacroDefine { SourceMacroDefine { event_id, - name: directive.name.as_ref().map(trace_token_value), - name_range: directive.name.as_ref().and_then(trace_token_range), + identity: directive.macro_definition_id.map(SourceMacroDefinitionKey::from), + 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(), @@ -209,12 +260,22 @@ 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), + } +} + +fn macro_actual_argument_from_trace( + (argument_index, argument): (usize, PreprocessorTraceActualArgument), +) -> SourceMacroActualArgument { + SourceMacroActualArgument { + argument_index, + argument_range: trace_range(&argument.range), + tokens: argument.tokens.into_iter().map(macro_token_from_trace).collect(), } } @@ -222,16 +283,90 @@ 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), } } -fn trace_token_value(token: &PreprocessorTraceToken) -> SmolStr { - token.value_text.to_smolstr() +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 trace_token_range(token: &PreprocessorTraceToken) -> Option { - token.range.as_ref().and_then(source_range_from_trace) +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, + identity, + 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(), + identity: Some(SourceMacroBodyIdentity::from(identity)), + call_range, + body_token_range, + } + } + PreprocessorTraceTokenProvenance::MacroArgument { + macro_name, + identity, + 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(), + identity: Some(SourceMacroArgumentIdentity::from(identity)), + call_range, + body_token_range, + argument_token_range, + } + } + PreprocessorTraceTokenProvenance::Builtin { name, identity } if !name.is_empty() => { + SourceTokenProvenanceFact::Builtin { + name: name.to_smolstr(), + 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, + } } fn required_event_range( @@ -239,13 +374,41 @@ fn required_event_range( 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), diff --git a/crates/preproc/src/source/types.rs b/crates/preproc/src/source/types.rs index de2bf8cf..a52d1c90 100644 --- a/crates/preproc/src/source/types.rs +++ b/crates/preproc/src/source/types.rs @@ -1,8 +1,9 @@ -use std::collections::BTreeMap; - use smol_str::SmolStr; +use syntax::TokenKind; use utils::line_index::{TextRange, TextSize}; +use super::provenance::SourcePreprocTables; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PreprocSourceId(u32); @@ -46,6 +47,62 @@ 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, Copy, PartialEq, Eq)] +pub struct SourceMacroBuiltinIdentity { + pub call: SourceMacroCallKey, + pub expansion: SourceMacroExpansionKey, + 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, @@ -80,6 +137,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, @@ -99,6 +157,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>, @@ -141,11 +200,23 @@ pub struct SourceMacroConditional { #[derive(Debug, Clone, PartialEq, Eq)] 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, @@ -153,31 +224,55 @@ pub struct SourceMacroToken { pub range: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourcePreprocModel { - pub(super) index: SourcePreprocIndex, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SourceTokenKind { + Unknown, + Syntax(TokenKind), } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SourceMacroEnvironment { - pub(super) definitions: BTreeMap, +#[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 struct SourceMacroBinding<'a> { - pub name: SmolStr, - pub event_id: SourcePreprocEventId, - pub define_index: usize, - pub define: &'a SourceMacroDefine, +pub enum SourceTokenProvenanceFact { + Source { + token_range: SourceRange, + }, + 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, + }, + Builtin { + name: SmolStr, + identity: Option, + }, + TokenPaste { + identity: Option, + }, + Stringification { + identity: Option, + }, + Unavailable, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceMacroResolution<'a> { - pub usage_index: usize, - pub usage: &'a SourceMacroUsage, - pub definition: SourceMacroBinding<'a>, - pub definition_provenance: SourcePreprocProvenance, - pub definition_include_chain: Vec, +pub struct SourcePreprocModel { + pub(super) index: SourcePreprocIndex, + pub(super) tables: SourcePreprocTables, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -243,11 +338,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 { @@ -272,32 +362,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 { diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index ac90d4c1..13c910df 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,9 +93,17 @@ 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() } + + 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)] @@ -166,6 +178,7 @@ impl Workspace { fn from_toml(toml: TomlWorkspace, is_lib: bool) -> anyhow::Result { let TomlWorkspace { + manifest_path, top_modules, workspace_root, macro_defs, @@ -209,14 +222,15 @@ impl Workspace { source, source_directories, source_files: source_locations.source_files, + extra_files: vec![manifest_path.clone()], include_dirs: include_dirs.clone(), exclude_globs, }; 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()) - .then(|| semantic_profile(top_modules, macro_defs, include_dirs)); + .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 }) } @@ -230,14 +244,15 @@ impl Workspace { source: source.clone(), source_directories: source, source_files: Vec::new(), + extra_files: Vec::new(), include_dirs: include_dirs.clone(), exclude_globs: None, }; let roots = workspace_roots(kind, &ManifestSourcePolicy::DefaultIndex, true, root_parts); let semantic_profile = roots .iter() - .any(|root| root.role.participates_in_semantic_profile()) - .then(|| semantic_profile(Vec::new(), MacroDef::default(), include_dirs)); + .any(WorkspaceRoot::contributes_semantic_profile) + .then(|| semantic_profile(Vec::new(), MacroDef::default(), include_dirs, None)); Self { workspace_root: path.clone(), @@ -256,7 +271,7 @@ impl Workspace { self.semantic_profile.as_ref() } - fn root(&self) -> &AbsPathBuf { + pub fn root(&self) -> &AbsPathBuf { &self.workspace_root } @@ -273,11 +288,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 +306,7 @@ struct WorkspaceRootParts { source: PathMatcher, source_directories: PathMatcher, source_files: Vec, + extra_files: Vec, include_dirs: Vec, exclude_globs: Option, } @@ -300,6 +317,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 +328,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 +377,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 +630,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 +672,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 +942,38 @@ 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!(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.has_compilation_profiles()); assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); + assert_eq!(project_config.profile_for_root(SourceRootId(1)), None); } #[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,9 +982,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!(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.has_compilation_profiles()); assert_eq!(project_config.profile_for_root(SourceRootId(0)), None); } @@ -961,7 +1014,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,15 +1044,14 @@ 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] ); - 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(); @@ -1047,6 +1099,45 @@ 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, project_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); + 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] fn exclude_globs_filter_loaded_source_files() { let base = TestDir::new("project-model-excluded-source-root"); @@ -1066,12 +1157,13 @@ 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:?}"), }; assert!(!dirs.contains_file(top.as_path())); + assert!(project_config.has_compilation_profiles()); assert!(project_config.profile_for_root(SourceRootId(0)).is_some()); } @@ -1096,10 +1188,11 @@ 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(), 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:?}"), @@ -1119,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] @@ -1224,7 +1320,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/crates/slang/bindings/rust/ast.rs b/crates/slang/bindings/rust/ast.rs old mode 100644 new mode 100755 index fdad1b27..5e8328e8 --- a/crates/slang/bindings/rust/ast.rs +++ b/crates/slang/bindings/rust/ast.rs @@ -51,6 +51,15 @@ impl<'a, T: AstNode<'a>> SyntaxList<'a, T> { pub fn children(&self) -> impl Iterator + 'a { SyntaxChildren::new(self.syntax).map(|elem| T::cast(elem.as_node().unwrap()).unwrap()) } + + pub fn only_children(&self) -> Option { + let mut children = SyntaxChildren::new(self.syntax); + let first = children.next()?; + if children.next().is_some() { + return None; + } + T::cast(first.as_node().unwrap()) + } } impl<'a, T: AstNode<'a>> AstNode<'a> for SyntaxList<'a, T> { @@ -79,6 +88,15 @@ impl<'a, T: AstNode<'a>> SeparatedList<'a, T> { .step_by(2) .map(|elem| T::cast(elem.as_node().unwrap()).unwrap()) } + + pub fn only_children(&self) -> Option { + let mut children = SyntaxChildren::new(self.syntax); + let first = children.next()?; + if children.next().is_some() { + return None; + } + T::cast(first.as_node().unwrap()) + } } impl<'a, T: AstNode<'a>> AstNode<'a> for SeparatedList<'a, T> { diff --git a/crates/slang/bindings/rust/ffi.rs b/crates/slang/bindings/rust/ffi.rs index a06b6324..4ec5186d 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, } @@ -86,6 +88,7 @@ mod slang_ffi { struct RawPreprocessorTraceToken { raw_text: String, value_text: String, + token_kind: u16, range: RawSourceBufferRange, has_token: bool, } @@ -98,20 +101,62 @@ 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, kind: u16, range: RawSourceBufferRange, + macro_definition_id: u32, + 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, } + #[derive(Debug, Clone, PartialEq, Eq)] + struct RawPreprocessorTraceEmittedToken { + raw_text: String, + value_text: String, + 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, + argument_token_range: RawSourceBufferRange, + } + #[derive(Debug, Clone, PartialEq, Eq)] struct RawPreprocessorTraceIncludeEdge { include_event_id: u32, @@ -125,6 +170,7 @@ mod slang_ffi { source_buffers: Vec, events: Vec, include_edges: Vec, + emitted_tokens: Vec, } #[namespace = "slang"] @@ -419,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, @@ -475,15 +532,7 @@ mod slang_ffi { ) -> 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; + fn SyntaxTree_preprocessorTraceFromParsed(tree: &SyntaxTree) -> RawPreprocessorTrace; #[namespace = "wrapper::syntax"] fn SyntaxTree_buffer_id(tree: &SyntaxTree) -> u32; @@ -606,6 +655,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; @@ -614,7 +664,7 @@ 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 6bc464d6..9eecd5ec 100644 --- a/crates/slang/bindings/rust/ffi/wrapper.cc +++ b/crates/slang/bindings/rust/ffi/wrapper.cc @@ -2,10 +2,13 @@ #include "slang/parsing/ExpectedSyntax.h" #include "slang/parsing/ParserMetadata.h" +#include "slang/parsing/PreprocessorTrace.h" #include "slang/syntax/AllSyntax.h" +#include #include #include +#include #include namespace wrapper { @@ -160,6 +163,116 @@ ::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; +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; + 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.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(); + token.argument_token_range = empty_source_buffer_range(); + return token; +} + +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 (!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; @@ -187,10 +300,123 @@ std::optional trace_source_location_key(slang::SourceLoc }; } +bool has_direct_macro_token_provenance( + const std::optional& provenance) { + return provenance && provenance->expansionId != 0 && provenance->callId != 0 && + 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) { + 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; +} + +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, + 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(); 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,11 +429,27 @@ ::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; } +::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) { @@ -217,6 +459,130 @@ 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) { + 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: + 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: + break; + } + + 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); + apply_direct_macro_token_provenance(result, *directProvenance); + result.provenance_kind = TRACE_TOKEN_PROVENANCE_BUILTIN; + return result; + } + + 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); + + auto formalRange = sourceManager.getExpansionRange(location); + result.body_token_range = to_rust_original_macro_loc_range(sourceManager, formalRange); + result.call_range = to_rust_macro_argument_callsite_range(sourceManager, formalRange); + + 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; + } + + 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 (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; + } + 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++) { @@ -262,10 +628,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) @@ -320,12 +685,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; @@ -348,17 +707,58 @@ ::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) { + uint32_t eventId, + uint32_t macroDefinitionId) { ::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.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>(); @@ -369,6 +769,8 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( switch (syntax.kind) { case slang::syntax::SyntaxKind::DefineDirective: { const auto& define = syntax.as(); + 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) @@ -401,11 +803,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(); - directive.name = to_rust_preprocessor_trace_token(usage.directive); - break; - } default: break; } @@ -413,6 +810,127 @@ ::RawPreprocessorTraceEvent to_rust_preprocessor_trace_event( return directive; } +::RawPreprocessorTraceEvent to_rust_preprocessor_trace_macro_usage_record( + const slang::parsing::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; +} + +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; +} + +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, @@ -439,8 +957,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)); } @@ -625,7 +1155,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) @@ -644,15 +1175,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()); @@ -712,6 +1248,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, @@ -828,115 +1385,12 @@ ::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) { - ::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>(); - - 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; +::RawPreprocessorTrace SyntaxTree_preprocessorTraceFromParsed(const SyntaxTree& tree) { + auto* trace = tree.inner().getPreprocessorTrace(); + if (!trace) + return empty_preprocessor_trace(); - 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); - } - preprocessor.pushSource(rootBuffer); - std::unordered_map - includeEventIdsByLocation; - - while (true) { - auto token = preprocessor.next(); - for (auto trivia : token.trivia()) { - if (trivia.kind != slang::parsing::TriviaKind::Directive) - continue; - - if (auto* syntax = trivia.syntax()) { - 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)); - } - } - - if (token.kind == slang::parsing::TokenKind::EndOfFile) - break; - } - - 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; + return to_rust_preprocessor_trace_snapshot(*trace, tree.inner().sourceManager()); } std::unique_ptr SyntaxNode_range(const SyntaxNode& node) { diff --git a/crates/slang/bindings/rust/ffi/wrapper.h b/crates/slang/bindings/rust/ffi/wrapper.h index 2048e956..5828fdbf 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, @@ -559,14 +569,7 @@ 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); } namespace ast { diff --git a/crates/slang/bindings/rust/lib.rs b/crates/slang/bindings/rust/lib.rs index 9ae28da6..a2130e91 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, @@ -103,6 +109,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, } @@ -135,11 +142,21 @@ 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)] 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, @@ -151,19 +168,99 @@ pub struct PreprocessorTraceEvent { pub event_id: PreprocessorTraceEventId, pub kind: SyntaxKind, 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, } +#[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, + 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, + identity: PreprocessorTraceMacroBuiltinIdentity, + }, + TokenPaste { + identity: PreprocessorTraceMacroOperationIdentity, + }, + Stringification { + identity: PreprocessorTraceMacroOperationIdentity, + }, + 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 PreprocessorTraceMacroBuiltinIdentity { + pub call_id: PreprocessorTraceMacroCallId, + pub expansion_id: PreprocessorTraceMacroExpansionId, + 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, pub value_text: String, + pub token_kind: TokenKind, pub range: Option, } @@ -174,6 +271,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>, @@ -276,6 +379,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), }) @@ -302,6 +406,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), }) @@ -315,6 +420,11 @@ impl PreprocessorTrace { included_buffer_id: edge.included_buffer_id, }) .collect(), + emitted_tokens: raw + .emitted_tokens + .into_iter() + .map(PreprocessorTraceEmittedToken::from_raw) + .collect(), }) } } @@ -326,10 +436,27 @@ 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)), + 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() @@ -349,12 +476,266 @@ 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, + value_text, + token_kind: TokenKind::from_id(token_kind), + provenance: PreprocessorTraceTokenProvenance::from_raw( + 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 BUILTIN: u8 = 4; + 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] + 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(raw.call_range) else { + return Self::Unavailable; + }; + let Some(body_token_range) = SourceBufferRange::from_raw(raw.body_token_range) + else { + return Self::Unavailable; + }; + let Some(identity) = PreprocessorTraceMacroBodyIdentity::from_raw(&raw.identity) + else { + return Self::Unavailable; + }; + Self::MacroBody { + macro_name: raw.macro_name, + identity, + call_range, + body_token_range, + } + } + Self::MACRO_ARGUMENT => { + let Some(call_range) = SourceBufferRange::from_raw(raw.call_range) else { + return Self::Unavailable; + }; + let Some(body_token_range) = SourceBufferRange::from_raw(raw.body_token_range) + else { + return Self::Unavailable; + }; + let Some(argument_token_range) = + SourceBufferRange::from_raw(raw.argument_token_range) + else { + return Self::Unavailable; + }; + let Some(identity) = + PreprocessorTraceMacroArgumentIdentity::from_raw(&raw.identity) + else { + return Self::Unavailable; + }; + Self::MacroArgument { + macro_name: raw.macro_name, + identity, + call_range, + body_token_range, + argument_token_range, + } + } + 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::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, + } + } +} + +impl PreprocessorTraceMacroBodyIdentity { + #[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)), + body_token_index: raw.has_body_token_index.then_some(raw.body_token_index)?, + }) + } +} + +impl PreprocessorTraceMacroArgumentIdentity { + #[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)), + 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)?, + }) + } +} + +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 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 { 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), }) } @@ -1214,6 +1595,24 @@ 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), + } + } +} + +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 { @@ -1236,19 +1635,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 { @@ -1306,14 +1721,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() @@ -1366,30 +1774,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(), - options - .include_buffers - .iter() - .map(|buffer| ffi::RawSourceBuffer { - path: buffer.path.clone(), - text: buffer.text.clone(), - }) - .collect(), - 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 a3e59fb6..42fcc9cb 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,19 +1058,602 @@ 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") })); } +#[test] +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"); + 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"); + 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] +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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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, + 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"); + + 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, + identity, + call_range, + body_token_range, + argument_token_range, + } = &arg.provenance + 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"); +} + +#[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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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, + 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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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_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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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"); + + 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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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] +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 = + preprocessor_trace(source, "source", "sample/rtl/top.sv", &SyntaxTreeOptions::default()); + + 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_reports_macro_operation_token_provenance() { + 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 = + 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"); + 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"); + 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] +fn preprocessor_trace_reports_predefine_and_intrinsic_emitted_token_facts() { + let source = r#"module m; +localparam int P = `FROM_API; +localparam int L = `__LINE__; +endmodule +"#; + let trace = preprocessor_trace( + source, + "source", + "sample/rtl/top.sv", + &SyntaxTreeOptions { + predefines: vec!["FROM_API=11".to_owned()], + ..SyntaxTreeOptions::default() + }, + ); + + 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 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() + .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_eq!(body_token_range.buffer_id, predefine_source.buffer_id); + + let intrinsic = trace + .emitted_tokens + .iter() + .find(|token| token.raw_text == "3") + .expect("intrinsic macro token should stay in emitted stream"); + assert!(matches!( + &intrinsic.provenance, + PreprocessorTraceTokenProvenance::Builtin { name, identity } + if name == "__LINE__" && identity.call_id.0 != 0 && identity.expansion_id.0 != 0 + )); +} + #[test] fn preprocessor_trace_records_nested_include_edges() { let dir = TestDir::new("slang-preprocessor-trace-nested"); @@ -1086,8 +1679,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 diff --git a/crates/slang/include/slang/parsing/Preprocessor.h b/crates/slang/include/slang/parsing/Preprocessor.h index e8c5abd8..a54e381c 100644 --- a/crates/slang/include/slang/parsing/Preprocessor.h +++ b/crates/slang/include/slang/parsing/Preprocessor.h @@ -12,7 +12,9 @@ #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" #include "slang/util/Bag.h" #include "slang/util/SmallVector.h" @@ -25,6 +27,7 @@ struct MacroActualArgumentListSyntax; struct MacroFormalArgumentListSyntax; struct MacroActualArgumentSyntax; struct MacroFormalArgumentSyntax; +struct MacroUsageSyntax; struct PragmaDirectiveSyntax; struct PragmaExpressionSyntax; @@ -71,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(); @@ -150,6 +154,15 @@ 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 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); Preprocessor& operator=(const Preprocessor& other) = delete; @@ -257,6 +270,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 +285,28 @@ 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; } + uint32_t getExpansionId() const { return expansionId; } + void setExpansionLoc(SourceLocation location); + 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,6 +315,8 @@ class SLANG_EXPORT Preprocessor { Token usageSite; bool any = false; bool isTopLevel = false; + SourceManager::MacroExpansionMetadata metadata; + uint32_t expansionId = 0; }; // Macro handling methods @@ -300,14 +326,23 @@ 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, + 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; } static bool isSameMacro(const syntax::DefineDirectiveSyntax& left, 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(); @@ -392,6 +427,11 @@ class SLANG_EXPORT Preprocessor { // map from macro name to macro definition flat_hash_map macros; + flat_hash_map macroDefinitionIds; + 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..38d6452b --- /dev/null +++ b/crates/slang/include/slang/parsing/PreprocessorTrace.h @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// 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; + bool isPredefine = false; +}; + +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, + bool isPredefine = false); + 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/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/include/slang/text/SourceManager.h b/crates/slang/include/slang/text/SourceManager.h index 2593ac5f..2cf83413 100644 --- a/crates/slang/include/slang/text/SourceManager.h +++ b/crates/slang/include/slang/text/SourceManager.h @@ -10,11 +10,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include @@ -42,6 +44,35 @@ class SLANG_EXPORT SourceManager { public: using BufferOrError = nonstd::expected; + enum class MacroExpansionKind : uint8_t { + Body, + Argument, + TokenPaste, + 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; + std::string builtinName; + + bool valid() const { return expansionId != 0 && callId != 0 && + (definitionId != 0 || !builtinName.empty()); } + }; + /// Default constructor. SourceManager(); SourceManager(const SourceManager&) = delete; @@ -101,6 +132,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; @@ -122,6 +156,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; @@ -148,6 +185,23 @@ 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); + + /// 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); @@ -282,17 +336,33 @@ class SLANG_EXPORT SourceManager { struct ExpansionInfo { SourceLocation originalLoc; SourceRange expansionRange; - bool isMacroArg = false; + MacroExpansionKind kind = MacroExpansionKind::Body; std::string_view macroName; + MacroExpansionMetadata metadata; 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) {} + + 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. @@ -318,6 +388,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/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 3cba1524..0b4501bf 100644 --- a/crates/slang/source/parsing/Preprocessor.cpp +++ b/crates/slang/source/parsing/Preprocessor.cpp @@ -24,21 +24,26 @@ 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) { 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 +126,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,8 +217,73 @@ 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::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(); + 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, true); +} + +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() { @@ -677,8 +749,11 @@ 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); } diff --git a/crates/slang/source/parsing/PreprocessorTrace.cpp b/crates/slang/source/parsing/PreprocessorTrace.cpp new file mode 100644 index 00000000..e73f386e --- /dev/null +++ b/crates/slang/source/parsing/PreprocessorTrace.cpp @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +// 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, 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) { + 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 diff --git a/crates/slang/source/parsing/Preprocessor_macros.cpp b/crates/slang/source/parsing/Preprocessor_macros.cpp index 8d83e526..2911a0f2 100644 --- a/crates/slang/source/parsing/Preprocessor_macros.cpp +++ b/crates/slang/source/parsing/Preprocessor_macros.cpp @@ -51,6 +51,7 @@ 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 @@ -93,9 +94,16 @@ 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(); + + 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 @@ -142,6 +150,33 @@ 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 provenance = sourceManager.getMacroTokenProvenance(loc); + auto originalLoc = loc; + auto expansionRange = opToken.range(); + if (sourceManager.isMacroLoc(loc)) { + originalLoc = sourceManager.getOriginalLoc(loc); + expansionRange = sourceManager.getExpansionRange(loc); + } + + 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); + }; + for (size_t i = 0; i < tokens.size(); i++) { Token newToken; bool nextDidConcat = false; @@ -167,6 +202,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 +220,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 +253,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 +293,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 +312,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 +375,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 @@ -362,22 +410,38 @@ 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(), - macroName); + SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, + macroName, + expansion.getMetadata()); + expansion.setExpansionLoc(expansionLoc); // 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, expansionRange, false, + expansion.tokenProvenance(bodyTokenIndex)); + bodyTokenIndex++; + } return true; } @@ -394,7 +458,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; @@ -419,32 +488,31 @@ 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(); - SourceRange expansionRange(expansion.getRange().start(), - endOfArgs.location() + endOfArgs.rawText().length()); - SourceLocation start = body[0].location(); SourceLocation expansionLoc = sourceManager.createExpansionLoc(start, expansionRange, - macroName); + macroName, + expansion.getMetadata()); + expansion.setExpansionLoc(expansionLoc); - 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(); @@ -454,7 +522,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.: @@ -467,14 +535,14 @@ 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. 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; @@ -488,7 +556,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; @@ -502,7 +570,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. @@ -521,16 +592,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); @@ -538,16 +613,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 @@ -555,13 +641,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; } @@ -575,7 +661,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; } @@ -583,7 +669,7 @@ bool Preprocessor::expandMacro(MacroDef macro, MacroExpansion& expansion, } } - if (!handleToken(token)) + if (!handleToken(token, bodyTokenIndex)) return false; } @@ -594,6 +680,22 @@ 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; + 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 { @@ -602,7 +704,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); @@ -610,13 +713,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()); @@ -635,12 +740,14 @@ 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)); } } bool Preprocessor::expandReplacementList( - std::span& tokens, SmallSet& alreadyExpanded) { + std::span& tokens, SmallSet& alreadyExpanded, + uint32_t parentExpansionId) { SmallVector outBuffer; SmallVector expansionBuffer; @@ -680,9 +787,29 @@ 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())) { + 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)) return false; + recordMacroUsageTrace(token, actualArgs, macro, metadata, expansion.getExpansionId()); // Recursively expand out nested macros; this ensures that we detect // any potentially recursive macros. @@ -704,6 +831,22 @@ 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()); + 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: { @@ -714,7 +857,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, false, provenance); break; } case MacroIntrinsic::Line: { @@ -723,7 +866,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, false, provenance); break; } case MacroIntrinsic::None: @@ -733,6 +876,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; } 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, diff --git a/crates/slang/source/text/SourceManager.cpp b/crates/slang/source/text/SourceManager.cpp index cd99c0f2..fe644bc5 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(); } @@ -233,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)) @@ -272,21 +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, + MacroExpansionMetadata metadata) { + std::unique_lock lock(mutex); + + 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); @@ -654,7 +723,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 diff --git a/crates/utils/src/uniq_vec.rs b/crates/utils/src/uniq_vec.rs index 40896c76..5343a1bf 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() } @@ -25,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) } @@ -41,6 +58,10 @@ impl UniqVec { self.items.is_empty() } + pub fn as_slice(&self) -> &[T] { + &self.items + } + pub fn into_vec(self) -> Vec { self.items } @@ -51,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) + } +} diff --git a/docs/src/content/docs/advanced-guide/advanced-installation.md b/docs/src/content/docs/advanced-guide/advanced-installation.md index ea773d59..d515f84f 100644 --- a/docs/src/content/docs/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/advanced-guide/advanced-installation.md @@ -78,42 +78,43 @@ npm run compile 1. 清理 `out` 和 `dist`,并执行 TypeScript typecheck。 2. 用 esbuild 把 `src/extension.ts` 打包到 `dist/extension.js`。 -3. 把诊断性能分析视图需要的 Speedscope 静态资源复制到 `dist/speedscope`。 +3. 默认不复制诊断性能分析用的 Speedscope 静态资源。 ### 打包 VS Code 扩展为 VSIX 如果你只想在本机调试,或者要打包一个带调试信息的 VSIX,在 `editors/vscode` 下运行: ```bash -npm run package:debug +npm run package:vsix:debug ``` 这个命令会: 1. 编译扩展,所以前面没手动执行 `npm run compile` 也可以。 -2. 针对当前宿主平台执行 `cargo build`。 -3. 把 `target/debug/vide` 或 `vide.exe` 复制到扩展的 `server/` 目录。 -4. 临时把服务器二进制放到运行时 `server` 目录。 -5. 调用 `vsce package --target ` 生成 `vide-vscode--debug.vsix`。 -6. 打包后清理临时运行时二进制。 +2. 复制诊断性能分析视图需要的 Speedscope 静态资源,并启用 `profile-trace` server feature。 +3. 通过 `cargo xtask vscode prepare-server` 针对当前宿主平台准备 debug 版语言服务器。 +4. 把 `target/debug/vide` 或 `vide.exe` 复制到扩展的 `server/` 目录。 +5. 临时把服务器二进制放到运行时 `server` 目录。 +6. 调用 `vsce package --target ` 生成 `vide-vscode--debug.vsix`。 +7. 打包后清理临时运行时二进制。 如果你要打包能安装特定平台发布版 Vide 的 VSIX,可以运行以下一个或多个命令: ```bash -npm run package:linux-x64 -npm run package:linux-arm64 -npm run package:win32-x64 -npm run package:darwin-arm64 -npm run package:alpine-x64 -npm run package:alpine-arm64 +npm run package:vsix -- --target linux-x64 +npm run package:vsix -- --target linux-arm64 +npm run package:vsix -- --target win32-x64 +npm run package:vsix -- --target darwin-arm64 +npm run package:vsix -- --target alpine-x64 +npm run package:vsix -- --target alpine-arm64 ``` -这些脚本会先编译扩展,然后准备目标平台的 release 版语言服务器,再生成 `vide-vscode-.vsix`。当前 release workflow 只覆盖上面这些目标:glibc Linux、Windows x64、macOS arm64,以及 Alpine/musl x64 和 arm64。 -这几项也是当前 CI 会实际构建的 VSIX 目标。其他平台即使在 `package.json` 里有脚本入口,也不表示它们在本地或当前 workflow 里一定能直接打包成功。 +这些脚本会先编译扩展,然后准备目标平台的 release 版语言服务器,再生成 `vide-vscode-.vsix`。release 包默认不启用 profile trace,也不包含 Speedscope 静态资源或 profiling 命令。当前 release workflow 只覆盖上面这些目标:glibc Linux、Windows x64、macOS arm64,以及 Alpine/musl x64 和 arm64。 +这几项也是当前 CI 会实际构建的 VSIX 目标。其他平台不是当前支持的打包目标。 -上面的打包命令都需要先准备目标平台的语言服务器二进制;这一步的具体规则由 `editors/vscode/scripts/package.ts` 决定: +上面的打包命令都需要先准备目标平台的语言服务器二进制;`editors/vscode/scripts/package.ts` 会调用 `cargo xtask vscode prepare-server`,而通用的 server 构建规则由 `cargo xtask server build` 承载: -- 目标等于当前宿主平台时,脚本执行 `cargo build --release` 并复制产物。 +- 目标等于当前宿主平台时,xtask 执行对应 profile 的 `cargo build` 并复制产物。 - Alpine 目标在 CI 的 musl 容器中构建;本地脚本会添加对应 Rust musl target,但仍需要可用的 musl 交叉编译环境。 - 其他非宿主平台目标不会自动交叉编译语言服务器,需要 `editors/vscode/server//` 下已经存在对应的 `vide` 或 `vide.exe`,或者在匹配的原生 runner 上打包。 diff --git a/docs/src/content/docs/advanced-guide/troubleshooting.md b/docs/src/content/docs/advanced-guide/troubleshooting.md index 1bd5c072..c2788e51 100644 --- a/docs/src/content/docs/advanced-guide/troubleshooting.md +++ b/docs/src/content/docs/advanced-guide/troubleshooting.md @@ -42,7 +42,7 @@ description: 报告 Vide 故障,并按常见症状处理启动和文件刷新 常见问题是: - `Bundled Vide Language Server binary not found` 或 `Unsupported platform-architecture combination`: - 先核对安装的 VSIX 和当前平台是否匹配。如果你是通过本地打包安装的 VSIX,需要确认打包时运行的是 `npm run package:*` 或 `npm run package:debug`。这些命令会把语言服务器二进制打进 VSIX;单独执行 `npm run compile` 只会编译扩展前端,安装后会缺少服务器。 + 先核对安装的 VSIX 和当前平台是否匹配。如果你是通过本地打包安装的 VSIX,需要确认打包时运行的是 `npm run package:vsix` 或 `npm run package:vsix:debug`。这些命令会把语言服务器二进制打进 VSIX;单独执行 `npm run compile` 只会编译扩展前端,安装后会缺少服务器。 - `Failed to start language server`、自定义命令不存在、无执行权限: 继续看下面的“扩展或自定义服务器无法启动”。 - 状态栏只是提示 `vide.toml`、`manifest` 或 `failed to load workspace`: diff --git a/docs/src/content/docs/en/advanced-guide/advanced-installation.md b/docs/src/content/docs/en/advanced-guide/advanced-installation.md index a0575034..798839c9 100644 --- a/docs/src/content/docs/en/advanced-guide/advanced-installation.md +++ b/docs/src/content/docs/en/advanced-guide/advanced-installation.md @@ -92,42 +92,43 @@ npm run compile 1. Removes `out` and `dist`, then runs the TypeScript typecheck. 2. Bundles `src/extension.ts` into `dist/extension.js` with esbuild. -3. Copies the speedscope static assets required by the diagnostics profiling view into `dist/speedscope`. +3. Does not copy the Speedscope static assets used by diagnostics profiling by default. ### Package the VS Code Extension as a VSIX If you want a local debug build or a VSIX with debug binaries, run this under `editors/vscode`: ```powershell -npm run package:debug +npm run package:vsix:debug ``` This command: 1. Compiles the extension, so it is fine if you did not run `npm run compile` manually first. -2. Runs `cargo build` for the current host platform. -3. Copies `target/debug/vide` or `vide.exe` into the extension's `server/` directory. -4. Temporarily stages the server binary in the runtime `server` directory. -5. Calls `vsce package --target ` to generate `vide-vscode--debug.vsix`. -6. Cleans up the temporary runtime binary after packaging. +2. Copies the Speedscope static assets required by diagnostics profiling and enables the `profile-trace` server feature. +3. Uses `cargo xtask vscode prepare-server` to prepare a debug server for the current host platform. +4. Copies `target/debug/vide` or `vide.exe` into the extension's `server/` directory. +5. Temporarily stages the server binary in the runtime `server` directory. +6. Calls `vsce package --target ` to generate `vide-vscode--debug.vsix`. +7. Cleans up the temporary runtime binary after packaging. If you want a release VSIX for a specific platform, run one or more of these commands: ```powershell -npm run package:linux-x64 -npm run package:linux-arm64 -npm run package:win32-x64 -npm run package:darwin-arm64 -npm run package:alpine-x64 -npm run package:alpine-arm64 +npm run package:vsix -- --target linux-x64 +npm run package:vsix -- --target linux-arm64 +npm run package:vsix -- --target win32-x64 +npm run package:vsix -- --target darwin-arm64 +npm run package:vsix -- --target alpine-x64 +npm run package:vsix -- --target alpine-arm64 ``` -These scripts compile the extension, prepare a release server binary for the target platform, and generate `vide-vscode-.vsix`. The current release workflow only covers those targets: glibc Linux, Windows x64, macOS arm64, and Alpine/musl x64 and arm64. -Those are also the VSIX targets currently built by CI. Even if `package.json` contains script entries for other platforms, that does not mean they can be packaged directly in a local environment or in the current workflows. +These scripts compile the extension, prepare a release server binary for the target platform, and generate `vide-vscode-.vsix`. Release packages do not enable profile trace, and they do not include Speedscope static assets or the profiling command by default. The current release workflow only covers those targets: glibc Linux, Windows x64, macOS arm64, and Alpine/musl x64 and arm64. +Those are also the VSIX targets currently built by CI. Other platforms are not current packaging targets. -All packaging commands above need to prepare the language server binary for the target platform first. The exact rules for that step are controlled by `editors/vscode/scripts/package.ts`: +All packaging commands above need to prepare the language server binary for the target platform first. `editors/vscode/scripts/package.ts` calls `cargo xtask vscode prepare-server`, and the reusable server build rules live under `cargo xtask server build`: -- When the target matches the current host platform, the script runs `cargo build --release` and copies the result. +- When the target matches the current host platform, xtask runs `cargo build` for the selected profile and copies the result. - Alpine targets are built in musl containers in CI. The local script adds the matching Rust musl target, but still needs a working musl cross-compilation environment. - Other non-host targets are not automatically cross-compiled; the matching `vide` or `vide.exe` must already exist under `editors/vscode/server//`, or you should package on a matching native runner. diff --git a/docs/src/content/docs/en/advanced-guide/troubleshooting.md b/docs/src/content/docs/en/advanced-guide/troubleshooting.md index 5893d01b..050ac523 100644 --- a/docs/src/content/docs/en/advanced-guide/troubleshooting.md +++ b/docs/src/content/docs/en/advanced-guide/troubleshooting.md @@ -42,7 +42,7 @@ Open the `Vide Language Server` output channel first. Focus on the last error, a Common branches are: - `Bundled Vide Language Server binary not found` or `Unsupported platform-architecture combination`: - first confirm that the installed VSIX matches the current platform. If you installed a locally packaged VSIX, confirm that it was built with `npm run package:*` or `npm run package:debug`. Those commands bundle the language server binary into the VSIX; `npm run compile` only builds the extension frontend, so the installed extension will not contain the server. + first confirm that the installed VSIX matches the current platform. If you installed a locally packaged VSIX, confirm that it was built with `npm run package:vsix` or `npm run package:vsix:debug`. Those commands bundle the language server binary into the VSIX; `npm run compile` only builds the extension frontend, so the installed extension will not contain the server. - `Failed to start language server`, missing custom command, or permission failure: continue with "The Extension or Custom Server Cannot Start" below. - The status bar only mentions `vide.toml`, `manifest`, or `failed to load workspace`: diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 151aab95..a239327c 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, @@ -408,7 +413,7 @@ } }, "scripts": { - "bundle:node": "esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --outfile=dist/extension.js && npm run copy-speedscope", + "bundle:node": "esbuild src/extension.ts --bundle --platform=node --target=node22 --format=cjs --external:vscode --outfile=dist/extension.js", "bundle:browser:extension": "esbuild src/browser/extension.ts --bundle --platform=browser --target=es2022 --format=cjs --external:vscode --outfile=dist/browser/extension.js", "bundle:browser:worker": "esbuild src/browser/worker.ts --bundle --platform=browser --target=es2022 --format=iife --outfile=dist/browser/vide-lsp.worker.js", "bundle:browser": "npm run bundle:browser:extension && npm run bundle:browser:worker && npm run copy-web-assets", @@ -418,19 +423,13 @@ "copy-web-assets": "tsx scripts/copy-web-assets.ts", "typecheck": "tsc -p . --noEmit && tsc -p tsconfig.browser.json --noEmit && tsc -p tsconfig.test-web.json --noEmit", "compile": "npm run clean && npm run typecheck && npm run bundle:node", + "compile:profile-trace": "npm run clean && npm run typecheck && npm run bundle:node && npm run copy-speedscope", "compile:web": "npm run clean && npm run typecheck && npm run bundle:node && npm run bundle:browser", "test": "npm run compile && node --import tsx --test \"test/**/*.test.ts\"", "test:web": "npm run bundle:browser && npm run bundle:test-web && npm --prefix test-web test", - "package:debug": "npm run compile && tsx scripts/package.ts --debug", - "package:alpine-arm64": "npm run compile && tsx scripts/package.ts alpine-arm64", - "package:alpine-x64": "npm run compile && tsx scripts/package.ts alpine-x64", - "package:darwin-arm64": "npm run compile && tsx scripts/package.ts darwin-arm64", - "package:darwin-x64": "npm run compile && tsx scripts/package.ts darwin-x64", - "package:linux-x64": "npm run compile && tsx scripts/package.ts linux-x64", - "package:linux-arm64": "npm run compile && tsx scripts/package.ts linux-arm64", - "package:web": "npm run compile:web && tsx scripts/package.ts web", - "package:win32-x64": "npm run compile && tsx scripts/package.ts win32-x64", - "package:win32-arm64": "npm run compile && tsx scripts/package.ts win32-arm64", + "package:vsix": "npm run compile && tsx scripts/package.ts", + "package:vsix:debug": "npm run compile:profile-trace && tsx scripts/package.ts --profile debug --profile-trace", + "package:vsix:web": "npm run compile:web && tsx scripts/package.ts --target web", "install-extension": "tsx scripts/install-extension.ts" }, "dependencies": { 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/scripts/install-extension.ts b/editors/vscode/scripts/install-extension.ts index 60d1ec3c..06b6f59e 100644 --- a/editors/vscode/scripts/install-extension.ts +++ b/editors/vscode/scripts/install-extension.ts @@ -48,7 +48,7 @@ function main(): void { if (vsixFiles.length === 0) { throw new Error( - 'No matching VSIX found. Run `npm run package:debug` first to create one, then rerun this command.', + 'No matching VSIX found. Run `npm run package:vsix:debug` first to create one, then rerun this command.', ); } diff --git a/editors/vscode/scripts/package.ts b/editors/vscode/scripts/package.ts index 0c01773e..8c6a9be0 100644 --- a/editors/vscode/scripts/package.ts +++ b/editors/vscode/scripts/package.ts @@ -1,411 +1,10 @@ -import { spawnSync } from 'node:child_process'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -import { - SUPPORTED_PLATFORM_FOLDERS, - type PlatformFolder, - getPlatformFolder, - isPlatformFolder, -} from '../src/platform'; - -const vscodeDir = findExtensionRoot(__dirname); -const repoRoot = path.resolve(vscodeDir, '..', '..'); -const binName = 'vide'; -const webTarget = 'web'; - -type BuildProfile = 'debug' | 'release'; -type ServerMode = 'build' | 'prebuilt'; -type PackageTarget = PlatformFolder | typeof webTarget; - -const cargoTargets: Partial> = { - 'alpine-arm64': 'aarch64-unknown-linux-musl', - 'alpine-x64': 'x86_64-unknown-linux-musl', -}; - -function findExtensionRoot(startDir: string): string { - let currentDir = path.resolve(startDir); - - while (true) { - if ( - fs.existsSync(path.join(currentDir, 'package.json')) && - fs.existsSync(path.join(currentDir, 'language-configuration.json')) - ) { - return currentDir; - } - - const parentDir = path.dirname(currentDir); - if (parentDir === currentDir) { - throw new Error(`could not find VS Code extension root from ${startDir}`); - } - currentDir = parentDir; - } -} - -function hostPlatformFolder(): PlatformFolder { - const folder = getPlatformFolder(process.platform, process.arch); - if (!folder) { - throw new Error(`unsupported host platform: ${process.platform}-${process.arch}`); - } - - return folder; -} - -function binaryFileForTarget(target: PlatformFolder): string { - return target.startsWith('win32-') ? `${binName}.exe` : binName; -} - -function run( - command: string, - args: string[], - cwd: string, - env: NodeJS.ProcessEnv = process.env, -): void { - const result = spawnSync(command, args, { - cwd, - env, - shell: false, - stdio: 'inherit', - }); - - if (result.error) { - throw result.error; - } - - if (result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); - } -} - -function sanitizedVsceEnv(): NodeJS.ProcessEnv { - const env = { ...process.env }; - - for (const key of Object.keys(env)) { - const normalized = key.toLowerCase(); - if ( - normalized === 'npm_config_verify_deps_before_run' || - normalized === 'npm_config_npm_globalconfig' || - normalized === 'npm_config__jsr_registry' - ) { - delete env[key]; - } - } - - return env; -} - -function cargoProfileDir(profile: BuildProfile): string { - return profile === 'release' ? 'release' : 'debug'; -} - -function cargoBuildArgs(profile: BuildProfile, cargoTarget?: string): string[] { - const args = ['build']; - if (profile === 'release') { - args.push('--release'); - } - if (cargoTarget) { - args.push('--target', cargoTarget); - } - - return args; -} - -function cargoTargetEnvName(cargoTarget: string): string { - return cargoTarget.toUpperCase().replace(/-/g, '_'); -} - -function cargoTargetLinkerEnvKey(cargoTarget: string): string { - return `CARGO_TARGET_${cargoTargetEnvName(cargoTarget)}_LINKER`; -} - -function cxxCompilerEnvKey(cargoTarget: string): string { - return `CXX_${cargoTarget.replace(/-/g, '_')}`; -} - -function cargoLinkerForTarget(target: PlatformFolder, cargoTarget: string): string | undefined { - if (!target.startsWith('alpine-')) { - return undefined; - } - - return ( - optionalEnv(cxxCompilerEnvKey(cargoTarget)) ?? - optionalEnv('TARGET_CXX') ?? - `${cargoTarget}-g++` - ); -} - -function lateRustLinkFlagsForTarget(target: PlatformFolder): string[] { - if (!target.startsWith('alpine-')) { - return []; - } - - // Static libstdc++ can introduce libc references after rustc's own musl -lc. - return ['-C', 'link-arg=-lc']; -} - -function appendRustFlags(env: NodeJS.ProcessEnv, flags: string[]): NodeJS.ProcessEnv { - if (flags.length === 0) { - return env; - } - - const encodedFlags = env.CARGO_ENCODED_RUSTFLAGS; - if (encodedFlags) { - return { - ...env, - CARGO_ENCODED_RUSTFLAGS: `${encodedFlags}\x1f${flags.join('\x1f')}`, - }; - } - - const rustFlags = env.RUSTFLAGS?.trim(); - return { - ...env, - RUSTFLAGS: rustFlags ? `${rustFlags} ${flags.join(' ')}` : flags.join(' '), - }; -} - -function cargoBuildEnv(target: PlatformFolder, cargoTarget?: string): NodeJS.ProcessEnv { - if (!cargoTarget) { - return process.env; - } - - let env = process.env; - const linkerEnvKey = cargoTargetLinkerEnvKey(cargoTarget); - if (!optionalEnv(linkerEnvKey)) { - const linker = cargoLinkerForTarget(target, cargoTarget); - if (linker) { - console.log(`Using Cargo linker for ${cargoTarget}: ${linker}`); - env = { ...env, [linkerEnvKey]: linker }; - } - } - - const lateLinkArgs = lateRustLinkFlagsForTarget(target); - if (lateLinkArgs.length > 0) { - console.log(`Adding Cargo link args for ${cargoTarget}: ${lateLinkArgs.join(' ')}`); - env = appendRustFlags(env, lateLinkArgs); - } - - return env; -} - -function cargoOutputDir(profile: BuildProfile, cargoTarget?: string): string { - const pathParts = [repoRoot, 'target']; - if (cargoTarget) { - pathParts.push(cargoTarget); - } - pathParts.push(cargoProfileDir(profile)); - - return path.join(...pathParts); -} - -function ensureServerExecutable(serverPath: string, target: PlatformFolder): void { - if (!target.startsWith('win32-')) { - fs.chmodSync(serverPath, 0o755); - } -} - -function ensureTargetServerBinary( - target: PlatformFolder, - binFile: string, - profile: BuildProfile, - serverMode: ServerMode, -): string { - const serverOutDir = path.join(vscodeDir, 'server', target); - const serverPath = path.join(serverOutDir, binFile); - if (serverMode === 'prebuilt') { - if (fs.existsSync(serverPath)) { - ensureServerExecutable(serverPath, target); - return serverPath; - } - throw new Error(`missing prebuilt server binary: ${serverPath}`); - } - - const hostTarget = hostPlatformFolder(); - const cargoTarget = cargoTargets[target]; - if (target !== hostTarget && !cargoTarget) { - throw new Error( - `missing bundled server binary: ${serverPath}\n` + - 'tip: run packaging on a matching native runner or copy the target binary first.', - ); - } - - if (cargoTarget) { - run('rustup', ['target', 'add', cargoTarget], repoRoot); - } - - run( - 'cargo', - cargoBuildArgs(profile, cargoTarget), - repoRoot, - cargoBuildEnv(target, cargoTarget), - ); - - const sourcePath = path.join(cargoOutputDir(profile, cargoTarget), binFile); - const destPath = path.join(serverOutDir, binFile); - fs.mkdirSync(serverOutDir, { recursive: true }); - fs.copyFileSync(sourcePath, destPath); - ensureServerExecutable(destPath, target); - - return destPath; -} - -function stageRuntimeServer(sourcePath: string, target: PlatformFolder, binFile: string): string { - const runtimeServerDir = path.join(vscodeDir, 'server'); - const runtimeServerPath = path.join(runtimeServerDir, binFile); - - fs.mkdirSync(runtimeServerDir, { recursive: true }); - fs.copyFileSync(sourcePath, runtimeServerPath); - if (!target.startsWith('win32-')) { - fs.chmodSync(runtimeServerPath, 0o755); - } - - return runtimeServerPath; -} - -function cleanRuntimeServerFiles(): void { - for (const binFile of [`${binName}.exe`, binName]) { - fs.rmSync(path.join(vscodeDir, 'server', binFile), { force: true }); - } -} - -function syncReadmeFromRepoRoot(): void { - fs.copyFileSync(path.join(repoRoot, 'README.md'), path.join(vscodeDir, 'README.md')); -} - -function readExtensionVersion(): string { - const packageJson = JSON.parse(fs.readFileSync(path.join(vscodeDir, 'package.json'), 'utf8')) as { - version?: unknown; - }; - if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) { - throw new Error('VS Code extension package.json must define a version.'); - } - return packageJson.version; -} - -function optionalEnv(name: string): string | undefined { - const value = process.env[name]?.trim(); - return value ? value : undefined; -} - -function writeBuildInfo(target: PackageTarget, profile: BuildProfile): void { - const buildInfo = { - version: readExtensionVersion(), - target, - profile, - kind: optionalEnv('VIDE_EXTENSION_BUILD_KIND') ?? 'local', - commitHash: optionalEnv('VIDE_EXTENSION_COMMIT_HASH'), - buildDate: optionalEnv('VIDE_EXTENSION_BUILD_DATE'), - }; - fs.writeFileSync( - path.join(vscodeDir, 'build-info.json'), - `${JSON.stringify(buildInfo, null, 2)}\n`, - ); -} - -function packageJsonPath(): string { - return path.join(vscodeDir, 'package.json'); -} - -function stagePackageJsonForTarget(target: PackageTarget): string | undefined { - if (target === webTarget) { - return undefined; - } - - const packagePath = packageJsonPath(); - const originalPackageJson = fs.readFileSync(packagePath, 'utf8'); - const packageJson = JSON.parse(originalPackageJson) as { browser?: unknown }; - delete packageJson.browser; - fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); - return originalPackageJson; -} - -function parseServerMode(value: string): ServerMode { - if (value === 'build' || value === 'prebuilt') { - return value; - } - throw new Error(`unsupported server mode: ${value}`); -} - -function parseArgs(): { target: PackageTarget; profile: BuildProfile; serverMode: ServerMode } { - const args = process.argv.slice(2); - let profile: BuildProfile = 'release'; - let serverMode: ServerMode = 'build'; - let target: string | undefined; - - for (const arg of args) { - if (arg === '--debug') { - profile = 'debug'; - } else if (arg.startsWith('--server=')) { - serverMode = parseServerMode(arg.slice('--server='.length)); - } else if (!target) { - target = arg; - } else { - throw new Error(`unexpected package argument: ${arg}`); - } - } - - target ??= hostPlatformFolder(); - if (target === webTarget) { - return { target, profile, serverMode }; - } - if (!isPlatformFolder(target)) { - throw new Error( - `unsupported target platform: ${target}\n` + - `supported targets: ${[...SUPPORTED_PLATFORM_FOLDERS, webTarget].join(', ')}`, - ); - } - - return { target, profile, serverMode }; -} - -function packageExtension( - target: PackageTarget, - profile: BuildProfile, - serverMode: ServerMode, -): string { - syncReadmeFromRepoRoot(); - writeBuildInfo(target, profile); - - const debugSuffix = profile === 'debug' ? '-debug' : ''; - const vsixOut = `vide-vscode-${target}${debugSuffix}.vsix`; - const vsceBin = path.join(vscodeDir, 'node_modules', '@vscode', 'vsce', 'vsce'); - - if (target === webTarget) { - cleanRuntimeServerFiles(); - run( - process.execPath, - [vsceBin, 'package', '--target', target, '--out', vsixOut], - vscodeDir, - sanitizedVsceEnv(), - ); - return path.join(vscodeDir, vsixOut); - } - - const binFile = binaryFileForTarget(target); - const targetServerPath = ensureTargetServerBinary(target, binFile, profile, serverMode); - cleanRuntimeServerFiles(); - const runtimeServerPath = stageRuntimeServer(targetServerPath, target, binFile); - const originalPackageJson = stagePackageJsonForTarget(target); - - try { - run( - process.execPath, - [vsceBin, 'package', '--target', target, '--out', vsixOut], - vscodeDir, - sanitizedVsceEnv(), - ); - } finally { - fs.rmSync(runtimeServerPath, { force: true }); - if (originalPackageJson) { - fs.writeFileSync(packageJsonPath(), originalPackageJson); - } - } - - return path.join(vscodeDir, vsixOut); -} +import { parsePackageCliArgs } from './package/cli'; +import { createPackageContext } from './package/context'; +import { packageExtension } from './package/packageExtension'; function main(): void { - const { target, profile, serverMode } = parseArgs(); - const vsixPath = packageExtension(target, profile, serverMode); + const options = parsePackageCliArgs(process.argv.slice(2)); + const vsixPath = packageExtension(options, createPackageContext(__dirname)); console.log(vsixPath); } diff --git a/editors/vscode/scripts/package/cli.ts b/editors/vscode/scripts/package/cli.ts new file mode 100644 index 00000000..de7c0e15 --- /dev/null +++ b/editors/vscode/scripts/package/cli.ts @@ -0,0 +1,51 @@ +import { + type BuildProfile, + type PackageOptions, + type ServerMode, + parseBuildProfile, + parseServerMode, +} from './targets'; + +export function parsePackageCliArgs(args: string[]): PackageOptions { + let profile: BuildProfile = 'release'; + let serverMode: ServerMode = 'build'; + let profileTrace = false; + let target: string | undefined; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === '--debug') { + profile = 'debug'; + } else if (arg === '--release') { + profile = 'release'; + } else if (arg === '--profile-trace') { + profileTrace = true; + } else if (arg === '--target') { + target = readFlagValue(args, ++index, arg); + } else if (arg.startsWith('--target=')) { + target = arg.slice('--target='.length); + } else if (arg === '--profile') { + profile = parseBuildProfile(readFlagValue(args, ++index, arg)); + } else if (arg.startsWith('--profile=')) { + profile = parseBuildProfile(arg.slice('--profile='.length)); + } else if (arg === '--server') { + serverMode = parseServerMode(readFlagValue(args, ++index, arg)); + } else if (arg.startsWith('--server=')) { + serverMode = parseServerMode(arg.slice('--server='.length)); + } else if (!arg.startsWith('-') && !target) { + target = arg; + } else { + throw new Error(`unexpected package argument: ${arg}`); + } + } + + return { target, profile, serverMode, profileTrace }; +} + +function readFlagValue(args: string[], index: number, flag: string): string { + const value = args[index]; + if (!value || value.startsWith('-')) { + throw new Error(`missing value for ${flag}`); + } + return value; +} diff --git a/editors/vscode/scripts/package/context.ts b/editors/vscode/scripts/package/context.ts new file mode 100644 index 00000000..e76478c6 --- /dev/null +++ b/editors/vscode/scripts/package/context.ts @@ -0,0 +1,34 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export interface PackageContext { + vscodeDir: string; + repoRoot: string; +} + +export function createPackageContext(startDir: string = __dirname): PackageContext { + const vscodeDir = findExtensionRoot(startDir); + return { + vscodeDir, + repoRoot: path.resolve(vscodeDir, '..', '..'), + }; +} + +export function findExtensionRoot(startDir: string): string { + let currentDir = path.resolve(startDir); + + while (true) { + if ( + fs.existsSync(path.join(currentDir, 'package.json')) && + fs.existsSync(path.join(currentDir, 'language-configuration.json')) + ) { + return currentDir; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + throw new Error(`could not find VS Code extension root from ${startDir}`); + } + currentDir = parentDir; + } +} diff --git a/editors/vscode/scripts/package/manifest.ts b/editors/vscode/scripts/package/manifest.ts new file mode 100644 index 00000000..190fee46 --- /dev/null +++ b/editors/vscode/scripts/package/manifest.ts @@ -0,0 +1,94 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { optionalEnv } from './process'; +import type { PackagePlan } from './targets'; + +export function syncReadmeFromRepoRoot(context: PackageContext): void { + fs.copyFileSync( + path.join(context.repoRoot, 'README.md'), + path.join(context.vscodeDir, 'README.md'), + ); +} + +export function writeBuildInfo(context: PackageContext, plan: PackagePlan): void { + const buildInfo = { + version: readExtensionVersion(context), + target: plan.target, + profile: plan.profile, + profileTrace: plan.profileTrace, + kind: optionalEnv('VIDE_EXTENSION_BUILD_KIND') ?? 'local', + commitHash: optionalEnv('VIDE_EXTENSION_COMMIT_HASH'), + buildDate: optionalEnv('VIDE_EXTENSION_BUILD_DATE'), + }; + fs.writeFileSync( + path.join(context.vscodeDir, 'build-info.json'), + `${JSON.stringify(buildInfo, null, 2)}\n`, + ); +} + +export function stagePackageJsonForTarget( + context: PackageContext, + plan: PackagePlan, +): string | undefined { + if (!plan.targetSpec.removeBrowserEntry && plan.profileTrace) { + return undefined; + } + + const packagePath = packageJsonPath(context); + const originalPackageJson = fs.readFileSync(packagePath, 'utf8'); + const packageJson = JSON.parse(originalPackageJson) as { + browser?: unknown; + contributes?: { commands?: Array<{ command?: unknown }> }; + }; + if (plan.targetSpec.removeBrowserEntry) { + delete packageJson.browser; + } + if (!plan.profileTrace) { + packageJson.contributes = packageJson.contributes ?? {}; + packageJson.contributes.commands = (packageJson.contributes.commands ?? []).filter( + (command) => command.command !== 'vide.profileDiagnostics', + ); + } + fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); + return originalPackageJson; +} + +export function stageProfileTraceAssets(context: PackageContext, plan: PackagePlan): void { + const speedscopeDir = path.join(context.vscodeDir, 'dist', 'speedscope'); + if (!plan.profileTrace) { + fs.rmSync(speedscopeDir, { recursive: true, force: true }); + return; + } + + const indexPath = path.join(speedscopeDir, 'index.html'); + if (!fs.existsSync(indexPath)) { + throw new Error( + `profile trace assets not found at ${speedscopeDir}; run npm run compile:profile-trace first`, + ); + } +} + +export function restorePackageJson( + context: PackageContext, + originalPackageJson: string | undefined, +): void { + if (originalPackageJson) { + fs.writeFileSync(packageJsonPath(context), originalPackageJson); + } +} + +function packageJsonPath(context: PackageContext): string { + return path.join(context.vscodeDir, 'package.json'); +} + +function readExtensionVersion(context: PackageContext): string { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath(context), 'utf8')) as { + version?: unknown; + }; + if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) { + throw new Error('VS Code extension package.json must define a version.'); + } + return packageJson.version; +} diff --git a/editors/vscode/scripts/package/packageExtension.ts b/editors/vscode/scripts/package/packageExtension.ts new file mode 100644 index 00000000..55298aaa --- /dev/null +++ b/editors/vscode/scripts/package/packageExtension.ts @@ -0,0 +1,51 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { type PackageContext, createPackageContext } from './context'; +import { + restorePackageJson, + stageProfileTraceAssets, + stagePackageJsonForTarget, + syncReadmeFromRepoRoot, + writeBuildInfo, +} from './manifest'; +import { cleanRuntimeServerFiles, ensureTargetServerBinary, stageRuntimeServer } from './server'; +import { type PackageOptions, createPackagePlan } from './targets'; +import { runVscePackage } from './vsce'; + +export function packageExtension( + options: PackageOptions, + context: PackageContext = createPackageContext(), +): string { + const plan = createPackagePlan(options); + + syncReadmeFromRepoRoot(context); + writeBuildInfo(context, plan); + stageProfileTraceAssets(context, plan); + + if (plan.targetSpec.kind === 'web') { + cleanRuntimeServerFiles(context); + runVscePackage(context, plan); + return path.join(context.vscodeDir, plan.vsixFile); + } + + const targetServerPath = ensureTargetServerBinary( + context, + plan.targetSpec, + plan.profile, + plan.serverMode, + plan.profileTrace, + ); + cleanRuntimeServerFiles(context); + const runtimeServerPath = stageRuntimeServer(context, targetServerPath, plan.targetSpec); + const originalPackageJson = stagePackageJsonForTarget(context, plan); + + try { + runVscePackage(context, plan); + } finally { + fs.rmSync(runtimeServerPath, { force: true }); + restorePackageJson(context, originalPackageJson); + } + + return path.join(context.vscodeDir, plan.vsixFile); +} diff --git a/editors/vscode/scripts/package/process.ts b/editors/vscode/scripts/package/process.ts new file mode 100644 index 00000000..20abd108 --- /dev/null +++ b/editors/vscode/scripts/package/process.ts @@ -0,0 +1,45 @@ +import { spawnSync } from 'node:child_process'; + +export function run( + command: string, + args: string[], + cwd: string, + env: NodeJS.ProcessEnv = process.env, +): void { + const result = spawnSync(command, args, { + cwd, + env, + shell: false, + stdio: 'inherit', + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); + } +} + +export function optionalEnv(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +export function sanitizedVsceEnv(): NodeJS.ProcessEnv { + const env = { ...process.env }; + + for (const key of Object.keys(env)) { + const normalized = key.toLowerCase(); + if ( + normalized === 'npm_config_verify_deps_before_run' || + normalized === 'npm_config_npm_globalconfig' || + normalized === 'npm_config__jsr_registry' + ) { + delete env[key]; + } + } + + return env; +} diff --git a/editors/vscode/scripts/package/server.ts b/editors/vscode/scripts/package/server.ts new file mode 100644 index 00000000..3908d884 --- /dev/null +++ b/editors/vscode/scripts/package/server.ts @@ -0,0 +1,65 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { run } from './process'; +import type { BuildProfile, NativeTargetSpec, ServerMode } from './targets'; + +export function ensureTargetServerBinary( + context: PackageContext, + spec: NativeTargetSpec, + profile: BuildProfile, + serverMode: ServerMode, + profileTrace: boolean, +): string { + const args = [ + 'xtask', + 'vscode', + 'prepare-server', + '--target', + spec.target, + '--profile', + profile, + '--server', + serverMode, + ]; + if (profileTrace) { + args.push('--profile-trace'); + } + + const serverPath = targetServerPath(context, spec); + run('cargo', args, context.repoRoot); + + if (!fs.existsSync(serverPath)) { + throw new Error(`prepared server binary was not found: ${serverPath}`); + } + + return serverPath; +} + +export function stageRuntimeServer( + context: PackageContext, + sourcePath: string, + spec: NativeTargetSpec, +): string { + const runtimeServerDir = path.join(context.vscodeDir, 'server'); + const runtimeServerPath = path.join(runtimeServerDir, spec.binaryFile); + + fs.mkdirSync(runtimeServerDir, { recursive: true }); + fs.copyFileSync(sourcePath, runtimeServerPath); + if (!spec.isWindows) { + fs.chmodSync(runtimeServerPath, 0o755); + } + + return runtimeServerPath; +} + +export function cleanRuntimeServerFiles(context: PackageContext): void { + for (const binFile of ['vide.exe', 'vide']) { + fs.rmSync(path.join(context.vscodeDir, 'server', binFile), { force: true }); + } +} + +function targetServerPath(context: PackageContext, spec: NativeTargetSpec): string { + return path.join(context.vscodeDir, 'server', spec.target, spec.binaryFile); +} diff --git a/editors/vscode/scripts/package/targets.ts b/editors/vscode/scripts/package/targets.ts new file mode 100644 index 00000000..c02db90e --- /dev/null +++ b/editors/vscode/scripts/package/targets.ts @@ -0,0 +1,119 @@ +import { + SUPPORTED_PLATFORM_FOLDERS, + type PlatformFolder, + getPlatformFolder, + isPlatformFolder, +} from '../../src/platform'; + +export const WEB_TARGET = 'web'; + +export type BuildProfile = 'debug' | 'release'; +export type ServerMode = 'build' | 'prebuilt'; +export type PackageTarget = PlatformFolder | typeof WEB_TARGET; + +export interface PackageOptions { + target?: string; + profile: BuildProfile; + serverMode: ServerMode; + profileTrace?: boolean; +} + +export interface WebTargetSpec { + kind: 'web'; + target: typeof WEB_TARGET; + removeBrowserEntry: false; +} + +export interface NativeTargetSpec { + kind: 'native'; + target: PlatformFolder; + binaryFile: string; + isWindows: boolean; + removeBrowserEntry: true; +} + +export type TargetSpec = WebTargetSpec | NativeTargetSpec; + +export interface PackagePlan { + target: PackageTarget; + profile: BuildProfile; + serverMode: ServerMode; + profileTrace: boolean; + targetSpec: TargetSpec; + vsixFile: string; +} + +export function createPackagePlan(options: PackageOptions): PackagePlan { + const target = resolvePackageTarget(options.target); + const targetSpec = targetSpecFor(target); + const debugSuffix = options.profile === 'debug' ? '-debug' : ''; + + return { + target, + profile: options.profile, + serverMode: options.serverMode, + profileTrace: options.profileTrace ?? false, + targetSpec, + vsixFile: `vide-vscode-${target}${debugSuffix}.vsix`, + }; +} + +export function parseBuildProfile(value: string): BuildProfile { + if (value === 'debug' || value === 'release') { + return value; + } + throw new Error(`unsupported build profile: ${value}`); +} + +export function parseServerMode(value: string): ServerMode { + if (value === 'build' || value === 'prebuilt') { + return value; + } + throw new Error(`unsupported server mode: ${value}`); +} + +export function hostPlatformFolder(): PlatformFolder { + const folder = getPlatformFolder(process.platform, process.arch); + if (!folder) { + throw new Error(`unsupported host platform: ${process.platform}-${process.arch}`); + } + + return folder; +} + +function resolvePackageTarget(target: string | undefined): PackageTarget { + target ??= hostPlatformFolder(); + if (target === WEB_TARGET) { + return target; + } + if (isPlatformFolder(target)) { + return target; + } + + throw new Error( + `unsupported target platform: ${target}\n` + + `supported targets: ${[...SUPPORTED_PLATFORM_FOLDERS, WEB_TARGET].join(', ')}`, + ); +} + +function targetSpecFor(target: PackageTarget): TargetSpec { + if (target === WEB_TARGET) { + return { + kind: 'web', + target, + removeBrowserEntry: false, + }; + } + + return { + kind: 'native', + target, + binaryFile: binaryFileForTarget(target), + isWindows: target.startsWith('win32-'), + removeBrowserEntry: true, + }; +} + +function binaryFileForTarget(target: PlatformFolder): string { + return target.startsWith('win32-') ? 'vide.exe' : 'vide'; +} diff --git a/editors/vscode/scripts/package/vsce.ts b/editors/vscode/scripts/package/vsce.ts new file mode 100644 index 00000000..a2249d5a --- /dev/null +++ b/editors/vscode/scripts/package/vsce.ts @@ -0,0 +1,15 @@ +import * as path from 'node:path'; + +import type { PackageContext } from './context'; +import { run, sanitizedVsceEnv } from './process'; +import type { PackagePlan } from './targets'; + +export function runVscePackage(context: PackageContext, plan: PackagePlan): void { + const vsceBin = path.join(context.vscodeDir, 'node_modules', '@vscode', 'vsce', 'vsce'); + run( + process.execPath, + [vsceBin, 'package', '--target', plan.target, '--out', plan.vsixFile], + context.vscodeDir, + sanitizedVsceEnv(), + ); +} diff --git a/editors/vscode/src/browser/extension.ts b/editors/vscode/src/browser/extension.ts index 08ebd31b..f792fa72 100644 --- a/editors/vscode/src/browser/extension.ts +++ b/editors/vscode/src/browser/extension.ts @@ -33,6 +33,7 @@ interface ExtensionBuildInfo { kind?: string; commitHash?: string; buildDate?: string; + profileTrace?: boolean; } let client: VideBrowserClient | undefined; @@ -285,10 +286,14 @@ export async function activate( ): Promise { outputChannel = vscode.window.createOutputChannel(languageServerOutputChannelName); context.subscriptions.push(outputChannel); + const buildInfo = await extensionBuildInfo(context); + const profileTraceEnabled = buildInfo?.profileTrace === true; videStatusController = new VideStatusController({ createManifest: (rootUris) => createProjectConfigsFromRootUris(context, rootUris), - profileDiagnostics: () => showUnavailableInBrowser("Diagnostics profiling"), + profileDiagnostics: profileTraceEnabled + ? () => showUnavailableInBrowser("Diagnostics profiling") + : undefined, reloadProject: () => queueRestart(context, "reload project"), restartServer: () => queueRestart(context, "restart command"), showOutput, @@ -321,10 +326,14 @@ export async function activate( vscode.commands.registerCommand(generateQiheOptionsCommand, async () => { await showUnavailableInBrowser("Qihe options generation"); }), - vscode.commands.registerCommand(profileDiagnosticsCommand, async () => { - await showUnavailableInBrowser("Diagnostics profiling"); - }), ); + if (profileTraceEnabled) { + context.subscriptions.push( + vscode.commands.registerCommand(profileDiagnosticsCommand, async () => { + await showUnavailableInBrowser("Diagnostics profiling"); + }), + ); + } registerDiagnosticActions(context); registerWorkspaceWatchers(context); diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index c18ec7e7..8298d194 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -60,6 +60,7 @@ interface ExtensionBuildInfo { kind?: string; commitHash?: string; buildDate?: string; + profileTrace?: boolean; } const activeQiheTokens = new Set(); @@ -122,6 +123,10 @@ function extensionBuildLabel(context: vscode.ExtensionContext): string { return details.length > 0 ? `${version} (${details.join(', ')})` : version; } +function isProfileTraceEnabled(context: vscode.ExtensionContext): boolean { + return extensionBuildInfo(context)?.profileTrace === true; +} + async function showLanguageServerErrorMessage(message: string): Promise { const showOutputAction = vscode.l10n.t('Show Output'); const selection = await vscode.window.showErrorMessage(message, showOutputAction); @@ -981,11 +986,14 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(outputChannel); qiheOutputChannel = vscode.window.createOutputChannel(qiheOutputChannelName); context.subscriptions.push(qiheOutputChannel); + const profileTraceEnabled = isProfileTraceEnabled(context); videStatusController = new VideStatusController({ createManifest: (rootUris) => createProjectConfigsFromRootUris(context, rootUris), - profileDiagnostics: async () => { - await vscode.commands.executeCommand(profileDiagnosticsCommand); - }, + profileDiagnostics: profileTraceEnabled + ? async () => { + await vscode.commands.executeCommand(profileDiagnosticsCommand); + } + : undefined, reloadProject: reloadWorkspace, restartServer: () => restartClient(context), showOutput, @@ -1045,12 +1053,14 @@ export async function activate(context: vscode.ExtensionContext): Promise }), ); - context.subscriptions.push( - registerProfilingCommand(context, { - resolveLaunch: () => resolveServerLaunch(context, readConfiguration()), - createEnv: createServerEnv, - }), - ); + if (profileTraceEnabled) { + context.subscriptions.push( + registerProfilingCommand(context, { + resolveLaunch: () => resolveServerLaunch(context, readConfiguration()), + createEnv: createServerEnv, + }), + ); + } const reloadWorkspaceRegistration = vscode.commands.registerCommand( reloadWorkspaceCommand, diff --git a/editors/vscode/src/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/editors/vscode/src/platform.ts b/editors/vscode/src/platform.ts index 06cc1689..7a8447de 100644 --- a/editors/vscode/src/platform.ts +++ b/editors/vscode/src/platform.ts @@ -4,10 +4,8 @@ export const SUPPORTED_PLATFORM_FOLDERS = [ 'alpine-arm64', 'alpine-x64', 'darwin-arm64', - 'darwin-x64', 'linux-arm64', 'linux-x64', - 'win32-arm64', 'win32-x64', ] as const; diff --git a/editors/vscode/src/videStatus.ts b/editors/vscode/src/videStatus.ts index 690c4ca7..d1bd05b4 100644 --- a/editors/vscode/src/videStatus.ts +++ b/editors/vscode/src/videStatus.ts @@ -23,7 +23,7 @@ export const projectStatusNotification = 'vide/projectStatus'; export interface VideStatusActions { createManifest: (rootUris: readonly string[]) => Promise; - profileDiagnostics: () => Promise; + profileDiagnostics?: () => Promise; reloadProject: () => Promise; restartServer: () => Promise; showOutput: () => void; @@ -104,7 +104,7 @@ export class VideStatusController implements vscode.Disposable { await this.actions.createManifest(status.unconfiguredRootUris); break; case 'profileDiagnostics': - await this.actions.profileDiagnostics(); + await this.actions.profileDiagnostics?.(); break; case 'reloadProject': await this.actions.reloadProject(); @@ -169,12 +169,15 @@ export class VideStatusController implements vscode.Disposable { }); } - items.push( - { + if (this.actions.profileDiagnostics) { + items.push({ label: vscode.l10n.t('$(pulse) Profile Diagnostics'), description: vscode.l10n.t('Measure current-file or workspace diagnostics performance'), action: 'profileDiagnostics', - }, + }); + } + + items.push( { label: vscode.l10n.t('$(refresh) Reload Project'), description: vscode.l10n.t('Refresh project manifests without restarting the server'), diff --git a/editors/vscode/test/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', diff --git a/editors/vscode/test/package.test.ts b/editors/vscode/test/package.test.ts new file mode 100644 index 00000000..0f5fce05 --- /dev/null +++ b/editors/vscode/test/package.test.ts @@ -0,0 +1,149 @@ +import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { describe, it } from 'node:test'; + +import type { PackageContext } from '../scripts/package/context'; +import { parsePackageCliArgs } from '../scripts/package/cli'; +import { + restorePackageJson, + stagePackageJsonForTarget, + stageProfileTraceAssets, +} from '../scripts/package/manifest'; +import { createPackagePlan } from '../scripts/package/targets'; + +describe('package cli', () => { + it('keeps the existing debug positional target syntax', () => { + assert.deepEqual(parsePackageCliArgs(['--debug', 'linux-x64', '--server=prebuilt']), { + target: 'linux-x64', + profile: 'debug', + serverMode: 'prebuilt', + profileTrace: false, + }); + }); + + it('accepts explicit target and profile flags', () => { + assert.deepEqual( + parsePackageCliArgs(['--target', 'web', '--profile', 'release', '--profile-trace']), + { + target: 'web', + profile: 'release', + serverMode: 'build', + profileTrace: true, + }, + ); + }); + + it('leaves profile trace disabled by default', () => { + assert.deepEqual(parsePackageCliArgs(['--target', 'web', '--profile', 'release']), { + target: 'web', + profile: 'release', + serverMode: 'build', + profileTrace: false, + }); + }); +}); + +describe('package staging', () => { + it('removes profiling command contributions when profile trace is disabled', () => { + const context = temporaryPackageContext(); + fs.writeFileSync( + path.join(context.vscodeDir, 'package.json'), + `${JSON.stringify( + { + browser: './dist/browser/extension.js', + contributes: { + commands: [ + { command: 'vide.profileDiagnostics' }, + { command: 'vide.showOutput' }, + ], + }, + }, + null, + 2, + )}\n`, + ); + + const plan = createPackagePlan({ + target: 'linux-x64', + profile: 'release', + serverMode: 'build', + }); + const originalPackageJson = stagePackageJsonForTarget(context, plan); + const packageJson = JSON.parse( + fs.readFileSync(path.join(context.vscodeDir, 'package.json'), 'utf8'), + ) as { + browser?: unknown; + contributes?: { commands?: Array<{ command?: unknown }> }; + }; + + assert.equal(packageJson.browser, undefined); + assert.deepEqual(packageJson.contributes?.commands, [{ command: 'vide.showOutput' }]); + + restorePackageJson(context, originalPackageJson); + assert.match( + fs.readFileSync(path.join(context.vscodeDir, 'package.json'), 'utf8'), + /vide\.profileDiagnostics/, + ); + }); + + it('removes stale profile trace assets when profile trace is disabled', () => { + const context = temporaryPackageContext(); + const speedscopeDir = path.join(context.vscodeDir, 'dist', 'speedscope'); + fs.mkdirSync(speedscopeDir, { recursive: true }); + fs.writeFileSync(path.join(speedscopeDir, 'index.html'), ''); + + const plan = createPackagePlan({ + target: 'web', + profile: 'release', + serverMode: 'build', + }); + stageProfileTraceAssets(context, plan); + + assert.equal(fs.existsSync(speedscopeDir), false); + }); +}); + +function temporaryPackageContext(): PackageContext { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vide-package-')); + return { + vscodeDir: root, + repoRoot: root, + }; +} + +describe('package plan', () => { + it('models web packages without native server staging', () => { + const plan = createPackagePlan({ + target: 'web', + profile: 'release', + serverMode: 'build', + }); + + assert.equal(plan.target, 'web'); + assert.equal(plan.profileTrace, false); + assert.equal(plan.vsixFile, 'vide-vscode-web.vsix'); + assert.equal(plan.targetSpec.kind, 'web'); + assert.equal(plan.targetSpec.removeBrowserEntry, false); + }); + + it('models native debug packages with target binary metadata', () => { + const plan = createPackagePlan({ + target: 'win32-x64', + profile: 'debug', + serverMode: 'prebuilt', + profileTrace: true, + }); + + assert.equal(plan.target, 'win32-x64'); + assert.equal(plan.profileTrace, true); + assert.equal(plan.vsixFile, 'vide-vscode-win32-x64-debug.vsix'); + assert.equal(plan.targetSpec.kind, 'native'); + if (plan.targetSpec.kind === 'native') { + assert.equal(plan.targetSpec.binaryFile, 'vide.exe'); + assert.equal(plan.targetSpec.isWindows, true); + assert.equal(plan.targetSpec.removeBrowserEntry, true); + } + }); +}); diff --git a/editors/vscode/test/platform.test.ts b/editors/vscode/test/platform.test.ts index 62693e7d..610884a1 100644 --- a/editors/vscode/test/platform.test.ts +++ b/editors/vscode/test/platform.test.ts @@ -14,16 +14,16 @@ test('maps supported Node platform and architecture pairs to VS Code target fold assert.equal(getPlatformFolder('alpine', 'arm64'), 'alpine-arm64'); assert.equal(getPlatformFolder('alpine', 'x64'), 'alpine-x64'); assert.equal(getPlatformFolder('darwin', 'arm64'), 'darwin-arm64'); - assert.equal(getPlatformFolder('darwin', 'x64'), 'darwin-x64'); assert.equal(getPlatformFolder('linux', 'arm64'), 'linux-arm64'); assert.equal(getPlatformFolder('linux', 'x64'), 'linux-x64'); - assert.equal(getPlatformFolder('win32', 'arm64'), 'win32-arm64'); assert.equal(getPlatformFolder('win32', 'x64'), 'win32-x64'); }); test('rejects unsupported platform and architecture pairs', () => { + assert.equal(getPlatformFolder('darwin', 'x64'), undefined); assert.equal(getPlatformFolder('freebsd', 'x64'), undefined); assert.equal(getPlatformFolder('linux', 'ia32'), undefined); + assert.equal(getPlatformFolder('win32', 'arm64'), undefined); }); test('checks package targets with a type guard', () => { diff --git a/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/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() }); 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, diff --git a/src/i18n.rs b/src/i18n.rs index 28fac9cc..66b6efc8 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -95,10 +95,32 @@ pub(crate) mod keys { "code_action.sort_named_port_connections"; pub(crate) const CODE_ACTION_ADD_DEFAULT_CASE_ITEM: &str = "code_action.add_default_case_item"; pub(crate) const CODE_ACTION_INVERT_IF_ELSE: &str = "code_action.invert_if_else"; + pub(crate) const CODE_ACTION_EXTRACT_VARIABLE: &str = "code_action.extract_variable"; + pub(crate) const CODE_ACTION_REMOVE_REDUNDANT_PARENTHESES: &str = + "code_action.remove_redundant_parentheses"; pub(crate) const CODE_ACTION_UNWRAP_SINGLE_STATEMENT_BLOCK: &str = "code_action.unwrap_single_statement_block"; pub(crate) const CODE_ACTION_WRAP_STATEMENT_IN_BEGIN_END: &str = "code_action.wrap_statement_in_begin_end"; + pub(crate) const CODE_ACTION_EXPAND_NAMED_PORT_CONNECTION_SHORTHAND: &str = + "code_action.expand_named_port_connection_shorthand"; + pub(crate) const CODE_ACTION_COLLAPSE_NAMED_PORT_CONNECTION_SHORTHAND: &str = + "code_action.collapse_named_port_connection_shorthand"; + pub(crate) const CODE_ACTION_CONVERT_ANSI_PORTS_TO_NON_ANSI: &str = + "code_action.convert_ansi_ports_to_non_ansi"; + pub(crate) const CODE_ACTION_CONVERT_NON_ANSI_PORTS_TO_ANSI: &str = + "code_action.convert_non_ansi_ports_to_ansi"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_COMB: &str = + "code_action.convert_always_to_always_comb"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_FF: &str = + "code_action.convert_always_to_always_ff"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_COMB_TO_ALWAYS: &str = + "code_action.convert_always_comb_to_always"; + pub(crate) const CODE_ACTION_CONVERT_ALWAYS_FF_TO_ALWAYS: &str = + "code_action.convert_always_ff_to_always"; + pub(crate) const CODE_ACTION_MERGE_NESTED_IF: &str = "code_action.merge_nested_if"; + pub(crate) const CODE_ACTION_PULL_ASSIGNMENT_UP: &str = "code_action.pull_assignment_up"; + pub(crate) const CODE_ACTION_PULL_ASSIGNMENT_DOWN: &str = "code_action.pull_assignment_down"; pub(crate) const CODE_ACTION_EXPAND_POSTFIX_INC_DEC: &str = "code_action.expand_postfix_inc_dec"; pub(crate) const CODE_ACTION_EXPAND_PREFIX_INC_DEC: &str = "code_action.expand_prefix_inc_dec"; @@ -124,6 +146,8 @@ pub(crate) mod keys { "code_action.collapse_compound_assignment"; pub(crate) const CODE_ACTION_APPLY_DE_MORGAN: &str = "code_action.apply_de_morgan"; pub(crate) const CODE_ACTION_FACTOR_DE_MORGAN: &str = "code_action.factor_de_morgan"; + pub(crate) const CODE_ACTION_REMOVE_DIGIT_SEPARATORS: &str = + "code_action.remove_digit_separators"; pub(crate) const CODE_ACTION_INSERT_MISSING_TOKEN: &str = "code_action.insert_missing_token"; pub(crate) const CODE_ACTION_CONVERT_LITERAL_TO_BINARY: &str = "code_action.convert_literal_to_binary"; diff --git a/src/i18n/en.toml b/src/i18n/en.toml index fd942584..d695beb7 100644 --- a/src/i18n/en.toml +++ b/src/i18n/en.toml @@ -53,8 +53,21 @@ sort_named_parameter_assignments = "Sort named parameter assignments" sort_named_port_connections = "Sort named port connections" add_default_case_item = "Add default case item" invert_if_else = "Invert if/else" +extract_variable = "Extract into variable" +remove_redundant_parentheses = "Remove redundant parentheses" unwrap_single_statement_block = "Unwrap single-statement begin/end" wrap_statement_in_begin_end = "Wrap statement in begin/end" +expand_named_port_connection_shorthand = "Expand named port shorthand" +collapse_named_port_connection_shorthand = "Collapse named port to shorthand" +convert_ansi_ports_to_non_ansi = "Convert ANSI port declarations to non-ANSI" +convert_non_ansi_ports_to_ansi = "Convert non-ANSI port declarations to ANSI" +convert_always_to_always_comb = "Convert to always_comb" +convert_always_to_always_ff = "Convert to always_ff" +convert_always_comb_to_always = "Convert to always @(*)" +convert_always_ff_to_always = "Convert to always @(...)" +merge_nested_if = "Merge nested if" +pull_assignment_up = "Pull assignment up" +pull_assignment_down = "Pull assignment down" expand_postfix_inc_dec = "Expand postfix expression" expand_prefix_inc_dec = "Expand prefix expression" convert_postfix_to_prefix_inc_dec = "Convert postfix to prefix expression" @@ -69,6 +82,7 @@ expand_compound_assignment = "Expand compound assignment" collapse_compound_assignment = "Collapse compound assignment" apply_de_morgan = "Apply De Morgan's law" factor_de_morgan = "Factor De Morgan's law" +remove_digit_separators = "Remove digit separators" insert_missing_token = "Insert missing '{token}'" convert_literal_to_binary = "Convert literal to binary" convert_literal_to_octal = "Convert literal to octal" diff --git a/src/i18n/zh-CN.toml b/src/i18n/zh-CN.toml index c25d855b..30e33df6 100644 --- a/src/i18n/zh-CN.toml +++ b/src/i18n/zh-CN.toml @@ -53,8 +53,21 @@ sort_named_parameter_assignments = "排序命名参数赋值" sort_named_port_connections = "排序命名端口连接" add_default_case_item = "添加 default case 分支项" invert_if_else = "反转 if/else" +extract_variable = "提取为变量" +remove_redundant_parentheses = "移除冗余括号" unwrap_single_statement_block = "展开单语句 begin/end" wrap_statement_in_begin_end = "用 begin/end 包裹语句" +expand_named_port_connection_shorthand = "展开命名端口简写" +collapse_named_port_connection_shorthand = "折叠命名端口为简写" +convert_ansi_ports_to_non_ansi = "将 ANSI 端口声明转换为非 ANSI" +convert_non_ansi_ports_to_ansi = "将非 ANSI 端口声明转换为 ANSI" +convert_always_to_always_comb = "转换为 always_comb" +convert_always_to_always_ff = "转换为 always_ff" +convert_always_comb_to_always = "转换为 always @(*)" +convert_always_ff_to_always = "转换为 always @(...)" +merge_nested_if = "合并嵌套 if" +pull_assignment_up = "转换为嵌套三元表达式 ?:" +pull_assignment_down = "转换为 if/else 赋值" expand_postfix_inc_dec = "展开后缀表达式" expand_prefix_inc_dec = "展开前缀表达式" convert_postfix_to_prefix_inc_dec = "将后缀表达式转换为前缀表达式" @@ -67,8 +80,9 @@ convert_assignment_to_postfix_inc_dec = "将赋值转换为后缀表达式" convert_assignment_to_prefix_inc_dec = "将赋值转换为前缀表达式" expand_compound_assignment = "展开复合赋值" collapse_compound_assignment = "折叠复合赋值" -apply_de_morgan = "应用德摩根律" -factor_de_morgan = "提取德摩根律" +apply_de_morgan = "应用德摩根律,将取反操作分配到内层" +factor_de_morgan = "应用德摩根律,将内层取反提取到外层" +remove_digit_separators = "移除数字分隔符" insert_missing_token = "插入缺失的 '{token}'" convert_literal_to_binary = "将字面量转换为二进制" convert_literal_to_octal = "将字面量转换为八进制" diff --git a/src/lib.rs b/src/lib.rs index cc91736e..96497c91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,8 @@ pub struct Opt { /// This can also be set with VIDE_PROFILE_TRACE. The captured targets /// default to project crates and can be overridden with /// VIDE_PROFILE_TRACE_FILTER. - #[clap(long = "profile_trace", default_value = None)] + #[cfg_attr(feature = "profile-trace", clap(long = "profile_trace", default_value = None))] + #[cfg_attr(not(feature = "profile-trace"), clap(skip))] pub profile_trace: Option, } diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 1b5e53ee..1b81e22c 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -266,6 +266,7 @@ fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { SymbolKind::Genvar => LspSymbolKind::VARIABLE, SymbolKind::Specparam => LspSymbolKind::TYPE_PARAMETER, SymbolKind::Typedef => LspSymbolKind::TYPE_PARAMETER, + SymbolKind::Struct => LspSymbolKind::STRUCT, SymbolKind::Instance => LspSymbolKind::OBJECT, SymbolKind::Block => LspSymbolKind::NAMESPACE, SymbolKind::Stmt => LspSymbolKind::NAMESPACE, @@ -602,7 +603,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, }; @@ -746,6 +749,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, }; @@ -945,8 +949,25 @@ fn code_action_title_key(id: &str, label: &str) -> Option<&'static str> { "sort_named_port_connections" => keys::CODE_ACTION_SORT_NAMED_PORT_CONNECTIONS, "add_default_case_item" => keys::CODE_ACTION_ADD_DEFAULT_CASE_ITEM, "invert_if_else" => keys::CODE_ACTION_INVERT_IF_ELSE, + "extract_variable" => keys::CODE_ACTION_EXTRACT_VARIABLE, + "remove_parentheses" => keys::CODE_ACTION_REMOVE_REDUNDANT_PARENTHESES, "unwrap_single_statement_block" => keys::CODE_ACTION_UNWRAP_SINGLE_STATEMENT_BLOCK, "wrap_statement_in_begin_end" => keys::CODE_ACTION_WRAP_STATEMENT_IN_BEGIN_END, + "expand_named_port_connection_shorthand" => { + keys::CODE_ACTION_EXPAND_NAMED_PORT_CONNECTION_SHORTHAND + } + "collapse_named_port_connection_shorthand" => { + keys::CODE_ACTION_COLLAPSE_NAMED_PORT_CONNECTION_SHORTHAND + } + "convert_ansi_ports_to_non_ansi" => keys::CODE_ACTION_CONVERT_ANSI_PORTS_TO_NON_ANSI, + "convert_non_ansi_ports_to_ansi" => keys::CODE_ACTION_CONVERT_NON_ANSI_PORTS_TO_ANSI, + "convert_always_to_always_comb" => keys::CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_COMB, + "convert_always_to_always_ff" => keys::CODE_ACTION_CONVERT_ALWAYS_TO_ALWAYS_FF, + "convert_always_comb_to_always" => keys::CODE_ACTION_CONVERT_ALWAYS_COMB_TO_ALWAYS, + "convert_always_ff_to_always" => keys::CODE_ACTION_CONVERT_ALWAYS_FF_TO_ALWAYS, + "merge_nested_if" => keys::CODE_ACTION_MERGE_NESTED_IF, + "pull_assignment_up" => keys::CODE_ACTION_PULL_ASSIGNMENT_UP, + "pull_assignment_down" => keys::CODE_ACTION_PULL_ASSIGNMENT_DOWN, "expand_postfix_inc_dec" => keys::CODE_ACTION_EXPAND_POSTFIX_INC_DEC, "expand_prefix_inc_dec" => keys::CODE_ACTION_EXPAND_PREFIX_INC_DEC, "convert_postfix_to_prefix_inc_dec" => keys::CODE_ACTION_CONVERT_POSTFIX_TO_PREFIX_INC_DEC, @@ -973,6 +994,9 @@ fn code_action_title_key(id: &str, label: &str) -> Option<&'static str> { "collapse_compound_assignment" => keys::CODE_ACTION_COLLAPSE_COMPOUND_ASSIGNMENT, "apply_de_morgan" => keys::CODE_ACTION_APPLY_DE_MORGAN, "factor_de_morgan" => keys::CODE_ACTION_FACTOR_DE_MORGAN, + "reformat_number_literal" if label == "Remove digit separators" => { + keys::CODE_ACTION_REMOVE_DIGIT_SEPARATORS + } "convert_literal_base" => match label { "Convert literal to binary" => keys::CODE_ACTION_CONVERT_LITERAL_TO_BINARY, "Convert literal to octal" => keys::CODE_ACTION_CONVERT_LITERAL_TO_OCTAL, diff --git a/src/main.rs b/src/main.rs index 54822b36..d05459a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -use std::{ - env, fs, io, - path::{Path, PathBuf}, -}; +#[cfg(feature = "profile-trace")] +use std::path::Path; +use std::{env, fs, io, path::PathBuf}; use anyhow::Context; use clap::Parser; @@ -11,6 +10,7 @@ use tracing_subscriber::{ }; use vide::{Opt, run_server}; +#[cfg(feature = "profile-trace")] const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vide=trace,", "hir::base_db=trace,", @@ -23,10 +23,17 @@ const DEFAULT_PROFILE_TRACE_FILTER: &str = concat!( "vfs::notify=trace" ); +#[cfg(feature = "profile-trace")] +type ProfileTraceGuard = tracing_chrome::FlushGuard; + +#[cfg(not(feature = "profile-trace"))] +type ProfileTraceGuard = (); + fn profile_trace_path(opt: &Opt) -> Option { opt.profile_trace.clone().or_else(|| env::var_os("VIDE_PROFILE_TRACE").map(PathBuf::from)) } +#[cfg(feature = "profile-trace")] fn create_profile_trace_file(path: &Path) -> anyhow::Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent).with_context(|| { @@ -37,7 +44,7 @@ fn create_profile_trace_file(path: &Path) -> anyhow::Result { .with_context(|| format!("could not create profile trace file: {}", path.display())) } -fn setup_logging(opt: &Opt) -> anyhow::Result> { +fn setup_logging(opt: &Opt) -> anyhow::Result> { let target: Targets = opt.log.parse().with_context(|| format!("invalid log filter: `{}`", opt.log))?; @@ -59,30 +66,49 @@ fn setup_logging(opt: &Opt) -> anyhow::Result tracing_subscriber::fmt::layer().with_ansi(false).with_writer(writer).with_filter(target); let subscriber = Registry::default().with(fmt_layer); - let profile_guard = if let Some(path) = profile_trace_path(opt) { - let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") - .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); - let profile_filter = - profile_filter_text.parse::().context("invalid profile trace filter")?; - let file = create_profile_trace_file(&path)?; - let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() - .writer(file) - .include_args(true) - .include_locations(false) - .build(); - subscriber.with(chrome_layer.with_filter(profile_filter)).init(); - tracing::info!( - path = %path.display(), - filter = %profile_filter_text, - "profile trace enabled" - ); - Some(guard) - } else { + + let requested_profile_trace_path = profile_trace_path(opt); + + #[cfg(not(feature = "profile-trace"))] + { + if let Some(path) = requested_profile_trace_path { + anyhow::bail!( + "profile tracing was requested for {}, but this binary was built without the \ + `profile-trace` feature; rebuild with `cargo build --release --features \ + profile-trace` to enable --profile_trace and VIDE_PROFILE_TRACE", + path.display() + ); + } + subscriber.init(); - None - }; + Ok(None) + } - Ok(profile_guard) + #[cfg(feature = "profile-trace")] + { + if let Some(path) = requested_profile_trace_path { + let profile_filter_text = env::var("VIDE_PROFILE_TRACE_FILTER") + .unwrap_or_else(|_| DEFAULT_PROFILE_TRACE_FILTER.to_owned()); + let profile_filter = + profile_filter_text.parse::().context("invalid profile trace filter")?; + let file = create_profile_trace_file(&path)?; + let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new() + .writer(file) + .include_args(true) + .include_locations(false) + .build(); + subscriber.with(chrome_layer.with_filter(profile_filter)).init(); + tracing::info!( + path = %path.display(), + filter = %profile_filter_text, + "profile trace enabled" + ); + Ok(Some(guard)) + } else { + subscriber.init(); + Ok(None) + } + } } fn main() -> anyhow::Result<()> { diff --git a/src/tests.rs b/src/tests.rs index 0037b78a..6266468b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -736,11 +736,20 @@ fn goto_definition_response_uris(response: GotoDefinitionResponse) -> Vec { fn position_of(text: &str, needle: &str) -> Position { let offset = text.find(needle).unwrap_or_else(|| panic!("missing {needle:?}")); + position_at_offset(text, offset) +} + +fn position_at_offset(text: &str, offset: usize) -> Position { let line = text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32; let line_start = text[..offset].rfind('\n').map(|idx| idx + 1).unwrap_or(0); Position { line, character: (offset - line_start) as u32 } } +fn range_of(text: &str, needle: &str) -> Range { + let start = text.find(needle).unwrap_or_else(|| panic!("missing {needle:?}")); + Range::new(position_at_offset(text, start), position_at_offset(text, start + needle.len())) +} + fn code_action_client_caps() -> ClientCapabilities { ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { @@ -751,6 +760,7 @@ fn code_action_client_caps() -> ClientCapabilities { CodeActionKind::EMPTY, CodeActionKind::QUICKFIX, CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, CodeActionKind::REFACTOR_REWRITE, ] .into_iter() @@ -814,6 +824,48 @@ fn request_code_actions( unreachable!("codeAction retries should either return or panic") } +fn request_code_actions_with_range( + client: &Connection, + uri: Url, + range: Range, + context: CodeActionContext, + request_id: i32, +) -> Vec { + const CONTENT_MODIFIED_RETRIES: i32 = 5; + + for attempt in 0..=CONTENT_MODIFIED_RETRIES { + let request_id = lsp_server::RequestId::from(request_id + attempt); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + CodeActionRequest::METHOD.to_string(), + CodeActionParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + range, + context: context.clone(), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + + let response = recv_raw_response(client, request_id, "codeAction"); + if response.error.is_none() { + return serde_json::from_value(response.result.unwrap_or(serde_json::Value::Null)) + .unwrap_or_else(|err| panic!("failed to decode codeAction response: {err}")); + } + + if is_content_modified(&response) && attempt < CONTENT_MODIFIED_RETRIES { + continue; + } + + panic!("codeAction returned error: {:?}", response.error); + } + + unreachable!("codeAction retries should either return or panic") +} + fn is_content_modified(response: &lsp_server::Response) -> bool { response .error @@ -1221,6 +1273,77 @@ endmodule shutdown_test_server(&client, server_thread); } +#[test] +fn code_action_request_returns_extract_variable_for_selected_expression() { + let text = "\ +module top; + always_comb begin + y = a + b; + end +endmodule +"; + let (_temp_dir, client, server_thread, uri) = + setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + + let actions = request_code_actions_with_range( + &client, + uri, + range_of(text, "a + b"), + CodeActionContext { + diagnostics: Vec::new(), + only: Some(vec![CodeActionKind::REFACTOR_EXTRACT]), + trigger_kind: None, + }, + 201, + ); + let titles = code_action_titles(&actions); + + assert!( + titles.iter().any(|title| title == "Extract into variable"), + "expected extract variable refactor, got {titles:?}" + ); + + shutdown_test_server(&client, server_thread); +} + +#[test] +fn code_action_request_returns_extract_variable_for_selected_continuous_assign_rhs() { + let text = "\ +module top ( + c, + led0 +); + input wire c; + output led0; + reg led0; + + assign led0 = c * 2 + c; +endmodule +"; + let (_temp_dir, client, server_thread, uri) = + setup_diagnostics_test(code_action_client_caps(), UserConfig::default(), text); + + let actions = request_code_actions_with_range( + &client, + uri, + range_of(text, "c * 2 + c"), + CodeActionContext { + diagnostics: Vec::new(), + only: Some(vec![CodeActionKind::REFACTOR_EXTRACT]), + trigger_kind: None, + }, + 202, + ); + let titles = code_action_titles(&actions); + + assert!( + titles.iter().any(|title| title == "Extract into variable"), + "expected extract variable refactor, got {titles:?}" + ); + + shutdown_test_server(&client, server_thread); +} + #[test] fn code_action_request_uses_server_diagnostics_when_client_diagnostic_has_no_data() { let text = "\ @@ -4155,6 +4278,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"); diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c38d49e0..a3ceec6a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true +clap.workspace = true project-model = { workspace = true, features = ["manifest-schema"] } serde_json.workspace = true vide = { path = "..", features = ["user-config-schema"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index e9028e37..42313ace 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,11 +1,15 @@ #![recursion_limit = "512"] +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::{ env, fs, path::{Path, PathBuf}, + process::Command as ProcessCommand, }; use anyhow::{Context, Result, bail}; +use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; const VSCODE_SCHEMA_CONSTANTS_PATH: &str = "editors/vscode/src/generated/projectConfigSchema.ts"; const VSCODE_CONFIGURATION_PATH: &str = "editors/vscode/src/generated/configuration.ts"; @@ -13,33 +17,425 @@ const VSCODE_PACKAGE_PATH: &str = "editors/vscode/package.json"; const USER_CONFIG_SCHEMA_PATH: &str = "/schemas/v1/user-config.schema.json"; fn main() -> Result<()> { - let mut args = env::args().skip(1); - let Some(command) = args.next() else { - print_help(); - return Ok(()); - }; + let cli = Cli::parse(); + let workspace_root = workspace_root()?; + + match cli.command { + Some(XtaskCommand::GenerateConfigArtifacts) => write_config_artifacts(&workspace_root), + Some(XtaskCommand::CheckConfigArtifacts) => check_config_artifacts(&workspace_root), + Some(XtaskCommand::GenerateSchemas) => write_schemas(&workspace_root), + Some(XtaskCommand::CheckSchemas) => check_schemas(&workspace_root), + Some(XtaskCommand::Server(server)) => run_server_command(&workspace_root, server), + Some(XtaskCommand::Vscode(vscode)) => run_vscode_command(&workspace_root, vscode), + None => { + Cli::command().print_help()?; + eprintln!(); + Ok(()) + } + } +} + +#[derive(Debug, Parser)] +#[command(name = "xtask", bin_name = "cargo xtask")] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum XtaskCommand { + GenerateConfigArtifacts, + CheckConfigArtifacts, + #[command(alias = "generate-manifest-schema")] + GenerateSchemas, + #[command(alias = "check-manifest-schema")] + CheckSchemas, + Server(ServerArgs), + Vscode(VscodeArgs), +} + +#[derive(Debug, Args)] +struct ServerArgs { + #[command(subcommand)] + command: ServerCommand, +} + +#[derive(Debug, Subcommand)] +enum ServerCommand { + Build(ServerBuildArgs), +} + +#[derive(Debug, Clone, PartialEq, Eq, Args)] +struct ServerBuildArgs { + #[arg(long, value_enum, default_value = "debug")] + profile: ExtensionBuildProfile, + #[arg(long)] + cargo_target: Option, + #[arg(long)] + alpine_linker: bool, + #[arg(long)] + profile_trace: bool, +} + +#[derive(Debug, Args)] +struct VscodeArgs { + #[command(subcommand)] + command: VscodeCommand, +} + +#[derive(Debug, Subcommand)] +enum VscodeCommand { + PrepareServer(VscodePrepareServerArgs), +} + +#[derive(Debug, PartialEq, Eq, Args)] +struct VscodePrepareServerArgs { + #[arg(long, value_enum)] + target: VscodeServerTarget, + #[arg(long, value_enum, default_value = "release")] + profile: ExtensionBuildProfile, + #[arg(long, value_enum, default_value = "build")] + server: ExtensionServerMode, + #[arg(long)] + profile_trace: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ExtensionBuildProfile { + Debug, + Release, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum ExtensionServerMode { + Build, + Prebuilt, +} - if args.next().is_some() { - bail!("unexpected extra arguments"); +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +enum VscodeServerTarget { + AlpineArm64, + AlpineX64, + DarwinArm64, + LinuxArm64, + LinuxX64, + Win32X64, +} + +impl VscodeServerTarget { + fn folder(self) -> &'static str { + match self { + VscodeServerTarget::AlpineArm64 => "alpine-arm64", + VscodeServerTarget::AlpineX64 => "alpine-x64", + VscodeServerTarget::DarwinArm64 => "darwin-arm64", + VscodeServerTarget::LinuxArm64 => "linux-arm64", + VscodeServerTarget::LinuxX64 => "linux-x64", + VscodeServerTarget::Win32X64 => "win32-x64", + } + } + + fn binary_file(self) -> &'static str { + if self.is_windows() { "vide.exe" } else { "vide" } + } + + fn cargo_target(self) -> Option<&'static str> { + match self { + VscodeServerTarget::AlpineArm64 => Some("aarch64-unknown-linux-musl"), + VscodeServerTarget::AlpineX64 => Some("x86_64-unknown-linux-musl"), + _ => None, + } } - match command.as_str() { - "generate-config-artifacts" => write_config_artifacts(&workspace_root()?), - "check-config-artifacts" => check_config_artifacts(&workspace_root()?), - "generate-schemas" | "generate-manifest-schema" => write_schemas(&workspace_root()?), - "check-schemas" | "check-manifest-schema" => check_schemas(&workspace_root()?), - "-h" | "--help" | "help" => { - print_help(); + fn is_windows(self) -> bool { + matches!(self, VscodeServerTarget::Win32X64) + } + + fn requires_alpine_linker(self) -> bool { + matches!(self, VscodeServerTarget::AlpineArm64 | VscodeServerTarget::AlpineX64) + } +} + +fn run_vscode_command(workspace_root: &Path, args: VscodeArgs) -> Result<()> { + match args.command { + VscodeCommand::PrepareServer(args) => prepare_vscode_server(workspace_root, args), + } +} + +fn run_server_command(workspace_root: &Path, args: ServerArgs) -> Result<()> { + match args.command { + ServerCommand::Build(args) => { + let server_path = build_server(workspace_root, &args)?; + println!("{}", server_path.display()); Ok(()) } - _ => bail!("unknown xtask command: {command}"), } } -fn print_help() { - eprintln!( - "Usage: cargo xtask \n\nCommands:\n generate-config-artifacts\n check-config-artifacts\n generate-schemas\n check-schemas" - ); +fn prepare_vscode_server(workspace_root: &Path, args: VscodePrepareServerArgs) -> Result<()> { + let server_path = ensure_vscode_server_binary( + workspace_root, + args.target, + args.profile, + args.server, + args.profile_trace, + )?; + println!("{}", server_path.display()); + Ok(()) +} + +fn ensure_vscode_server_binary( + workspace_root: &Path, + target: VscodeServerTarget, + profile: ExtensionBuildProfile, + server_mode: ExtensionServerMode, + profile_trace: bool, +) -> Result { + let server_path = vscode_target_server_path(workspace_root, target); + if server_mode == ExtensionServerMode::Prebuilt { + if server_path.exists() { + ensure_vscode_server_executable(&server_path, target)?; + return Ok(server_path); + } + bail!("missing prebuilt server binary: {}", server_path.display()); + } + + let host_target = host_vscode_server_target()?; + let cargo_target = target.cargo_target(); + if target != host_target && cargo_target.is_none() { + bail!( + "missing bundled server binary: {}\n\ + tip: run packaging on a matching native runner or copy the target binary first.", + server_path.display() + ); + } + + let build_args = server_build_args_for_vscode_target(target, profile, profile_trace); + let source_path = build_server(workspace_root, &build_args)?; + let parent = server_path.parent().context("VS Code server output path has no parent")?; + fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?; + fs::copy(&source_path, &server_path).with_context(|| { + format!( + "failed to copy server binary from {} to {}", + source_path.display(), + server_path.display() + ) + })?; + ensure_vscode_server_executable(&server_path, target)?; + + Ok(server_path) +} + +fn build_server(workspace_root: &Path, args: &ServerBuildArgs) -> Result { + if let Some(cargo_target) = args.cargo_target.as_deref() { + run_command( + "rustup", + &["target".to_owned(), "add".to_owned(), cargo_target.to_owned()], + workspace_root, + &[], + )?; + } + + run_command("cargo", &cargo_build_args(args), workspace_root, &cargo_build_env_updates(args))?; + + Ok(cargo_output_dir(workspace_root, args).join(server_binary_file(args))) +} + +fn host_vscode_server_target() -> Result { + match (env::consts::OS, env::consts::ARCH) { + ("linux", "aarch64") => Ok(VscodeServerTarget::LinuxArm64), + ("linux", "x86_64") => Ok(VscodeServerTarget::LinuxX64), + ("macos", "aarch64") => Ok(VscodeServerTarget::DarwinArm64), + ("windows", "x86_64") => Ok(VscodeServerTarget::Win32X64), + _ => bail!("unsupported host platform: {}-{}", env::consts::OS, env::consts::ARCH), + } +} + +fn vscode_target_server_path(workspace_root: &Path, target: VscodeServerTarget) -> PathBuf { + workspace_root + .join("editors") + .join("vscode") + .join("server") + .join(target.folder()) + .join(target.binary_file()) +} + +fn server_build_args_for_vscode_target( + target: VscodeServerTarget, + profile: ExtensionBuildProfile, + profile_trace: bool, +) -> ServerBuildArgs { + ServerBuildArgs { + profile, + cargo_target: target.cargo_target().map(str::to_owned), + alpine_linker: target.requires_alpine_linker(), + profile_trace, + } +} + +fn cargo_build_args(args: &ServerBuildArgs) -> Vec { + let mut command_args = vec!["build".to_owned()]; + if args.profile == ExtensionBuildProfile::Release { + command_args.push("--release".to_owned()); + } + if let Some(cargo_target) = &args.cargo_target { + command_args.push("--target".to_owned()); + command_args.push(cargo_target.clone()); + } + if args.profile_trace { + command_args.push("--features".to_owned()); + command_args.push("profile-trace".to_owned()); + } + command_args +} + +fn cargo_profile_dir(profile: ExtensionBuildProfile) -> &'static str { + match profile { + ExtensionBuildProfile::Debug => "debug", + ExtensionBuildProfile::Release => "release", + } +} + +fn cargo_output_dir(workspace_root: &Path, args: &ServerBuildArgs) -> PathBuf { + let mut path = workspace_root.join("target"); + if let Some(cargo_target) = &args.cargo_target { + path = path.join(cargo_target); + } + path.join(cargo_profile_dir(args.profile)) +} + +fn server_binary_file(args: &ServerBuildArgs) -> &'static str { + if args.cargo_target.as_deref().is_some_and(|target| target.contains("windows")) + || (args.cargo_target.is_none() && cfg!(windows)) + { + "vide.exe" + } else { + "vide" + } +} + +fn cargo_build_env_updates(args: &ServerBuildArgs) -> Vec<(String, String)> { + let Some(cargo_target) = args.cargo_target.as_deref() else { + return Vec::new(); + }; + + let mut updates = Vec::new(); + let linker_env_key = cargo_target_linker_env_key(cargo_target); + if optional_env(&linker_env_key).is_none() + && let Some(linker) = cargo_linker_for_target(args, cargo_target) + { + eprintln!("Using Cargo linker for {cargo_target}: {linker}"); + updates.push((linker_env_key, linker)); + } + + let late_link_args = late_rust_link_flags_for_target(args); + if !late_link_args.is_empty() { + eprintln!("Adding Cargo link args for {cargo_target}: {}", late_link_args.join(" ")); + updates.push(rust_flags_env_update(&late_link_args)); + } + + updates +} + +fn cargo_target_linker_env_key(cargo_target: &str) -> String { + format!("CARGO_TARGET_{}_LINKER", cargo_target_env_name(cargo_target)) +} + +fn cargo_target_env_name(cargo_target: &str) -> String { + cargo_target.to_uppercase().replace('-', "_") +} + +fn cargo_linker_for_target(args: &ServerBuildArgs, cargo_target: &str) -> Option { + if !args.alpine_linker { + return None; + } + + optional_env(&cxx_compiler_env_key(cargo_target)) + .or_else(|| optional_env("TARGET_CXX")) + .or_else(|| Some(format!("{cargo_target}-g++"))) +} + +fn cxx_compiler_env_key(cargo_target: &str) -> String { + format!("CXX_{}", cargo_target.replace('-', "_")) +} + +fn late_rust_link_flags_for_target(args: &ServerBuildArgs) -> Vec<&'static str> { + if args.alpine_linker { + // Static libstdc++ can introduce libc references after rustc's own musl -lc. + vec!["-C", "link-arg=-lc"] + } else { + Vec::new() + } +} + +fn rust_flags_env_update(flags: &[&str]) -> (String, String) { + if let Some(encoded_flags) = optional_env("CARGO_ENCODED_RUSTFLAGS") { + return ( + "CARGO_ENCODED_RUSTFLAGS".to_owned(), + format!("{encoded_flags}\x1f{}", flags.join("\x1f")), + ); + } + + let rust_flags = optional_env("RUSTFLAGS"); + let flags = flags.join(" "); + ( + "RUSTFLAGS".to_owned(), + rust_flags.map_or(flags.clone(), |rust_flags| format!("{rust_flags} {flags}")), + ) +} + +fn optional_env(name: &str) -> Option { + env::var(name).ok().map(|value| value.trim().to_owned()).filter(|value| !value.is_empty()) +} + +fn ensure_vscode_server_executable(path: &Path, target: VscodeServerTarget) -> Result<()> { + if target.is_windows() { + return Ok(()); + } + + #[cfg(not(unix))] + { + let _ = path; + } + + #[cfg(unix)] + { + let mut permissions = fs::metadata(path) + .with_context(|| format!("failed to stat {}", path.display()))? + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions) + .with_context(|| format!("failed to chmod {}", path.display()))?; + } + + Ok(()) +} + +fn run_command( + command: &str, + args: &[String], + cwd: &Path, + env_updates: &[(String, String)], +) -> Result<()> { + let mut child = ProcessCommand::new(command_for_host(command)); + child.args(args).current_dir(cwd); + for (key, value) in env_updates { + child.env(key, value); + } + + let status = child + .status() + .with_context(|| format!("failed to run `{}` in {}", command, cwd.display()))?; + + if !status.success() { + bail!("`{} {}` failed with {}", command, args.join(" "), status); + } + + Ok(()) +} + +fn command_for_host(command: &str) -> String { + if cfg!(windows) { format!("{command}.cmd") } else { command.to_owned() } } fn workspace_root() -> Result { @@ -218,10 +614,76 @@ fn check_file_matches(path: &Path, expected: &str) -> Result<()> { #[cfg(test)] mod tests { + use clap::Parser as _; + use super::*; #[test] fn checked_in_schemas_match_generated_schemas() { check_schemas(&workspace_root().unwrap()).unwrap(); } + + #[test] + fn parses_vscode_prepare_server_command_with_clap() { + let cli = Cli::try_parse_from([ + "xtask", + "vscode", + "prepare-server", + "--target", + "linux-x64", + "--profile", + "release", + "--server", + "prebuilt", + ]) + .unwrap(); + + let Some(XtaskCommand::Vscode(VscodeArgs { command: VscodeCommand::PrepareServer(args) })) = + cli.command + else { + panic!("expected vscode prepare-server command"); + }; + + assert_eq!( + args, + VscodePrepareServerArgs { + target: VscodeServerTarget::LinuxX64, + profile: ExtensionBuildProfile::Release, + server: ExtensionServerMode::Prebuilt, + profile_trace: false, + } + ); + } + + #[test] + fn maps_alpine_vscode_target_to_server_build_args() { + let args = server_build_args_for_vscode_target( + VscodeServerTarget::AlpineX64, + ExtensionBuildProfile::Release, + true, + ); + + assert_eq!( + args, + ServerBuildArgs { + profile: ExtensionBuildProfile::Release, + cargo_target: Some("x86_64-unknown-linux-musl".to_owned()), + alpine_linker: true, + profile_trace: true, + } + ); + assert_eq!( + cargo_build_args(&args), + [ + "build", + "--release", + "--target", + "x86_64-unknown-linux-musl", + "--features", + "profile-trace", + ] + .map(str::to_owned) + ); + assert_eq!(server_binary_file(&args), "vide"); + } }