From 315a2f53ce68dc617b0a1ffdf194440c4e61e498 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 3 Mar 2026 14:35:07 +0100 Subject: [PATCH 01/32] Agentically coded SCIP backend for the LSP Draft/suggestion feature, not guaranteed to work even beyond known limitations on the DLS --- Cargo.toml | 2 + USAGE.md | 77 +++++++ src/actions/requests.rs | 139 ++++++++++++ src/cmd.rs | 18 ++ src/dfa/client.rs | 33 +++ src/dfa/main.rs | 37 +++- src/lib.rs | 1 + src/scip/mod.rs | 452 ++++++++++++++++++++++++++++++++++++++++ src/server/dispatch.rs | 1 + src/server/mod.rs | 3 +- 10 files changed, 761 insertions(+), 2 deletions(-) create mode 100644 src/scip/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 949c7f98..6c164801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,5 +51,7 @@ subprocess = "1.0" thiserror = "2.0" urlencoding = "2.1" utf8-read = "0.4" +scip = "0.6.1" +protobuf = "3" walkdir = "2" heck = "0.5" diff --git a/USAGE.md b/USAGE.md index ebfa9ae7..2a567105 100644 --- a/USAGE.md +++ b/USAGE.md @@ -126,3 +126,80 @@ method now_we_can_declare_this_method_with_a_really_really_really_really_long na ``` Will allow 'long_lines' globally, 'nsp_unary' and 'indent_no_tabs' on the `param p = (1 ++ *` line, and 'indent_paren_expr' on the `'4);` line. + +## SCIP Export + +The DLS can export a [SCIP index](https://sourcegraph.com/docs/code-search/code-navigation/scip) +of analyzed DML devices. SCIP (Source Code Intelligence Protocol) is a +language-agnostic format for code intelligence data, used by tools such as +Sourcegraph for cross-repository navigation and code search. + +### Invocation + +SCIP export is available through the DFA (DML File Analyzer) binary via the +`--scip-output ` flag: +``` +dfa --compile-info --workspace --scip-output [list of devices to analyze, ] +``` + +It is worth noting that SCIP format specifies that symbols from documents that are not under the project root (which we define as the workspace) get slotted under external symbols with no occurances tracked. + +### SCIP schema details +Here we list how we have mapped DML specifically to the SCIP format. + +#### SCIP symbol kind mappings + +DML symbol kinds are mapped to SCIP `SymbolInformation.Kind` as follows: + +- `Constant` — Parameter, Constant, Loggroup +- `Variable` — Extern, Saved, Session, Local +- `Parameter` — MethodArg +- `Function` — Hook +- `Method` — Method +- `Class` — Template +- `TypeAlias` — Typedef +- `Namespace` — All composite objects (Device, Bank, Register, Field, Group, Port, Connect, Attribute, Event, Subdevice) +- `Struct` — Implement +- `Interface` — Interface + +#### Symbol Naming Scheme + +SCIP symbols follow the format: +` ' ' ' ' ' ' ' ' ` + +For DML, the scheme is `dml`, the manager is `simics`, version is `.` (currently we cannot extract simics version here), and the +package is the device name. Descriptors are built from the fully qualified path +through the device hierarchy: + +``` +dml simics sample_device . sample_device.regs.r1.offset. + ^ term (parameter) +dml simics sample_device . sample_device.regs.r1.read(). + ^ method +dml simics sample_device . bank# + ^ 'type' (template) +``` + +Descriptor suffixes follow the SCIP standard: +- `.` (term) — used for composite objects, parameters, and other named values +- `#` (type) — used only for templates +- `().` (method) — used for methods + +#### Local Symbols + +Method arguments and method-local variables use SCIP local symbols of the form +`local _`, where `` is the internal symbol identifier. Local +symbols are scoped to a single document and are not navigable across files. + +#### Occurrence Roles + +DML declarations and definitions are both emitted with the SCIP `Definition` +role, since SCIP does not distinguish between the two. References (including +template instantiation sites from `is` statements) are emitted with +`ReadAccess`. + +#### Relationships + +Composite objects that instantiate templates (via `is some_template`) emit +SCIP `Relationship` entries with `is_implementation = true` pointing to the +template symbol. diff --git a/src/actions/requests.rs b/src/actions/requests.rs index 6fb4b50c..ef8cb18b 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -959,6 +959,145 @@ impl RequestAction for GetKnownContextsRequest { } } +// ---- SCIP Export Request ---- + +#[derive(Debug, Clone)] +pub struct ExportScipRequest; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportScipParams { + /// Device paths to export SCIP for. If empty, exports all known devices. + pub devices: Option>, + /// The file path where the SCIP index should be written. + pub output_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportScipResult { + /// Whether the export succeeded. + pub success: bool, + /// Number of documents in the exported index. + pub document_count: usize, + /// Error message, if any. + pub error: Option, +} + +impl LSPRequest for ExportScipRequest { + type Params = ExportScipParams; + type Result = ExportScipResult; + + const METHOD: &'static str = "$/exportScip"; +} + +impl RequestAction for ExportScipRequest { + type Response = ExportScipResult; + + fn timeout() -> std::time::Duration { + crate::server::dispatch::DEFAULT_REQUEST_TIMEOUT * 30 + } + + fn fallback_response() -> Result { + Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some("Request timed out".to_string()), + }) + } + + fn get_identifier(params: &Self::Params) -> String { + Self::request_identifier(¶ms.output_path) + } + + fn handle( + ctx: InitActionContext, + params: Self::Params, + ) -> Result { + info!("Handling SCIP export request to {}", params.output_path); + + // Determine which device paths to export + let device_paths: Vec = + if let Some(devices) = params.devices { + devices.iter().filter_map( + |uri| parse_file_path!(&uri, "ExportScip") + .ok() + .and_then(CanonPath::from_path_buf)) + .collect() + } else { + vec![] + }; + + // Wait for device analyses to be ready + if !device_paths.is_empty() { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::Paths(device_paths.clone())).ok(); + } else { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::All).ok(); + } + + let analysis = ctx.analysis.lock().unwrap(); + + // Collect device analyses + let devices: Vec<&crate::analysis::DeviceAnalysis> = + if device_paths.is_empty() { + // Export all device analyses + analysis.device_analysis.values() + .map(|ts| &ts.stored) + .collect() + } else { + device_paths.iter().filter_map(|path| { + analysis.get_device_analysis(path).ok() + }).collect() + }; + + if devices.is_empty() { + return Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some("No device analyses found".to_string()), + }); + } + + info!("Exporting SCIP for {} device(s)", devices.len()); + + // Determine project root from workspaces + let project_root = ctx.workspace_roots + .lock() + .unwrap() + .first() + .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + let index = crate::scip::build_scip_index(&devices, &project_root); + let doc_count = index.documents.len(); + + let output = std::path::Path::new(¶ms.output_path); + match crate::scip::write_scip_to_file(index, output) { + Ok(()) => { + info!("SCIP export complete: {} documents written to {}", + doc_count, params.output_path); + Ok(ExportScipResult { + success: true, + document_count: doc_count, + error: None, + }) + }, + Err(e) => { + error!("SCIP export failed: {}", e); + Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some(e), + }) + } + } + } +} + /// Server-to-client requests impl SentRequest for RegisterCapability { type Response = ::Result; diff --git a/src/cmd.rs b/src/cmd.rs index e809ebea..f066e8e2 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -322,6 +322,24 @@ pub fn set_contexts(paths: Vec) -> Notification, output_path: String) -> Request { + Request { + params: requests::ExportScipParams { + devices: if devices.is_empty() { + None + } else { + Some(devices.into_iter() + .map(|p| parse_uri(&p).unwrap()) + .collect()) + }, + output_path, + }, + action: PhantomData, + id: next_id(), + received: Instant::now(), + } +} + fn next_id() -> RequestId { static ID: AtomicU64 = AtomicU64::new(1); RequestId::Num(ID.fetch_add(1, Ordering::SeqCst)) diff --git a/src/dfa/client.rs b/src/dfa/client.rs index 8406ab26..36e0c637 100644 --- a/src/dfa/client.rs +++ b/src/dfa/client.rs @@ -412,4 +412,37 @@ impl ClientInterface { self.server.wait_timeout(Duration::from_millis(1000))?; Ok(()) } + + pub fn export_scip(&mut self, + device_paths: Vec, + output_path: String) + -> anyhow::Result { + debug!("Sending SCIP export request for {:?} -> {}", device_paths, output_path); + self.send( + cmd::export_scip(device_paths, output_path).to_string() + )?; + // Wait for the response + loop { + match self.receive_maybe() { + Ok(ServerMessage::Response(value)) => { + let result: crate::actions::requests::ExportScipResult + = serde_json::from_value(value) + .map_err(|e| RpcErrorKind::from(e.to_string()))?; + return Ok(result); + }, + Ok(ServerMessage::Error(e)) => { + return Err(anyhow::anyhow!( + "Server exited during SCIP export: {:?}", e)); + }, + Ok(_) => { + // Skip other messages (diagnostics, progress, etc.) + continue; + }, + Err(e) => { + trace!("Skipping message during SCIP export wait: {:?}", e); + continue; + } + } + } + } } diff --git a/src/dfa/main.rs b/src/dfa/main.rs index 476acb99..63b174bc 100644 --- a/src/dfa/main.rs +++ b/src/dfa/main.rs @@ -36,6 +36,7 @@ struct Args { lint_cfg_path: Option, test: bool, quiet: bool, + scip_output: Option, } fn parse_args() -> Args { @@ -91,6 +92,11 @@ fn parse_args() -> Args { .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) .required(false)) + .arg(Arg::new("scip-output").long("scip-output") + .help("Export SCIP index to the specified file after analysis") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .required(false)) .arg(arg!( ... "DML files to analyze") .value_parser(clap::value_parser!(PathBuf))) .arg_required_else_help(false) @@ -114,7 +120,9 @@ fn parse_args() -> Args { linting_enabled: args.get_one::("linting-enabled") .cloned(), lint_cfg_path: args.get_one::("lint-cfg-path") - .cloned() + .cloned(), + scip_output: args.get_one::("scip-output") + .cloned(), } } @@ -176,6 +184,33 @@ fn main_inner() -> Result<(), i32> { if arg.test && !dlsclient.no_errors() { exit_code = Err(1); } + + // Export SCIP if requested + if let Some(scip_path) = &arg.scip_output { + println!("Exporting SCIP index to {:?}", scip_path); + let scip_output_str = scip_path.to_string_lossy().to_string(); + let device_paths: Vec = arg.files.iter() + .filter_map(|f| f.canonicalize().ok()) + .map(|p| p.to_string_lossy().to_string()) + .collect(); + match dlsclient.export_scip(device_paths, scip_output_str) { + Ok(result) => { + if result.success { + println!("SCIP export complete: {} document(s) written", + result.document_count); + } else { + let err_msg = result.error.unwrap_or_else( + || "Unknown error".to_string()); + eprintln!("SCIP export failed: {}", err_msg); + exit_code = Err(1); + } + }, + Err(e) => { + eprintln!("SCIP export request failed: {}", e); + exit_code = Err(1); + } + } + } } // Disregard this result, we dont _really_ care about shutting down diff --git a/src/lib.rs b/src/lib.rs index 497835b6..e6d7c8d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub mod dfa; pub mod file_management; pub mod lint; pub mod lsp_data; +pub mod scip; pub mod server; pub mod span; pub mod utility; diff --git a/src/scip/mod.rs b/src/scip/mod.rs new file mode 100644 index 00000000..b63a9694 --- /dev/null +++ b/src/scip/mod.rs @@ -0,0 +1,452 @@ +// © 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 and MIT +//! SCIP (Source Code Intelligence Protocol) export support. +//! +//! This module converts DLS analysis data (DeviceAnalysis) into +//! the SCIP index format for use with code intelligence tools. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use protobuf::MessageField; +use protobuf::Enum; + +use scip::types::{ + Document, Index, Metadata, Occurrence, PositionEncoding, + Relationship, SymbolInformation, SymbolRole, ToolInfo, + symbol_information::Kind as ScipSymbolKind, +}; + +use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; +use crate::analysis::structure::objects::CompObjectKind; +use crate::analysis::templating::objects::{ + DMLHierarchyMember, DMLNamedMember, DMLObject, StructureContainer, +}; +use crate::analysis::DeviceAnalysis; +use crate::Span as ZeroSpan; + +use log::debug; + +/// Convert a ZeroSpan range into the SCIP occurrence range format. +/// +/// SCIP uses `[startLine, startChar, endLine, endChar]` (4 elements) +/// or `[startLine, startChar, endChar]` (3 elements, same-line). +/// All values are 0-based. +fn span_to_scip_range(span: &ZeroSpan) -> Vec { + let r = &span.range; + let start_line = r.row_start.0 as i32; + let start_char = r.col_start.0 as i32; + let end_line = r.row_end.0 as i32; + let end_char = r.col_end.0 as i32; + + if start_line == end_line { + vec![start_line, start_char, end_char] + } else { + vec![start_line, start_char, end_line, end_char] + } +} + +/// Map a DMLSymbolKind to a SCIP SymbolInformation Kind. +fn dml_kind_to_scip_kind(kind: &DMLSymbolKind) -> ScipSymbolKind { + match kind { + DMLSymbolKind::CompObject(comp_kind) => match comp_kind { + CompObjectKind::Interface => ScipSymbolKind::Interface, + CompObjectKind::Implement => ScipSymbolKind::Struct, + _ => ScipSymbolKind::Namespace, + }, + DMLSymbolKind::Parameter => ScipSymbolKind::Constant, + DMLSymbolKind::Constant => ScipSymbolKind::Constant, + DMLSymbolKind::Extern => ScipSymbolKind::Variable, + DMLSymbolKind::Hook => ScipSymbolKind::Function, + DMLSymbolKind::Local => ScipSymbolKind::Variable, + DMLSymbolKind::Loggroup => ScipSymbolKind::Constant, + DMLSymbolKind::Method => ScipSymbolKind::Method, + DMLSymbolKind::MethodArg => ScipSymbolKind::Parameter, + DMLSymbolKind::Saved => ScipSymbolKind::Variable, + DMLSymbolKind::Session => ScipSymbolKind::Variable, + DMLSymbolKind::Template => ScipSymbolKind::Class, + DMLSymbolKind::Typedef => ScipSymbolKind::TypeAlias, + } +} + +/// Sanitize a name for use in SCIP symbol strings. +/// +/// SCIP descriptors use backtick-escaping for names that contain +/// non-identifier characters, but to keep things simple we sanitize +/// to `[a-zA-Z0-9_]+`. +fn sanitize_name(name: &str) -> String { + name.chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }) + .collect() +} + +/// Build a `local` SCIP symbol string (document-scoped). +/// +/// Used for method arguments, method locals, and other symbols that +/// are only visible within a single file scope. +fn make_local_symbol(name: &str, id: u64) -> String { + format!("local {}_{}", sanitize_name(name), id) +} + +/// Build a global SCIP symbol string from a qualified path. +/// +/// Global symbols use the format: +/// `scheme ' ' manager ' ' package ' ' version ' ' descriptors...` +/// +/// We use: +/// - scheme: `dml` +/// - manager: `simics` +/// - package: device name +/// - version: `.` (single dot = no version) +/// - descriptors: built from the qualified path segments +/// +/// SCIP descriptor suffixes: +/// - `.` = namespace/term (banks, groups, etc.) +/// - `#` = type (templates, comp objects) +/// - `()` = method +fn make_global_symbol(device_name: &str, qualified_path: &str, + kind: &DMLSymbolKind) -> String { + let segments: Vec<&str> = qualified_path.split('.').collect(); + let mut descriptors = String::new(); + for (i, seg) in segments.iter().enumerate() { + let sanitized = sanitize_name(seg); + if i == segments.len() - 1 { + // Last segment gets suffix based on kind + match kind { + DMLSymbolKind::Method => { + descriptors.push_str(&sanitized); + descriptors.push_str("()."); + } + DMLSymbolKind::Template => { + // Templates are the type-like concept in DML + descriptors.push_str(&sanitized); + descriptors.push('#'); + } + _ => { + // Composite objects (device, bank, register, ...) + // are instances, not types — use term descriptor + descriptors.push_str(&sanitized); + descriptors.push('.'); + } + } + } else { + // Intermediate segments are namespace-like + descriptors.push_str(&sanitized); + descriptors.push('.'); + } + } + format!("dml simics {} . {}", sanitize_name(device_name), descriptors) +} + +/// Build the SCIP symbol string for a given SymbolSource. +/// +/// - DMLObject (comp or shallow): uses global symbol with qualified_name() +/// - Method: uses global symbol with parent's qualified_name + method name +/// - Template: uses global symbol at top level +/// - MethodArg / MethodLocal: uses local symbol +/// - Type: returns None (these are skipped) +fn scip_symbol_for_source( + source: &SymbolSource, + kind: &DMLSymbolKind, + id: u64, + device_name: &str, + container: &StructureContainer, +) -> Option<(String, String)> { + // Returns Some((scip_symbol, display_name)) + match source { + SymbolSource::DMLObject(dml_obj) => { + match dml_obj { + DMLObject::CompObject(key) => { + if let Some(comp) = container.get(*key) { + let qname = comp.qualified_name(container); + let display = comp.identity().to_string(); + let sym = make_global_symbol(device_name, + &qname, kind); + Some((sym, display)) + } else { + None + } + } + DMLObject::ShallowObject(shallow) => { + let qname = shallow.qualified_name(container); + let display = shallow.identity().to_string(); + let sym = make_global_symbol(device_name, + &qname, kind); + Some((sym, display)) + } + } + } + SymbolSource::Method(parent_key, methref) => { + let parent_qname = container.get(*parent_key) + .map(|p| p.qualified_name(container)) + .unwrap_or_default(); + let method_name = methref.identity(); + let qname = if parent_qname.is_empty() { + method_name.to_string() + } else { + format!("{}.{}", parent_qname, method_name) + }; + let sym = make_global_symbol( + device_name, &qname, &DMLSymbolKind::Method); + Some((sym, method_name.to_string())) + } + SymbolSource::Template(templ) => { + let sym = make_global_symbol( + device_name, &templ.name, &DMLSymbolKind::Template); + Some((sym, templ.name.clone())) + } + SymbolSource::MethodArg(_, name) => { + let sym = make_local_symbol(&name.val, id); + Some((sym, name.val.clone())) + } + SymbolSource::MethodLocal(_, name) => { + let sym = make_local_symbol(&name.val, id); + Some((sym, name.val.clone())) + } + SymbolSource::Type(_) => None, + } +} + +/// Build a human-readable documentation string for a DML symbol. +fn make_documentation(sym: &crate::analysis::symbols::Symbol, + display_name: &str) -> Vec { + let kind_str = format!("{:?}", sym.kind); + let typed_str = sym.typed.as_ref() + .map(|t| format!(" : {:?}", t)) + .unwrap_or_default(); + vec![format!("{} `{}`{}", kind_str, display_name, typed_str)] +} + +/// Holds per-file occurrence and symbol information data +/// that will be assembled into SCIP Documents. +#[derive(Default)] +struct FileData { + occurrences: Vec, + symbols: Vec, +} + +/// Convert a single DeviceAnalysis into SCIP Documents. +/// +/// Returns a tuple of (documents, external_symbols). Files under the +/// project root become Documents with relative paths; files outside +/// (e.g. Simics builtins) contribute only their SymbolInformation to +/// `external_symbols` for hover/navigation support. +fn device_analysis_to_documents( + device: &DeviceAnalysis, + project_root: &Path, +) -> (Vec, Vec) { + let mut file_data: HashMap = HashMap::new(); + let container = &device.objects; + let device_name = &device.name; + + // Iterate over all symbols in the device analysis + for symbol_ref in device.symbol_info.all_symbols() { + let sym = symbol_ref.symbol.lock().unwrap(); + + // Build the SCIP symbol and display name from the source + let (scip_symbol, display_name) = match scip_symbol_for_source( + &sym.source, &sym.kind, sym.id, device_name, container, + ) { + Some(pair) => pair, + None => continue, // Type symbols and unresolvable objects + }; + + debug!("SCIP symbol id={} kind={:?} scip={} defs={} decls={} refs={} impls={}", + sym.id, sym.kind, &scip_symbol, + sym.definitions.len(), sym.declarations.len(), + sym.references.len(), sym.implementations.len()); + + let kind = dml_kind_to_scip_kind(&sym.kind); + let documentation = make_documentation(&sym, &display_name); + + // Record the primary location as a definition occurrence + { + let loc = &sym.loc; + let file_path = loc.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(loc); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + + data.occurrences.push(occ); + + // Add SymbolInformation for this symbol (only once, at def site) + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = scip_symbol.clone(); + sym_info.kind = kind.into(); + sym_info.display_name = display_name; + sym_info.documentation = documentation; + + // For comp objects, add Relationship entries for each + // instantiated template (`is` declarations). + if let SymbolSource::DMLObject( + DMLObject::CompObject(key)) = &sym.source { + if let Some(comp) = container.get(*key) { + for templ_name in comp.templates.keys() { + let templ_symbol = make_global_symbol( + device_name, templ_name, + &DMLSymbolKind::Template); + let mut rel = Relationship::new(); + rel.symbol = templ_symbol; + rel.is_implementation = true; + sym_info.relationships.push(rel); + } + } + } + + data.symbols.push(sym_info); + } + + // Record additional definitions + for def_span in &sym.definitions { + // Skip if same as primary loc + if *def_span == sym.loc { + continue; + } + let file_path = def_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(def_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + data.occurrences.push(occ); + } + + // Record declarations + for decl_span in &sym.declarations { + if *decl_span == sym.loc { + continue; + } + let file_path = decl_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(decl_span); + occ.symbol = scip_symbol.clone(); + // Declarations get the Definition role in SCIP + // (SCIP doesn't distinguish declaration vs definition) + occ.symbol_roles = SymbolRole::Definition.value(); + data.occurrences.push(occ); + } + + // Record references (read accesses) + for ref_span in &sym.references { + let file_path = ref_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(ref_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::ReadAccess.value(); + data.occurrences.push(occ); + } + + // Record implementation sites (`is template` occurrences) + // These are references to the template, not definitions. + // The actual implementation relationship is expressed via + // Relationship entries on the comp object's SymbolInformation. + for impl_span in &sym.implementations { + let file_path = impl_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(impl_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::ReadAccess.value(); + data.occurrences.push(occ); + } + } + + // Assemble Documents, separating in-project from external files. + let mut documents = Vec::new(); + let mut external_symbols = Vec::new(); + + for (path, data) in file_data { + match path.strip_prefix(project_root) { + Ok(rel) => { + let mut doc = Document::new(); + doc.relative_path = rel.to_string_lossy().to_string(); + doc.language = "dml".to_string(); + doc.position_encoding = + PositionEncoding::UTF8CodeUnitOffsetFromLineStart.into(); + doc.occurrences = data.occurrences; + doc.symbols = data.symbols; + documents.push(doc); + } + Err(_) => { + // External file: keep symbol info for hover/navigation + // but don't emit a document or occurrences + external_symbols.extend(data.symbols); + } + } + } + + (documents, external_symbols) +} + +/// Build a complete SCIP Index from one or more DeviceAnalyses. +/// +/// # Arguments +/// * `devices` - The device analyses to export +/// * `project_root` - The workspace root path, used to compute relative paths +pub fn build_scip_index( + devices: &[&DeviceAnalysis], + project_root: &Path, +) -> Index { + debug!("Building SCIP index for {} device(s) rooted at {:?}", + devices.len(), project_root); + + let mut tool_info = ToolInfo::new(); + tool_info.name = "dls".to_string(); + tool_info.version = crate::version(); + + let mut metadata = Metadata::new(); + metadata.tool_info = MessageField::some(tool_info); + let root_str = project_root.to_string_lossy(); + metadata.project_root = if root_str.ends_with('/') { + format!("file://{}", root_str) + } else { + format!("file://{}/", root_str) + }; + metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); + + // Collect documents from all devices, merging by relative_path + let mut merged_docs: HashMap = HashMap::new(); + let mut all_external_symbols: Vec = Vec::new(); + + for device in devices { + let (docs, ext_syms) = device_analysis_to_documents(device, project_root); + for doc in docs { + let entry = merged_docs.entry(doc.relative_path.clone()) + .or_insert_with(|| { + let mut d = Document::new(); + d.relative_path = doc.relative_path.clone(); + d.language = doc.language.clone(); + d.position_encoding = doc.position_encoding; + d + }); + entry.occurrences.extend(doc.occurrences); + entry.symbols.extend(doc.symbols); + } + all_external_symbols.extend(ext_syms); + } + + let mut index = Index::new(); + index.metadata = MessageField::some(metadata); + index.documents = merged_docs.into_values().collect(); + index.external_symbols = all_external_symbols; + + debug!("SCIP index built with {} document(s)", index.documents.len()); + index +} + +/// Write a SCIP index to a file. +pub fn write_scip_to_file(index: Index, output_path: &Path) + -> Result<(), String> { + debug!("Writing SCIP index to {:?}", output_path); + scip::write_message_to_file(output_path, index) + .map_err(|e| format!("Failed to write SCIP index: {}", e)) +} diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 5e4ee43e..7eceaec5 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -113,6 +113,7 @@ define_dispatch_request_enum!( ExecuteCommand, CodeLensRequest, GetKnownContextsRequest, + ExportScipRequest, ); /// Provides ability to dispatch requests to a worker thread that will diff --git a/src/server/mod.rs b/src/server/mod.rs index 260c2e58..6ba9f718 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -594,7 +594,8 @@ impl LsService { requests::References, requests::Completion, requests::CodeLensRequest, - requests::GetKnownContextsRequest; + requests::GetKnownContextsRequest, + requests::ExportScipRequest; ); Ok(()) } From ec3f6aee69193779c6c8fcb1e4f601480ce2007c Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:07:37 +0100 Subject: [PATCH 02/32] Correct encoding declaration --- src/scip/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index b63a9694..23127904 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -371,7 +371,7 @@ fn device_analysis_to_documents( doc.relative_path = rel.to_string_lossy().to_string(); doc.language = "dml".to_string(); doc.position_encoding = - PositionEncoding::UTF8CodeUnitOffsetFromLineStart.into(); + PositionEncoding::UTF16CodeUnitOffsetFromLineStart.into(); doc.occurrences = data.occurrences; doc.symbols = data.symbols; documents.push(doc); From dffe22855dfe6459d47993fe17ab26df349cfbf3 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:11:09 +0100 Subject: [PATCH 03/32] Avoid duplicating info when an analysing multiple devices --- src/scip/mod.rs | 89 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 23127904..8fc14869 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -219,10 +219,37 @@ fn make_documentation(sym: &crate::analysis::symbols::Symbol, /// Holds per-file occurrence and symbol information data /// that will be assembled into SCIP Documents. +/// +/// Uses HashMaps keyed by dedup keys so that duplicate entries +/// from multiple device analyses are naturally collapsed. #[derive(Default)] struct FileData { - occurrences: Vec, - symbols: Vec, + /// Occurrences keyed by (symbol, range, roles) to avoid duplicates. + occurrences: HashMap<(String, Vec, i32), Occurrence>, + /// SymbolInformation keyed by SCIP symbol string. + symbols: HashMap, +} + +impl FileData { + /// Insert an occurrence, deduplicating by (symbol, range, roles). + fn add_occurrence(&mut self, occ: Occurrence) { + let key = ( + occ.symbol.clone(), + occ.range.clone(), + occ.symbol_roles, + ); + self.occurrences.entry(key).or_insert(occ); + } + + /// Insert a SymbolInformation entry, deduplicating by symbol string. + fn add_symbol_info(&mut self, sym_info: SymbolInformation) { + self.symbols.entry(sym_info.symbol.clone()).or_insert(sym_info); + } + + fn into_vecs(self) -> (Vec, Vec) { + (self.occurrences.into_values().collect(), + self.symbols.into_values().collect()) + } } /// Convert a single DeviceAnalysis into SCIP Documents. @@ -270,7 +297,7 @@ fn device_analysis_to_documents( occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::Definition.value(); - data.occurrences.push(occ); + data.add_occurrence(occ); // Add SymbolInformation for this symbol (only once, at def site) let mut sym_info = SymbolInformation::new(); @@ -296,7 +323,7 @@ fn device_analysis_to_documents( } } - data.symbols.push(sym_info); + data.add_symbol_info(sym_info); } // Record additional definitions @@ -312,7 +339,7 @@ fn device_analysis_to_documents( occ.range = span_to_scip_range(def_span); occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::Definition.value(); - data.occurrences.push(occ); + data.add_occurrence(occ); } // Record declarations @@ -329,7 +356,7 @@ fn device_analysis_to_documents( // Declarations get the Definition role in SCIP // (SCIP doesn't distinguish declaration vs definition) occ.symbol_roles = SymbolRole::Definition.value(); - data.occurrences.push(occ); + data.add_occurrence(occ); } // Record references (read accesses) @@ -341,7 +368,7 @@ fn device_analysis_to_documents( occ.range = span_to_scip_range(ref_span); occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::ReadAccess.value(); - data.occurrences.push(occ); + data.add_occurrence(occ); } // Record implementation sites (`is template` occurrences) @@ -356,7 +383,7 @@ fn device_analysis_to_documents( occ.range = span_to_scip_range(impl_span); occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::ReadAccess.value(); - data.occurrences.push(occ); + data.add_occurrence(occ); } } @@ -365,6 +392,7 @@ fn device_analysis_to_documents( let mut external_symbols = Vec::new(); for (path, data) in file_data { + let (occs, syms) = data.into_vecs(); match path.strip_prefix(project_root) { Ok(rel) => { let mut doc = Document::new(); @@ -372,14 +400,14 @@ fn device_analysis_to_documents( doc.language = "dml".to_string(); doc.position_encoding = PositionEncoding::UTF16CodeUnitOffsetFromLineStart.into(); - doc.occurrences = data.occurrences; - doc.symbols = data.symbols; + doc.occurrences = occs; + doc.symbols = syms; documents.push(doc); } Err(_) => { // External file: keep symbol info for hover/navigation // but don't emit a document or occurrences - external_symbols.extend(data.symbols); + external_symbols.extend(syms); } } } @@ -413,31 +441,50 @@ pub fn build_scip_index( }; metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); - // Collect documents from all devices, merging by relative_path - let mut merged_docs: HashMap = HashMap::new(); - let mut all_external_symbols: Vec = Vec::new(); + // Collect documents from all devices, merging by relative_path. + // We use FileData for deduplication across devices: the same symbol + // or occurrence can appear in multiple DeviceAnalyses when they + // share source files (e.g. common library code). + let mut merged: HashMap = HashMap::new(); + let mut ext_dedup = FileData::default(); for device in devices { let (docs, ext_syms) = device_analysis_to_documents(device, project_root); for doc in docs { - let entry = merged_docs.entry(doc.relative_path.clone()) + let (_, dedup) = merged + .entry(doc.relative_path.clone()) .or_insert_with(|| { let mut d = Document::new(); d.relative_path = doc.relative_path.clone(); d.language = doc.language.clone(); d.position_encoding = doc.position_encoding; - d + (d, FileData::default()) }); - entry.occurrences.extend(doc.occurrences); - entry.symbols.extend(doc.symbols); + for occ in doc.occurrences { + dedup.add_occurrence(occ); + } + for sym in doc.symbols { + dedup.add_symbol_info(sym); + } + } + for sym in ext_syms { + ext_dedup.add_symbol_info(sym); } - all_external_symbols.extend(ext_syms); } + // Move deduplicated data into the final documents + let documents: Vec = merged.into_values().map(|(mut doc, dedup)| { + let (occs, syms) = dedup.into_vecs(); + doc.occurrences = occs; + doc.symbols = syms; + doc + }).collect(); + let mut index = Index::new(); index.metadata = MessageField::some(metadata); - index.documents = merged_docs.into_values().collect(); - index.external_symbols = all_external_symbols; + index.documents = documents; + let (_, ext_syms) = ext_dedup.into_vecs(); + index.external_symbols = ext_syms; debug!("SCIP index built with {} document(s)", index.documents.len()); index From ddc5bfe999bfdac461388d2c10e005a2ef1a172c Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:20:44 +0100 Subject: [PATCH 04/32] Minor doc fix --- src/scip/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 8fc14869..2268ab46 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -130,7 +130,8 @@ fn make_global_symbol(device_name: &str, qualified_path: &str, } } } else { - // Intermediate segments are namespace-like + // Intermediate segments are enclosing object instances + // (device, bank, register, ...) — use term descriptor descriptors.push_str(&sanitized); descriptors.push('.'); } From d87b4327123112df33c668bb7a3c9b5f74a0a7fb Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:25:28 +0100 Subject: [PATCH 05/32] Sort outputs for determinism purposes --- src/scip/mod.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 2268ab46..b7781beb 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -248,8 +248,11 @@ impl FileData { } fn into_vecs(self) -> (Vec, Vec) { - (self.occurrences.into_values().collect(), - self.symbols.into_values().collect()) + let mut occs: Vec<_> = self.occurrences.into_values().collect(); + occs.sort_by(|a, b| a.range.cmp(&b.range)); + let mut syms: Vec<_> = self.symbols.into_values().collect(); + syms.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + (occs, syms) } } @@ -324,6 +327,7 @@ fn device_analysis_to_documents( } } + sym_info.relationships.sort_by(|a, b| a.symbol.cmp(&b.symbol)); data.add_symbol_info(sym_info); } @@ -473,18 +477,21 @@ pub fn build_scip_index( } } - // Move deduplicated data into the final documents - let documents: Vec = merged.into_values().map(|(mut doc, dedup)| { + // Move deduplicated data into the final documents, sorted for + // deterministic output. + let mut documents: Vec = merged.into_values().map(|(mut doc, dedup)| { let (occs, syms) = dedup.into_vecs(); doc.occurrences = occs; doc.symbols = syms; doc }).collect(); + documents.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); let mut index = Index::new(); index.metadata = MessageField::some(metadata); index.documents = documents; - let (_, ext_syms) = ext_dedup.into_vecs(); + let (_, mut ext_syms) = ext_dedup.into_vecs(); + ext_syms.sort_by(|a, b| a.symbol.cmp(&b.symbol)); index.external_symbols = ext_syms; debug!("SCIP index built with {} document(s)", index.documents.len()); From 7aa4059ab2955070f85fd6af8db46fc4326e9132 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:33:46 +0100 Subject: [PATCH 06/32] Use plain references instead of readaccess --- src/scip/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index b7781beb..ae1028d9 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -372,7 +372,10 @@ fn device_analysis_to_documents( let mut occ = Occurrence::new(); occ.range = span_to_scip_range(ref_span); occ.symbol = scip_symbol.clone(); - occ.symbol_roles = SymbolRole::ReadAccess.value(); + // Plain reference (no Definition/ReadAccess/WriteAccess role). + // TODO: narrow down to ReadAccess/WriteAccess once the + // analysis tracks access kinds. + occ.symbol_roles = 0; data.add_occurrence(occ); } @@ -387,7 +390,9 @@ fn device_analysis_to_documents( let mut occ = Occurrence::new(); occ.range = span_to_scip_range(impl_span); occ.symbol = scip_symbol.clone(); - occ.symbol_roles = SymbolRole::ReadAccess.value(); + // Plain reference — the implementation relationship is + // expressed via Relationship entries, not occurrence roles. + occ.symbol_roles = 0; data.add_occurrence(occ); } } From 14c60cb07119fe35e12b39ba64a2b0cd7878fa21 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 09:39:43 +0100 Subject: [PATCH 07/32] Distinguish forward-decls --- src/scip/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index ae1028d9..40bcc426 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -358,9 +358,14 @@ fn device_analysis_to_documents( let mut occ = Occurrence::new(); occ.range = span_to_scip_range(decl_span); occ.symbol = scip_symbol.clone(); - // Declarations get the Definition role in SCIP - // (SCIP doesn't distinguish declaration vs definition) - occ.symbol_roles = SymbolRole::Definition.value(); + // If this declaration site also appears in definitions, + // it defines a value and gets the Definition role. + // Otherwise it's an abstract/forward declaration. + if sym.definitions.contains(decl_span) { + occ.symbol_roles = SymbolRole::Definition.value(); + } else { + occ.symbol_roles = SymbolRole::ForwardDefinition.value(); + } data.add_occurrence(occ); } From 8748516f8e6c1ee1618e02575557e0eaa7350311 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 27 May 2026 15:22:47 +0200 Subject: [PATCH 08/32] Make all_decls be full specs again This should technically be a smaller memory footprint due to Arc, and it allows us more access Signed-off-by: Jonatan Waern --- src/analysis/mod.rs | 2 +- src/analysis/templating/objects.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analysis/mod.rs b/src/analysis/mod.rs index 01ceef10..f65890e6 100644 --- a/src/analysis/mod.rs +++ b/src/analysis/mod.rs @@ -1932,7 +1932,7 @@ fn extend_with_templates(maker: &SymbolMaker, fn new_symbol_from_object(maker: &SymbolMaker, object: &DMLCompositeObject) -> SymbolRef { - let all_decl_defs = &object.all_decls; + let all_decl_defs: Vec = object.all_decls.iter().map(|spec|*spec.loc_span()).collect(); symbol_ref!( maker, object.declloc, diff --git a/src/analysis/templating/objects.rs b/src/analysis/templating/objects.rs index d1318030..8172a7b9 100644 --- a/src/analysis/templating/objects.rs +++ b/src/analysis/templating/objects.rs @@ -842,7 +842,7 @@ pub struct DMLCompositeObject { pub declloc: ZeroSpan, // These are the ranges of the objectspecs declared with the // objects name - pub all_decls: Vec, + pub all_decls: Vec>, pub identity: DMLString, // Reference to self, let's us pass obj refs rather than // keys to functions unless necessary @@ -1382,7 +1382,7 @@ fn create_object_instance(loc: Option, all_decls); let obj = DMLCompositeObject { declloc: loc.unwrap_or(identity.span), - all_decls: all_decls.iter().map(|s|*s.loc_span()).collect(), + all_decls: all_decls.clone(), identity: identity.clone(), used_ineach_locs: vec![], key: StructureKey::null(), From b5dbcf973f2ec616c072dc3847f4bb97157e76b3 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 10:13:50 +0100 Subject: [PATCH 09/32] Add enclosing ranges --- src/scip/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 40bcc426..1b289f59 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -218,6 +218,38 @@ fn make_documentation(sym: &crate::analysis::symbols::Symbol, vec![format!("{} `{}`{}", kind_str, display_name, typed_str)] } +/// Build a map from definition/declaration name locations to their +/// enclosing AST spans, for use as SCIP `enclosing_range`. +/// +/// For composite objects, each ObjectSpec has a `loc` (name span) and +/// a `span` (full `group foo is bar { ... }` range). For methods, +/// the MethodDecl has a name location and a full declaration span. +fn enclosing_ranges_for_source( + source: &SymbolSource, + container: &StructureContainer, +) -> HashMap { + let mut map = HashMap::new(); + match source { + SymbolSource::DMLObject(DMLObject::CompObject(key)) => { + if let Some(comp) = container.get(*key) { + for spec in &comp.all_decls { + map.insert(spec.loc, spec.span); + } + // definitions may include specs not in all_decls + for spec in &comp.definitions { + map.entry(spec.loc).or_insert(spec.span); + } + } + } + SymbolSource::Method(_, methref) => { + let decl = methref.get_decl(); + map.insert(decl.name.span, decl.span); + } + _ => {} + } + map +} + /// Holds per-file occurrence and symbol information data /// that will be assembled into SCIP Documents. /// @@ -289,6 +321,7 @@ fn device_analysis_to_documents( let kind = dml_kind_to_scip_kind(&sym.kind); let documentation = make_documentation(&sym, &display_name); + let enclosing = enclosing_ranges_for_source(&sym.source, container); // Record the primary location as a definition occurrence { @@ -300,6 +333,9 @@ fn device_analysis_to_documents( occ.range = span_to_scip_range(loc); occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::Definition.value(); + if let Some(enc) = enclosing.get(loc) { + occ.enclosing_range = span_to_scip_range(enc); + } data.add_occurrence(occ); @@ -344,6 +380,9 @@ fn device_analysis_to_documents( occ.range = span_to_scip_range(def_span); occ.symbol = scip_symbol.clone(); occ.symbol_roles = SymbolRole::Definition.value(); + if let Some(enc) = enclosing.get(def_span) { + occ.enclosing_range = span_to_scip_range(enc); + } data.add_occurrence(occ); } @@ -366,6 +405,9 @@ fn device_analysis_to_documents( } else { occ.symbol_roles = SymbolRole::ForwardDefinition.value(); } + if let Some(enc) = enclosing.get(decl_span) { + occ.enclosing_range = span_to_scip_range(enc); + } data.add_occurrence(occ); } From af91a4edf1822e78a23a840b393b988f4b3b4e35 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 10:42:46 +0100 Subject: [PATCH 10/32] Add declaration meta-info to documentation --- USAGE.md | 50 +++++++++++++++++++++++++++++++++++------ src/scip/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 20 deletions(-) diff --git a/USAGE.md b/USAGE.md index 2a567105..8cc738ae 100644 --- a/USAGE.md +++ b/USAGE.md @@ -154,14 +154,29 @@ DML symbol kinds are mapped to SCIP `SymbolInformation.Kind` as follows: - `Constant` — Parameter, Constant, Loggroup - `Variable` — Extern, Saved, Session, Local - `Parameter` — MethodArg -- `Function` — Hook +- `Event` — Hook - `Method` — Method - `Class` — Template - `TypeAlias` — Typedef -- `Namespace` — All composite objects (Device, Bank, Register, Field, Group, Port, Connect, Attribute, Event, Subdevice) -- `Struct` — Implement +- `Object` — All composite objects (Device, Bank, Register, Field, Group, Port, Connect, Attribute, Event, Subdevice, Implement) - `Interface` — Interface +Note: SCIP's `Object` kind is used for DML composite objects because they are +instantiated structural components in the device hierarchy, not types or +namespaces. `Event` is used for DML hooks because they represent named event +points that can be sent or listened to. + +Since SCIP's `Kind` enum is too coarse to distinguish between the various DML +composite object kinds (e.g. `register` vs `bank` vs `attribute`), the +`SymbolInformation.documentation` field carries a short-form declaration +signature that disambiguates: + +- **Composite objects:** the DML keyword for the object kind, e.g. `register`, + `bank`, `attribute`, `group`, `field`, `device`, etc. +- **Methods:** the DML declaration modifiers, e.g. `method`, + `independent method default`, `shared method throws`. +- **Other symbol kinds:** no documentation is emitted. + #### Symbol Naming Scheme SCIP symbols follow the format: @@ -193,10 +208,31 @@ symbols are scoped to a single document and are not navigable across files. #### Occurrence Roles -DML declarations and definitions are both emitted with the SCIP `Definition` -role, since SCIP does not distinguish between the two. References (including -template instantiation sites from `is` statements) are emitted with -`ReadAccess`. +DML definitions (including the primary symbol location) are emitted with the +SCIP `Definition` role. Declarations that also appear as definitions share +this role. Declarations that do _not_ define a value (e.g. abstract method +declarations, or `default` parameter declarations that are overridden) are +emitted with the `ForwardDefinition` role. + +References (including template instantiation sites from `is` statements) are +emitted as plain references with no additional role flags. Access-kind +refinement (`ReadAccess` / `WriteAccess`) is not yet tracked. + +#### Enclosing Ranges + +For composite object definitions and method declarations, each `Definition` +or `ForwardDefinition` occurrence includes an `enclosing_range` that spans +the full AST node (e.g. the complete `register r1 is ... { ... }` block or +the full method body). This allows consumers to associate the definition site +with the extent of the construct it names. + +#### Deduplication and Determinism + +When multiple device analyses share source files (e.g. common library code), +the SCIP export deduplicates occurrences and symbol information so that each +(symbol, range, role) triple and each symbol entry appears at most once. +All output is sorted deterministically: documents by relative path, +occurrences by range, symbols by symbol string, and relationships by symbol. #### Relationships diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 1b289f59..d30f5786 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -18,7 +18,7 @@ use scip::types::{ }; use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; -use crate::analysis::structure::objects::CompObjectKind; +use crate::analysis::structure::objects::{CompObjectKind, MethodModifier}; use crate::analysis::templating::objects::{ DMLHierarchyMember, DMLNamedMember, DMLObject, StructureContainer, }; @@ -51,13 +51,13 @@ fn dml_kind_to_scip_kind(kind: &DMLSymbolKind) -> ScipSymbolKind { match kind { DMLSymbolKind::CompObject(comp_kind) => match comp_kind { CompObjectKind::Interface => ScipSymbolKind::Interface, - CompObjectKind::Implement => ScipSymbolKind::Struct, - _ => ScipSymbolKind::Namespace, + CompObjectKind::Implement => ScipSymbolKind::Object, + _ => ScipSymbolKind::Object, }, DMLSymbolKind::Parameter => ScipSymbolKind::Constant, DMLSymbolKind::Constant => ScipSymbolKind::Constant, DMLSymbolKind::Extern => ScipSymbolKind::Variable, - DMLSymbolKind::Hook => ScipSymbolKind::Function, + DMLSymbolKind::Hook => ScipSymbolKind::Event, DMLSymbolKind::Local => ScipSymbolKind::Variable, DMLSymbolKind::Loggroup => ScipSymbolKind::Constant, DMLSymbolKind::Method => ScipSymbolKind::Method, @@ -208,14 +208,47 @@ fn scip_symbol_for_source( } } -/// Build a human-readable documentation string for a DML symbol. -fn make_documentation(sym: &crate::analysis::symbols::Symbol, - display_name: &str) -> Vec { - let kind_str = format!("{:?}", sym.kind); - let typed_str = sym.typed.as_ref() - .map(|t| format!(" : {:?}", t)) - .unwrap_or_default(); - vec![format!("{} `{}`{}", kind_str, display_name, typed_str)] +/// Build a short-form declaration signature for a DML symbol. +/// +/// For composite objects this is just the object kind keyword +/// (e.g. `"register"`, `"bank"`). +/// For methods this is the modifier keywords from the declaration +/// (e.g. `"independent method default"`, `"shared method throws"`). +/// Other symbol kinds currently produce no documentation. +fn make_documentation( + source: &SymbolSource, + container: &StructureContainer, +) -> Vec { + match source { + SymbolSource::DMLObject(DMLObject::CompObject(key)) => { + if let Some(comp) = container.get(*key) { + vec![comp.kind.kind_name().to_string()] + } else { + vec![] + } + } + SymbolSource::Method(_, methref) => { + let decl = methref.get_decl(); + let mut parts = Vec::new(); + if decl.independent { + parts.push("independent"); + } + match decl.modifier { + MethodModifier::Shared => parts.push("shared"), + MethodModifier::Inline => parts.push("inline"), + MethodModifier::None => {} + } + parts.push("method"); + if decl.default { + parts.push("default"); + } + if decl.throws { + parts.push("throws"); + } + vec![parts.join(" ")] + } + _ => vec![], + } } /// Build a map from definition/declaration name locations to their @@ -320,7 +353,7 @@ fn device_analysis_to_documents( sym.references.len(), sym.implementations.len()); let kind = dml_kind_to_scip_kind(&sym.kind); - let documentation = make_documentation(&sym, &display_name); + let documentation = make_documentation(&sym.source, container); let enclosing = enclosing_ranges_for_source(&sym.source, container); // Record the primary location as a definition occurrence From f663c438321d6f782b809ad6ef77c7301fe4863a Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 10:55:38 +0100 Subject: [PATCH 11/32] Add file import relations --- USAGE.md | 17 ++++++ src/actions/requests.rs | 10 +++- src/scip/mod.rs | 118 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/USAGE.md b/USAGE.md index 8cc738ae..431585e8 100644 --- a/USAGE.md +++ b/USAGE.md @@ -239,3 +239,20 @@ occurrences by range, symbols by symbol string, and relationships by symbol. Composite objects that instantiate templates (via `is some_template`) emit SCIP `Relationship` entries with `is_implementation = true` pointing to the template symbol. + +#### File Symbols and Imports + +Each source file involved in the analysis gets a dedicated SCIP symbol of kind +`File`. A `Definition` occurrence is emitted at line 0 of each file so that +navigation to the file symbol opens the file itself. + +For each `import "..."` statement, an `Import` occurrence is emitted at the +import statement's span, referencing the imported file's symbol. This lets +consumers navigate from import statements to the imported file and visualize +file-level dependency graphs. + +File symbols use the format: +``` +dml simics . . path/to/file_dml. +``` +where path segments are separated by term descriptors (`.`). diff --git a/src/actions/requests.rs b/src/actions/requests.rs index ef8cb18b..5a9c32a6 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -1064,6 +1064,13 @@ impl RequestAction for ExportScipRequest { info!("Exporting SCIP for {} device(s)", devices.len()); + // Extract import resolution data for the SCIP export + let import_data = crate::scip::extract_import_data( + &analysis.isolated_analysis, + &analysis.import_map, + &devices, + ); + // Determine project root from workspaces let project_root = ctx.workspace_roots .lock() @@ -1072,7 +1079,8 @@ impl RequestAction for ExportScipRequest { .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) .unwrap_or_else(|| std::path::PathBuf::from(".")); - let index = crate::scip::build_scip_index(&devices, &project_root); + let index = crate::scip::build_scip_index(&devices, &project_root, + &import_data); let doc_count = index.documents.len(); let output = std::path::Path::new(¶ms.output_path); diff --git a/src/scip/mod.rs b/src/scip/mod.rs index d30f5786..5d4284bc 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -23,10 +23,18 @@ use crate::analysis::templating::objects::{ DMLHierarchyMember, DMLNamedMember, DMLObject, StructureContainer, }; use crate::analysis::DeviceAnalysis; +use crate::analysis::IsolatedAnalysis; use crate::Span as ZeroSpan; +use crate::file_management::CanonPath; use log::debug; +/// Per-file import resolution data for SCIP export. +/// +/// Maps each source file (canonical path) to its list of +/// (import_statement_span, resolved_target_canonical_path) pairs. +pub type FileImportData = HashMap>; + /// Convert a ZeroSpan range into the SCIP occurrence range format. /// /// SCIP uses `[startLine, startChar, endLine, endChar]` (4 elements) @@ -80,6 +88,70 @@ fn sanitize_name(name: &str) -> String { .collect() } +/// Build a SCIP symbol string representing a DML source file. +/// +/// File symbols use the path relative to the project root (or the +/// full path for external files) as the descriptor, with dots and +/// slashes sanitized. +fn make_file_symbol(path: &Path, project_root: &Path) -> String { + let display = path.strip_prefix(project_root) + .unwrap_or(path) + .to_string_lossy(); + let sanitized = display.chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } + else if c == '/' || c == '\\' { '/' } + else { '_' }) + .collect::(); + // Use the path segments as nested term descriptors + let descriptors: String = sanitized.split('/') + .filter(|s| !s.is_empty()) + .map(|s| format!("{}.", s)) + .collect(); + format!("dml simics . . {}", descriptors) +} + +/// Extract import resolution data from an AnalysisStorage for a set +/// of device analyses. +/// +/// For each file involved in any of the given devices, collects the +/// (import_span, resolved_path) pairs from the IsolatedAnalysis +/// import data and the import_map resolution data. +pub fn extract_import_data( + isolated_analyses: &HashMap>, + import_map: &HashMap, + HashMap>>, + devices: &[&DeviceAnalysis], +) -> FileImportData { + let mut result = FileImportData::new(); + for device in devices { + let device_context = Some(device.path.clone()); + for file_path in &device.dependant_files { + if result.contains_key(file_path) { + continue; + } + let mut imports = Vec::new(); + if let Some(analysis) = isolated_analyses.get(file_path) { + let context_map = import_map.get(file_path); + let resolved = context_map + .and_then(|cm| cm.get(&device_context)) + .or_else(|| context_map.and_then(|cm| cm.get(&None))); + for import_decl in analysis.stored.get_imports() { + let import = &import_decl.obj; + if let Some(resolved_map) = resolved { + if let Some(canon) = resolved_map.get(import) { + imports.push((import.span, canon.clone())); + } + } + } + } + result.insert(file_path.clone(), imports); + } + } + result +} + /// Build a `local` SCIP symbol string (document-scoped). /// /// Used for method arguments, method locals, and other symbols that @@ -330,6 +402,7 @@ impl FileData { fn device_analysis_to_documents( device: &DeviceAnalysis, project_root: &Path, + import_data: &FileImportData, ) -> (Vec, Vec) { let mut file_data: HashMap = HashMap::new(); let container = &device.objects; @@ -477,6 +550,48 @@ fn device_analysis_to_documents( } } + // Emit file-level symbols and import occurrences. + // + // For each file in the device analysis, we create a file-level + // symbol (with a Definition occurrence at line 0) and then emit + // Import occurrences at each `import "..."` statement pointing + // to the imported file's symbol. + for dep_path in &device.dependant_files { + let file_pathbuf: PathBuf = dep_path.clone().into(); + let file_sym = make_file_symbol(&file_pathbuf, project_root); + + // Definition occurrence at line 0 of the file + let data = file_data.entry(file_pathbuf.clone()).or_default(); + let mut def_occ = Occurrence::new(); + def_occ.range = vec![0, 0, 0]; // line 0, char 0, end char 0 + def_occ.symbol = file_sym.clone(); + def_occ.symbol_roles = SymbolRole::Definition.value(); + data.add_occurrence(def_occ); + + // SymbolInformation for the file + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = file_sym.clone(); + sym_info.kind = ScipSymbolKind::File.into(); + sym_info.display_name = file_pathbuf.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + data.add_symbol_info(sym_info); + + // Import occurrences for each `import "..."` in this file + if let Some(imports) = import_data.get(dep_path) { + for (import_span, resolved_path) in imports { + let target_pathbuf: PathBuf = resolved_path.clone().into(); + let target_sym = make_file_symbol(&target_pathbuf, project_root); + + let mut imp_occ = Occurrence::new(); + imp_occ.range = span_to_scip_range(import_span); + imp_occ.symbol = target_sym; + imp_occ.symbol_roles = SymbolRole::Import.value(); + data.add_occurrence(imp_occ); + } + } + } + // Assemble Documents, separating in-project from external files. let mut documents = Vec::new(); let mut external_symbols = Vec::new(); @@ -513,6 +628,7 @@ fn device_analysis_to_documents( pub fn build_scip_index( devices: &[&DeviceAnalysis], project_root: &Path, + import_data: &FileImportData, ) -> Index { debug!("Building SCIP index for {} device(s) rooted at {:?}", devices.len(), project_root); @@ -539,7 +655,7 @@ pub fn build_scip_index( let mut ext_dedup = FileData::default(); for device in devices { - let (docs, ext_syms) = device_analysis_to_documents(device, project_root); + let (docs, ext_syms) = device_analysis_to_documents(device, project_root, import_data); for doc in docs { let (_, dedup) = merged .entry(doc.relative_path.clone()) From f9a20b33923af3850b92842fce80ddf3ebcb42dd Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 23 Mar 2026 11:13:05 +0100 Subject: [PATCH 12/32] Fix symbol duplication issue --- src/scip/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 5d4284bc..bedd03b3 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -5,7 +5,7 @@ //! This module converts DLS analysis data (DeviceAnalysis) into //! the SCIP index format for use with code intelligence tools. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use protobuf::MessageField; @@ -617,6 +617,16 @@ fn device_analysis_to_documents( } } + // Remove from external_symbols any symbol that already appears + // in a document. This can happen when multiple internal Symbol + // objects (e.g. from different templates) produce the same SCIP + // symbol string but have their primary locations in different + // files — one in-project and one external. + let doc_symbol_strings: HashSet<&str> = documents.iter() + .flat_map(|doc| doc.symbols.iter().map(|s| s.symbol.as_str())) + .collect(); + external_symbols.retain(|s| !doc_symbol_strings.contains(s.symbol.as_str())); + (documents, external_symbols) } From fdd36a2c49f8d9148d6a403dbb958bf87791c934 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 24 Mar 2026 08:26:08 +0100 Subject: [PATCH 13/32] Add enclosing ranges for templates --- src/scip/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index bedd03b3..ba192952 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -350,6 +350,11 @@ fn enclosing_ranges_for_source( let decl = methref.get_decl(); map.insert(decl.name.span, decl.span); } + SymbolSource::Template(templ) => { + if let Some(loc) = templ.location { + map.insert(loc, templ.spec.span); + } + } _ => {} } map From 1dcdc38e0cef9b4e0a86009d8dbc4aa071e5ddfe Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 24 Mar 2026 10:57:17 +0100 Subject: [PATCH 14/32] Add relations from connects to interfaces --- USAGE.md | 4 ++++ src/scip/mod.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/USAGE.md b/USAGE.md index 431585e8..a6af4229 100644 --- a/USAGE.md +++ b/USAGE.md @@ -240,6 +240,10 @@ Composite objects that instantiate templates (via `is some_template`) emit SCIP `Relationship` entries with `is_implementation = true` pointing to the template symbol. +Connect objects additionally emit `is_implementation` relationships pointing to +each interface symbol nested under their implement children. This captures the +semantic link between a connect and the interfaces it provides. + #### File Symbols and Imports Each source file involved in the analysis gets a dedicated SCIP symbol of kind diff --git a/src/scip/mod.rs b/src/scip/mod.rs index ba192952..93336321 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -471,6 +471,37 @@ fn device_analysis_to_documents( rel.is_implementation = true; sym_info.relationships.push(rel); } + + // For connects, emit is_implementation pointing + // to each interface object nested under its + // implement children. + if comp.kind == CompObjectKind::Connect { + for child_obj in comp.components.values() { + if let DMLObject::CompObject(impl_key) = child_obj { + if let Some(impl_obj) = container.get(*impl_key) { + if impl_obj.kind != CompObjectKind::Implement { + continue; + } + for grandchild in impl_obj.components.values() { + if let DMLObject::CompObject(iface_key) = grandchild { + if let Some(iface_obj) = container.get(*iface_key) { + if iface_obj.kind == CompObjectKind::Interface { + let iface_qname = iface_obj.qualified_name(container); + let iface_sym = make_global_symbol( + device_name, &iface_qname, + &DMLSymbolKind::CompObject(CompObjectKind::Interface)); + let mut rel = Relationship::new(); + rel.symbol = iface_sym; + rel.is_implementation = true; + sym_info.relationships.push(rel); + } + } + } + } + } + } + } + } } } From bc8dc02164da094f2c90e39758cb995f2a255f3e Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 24 Mar 2026 11:07:14 +0100 Subject: [PATCH 15/32] Correct the name sanitization --- src/scip/mod.rs | 59 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 93336321..20b51a87 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -79,33 +79,52 @@ fn dml_kind_to_scip_kind(kind: &DMLSymbolKind) -> ScipSymbolKind { /// Sanitize a name for use in SCIP symbol strings. /// -/// SCIP descriptors use backtick-escaping for names that contain -/// non-identifier characters, but to keep things simple we sanitize -/// to `[a-zA-Z0-9_]+`. +/// Check whether a character is a SCIP identifier character. +/// +/// Per the SCIP symbol grammar: +/// ` ::= '_' | '+' | '-' | '$' | ASCII letter or digit` +fn is_scip_identifier_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '_' | '+' | '-' | '$') +} + +/// Encode a name as a SCIP descriptor identifier. +/// +/// Names consisting entirely of SCIP identifier characters are emitted +/// as-is (a "simple identifier"). Names that contain other characters +/// (e.g. dots, spaces) are backtick-escaped, with interior backticks +/// doubled. fn sanitize_name(name: &str) -> String { - name.chars() - .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }) - .collect() + if !name.is_empty() && name.chars().all(is_scip_identifier_char) { + name.to_string() + } else { + // Escaped identifier: `+` + // Interior backticks are escaped by doubling them. + let mut out = String::new(); + out.push('`'); + for c in name.chars() { + if c == '`' { + out.push_str("``"); + } else { + out.push(c); + } + } + out.push('`'); + out + } } /// Build a SCIP symbol string representing a DML source file. /// /// File symbols use the path relative to the project root (or the -/// full path for external files) as the descriptor, with dots and -/// slashes sanitized. +/// full path for external files) as the descriptor. Each path +/// component becomes a term descriptor with proper SCIP escaping. fn make_file_symbol(path: &Path, project_root: &Path) -> String { - let display = path.strip_prefix(project_root) - .unwrap_or(path) - .to_string_lossy(); - let sanitized = display.chars() - .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } - else if c == '/' || c == '\\' { '/' } - else { '_' }) - .collect::(); - // Use the path segments as nested term descriptors - let descriptors: String = sanitized.split('/') - .filter(|s| !s.is_empty()) - .map(|s| format!("{}.", s)) + let rel = path.strip_prefix(project_root).unwrap_or(path); + let descriptors: String = rel.components() + .filter_map(|c| { + let s = c.as_os_str().to_str()?; + Some(format!("{}.", sanitize_name(s))) + }) .collect(); format!("dml simics . . {}", descriptors) } From 6d976e7e15de2a8b3d391fc69b040853a057cd38 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 24 Mar 2026 11:49:25 +0100 Subject: [PATCH 16/32] Store relationship info for file imports --- src/scip/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 20b51a87..62e7e5b9 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -623,28 +623,39 @@ fn device_analysis_to_documents( def_occ.symbol_roles = SymbolRole::Definition.value(); data.add_occurrence(def_occ); - // SymbolInformation for the file + // SymbolInformation for the file, with is_reference + // relationships to each imported file's symbol. let mut sym_info = SymbolInformation::new(); sym_info.symbol = file_sym.clone(); sym_info.kind = ScipSymbolKind::File.into(); sym_info.display_name = file_pathbuf.file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_default(); - data.add_symbol_info(sym_info); - // Import occurrences for each `import "..."` in this file + // Import occurrences for each `import "..."` in this file, + // plus is_reference relationships on the file symbol. if let Some(imports) = import_data.get(dep_path) { + let mut seen_targets = HashSet::new(); for (import_span, resolved_path) in imports { let target_pathbuf: PathBuf = resolved_path.clone().into(); let target_sym = make_file_symbol(&target_pathbuf, project_root); let mut imp_occ = Occurrence::new(); imp_occ.range = span_to_scip_range(import_span); - imp_occ.symbol = target_sym; + imp_occ.symbol = target_sym.clone(); imp_occ.symbol_roles = SymbolRole::Import.value(); data.add_occurrence(imp_occ); + + if seen_targets.insert(target_sym.clone()) { + let mut rel = Relationship::new(); + rel.symbol = target_sym; + rel.is_reference = true; + sym_info.relationships.push(rel); + } } } + sym_info.relationships.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + data.add_symbol_info(sym_info); } // Assemble Documents, separating in-project from external files. From 3d988a57af18d297b5520847e73c7427b99b656b Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 24 Mar 2026 12:04:10 +0100 Subject: [PATCH 17/32] Always emit 4-tuple ranges --- src/scip/mod.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 62e7e5b9..6b684eb3 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -42,16 +42,12 @@ pub type FileImportData = HashMap>; /// All values are 0-based. fn span_to_scip_range(span: &ZeroSpan) -> Vec { let r = &span.range; - let start_line = r.row_start.0 as i32; - let start_char = r.col_start.0 as i32; - let end_line = r.row_end.0 as i32; - let end_char = r.col_end.0 as i32; - - if start_line == end_line { - vec![start_line, start_char, end_char] - } else { - vec![start_line, start_char, end_line, end_char] - } + vec![ + r.row_start.0 as i32, + r.col_start.0 as i32, + r.row_end.0 as i32, + r.col_end.0 as i32, + ] } /// Map a DMLSymbolKind to a SCIP SymbolInformation Kind. From 41e464540480775ee7f0ab16689b53c91fd09635 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 25 Mar 2026 09:07:25 +0100 Subject: [PATCH 18/32] Bump scip --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6c164801..b0ce1abd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ subprocess = "1.0" thiserror = "2.0" urlencoding = "2.1" utf8-read = "0.4" -scip = "0.6.1" +scip = "0.7" protobuf = "3" walkdir = "2" heck = "0.5" From 1582e3e9682a6698fff5045be8813a0ba79b732f Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Thu, 26 Mar 2026 12:23:28 +0100 Subject: [PATCH 19/32] Add 'implements' relations from overriding method to overridden --- USAGE.md | 7 +++++++ src/scip/mod.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/USAGE.md b/USAGE.md index a6af4229..a174eb8d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -244,6 +244,13 @@ Connect objects additionally emit `is_implementation` relationships pointing to each interface symbol nested under their implement children. This captures the semantic link between a connect and the interfaces it provides. +Methods that override a default or abstract method from a template also emit +`is_implementation` relationships pointing to the overridden method's symbol. +For example, if template `foo` defines `method a` as `default` and object `b` +implements `foo` with its own definition of `method a`, then `b.a` will carry +an `is_implementation` relationship to `foo.a`. This lets consumers identify +which method version is the active override and navigate the override chain. + #### File Symbols and Imports Each source file involved in the analysis gets a dedicated SCIP symbol of kind diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 6b684eb3..340ff8db 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -428,6 +428,12 @@ fn device_analysis_to_documents( let container = &device.objects; let device_name = &device.name; + // Track method declaration locations → SCIP symbol strings so we + // can wire up override relationships in a second pass. + let mut method_span_to_scip_sym: HashMap = HashMap::new(); + // Collect (file_path, overriding_symbol, overridden_locations) edges. + let mut override_edges: Vec<(PathBuf, String, Vec)> = Vec::new(); + // Iterate over all symbols in the device analysis for symbol_ref in device.symbol_info.all_symbols() { let sym = symbol_ref.symbol.lock().unwrap(); @@ -445,6 +451,26 @@ fn device_analysis_to_documents( sym.definitions.len(), sym.declarations.len(), sym.references.len(), sym.implementations.len()); + // Track method symbols for override relationship resolution. + if let SymbolSource::Method(_, methref) = &sym.source { + method_span_to_scip_sym.insert( + *methref.location(), scip_symbol.clone()); + if let Some(default_call) = methref.get_default() { + let overridden_locs: Vec = default_call + .flat_refs() + .iter() + .map(|r| *r.location()) + .collect(); + if !overridden_locs.is_empty() { + override_edges.push(( + sym.loc.path(), + scip_symbol.clone(), + overridden_locs, + )); + } + } + } + let kind = dml_kind_to_scip_kind(&sym.kind); let documentation = make_documentation(&sym.source, container); let enclosing = enclosing_ranges_for_source(&sym.source, container); @@ -601,6 +627,31 @@ fn device_analysis_to_documents( } } + // Add override relationships to method SymbolInformation entries. + // A method that overrides a default/abstract method from a template + // gets an is_implementation relationship pointing to the overridden + // method's SCIP symbol. + for (def_file, overriding_sym, overridden_locs) in &override_edges { + if let Some(data) = file_data.get_mut(def_file) { + if let Some(sym_info) = data.symbols.get_mut(overriding_sym) { + for loc in overridden_locs { + if let Some(overridden_sym) = + method_span_to_scip_sym.get(loc) + { + if overridden_sym != overriding_sym { + let mut rel = Relationship::new(); + rel.symbol = overridden_sym.clone(); + rel.is_implementation = true; + sym_info.relationships.push(rel); + } + } + } + sym_info.relationships + .sort_by(|a, b| a.symbol.cmp(&b.symbol)); + } + } + } + // Emit file-level symbols and import occurrences. // // For each file in the device analysis, we create a file-level From 5e7ab6b5507909382237b63b06a6d5fb87da6087 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 30 Mar 2026 13:10:13 +0200 Subject: [PATCH 20/32] Move to a hierarchical symbol naming scheme --- USAGE.md | 7 +- src/actions/requests.rs | 17 +- src/scip/mod.rs | 1265 ++++++++++++++++++++++----------------- 3 files changed, 736 insertions(+), 553 deletions(-) diff --git a/USAGE.md b/USAGE.md index a174eb8d..8e5e9feb 100644 --- a/USAGE.md +++ b/USAGE.md @@ -258,9 +258,10 @@ Each source file involved in the analysis gets a dedicated SCIP symbol of kind navigation to the file symbol opens the file itself. For each `import "..."` statement, an `Import` occurrence is emitted at the -import statement's span, referencing the imported file's symbol. This lets -consumers navigate from import statements to the imported file and visualize -file-level dependency graphs. +import statement's span, referencing the imported file's symbol. Additionally, +the importing file's `SymbolInformation` carries an `is_reference = true` +`Relationship` entry for each imported file, enabling explicit file-level +dependency tracking without needing to scan occurrences. File symbols use the format: ``` diff --git a/src/actions/requests.rs b/src/actions/requests.rs index 5a9c32a6..d6b98627 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -1071,6 +1071,19 @@ impl RequestAction for ExportScipRequest { &devices, ); + // Collect per-file isolated analyses from all device + // dependant files (deduplicates via HashMap key). + let mut isolated_map = std::collections::HashMap::new(); + for device in &devices { + for file_path in &device.dependant_files { + if !isolated_map.contains_key(file_path) { + if let Some(ts) = analysis.isolated_analysis.get(file_path) { + isolated_map.insert(file_path.clone(), &ts.stored); + } + } + } + } + // Determine project root from workspaces let project_root = ctx.workspace_roots .lock() @@ -1079,8 +1092,8 @@ impl RequestAction for ExportScipRequest { .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) .unwrap_or_else(|| std::path::PathBuf::from(".")); - let index = crate::scip::build_scip_index(&devices, &project_root, - &import_data); + let index = crate::scip::build_scip_index( + &isolated_map, &project_root, Some(&import_data), &devices); let doc_count = index.documents.len(); let output = std::path::Path::new(¶ms.output_path); diff --git a/src/scip/mod.rs b/src/scip/mod.rs index 340ff8db..b35b6342 100644 --- a/src/scip/mod.rs +++ b/src/scip/mod.rs @@ -2,25 +2,38 @@ // SPDX-License-Identifier: Apache-2.0 and MIT //! SCIP (Source Code Intelligence Protocol) export support. //! -//! This module converts DLS analysis data (DeviceAnalysis) into -//! the SCIP index format for use with code intelligence tools. +//! This module walks the structural tree of each `IsolatedAnalysis` +//! to produce SCIP symbols based on code namespaces. +//! +//! For example, a method `foo` inside template `t` in file +//! `src/dev.dml` gets the symbol: +//! +//! ```text +//! dml simics . . src. `dev.dml`. t# foo(). +//! ``` +//! +//! The symbol path is: file path segments (as term descriptors) → +//! template/object nesting (as type/term descriptors) → leaf symbol. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::{Path, PathBuf}; -use protobuf::MessageField; use protobuf::Enum; +use protobuf::MessageField; use scip::types::{ - Document, Index, Metadata, Occurrence, PositionEncoding, - Relationship, SymbolInformation, SymbolRole, ToolInfo, + Document, Index, Metadata, Occurrence, PositionEncoding, Relationship, + SymbolInformation, SymbolRole, ToolInfo, symbol_information::Kind as ScipSymbolKind, }; +use crate::analysis::structure::objects::{ + CompObjectKind, DMLObjectCommon, MethodArgument, MethodModifier, +}; +use crate::analysis::structure::toplevel::{ObjectDecl, StatementSpec}; use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; -use crate::analysis::structure::objects::{CompObjectKind, MethodModifier}; use crate::analysis::templating::objects::{ - DMLHierarchyMember, DMLNamedMember, DMLObject, StructureContainer, + DMLNamedMember, DMLObject, }; use crate::analysis::DeviceAnalysis; use crate::analysis::IsolatedAnalysis; @@ -29,12 +42,144 @@ use crate::file_management::CanonPath; use log::debug; +// --------------------------------------------------------------------------- +// Shared types and utilities +// --------------------------------------------------------------------------- + /// Per-file import resolution data for SCIP export. /// /// Maps each source file (canonical path) to its list of /// (import_statement_span, resolved_target_canonical_path) pairs. pub type FileImportData = HashMap>; +/// Map from declaration name spans to the SCIP symbol strings +/// emitted during the structural walk. Built in the first pass +/// and consumed by the relationship-extraction pass to resolve +/// both source and target symbols from actual emitted data. +pub type SpanSymbolMap = HashMap; + +/// Extract relationship data from DeviceAnalysis semantic models. +/// +/// Uses the `span_map` (built during the structural walk) to resolve +/// both source and target SCIP symbols from their declaration spans. +/// Returns a map from source SCIP symbol string → relationships, +/// which can be applied to SymbolInformation entries in the final index. +/// +/// This covers: +/// - Template instantiation: composite objects → templates they `is` +/// - Method overrides: methods → the method they override via `default` +fn extract_relationships( + devices: &[&DeviceAnalysis], + span_map: &SpanSymbolMap, +) -> HashMap> { + let mut result: HashMap> = HashMap::new(); + + for device in devices { + let container = &device.objects; + + // --- Composite object → template instantiation --- + for (key, sym_ref) in &device.symbol_info.object_symbols { + let sym = sym_ref.symbol.lock().unwrap(); + let source_span = &sym.loc; + + // Look up the SCIP symbol emitted for this object + let Some(source_scip) = span_map.get(source_span) else { + continue; + }; + + if let Some(comp_obj) = container.get(*key) { + let mut rels: Vec = Vec::new(); + + // Template instantiation relationships + for (_templ_name, templ_arc) in &comp_obj.templates { + if let Some(loc) = &templ_arc.location { + // Look up the template's emitted SCIP symbol + if let Some(target_scip) = span_map.get(loc) { + let mut rel = Relationship::new(); + rel.symbol = target_scip.clone(); + rel.is_implementation = true; + rels.push(rel); + } + } + } + + // Connect → Interface relationships + if comp_obj.kind == CompObjectKind::Connect { + for child_obj in comp_obj.components.values() { + if let DMLObject::CompObject(impl_key) = child_obj { + if let Some(impl_obj) = container.get(*impl_key) { + if impl_obj.kind != CompObjectKind::Implement { + continue; + } + for grandchild in impl_obj.components.values() { + if let DMLObject::CompObject(iface_key) = grandchild { + if let Some(iface_obj) = container.get(*iface_key) { + if iface_obj.kind == CompObjectKind::Interface { + let iface_span = &iface_obj.declloc; + if let Some(target_scip) = span_map.get(iface_span) { + let mut rel = Relationship::new(); + rel.symbol = target_scip.clone(); + rel.is_implementation = true; + rels.push(rel); + } + } + } + } + } + } + } + } + } + + if !rels.is_empty() { + rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + rels.dedup_by(|a, b| a.symbol == b.symbol); + result.entry(source_scip.clone()) + .or_default() + .extend(rels); + } + } + } + + // --- Method → override relationships --- + for key_map in device.symbol_info.method_symbols.values() { + for sym_ref in key_map.values() { + let sym = sym_ref.symbol.lock().unwrap(); + if let SymbolSource::Method(_, methref) = &sym.source { + let source_span = methref.location(); + let Some(source_scip) = span_map.get(source_span) else { + continue; + }; + + if let Some(default_call) = methref.get_default() { + let mut rels: Vec = Vec::new(); + for overridden in default_call.flat_refs() { + let target_span = overridden.location(); + if let Some(target_scip) = span_map.get(target_span) { + if target_scip != source_scip { + let mut rel = Relationship::new(); + rel.symbol = target_scip.clone(); + rel.is_implementation = true; + rels.push(rel); + } + } + } + if !rels.is_empty() { + rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + rels.dedup_by(|a, b| a.symbol == b.symbol); + result.entry(source_scip.clone()) + .or_default() + .extend(rels); + } + } + } + } + } + } + + result +} + /// Convert a ZeroSpan range into the SCIP occurrence range format. /// /// SCIP uses `[startLine, startChar, endLine, endChar]` (4 elements) @@ -73,8 +218,6 @@ fn dml_kind_to_scip_kind(kind: &DMLSymbolKind) -> ScipSymbolKind { } } -/// Sanitize a name for use in SCIP symbol strings. -/// /// Check whether a character is a SCIP identifier character. /// /// Per the SCIP symbol grammar: @@ -93,8 +236,6 @@ fn sanitize_name(name: &str) -> String { if !name.is_empty() && name.chars().all(is_scip_identifier_char) { name.to_string() } else { - // Escaped identifier: `+` - // Interior backticks are escaped by doubling them. let mut out = String::new(); out.push('`'); for c in name.chars() { @@ -167,214 +308,6 @@ pub fn extract_import_data( result } -/// Build a `local` SCIP symbol string (document-scoped). -/// -/// Used for method arguments, method locals, and other symbols that -/// are only visible within a single file scope. -fn make_local_symbol(name: &str, id: u64) -> String { - format!("local {}_{}", sanitize_name(name), id) -} - -/// Build a global SCIP symbol string from a qualified path. -/// -/// Global symbols use the format: -/// `scheme ' ' manager ' ' package ' ' version ' ' descriptors...` -/// -/// We use: -/// - scheme: `dml` -/// - manager: `simics` -/// - package: device name -/// - version: `.` (single dot = no version) -/// - descriptors: built from the qualified path segments -/// -/// SCIP descriptor suffixes: -/// - `.` = namespace/term (banks, groups, etc.) -/// - `#` = type (templates, comp objects) -/// - `()` = method -fn make_global_symbol(device_name: &str, qualified_path: &str, - kind: &DMLSymbolKind) -> String { - let segments: Vec<&str> = qualified_path.split('.').collect(); - let mut descriptors = String::new(); - for (i, seg) in segments.iter().enumerate() { - let sanitized = sanitize_name(seg); - if i == segments.len() - 1 { - // Last segment gets suffix based on kind - match kind { - DMLSymbolKind::Method => { - descriptors.push_str(&sanitized); - descriptors.push_str("()."); - } - DMLSymbolKind::Template => { - // Templates are the type-like concept in DML - descriptors.push_str(&sanitized); - descriptors.push('#'); - } - _ => { - // Composite objects (device, bank, register, ...) - // are instances, not types — use term descriptor - descriptors.push_str(&sanitized); - descriptors.push('.'); - } - } - } else { - // Intermediate segments are enclosing object instances - // (device, bank, register, ...) — use term descriptor - descriptors.push_str(&sanitized); - descriptors.push('.'); - } - } - format!("dml simics {} . {}", sanitize_name(device_name), descriptors) -} - -/// Build the SCIP symbol string for a given SymbolSource. -/// -/// - DMLObject (comp or shallow): uses global symbol with qualified_name() -/// - Method: uses global symbol with parent's qualified_name + method name -/// - Template: uses global symbol at top level -/// - MethodArg / MethodLocal: uses local symbol -/// - Type: returns None (these are skipped) -fn scip_symbol_for_source( - source: &SymbolSource, - kind: &DMLSymbolKind, - id: u64, - device_name: &str, - container: &StructureContainer, -) -> Option<(String, String)> { - // Returns Some((scip_symbol, display_name)) - match source { - SymbolSource::DMLObject(dml_obj) => { - match dml_obj { - DMLObject::CompObject(key) => { - if let Some(comp) = container.get(*key) { - let qname = comp.qualified_name(container); - let display = comp.identity().to_string(); - let sym = make_global_symbol(device_name, - &qname, kind); - Some((sym, display)) - } else { - None - } - } - DMLObject::ShallowObject(shallow) => { - let qname = shallow.qualified_name(container); - let display = shallow.identity().to_string(); - let sym = make_global_symbol(device_name, - &qname, kind); - Some((sym, display)) - } - } - } - SymbolSource::Method(parent_key, methref) => { - let parent_qname = container.get(*parent_key) - .map(|p| p.qualified_name(container)) - .unwrap_or_default(); - let method_name = methref.identity(); - let qname = if parent_qname.is_empty() { - method_name.to_string() - } else { - format!("{}.{}", parent_qname, method_name) - }; - let sym = make_global_symbol( - device_name, &qname, &DMLSymbolKind::Method); - Some((sym, method_name.to_string())) - } - SymbolSource::Template(templ) => { - let sym = make_global_symbol( - device_name, &templ.name, &DMLSymbolKind::Template); - Some((sym, templ.name.clone())) - } - SymbolSource::MethodArg(_, name) => { - let sym = make_local_symbol(&name.val, id); - Some((sym, name.val.clone())) - } - SymbolSource::MethodLocal(_, name) => { - let sym = make_local_symbol(&name.val, id); - Some((sym, name.val.clone())) - } - SymbolSource::Type(_) => None, - } -} - -/// Build a short-form declaration signature for a DML symbol. -/// -/// For composite objects this is just the object kind keyword -/// (e.g. `"register"`, `"bank"`). -/// For methods this is the modifier keywords from the declaration -/// (e.g. `"independent method default"`, `"shared method throws"`). -/// Other symbol kinds currently produce no documentation. -fn make_documentation( - source: &SymbolSource, - container: &StructureContainer, -) -> Vec { - match source { - SymbolSource::DMLObject(DMLObject::CompObject(key)) => { - if let Some(comp) = container.get(*key) { - vec![comp.kind.kind_name().to_string()] - } else { - vec![] - } - } - SymbolSource::Method(_, methref) => { - let decl = methref.get_decl(); - let mut parts = Vec::new(); - if decl.independent { - parts.push("independent"); - } - match decl.modifier { - MethodModifier::Shared => parts.push("shared"), - MethodModifier::Inline => parts.push("inline"), - MethodModifier::None => {} - } - parts.push("method"); - if decl.default { - parts.push("default"); - } - if decl.throws { - parts.push("throws"); - } - vec![parts.join(" ")] - } - _ => vec![], - } -} - -/// Build a map from definition/declaration name locations to their -/// enclosing AST spans, for use as SCIP `enclosing_range`. -/// -/// For composite objects, each ObjectSpec has a `loc` (name span) and -/// a `span` (full `group foo is bar { ... }` range). For methods, -/// the MethodDecl has a name location and a full declaration span. -fn enclosing_ranges_for_source( - source: &SymbolSource, - container: &StructureContainer, -) -> HashMap { - let mut map = HashMap::new(); - match source { - SymbolSource::DMLObject(DMLObject::CompObject(key)) => { - if let Some(comp) = container.get(*key) { - for spec in &comp.all_decls { - map.insert(spec.loc, spec.span); - } - // definitions may include specs not in all_decls - for spec in &comp.definitions { - map.entry(spec.loc).or_insert(spec.span); - } - } - } - SymbolSource::Method(_, methref) => { - let decl = methref.get_decl(); - map.insert(decl.name.span, decl.span); - } - SymbolSource::Template(templ) => { - if let Some(loc) = templ.location { - map.insert(loc, templ.spec.span); - } - } - _ => {} - } - map -} - /// Holds per-file occurrence and symbol information data /// that will be assembled into SCIP Documents. /// @@ -413,276 +346,450 @@ impl FileData { } } -/// Convert a single DeviceAnalysis into SCIP Documents. +// --------------------------------------------------------------------------- +// Namespace path construction +// --------------------------------------------------------------------------- + +/// Descriptor suffix for SCIP symbol construction. +#[derive(Debug, Clone, Copy)] +enum DescriptorSuffix { + /// Term descriptor: `name.` + Term, + /// Type descriptor: `name#` + Type, + /// Method descriptor: `name().` + Method, +} + +impl DescriptorSuffix { + fn as_str(self) -> &'static str { + match self { + DescriptorSuffix::Term => ".", + DescriptorSuffix::Type => "#", + DescriptorSuffix::Method => "().", + } + } +} + +/// A single segment of the code-namespace path embedded in a SCIP +/// symbol string. +struct NamespaceSegment { + name: String, + suffix: DescriptorSuffix, +} + +/// Build a global SCIP symbol string from file-relative path and +/// a chain of namespace segments. /// -/// Returns a tuple of (documents, external_symbols). Files under the -/// project root become Documents with relative paths; files outside -/// (e.g. Simics builtins) contribute only their SymbolInformation to -/// `external_symbols` for hover/navigation support. -fn device_analysis_to_documents( - device: &DeviceAnalysis, +/// Format: +/// `dml simics . . ` +fn make_namespace_symbol( + file_path: &Path, project_root: &Path, - import_data: &FileImportData, -) -> (Vec, Vec) { - let mut file_data: HashMap = HashMap::new(); - let container = &device.objects; - let device_name = &device.name; - - // Track method declaration locations → SCIP symbol strings so we - // can wire up override relationships in a second pass. - let mut method_span_to_scip_sym: HashMap = HashMap::new(); - // Collect (file_path, overriding_symbol, overridden_locations) edges. - let mut override_edges: Vec<(PathBuf, String, Vec)> = Vec::new(); - - // Iterate over all symbols in the device analysis - for symbol_ref in device.symbol_info.all_symbols() { - let sym = symbol_ref.symbol.lock().unwrap(); - - // Build the SCIP symbol and display name from the source - let (scip_symbol, display_name) = match scip_symbol_for_source( - &sym.source, &sym.kind, sym.id, device_name, container, - ) { - Some(pair) => pair, - None => continue, // Type symbols and unresolvable objects - }; + namespace: &[NamespaceSegment], +) -> String { + let rel = file_path.strip_prefix(project_root).unwrap_or(file_path); + let mut descriptors = String::new(); - debug!("SCIP symbol id={} kind={:?} scip={} defs={} decls={} refs={} impls={}", - sym.id, sym.kind, &scip_symbol, - sym.definitions.len(), sym.declarations.len(), - sym.references.len(), sym.implementations.len()); - - // Track method symbols for override relationship resolution. - if let SymbolSource::Method(_, methref) = &sym.source { - method_span_to_scip_sym.insert( - *methref.location(), scip_symbol.clone()); - if let Some(default_call) = methref.get_default() { - let overridden_locs: Vec = default_call - .flat_refs() - .iter() - .map(|r| *r.location()) - .collect(); - if !overridden_locs.is_empty() { - override_edges.push(( - sym.loc.path(), - scip_symbol.clone(), - overridden_locs, - )); - } - } + // File path components as term descriptors + for component in rel.components() { + if let Some(s) = component.as_os_str().to_str() { + descriptors.push_str(&sanitize_name(s)); + descriptors.push('.'); } + } - let kind = dml_kind_to_scip_kind(&sym.kind); - let documentation = make_documentation(&sym.source, container); - let enclosing = enclosing_ranges_for_source(&sym.source, container); - - // Record the primary location as a definition occurrence - { - let loc = &sym.loc; - let file_path = loc.path(); - let data = file_data.entry(file_path).or_default(); - - let mut occ = Occurrence::new(); - occ.range = span_to_scip_range(loc); - occ.symbol = scip_symbol.clone(); - occ.symbol_roles = SymbolRole::Definition.value(); - if let Some(enc) = enclosing.get(loc) { - occ.enclosing_range = span_to_scip_range(enc); - } + // Code-level namespace descriptors + for seg in namespace { + descriptors.push_str(&sanitize_name(&seg.name)); + descriptors.push_str(seg.suffix.as_str()); + } - data.add_occurrence(occ); - - // Add SymbolInformation for this symbol (only once, at def site) - let mut sym_info = SymbolInformation::new(); - sym_info.symbol = scip_symbol.clone(); - sym_info.kind = kind.into(); - sym_info.display_name = display_name; - sym_info.documentation = documentation; - - // For comp objects, add Relationship entries for each - // instantiated template (`is` declarations). - if let SymbolSource::DMLObject( - DMLObject::CompObject(key)) = &sym.source { - if let Some(comp) = container.get(*key) { - for templ_name in comp.templates.keys() { - let templ_symbol = make_global_symbol( - device_name, templ_name, - &DMLSymbolKind::Template); - let mut rel = Relationship::new(); - rel.symbol = templ_symbol; - rel.is_implementation = true; - sym_info.relationships.push(rel); - } + format!("dml simics . . {descriptors}") +} - // For connects, emit is_implementation pointing - // to each interface object nested under its - // implement children. - if comp.kind == CompObjectKind::Connect { - for child_obj in comp.components.values() { - if let DMLObject::CompObject(impl_key) = child_obj { - if let Some(impl_obj) = container.get(*impl_key) { - if impl_obj.kind != CompObjectKind::Implement { - continue; - } - for grandchild in impl_obj.components.values() { - if let DMLObject::CompObject(iface_key) = grandchild { - if let Some(iface_obj) = container.get(*iface_key) { - if iface_obj.kind == CompObjectKind::Interface { - let iface_qname = iface_obj.qualified_name(container); - let iface_sym = make_global_symbol( - device_name, &iface_qname, - &DMLSymbolKind::CompObject(CompObjectKind::Interface)); - let mut rel = Relationship::new(); - rel.symbol = iface_sym; - rel.is_implementation = true; - sym_info.relationships.push(rel); - } - } - } - } - } - } - } - } - } - } +/// Build a document-local SCIP symbol for a method argument or local. +fn make_local_symbol(name: &str, id: u64) -> String { + format!("local {}_{}", sanitize_name(name), id) +} - sym_info.relationships.sort_by(|a, b| a.symbol.cmp(&b.symbol)); - data.add_symbol_info(sym_info); - } +// --------------------------------------------------------------------------- +// Symbol emission helpers +// --------------------------------------------------------------------------- + +/// Emit a definition occurrence + SymbolInformation for a named +/// declaration whose SCIP symbol uses a term descriptor. +fn emit_term_symbol( + object: &DMLObjectCommon, + scip_kind: ScipSymbolKind, + doc_text: &str, + file_path: &Path, + project_root: &Path, + namespace: &mut Vec, + file_data: &mut FileData, + span_map: &mut SpanSymbolMap, +) { + let name = object.name.val.clone(); + let name_span = &object.name.span; + let full_span = &object.span; + + namespace.push(NamespaceSegment { + name: name.clone(), + suffix: DescriptorSuffix::Term, + }); + + let sym = make_namespace_symbol(file_path, project_root, namespace); + + span_map.insert(*name_span, sym.clone()); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(name_span); + occ.symbol = sym.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + occ.enclosing_range = span_to_scip_range(full_span); + file_data.add_occurrence(occ); + + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = sym; + sym_info.kind = scip_kind.into(); + sym_info.display_name = name; + sym_info.documentation = vec![doc_text.to_string()]; + file_data.add_symbol_info(sym_info); + + namespace.pop(); +} - // Record additional definitions - for def_span in &sym.definitions { - // Skip if same as primary loc - if *def_span == sym.loc { - continue; - } - let file_path = def_span.path(); - let data = file_data.entry(file_path).or_default(); - - let mut occ = Occurrence::new(); - occ.range = span_to_scip_range(def_span); - occ.symbol = scip_symbol.clone(); - occ.symbol_roles = SymbolRole::Definition.value(); - if let Some(enc) = enclosing.get(def_span) { - occ.enclosing_range = span_to_scip_range(enc); - } - data.add_occurrence(occ); - } +// --------------------------------------------------------------------------- +// Recursive structural-tree walk +// --------------------------------------------------------------------------- - // Record declarations - for decl_span in &sym.declarations { - if *decl_span == sym.loc { - continue; - } - let file_path = decl_span.path(); - let data = file_data.entry(file_path).or_default(); - - let mut occ = Occurrence::new(); - occ.range = span_to_scip_range(decl_span); - occ.symbol = scip_symbol.clone(); - // If this declaration site also appears in definitions, - // it defines a value and gets the Definition role. - // Otherwise it's an abstract/forward declaration. - if sym.definitions.contains(decl_span) { - occ.symbol_roles = SymbolRole::Definition.value(); - } else { - occ.symbol_roles = SymbolRole::ForwardDefinition.value(); - } - if let Some(enc) = enclosing.get(decl_span) { - occ.enclosing_range = span_to_scip_range(enc); - } - data.add_occurrence(occ); - } +/// Recursively walk a `StatementSpec` and emit SCIP definition +/// occurrences and SymbolInformation entries for every declaration. +fn walk_spec( + spec: &StatementSpec, + file_path: &Path, + project_root: &Path, + namespace: &mut Vec, + file_data: &mut FileData, + local_counter: &mut u64, + span_map: &mut SpanSymbolMap, +) { + // --- Templates --- + for template_decl in &spec.templates { + emit_template(template_decl, file_path, project_root, + namespace, file_data, local_counter, span_map); + } - // Record references (read accesses) - for ref_span in &sym.references { - let file_path = ref_span.path(); - let data = file_data.entry(file_path).or_default(); - - let mut occ = Occurrence::new(); - occ.range = span_to_scip_range(ref_span); - occ.symbol = scip_symbol.clone(); - // Plain reference (no Definition/ReadAccess/WriteAccess role). - // TODO: narrow down to ReadAccess/WriteAccess once the - // analysis tracks access kinds. - occ.symbol_roles = 0; - data.add_occurrence(occ); - } + // --- Composite objects (bank, register, group, …) --- + for obj_decl in &spec.objects { + emit_composite_object(obj_decl, file_path, project_root, + namespace, file_data, local_counter, span_map); + } + + // --- Methods --- + for method_decl in &spec.methods { + emit_method(method_decl, file_path, project_root, + namespace, file_data, local_counter, span_map); + } - // Record implementation sites (`is template` occurrences) - // These are references to the template, not definitions. - // The actual implementation relationship is expressed via - // Relationship entries on the comp object's SymbolInformation. - for impl_span in &sym.implementations { - let file_path = impl_span.path(); - let data = file_data.entry(file_path).or_default(); - - let mut occ = Occurrence::new(); - occ.range = span_to_scip_range(impl_span); - occ.symbol = scip_symbol.clone(); - // Plain reference — the implementation relationship is - // expressed via Relationship entries, not occurrence roles. - occ.symbol_roles = 0; - data.add_occurrence(occ); + // --- Parameters --- + for param_decl in &spec.params { + let doc = if param_decl.obj.is_default { + "parameter default" + } else { + "parameter" + }; + emit_term_symbol( + ¶m_decl.obj.object, + ScipSymbolKind::Constant, + doc, + file_path, project_root, namespace, file_data, span_map, + ); + } + + // --- Session variables --- + for sess_decl in &spec.sessions { + for var_decl in &sess_decl.obj.vars { + emit_term_symbol( + &var_decl.object, + ScipSymbolKind::Variable, + "session", + file_path, project_root, namespace, file_data, span_map, + ); } } - // Add override relationships to method SymbolInformation entries. - // A method that overrides a default/abstract method from a template - // gets an is_implementation relationship pointing to the overridden - // method's SCIP symbol. - for (def_file, overriding_sym, overridden_locs) in &override_edges { - if let Some(data) = file_data.get_mut(def_file) { - if let Some(sym_info) = data.symbols.get_mut(overriding_sym) { - for loc in overridden_locs { - if let Some(overridden_sym) = - method_span_to_scip_sym.get(loc) - { - if overridden_sym != overriding_sym { - let mut rel = Relationship::new(); - rel.symbol = overridden_sym.clone(); - rel.is_implementation = true; - sym_info.relationships.push(rel); - } - } - } - sym_info.relationships - .sort_by(|a, b| a.symbol.cmp(&b.symbol)); - } + // --- Saved variables --- + for saved_decl in &spec.saveds { + for var_decl in &saved_decl.obj.vars { + emit_term_symbol( + &var_decl.object, + ScipSymbolKind::Variable, + "saved", + file_path, project_root, namespace, file_data, span_map, + ); } } - // Emit file-level symbols and import occurrences. - // - // For each file in the device analysis, we create a file-level - // symbol (with a Definition occurrence at line 0) and then emit - // Import occurrences at each `import "..."` statement pointing - // to the imported file's symbol. - for dep_path in &device.dependant_files { - let file_pathbuf: PathBuf = dep_path.clone().into(); - let file_sym = make_file_symbol(&file_pathbuf, project_root); - - // Definition occurrence at line 0 of the file - let data = file_data.entry(file_pathbuf.clone()).or_default(); + // --- Hooks --- + for hook_decl in &spec.hooks { + let doc = if hook_decl.obj.shared { "shared hook" } else { "hook" }; + emit_term_symbol( + &hook_decl.obj.object, + ScipSymbolKind::Event, + doc, + file_path, project_root, namespace, file_data, span_map, + ); + } + + // --- Constants --- + for const_decl in &spec.constants { + emit_term_symbol( + &const_decl.obj.object, + ScipSymbolKind::Constant, + "constant", + file_path, project_root, namespace, file_data, span_map, + ); + } +} + +// --------------------------------------------------------------------------- +// Per-declaration emitters +// --------------------------------------------------------------------------- + +/// Emit SCIP data for a template declaration and recurse into its body. +fn emit_template( + template_decl: &ObjectDecl, + file_path: &Path, + project_root: &Path, + namespace: &mut Vec, + file_data: &mut FileData, + local_counter: &mut u64, + span_map: &mut SpanSymbolMap, +) { + let tmpl = &template_decl.obj; + let name = tmpl.object.name.val.clone(); + let name_span = &tmpl.object.name.span; + let full_span = &tmpl.object.span; + + namespace.push(NamespaceSegment { + name: name.clone(), + suffix: DescriptorSuffix::Type, + }); + + let sym = make_namespace_symbol(file_path, project_root, namespace); + + span_map.insert(*name_span, sym.clone()); + + // Definition occurrence + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(name_span); + occ.symbol = sym.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + occ.enclosing_range = span_to_scip_range(full_span); + file_data.add_occurrence(occ); + + // SymbolInformation + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = sym; + sym_info.kind = ScipSymbolKind::Class.into(); + sym_info.display_name = name; + sym_info.documentation = vec!["template".to_string()]; + file_data.add_symbol_info(sym_info); + + // Recurse into the template's flattened spec + walk_spec( + &template_decl.spec, + file_path, + project_root, + namespace, + file_data, + local_counter, + span_map, + ); + + namespace.pop(); +} + +/// Emit SCIP data for a composite object declaration and recurse. +fn emit_composite_object( + obj_decl: &ObjectDecl, + file_path: &Path, + project_root: &Path, + namespace: &mut Vec, + file_data: &mut FileData, + local_counter: &mut u64, + span_map: &mut SpanSymbolMap, +) { + let comp = &obj_decl.obj; + let name = comp.object.name.val.clone(); + let name_span = &comp.object.name.span; + let full_span = &comp.object.span; + let scip_kind = dml_kind_to_scip_kind( + &DMLSymbolKind::CompObject(comp.kind.kind)); + + namespace.push(NamespaceSegment { + name: name.clone(), + suffix: DescriptorSuffix::Term, + }); + + let sym = make_namespace_symbol(file_path, project_root, namespace); + + span_map.insert(*name_span, sym.clone()); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(name_span); + occ.symbol = sym.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + occ.enclosing_range = span_to_scip_range(full_span); + file_data.add_occurrence(occ); + + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = sym; + sym_info.kind = scip_kind.into(); + sym_info.display_name = name; + sym_info.documentation = vec![comp.kind.kind.kind_name().to_string()]; + file_data.add_symbol_info(sym_info); + + // Recurse into nested declarations + walk_spec( + &obj_decl.spec, + file_path, + project_root, + namespace, + file_data, + local_counter, + span_map, + ); + + namespace.pop(); +} + +/// Emit SCIP data for a method declaration, including its arguments. +fn emit_method( + method_decl: &ObjectDecl, + file_path: &Path, + project_root: &Path, + namespace: &mut Vec, + file_data: &mut FileData, + local_counter: &mut u64, + span_map: &mut SpanSymbolMap, +) { + let meth = &method_decl.obj; + let name = meth.object.name.val.clone(); + let name_span = &meth.object.name.span; + let full_span = &meth.object.span; + + namespace.push(NamespaceSegment { + name: name.clone(), + suffix: DescriptorSuffix::Method, + }); + + let sym = make_namespace_symbol(file_path, project_root, namespace); + + span_map.insert(*name_span, sym.clone()); + + // Definition occurrence + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(name_span); + occ.symbol = sym.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + occ.enclosing_range = span_to_scip_range(full_span); + file_data.add_occurrence(occ); + + // Documentation: modifier keywords + let mut doc_parts: Vec<&str> = Vec::new(); + if meth.independent { + doc_parts.push("independent"); + } + match meth.modifier { + MethodModifier::Shared => doc_parts.push("shared"), + MethodModifier::Inline => doc_parts.push("inline"), + MethodModifier::None => {} + } + doc_parts.push("method"); + if meth.default { + doc_parts.push("default"); + } + if meth.throws { + doc_parts.push("throws"); + } + + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = sym; + sym_info.kind = ScipSymbolKind::Method.into(); + sym_info.display_name = name; + sym_info.documentation = vec![doc_parts.join(" ")]; + file_data.add_symbol_info(sym_info); + + // Method arguments as document-local symbols + for arg in &meth.arguments { + let arg_name_str = match arg { + MethodArgument::Typed(n, _) | MethodArgument::Inline(n) => n, + }; + + *local_counter += 1; + let arg_sym = make_local_symbol(&arg_name_str.val, *local_counter); + + let mut arg_occ = Occurrence::new(); + arg_occ.range = span_to_scip_range(&arg_name_str.span); + arg_occ.symbol = arg_sym.clone(); + arg_occ.symbol_roles = SymbolRole::Definition.value(); + arg_occ.enclosing_range = span_to_scip_range(full_span); + file_data.add_occurrence(arg_occ); + + let mut arg_info = SymbolInformation::new(); + arg_info.symbol = arg_sym; + arg_info.kind = ScipSymbolKind::Parameter.into(); + arg_info.display_name = arg_name_str.val.clone(); + file_data.add_symbol_info(arg_info); + } + + namespace.pop(); +} + +// --------------------------------------------------------------------------- +// Per-file processing +// --------------------------------------------------------------------------- + +/// Process a single `IsolatedAnalysis` into SCIP file data. +/// +/// Returns a `FileData` containing all occurrences and symbol-info +/// entries for the file, plus the source path. +fn process_file( + analysis: &IsolatedAnalysis, + project_root: &Path, + import_data: Option<&FileImportData>, + span_map: &mut SpanSymbolMap, +) -> (PathBuf, FileData) { + let file_path: PathBuf = analysis.path.clone().into(); + let mut file_data = FileData::default(); + let mut local_counter: u64 = 0; + let mut namespace = Vec::new(); + + // File-level symbol (definition at line 0) + let file_sym = make_file_symbol(&file_path, project_root); + { let mut def_occ = Occurrence::new(); def_occ.range = vec![0, 0, 0]; // line 0, char 0, end char 0 def_occ.symbol = file_sym.clone(); def_occ.symbol_roles = SymbolRole::Definition.value(); - data.add_occurrence(def_occ); + file_data.add_occurrence(def_occ); - // SymbolInformation for the file, with is_reference - // relationships to each imported file's symbol. let mut sym_info = SymbolInformation::new(); sym_info.symbol = file_sym.clone(); sym_info.kind = ScipSymbolKind::File.into(); - sym_info.display_name = file_pathbuf.file_name() + sym_info.display_name = file_path + .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_default(); - // Import occurrences for each `import "..."` in this file, - // plus is_reference relationships on the file symbol. - if let Some(imports) = import_data.get(dep_path) { - let mut seen_targets = HashSet::new(); + // Import occurrences and relationships: for each import statement, + // emit an Import-role occurrence referencing the imported file's + // symbol, and also record an is_reference Relationship on this + // file's SymbolInformation for explicit dependency tracking. + let canon = &analysis.path; + if let Some(imports) = import_data.and_then(|id| id.get(canon)) { for (import_span, resolved_path) in imports { let target_pathbuf: PathBuf = resolved_path.clone().into(); let target_sym = make_file_symbol(&target_pathbuf, project_root); @@ -691,70 +798,125 @@ fn device_analysis_to_documents( imp_occ.range = span_to_scip_range(import_span); imp_occ.symbol = target_sym.clone(); imp_occ.symbol_roles = SymbolRole::Import.value(); - data.add_occurrence(imp_occ); + file_data.add_occurrence(imp_occ); - if seen_targets.insert(target_sym.clone()) { - let mut rel = Relationship::new(); - rel.symbol = target_sym; - rel.is_reference = true; - sym_info.relationships.push(rel); - } + let mut rel = Relationship::new(); + rel.symbol = target_sym; + rel.is_reference = true; + sym_info.relationships.push(rel); } } - sym_info.relationships.sort_by(|a, b| a.symbol.cmp(&b.symbol)); - data.add_symbol_info(sym_info); - } - // Assemble Documents, separating in-project from external files. - let mut documents = Vec::new(); - let mut external_symbols = Vec::new(); + file_data.add_symbol_info(sym_info); + } - for (path, data) in file_data { - let (occs, syms) = data.into_vecs(); - match path.strip_prefix(project_root) { - Ok(rel) => { - let mut doc = Document::new(); - doc.relative_path = rel.to_string_lossy().to_string(); - doc.language = "dml".to_string(); - doc.position_encoding = - PositionEncoding::UTF16CodeUnitOffsetFromLineStart.into(); - doc.occurrences = occs; - doc.symbols = syms; - documents.push(doc); - } - Err(_) => { - // External file: keep symbol info for hover/navigation - // but don't emit a document or occurrences - external_symbols.extend(syms); - } + let tl = &analysis.toplevel; + + // Top-level externs (not in StatementSpec) + for ext_var in &tl.externs { + for var_decl in &ext_var.vars { + emit_term_symbol( + &var_decl.object, + ScipSymbolKind::Variable, + "extern", + &file_path, + project_root, + &mut namespace, + &mut file_data, + span_map, + ); } } - // Remove from external_symbols any symbol that already appears - // in a document. This can happen when multiple internal Symbol - // objects (e.g. from different templates) produce the same SCIP - // symbol string but have their primary locations in different - // files — one in-project and one external. - let doc_symbol_strings: HashSet<&str> = documents.iter() - .flat_map(|doc| doc.symbols.iter().map(|s| s.symbol.as_str())) - .collect(); - external_symbols.retain(|s| !doc_symbol_strings.contains(s.symbol.as_str())); + // Top-level typedefs (not in StatementSpec) + for typedef in &tl.typedefs { + // Typedefs are type-like; use a type descriptor. + namespace.push(NamespaceSegment { + name: typedef.object.name.val.clone(), + suffix: DescriptorSuffix::Type, + }); + let sym = make_namespace_symbol(&file_path, project_root, &namespace); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(&typedef.object.name.span); + occ.symbol = sym.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + occ.enclosing_range = span_to_scip_range(&typedef.object.span); + file_data.add_occurrence(occ); + + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = sym; + sym_info.kind = ScipSymbolKind::TypeAlias.into(); + sym_info.display_name = typedef.object.name.val.clone(); + sym_info.documentation = vec![if typedef.is_extern { + "extern typedef".to_string() + } else { + "typedef".to_string() + }]; + file_data.add_symbol_info(sym_info); + + namespace.pop(); + } - (documents, external_symbols) + // Top-level loggroups (not in StatementSpec) + for loggroup in &tl.loggroups { + emit_term_symbol( + &DMLObjectCommon { + name: loggroup.name.clone(), + span: loggroup.span, + }, + ScipSymbolKind::Constant, + "loggroup", + &file_path, + project_root, + &mut namespace, + &mut file_data, + span_map, + ); + } + + // Walk the main StatementSpec (templates, objects, methods, …) + walk_spec( + &tl.spec, + &file_path, + project_root, + &mut namespace, + &mut file_data, + &mut local_counter, + span_map, + ); + + (file_path, file_data) } -/// Build a complete SCIP Index from one or more DeviceAnalyses. +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// Build a complete SCIP Index from a set of isolated (per-file) +/// analyses, using code-namespace-based symbol names. +/// +/// Each `IsolatedAnalysis` contributes one Document in the resulting +/// index. Symbols are named after their position in the structural +/// tree (file → template → object → method, etc.) rather than the +/// merged device hierarchy. /// /// # Arguments -/// * `devices` - The device analyses to export -/// * `project_root` - The workspace root path, used to compute relative paths +/// * `analyses` – map from canonical path to the per-file analysis +/// * `project_root` – workspace root, used to compute relative paths +/// * `import_data` – optional pre-resolved import data for emitting +/// import occurrences pub fn build_scip_index( - devices: &[&DeviceAnalysis], + analyses: &HashMap, project_root: &Path, - import_data: &FileImportData, + import_data: Option<&FileImportData>, + devices: &[&DeviceAnalysis], ) -> Index { - debug!("Building SCIP index for {} device(s) rooted at {:?}", - devices.len(), project_root); + debug!( + "Building namespace-based SCIP index for {} file(s) rooted at {:?}", + analyses.len(), + project_root + ); let mut tool_info = ToolInfo::new(); tool_info.name = "dls".to_string(); @@ -764,61 +926,68 @@ pub fn build_scip_index( metadata.tool_info = MessageField::some(tool_info); let root_str = project_root.to_string_lossy(); metadata.project_root = if root_str.ends_with('/') { - format!("file://{}", root_str) + format!("file://{root_str}") } else { - format!("file://{}/", root_str) + format!("file://{root_str}/") }; metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); - // Collect documents from all devices, merging by relative_path. - // We use FileData for deduplication across devices: the same symbol - // or occurrence can appear in multiple DeviceAnalyses when they - // share source files (e.g. common library code). - let mut merged: HashMap = HashMap::new(); - let mut ext_dedup = FileData::default(); + // --- Pass 1: walk all files, emit symbols, build span→symbol map --- + let mut span_map = SpanSymbolMap::new(); + let mut file_results: Vec<(PathBuf, FileData)> = Vec::new(); - for device in devices { - let (docs, ext_syms) = device_analysis_to_documents(device, project_root, import_data); - for doc in docs { - let (_, dedup) = merged - .entry(doc.relative_path.clone()) - .or_insert_with(|| { - let mut d = Document::new(); - d.relative_path = doc.relative_path.clone(); - d.language = doc.language.clone(); - d.position_encoding = doc.position_encoding; - (d, FileData::default()) - }); - for occ in doc.occurrences { - dedup.add_occurrence(occ); - } - for sym in doc.symbols { - dedup.add_symbol_info(sym); + for (_, analysis) in analyses { + let (path, data) = process_file( + analysis, project_root, import_data, &mut span_map); + file_results.push((path, data)); + } + + // --- Pass 2: extract relationships from DeviceAnalysis using span_map --- + let sym_rels = extract_relationships(devices, &span_map); + + // --- Pass 3: assemble documents, injecting relationships --- + let mut documents = Vec::new(); + let mut external_symbols = Vec::new(); + + for (path, mut data) in file_results { + // Apply relationships to SymbolInformation entries + for sym_info in data.symbols.values_mut() { + if let Some(rels) = sym_rels.get(&sym_info.symbol) { + sym_info.relationships = rels.clone(); } } - for sym in ext_syms { - ext_dedup.add_symbol_info(sym); + + let (occs, syms) = data.into_vecs(); + + match path.strip_prefix(project_root) { + Ok(rel) => { + let mut doc = Document::new(); + doc.relative_path = rel.to_string_lossy().to_string(); + doc.language = "dml".to_string(); + doc.position_encoding = + PositionEncoding::UTF16CodeUnitOffsetFromLineStart.into(); + doc.occurrences = occs; + doc.symbols = syms; + documents.push(doc); + } + Err(_) => { + external_symbols.extend(syms); + } } } - // Move deduplicated data into the final documents, sorted for - // deterministic output. - let mut documents: Vec = merged.into_values().map(|(mut doc, dedup)| { - let (occs, syms) = dedup.into_vecs(); - doc.occurrences = occs; - doc.symbols = syms; - doc - }).collect(); documents.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); + external_symbols.sort_by(|a, b| a.symbol.cmp(&b.symbol)); let mut index = Index::new(); index.metadata = MessageField::some(metadata); index.documents = documents; - let (_, mut ext_syms) = ext_dedup.into_vecs(); - ext_syms.sort_by(|a, b| a.symbol.cmp(&b.symbol)); - index.external_symbols = ext_syms; + index.external_symbols = external_symbols; - debug!("SCIP index built with {} document(s)", index.documents.len()); + debug!( + "Namespace-based SCIP index built with {} document(s)", + index.documents.len() + ); index } From 926c2466a43f96b7565c7c9bf42ec51ca8050dad Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 30 Mar 2026 17:11:59 +0200 Subject: [PATCH 21/32] Add device hierarchy output mode --- USAGE.md | 121 ++++++++ src/actions/requests.rs | 157 ++++++++++- src/backends/hierarchy.rs | 385 ++++++++++++++++++++++++++ src/backends/mod.rs | 9 + src/{scip/mod.rs => backends/scip.rs} | 15 + src/cmd.rs | 20 ++ src/dfa/client.rs | 31 +++ src/dfa/main.rs | 38 +++ src/lib.rs | 2 +- src/server/dispatch.rs | 1 + src/server/mod.rs | 3 +- 11 files changed, 777 insertions(+), 5 deletions(-) create mode 100644 src/backends/hierarchy.rs create mode 100644 src/backends/mod.rs rename src/{scip/mod.rs => backends/scip.rs} (98%) diff --git a/USAGE.md b/USAGE.md index 8e5e9feb..b6a7624c 100644 --- a/USAGE.md +++ b/USAGE.md @@ -268,3 +268,124 @@ File symbols use the format: dml simics . . path/to/file_dml. ``` where path segments are separated by term descriptors (`.`). + +## Device Object Hierarchy Export + +The DLS can export a JSON representation of the device-semantic object +hierarchy for analyzed DML devices. This gives a structured view of the +instantiated device tree — the composite objects, parameters, and methods +that make up the device after all templates have been merged — rather than +the raw syntactic AST. + +### Invocation + +Object hierarchy export is available through the DFA binary via the +`--object-hierarchy ` flag: + +``` +dfa --workspace --object-hierarchy output.json +``` + +This can be combined with other flags such as `--compile-info`, +`--scip-output`, etc. + +### Output Format + +The output is a JSON file whose top-level keys are device names (one per +analysed device). Each device maps to a recursive object hierarchy: + +```json +{ + "sample_device": { + "scip_name": "dml simics . . src. sample.dml. sample_device.", + "kind": "device", + "parameters": { + "register_size": { + "scip_name": "dml simics . . src. sample.dml. sample_device. register_size.", + "value_expression": "4", + "type": "int" + } + }, + "methods": { + "init": { + "scip_name_of_method_used": "dml simics . . src. sample.dml. sample_device. init().", + "arg_list": [], + "return_types": [], + "modifiers": ["default"] + } + }, + "objects": { + "regs": { + "scip_name": "dml simics . . src. sample.dml. regs.", + "kind": "bank", + "parameters": { ... }, + "methods": { ... }, + "objects": { + "r0": { + "scip_name": "...", + "kind": "register", + ... + } + } + } + } + } +} +``` + +### Schema Reference + +#### Top Level + +A map from device short name (string) to a **HierarchyObject**. + +#### HierarchyObject + +| Field | Type | Description | +|--------------|---------------------------------------|-------------| +| `scip_name` | string | Full SCIP symbol path for this object. | +| `kind` | string | DML composite object kind (`device`, `bank`, `register`, `field`, `group`, `port`, `connect`, `attribute`, `event`, `subdevice`, `implement`, `interface`). | +| `parameters` | map\ | Parameters declared on this object, keyed by short name. Omitted when empty. | +| `methods` | map\ | Methods declared on this object, keyed by short name. Omitted when empty. | +| `objects` | map\ | Child composite objects, keyed by short name. Omitted when empty. | + +#### HierarchyParameter + +| Field | Type | Description | +|--------------------|-----------------|-------------| +| `scip_name` | string | Full SCIP symbol path for this parameter. | +| `value_expression` | string or null | Source text of the parameter value expression (e.g. `0x100`, `"hello"`), or `"auto"` for auto-parameters. Null if abstract. | +| `type` | string or null | Source text of the declared type annotation (e.g. `uint64`). Null if untyped. | + +#### HierarchyMethod + +| Field | Type | Description | +|----------------------------|----------------------------|-------------| +| `scip_name_of_method_used` | string | Full SCIP symbol path of the concrete method definition used. | +| `arg_list` | array of HierarchyMethodArg | Method arguments in declaration order. | +| `return_types` | array of string | Source text of each return type. | +| `modifiers` | array of string | Applicable modifiers: `shared`, `inline`, `independent`, `default`, `throws`. | + +#### HierarchyMethodArg + +| Field | Type | Description | +|--------|--------|-------------| +| `name` | string | Argument name. | +| `type` | string | Source text of the argument type, or `"inline"` for untyped inline arguments. | + +### Relationship to SCIP + +The `scip_name` fields in the hierarchy correspond directly to the SCIP symbol +strings produced by the SCIP export (see previous section). This allows +consumers to cross-reference the hierarchy with the SCIP index — for example, +looking up the precise source location of a parameter via its SCIP symbol, or +navigating override chains for methods. + +Auto-generated parameters that are synthesised during device analysis (such as +`_ident`, `indices`, `qname`, `parent`, `obj`) do not have a source-level +declaration of their own. Their `scip_name` is resolved by walking the +parameter's definition/declaration chain (used definitions → overridden +definitions → declarations) looking for the nearest declaration that has a SCIP +symbol — for example, the declaration in a built-in template. If no declaration +in the chain has a SCIP symbol, the short identity name is used (e.g. +`_ident`). diff --git a/src/actions/requests.rs b/src/actions/requests.rs index d6b98627..56495294 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -1065,7 +1065,7 @@ impl RequestAction for ExportScipRequest { info!("Exporting SCIP for {} device(s)", devices.len()); // Extract import resolution data for the SCIP export - let import_data = crate::scip::extract_import_data( + let import_data = crate::backends::scip::extract_import_data( &analysis.isolated_analysis, &analysis.import_map, &devices, @@ -1092,12 +1092,12 @@ impl RequestAction for ExportScipRequest { .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) .unwrap_or_else(|| std::path::PathBuf::from(".")); - let index = crate::scip::build_scip_index( + let index = crate::backends::scip::build_scip_index( &isolated_map, &project_root, Some(&import_data), &devices); let doc_count = index.documents.len(); let output = std::path::Path::new(¶ms.output_path); - match crate::scip::write_scip_to_file(index, output) { + match crate::backends::scip::write_scip_to_file(index, output) { Ok(()) => { info!("SCIP export complete: {} documents written to {}", doc_count, params.output_path); @@ -1119,6 +1119,157 @@ impl RequestAction for ExportScipRequest { } } +// ---- Object Hierarchy Export Request ---- + +#[derive(Debug, Clone)] +pub struct ExportObjectHierarchyRequest; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportObjectHierarchyParams { + /// Device paths to export hierarchy for. If empty, exports all known devices. + pub devices: Option>, + /// The file path where the JSON hierarchy should be written. + pub output_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportObjectHierarchyResult { + /// Whether the export succeeded. + pub success: bool, + /// Number of devices in the exported hierarchy. + pub device_count: usize, + /// Error message, if any. + pub error: Option, +} + +impl LSPRequest for ExportObjectHierarchyRequest { + type Params = ExportObjectHierarchyParams; + type Result = ExportObjectHierarchyResult; + + const METHOD: &'static str = "$/exportObjectHierarchy"; +} + +impl RequestAction for ExportObjectHierarchyRequest { + type Response = ExportObjectHierarchyResult; + + fn timeout() -> std::time::Duration { + crate::server::dispatch::DEFAULT_REQUEST_TIMEOUT * 30 + } + + fn fallback_response() -> Result { + Ok(ExportObjectHierarchyResult { + success: false, + device_count: 0, + error: Some("Request timed out".to_string()), + }) + } + + fn get_identifier(params: &Self::Params) -> String { + Self::request_identifier(¶ms.output_path) + } + + fn handle( + ctx: InitActionContext, + params: Self::Params, + ) -> Result { + info!("Handling object hierarchy export request to {}", params.output_path); + + let device_paths: Vec = + if let Some(devices) = params.devices { + devices.iter().filter_map( + |uri| parse_file_path!(&uri, "ExportObjectHierarchy") + .ok() + .and_then(CanonPath::from_path_buf)) + .collect() + } else { + vec![] + }; + + if !device_paths.is_empty() { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::Paths(device_paths.clone())).ok(); + } else { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::All).ok(); + } + + let analysis = ctx.analysis.lock().unwrap(); + + let devices: Vec<&crate::analysis::DeviceAnalysis> = + if device_paths.is_empty() { + analysis.device_analysis.values() + .map(|ts| &ts.stored) + .collect() + } else { + device_paths.iter().filter_map(|path| { + analysis.get_device_analysis(path).ok() + }).collect() + }; + + if devices.is_empty() { + return Ok(ExportObjectHierarchyResult { + success: false, + device_count: 0, + error: Some("No device analyses found".to_string()), + }); + } + + info!("Exporting object hierarchy for {} device(s)", devices.len()); + + // Build the SCIP span→symbol map so the hierarchy can use + // full SCIP symbol paths for scip_name fields. + let mut isolated_map = std::collections::HashMap::new(); + for device in &devices { + for file_path in &device.dependant_files { + if !isolated_map.contains_key(file_path) { + if let Some(ts) = analysis.isolated_analysis.get(file_path) { + isolated_map.insert(file_path.clone(), &ts.stored); + } + } + } + } + + let project_root = ctx.workspace_roots + .lock() + .unwrap() + .first() + .and_then(|ws| parse_file_path!(&ws.uri, "ExportObjectHierarchy").ok()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + let span_map = crate::backends::scip::build_span_symbol_map( + &isolated_map, &project_root); + + let hierarchy = crate::backends::hierarchy::build_hierarchy( + &devices, &span_map); + let device_count = hierarchy.len(); + + let output = std::path::Path::new(¶ms.output_path); + match crate::backends::hierarchy::write_hierarchy_to_file(&hierarchy, output) { + Ok(()) => { + info!("Object hierarchy export complete: {} device(s) written to {}", + device_count, params.output_path); + Ok(ExportObjectHierarchyResult { + success: true, + device_count, + error: None, + }) + }, + Err(e) => { + error!("Object hierarchy export failed: {}", e); + Ok(ExportObjectHierarchyResult { + success: false, + device_count: 0, + error: Some(e), + }) + } + } + } +} + /// Server-to-client requests impl SentRequest for RegisterCapability { type Response = ::Result; diff --git a/src/backends/hierarchy.rs b/src/backends/hierarchy.rs new file mode 100644 index 00000000..8e784b72 --- /dev/null +++ b/src/backends/hierarchy.rs @@ -0,0 +1,385 @@ +// © 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 and MIT +//! Device-semantic object hierarchy export. +//! +//! Walks the `DeviceAnalysis` composite-object tree and produces +//! a JSON representation of the device object hierarchy including +//! objects, parameters, and methods. +//! +//! SCIP symbol paths are resolved via a pre-built `SpanSymbolMap` +//! (from the SCIP backend) that maps declaration name-spans to +//! their full SCIP symbol strings. +//! +//! Type and expression strings are extracted from the actual source +//! text using the spans stored in the analysis structures. + +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; + +use log::{debug, trace}; +use serde::Serialize; +use serde_json; + +use crate::analysis::structure::objects::{ + MethodModifier, ParamValue, +}; +use crate::analysis::templating::methods::{DMLMethodArg, MethodDeclaration}; +use crate::analysis::templating::objects::{ + DMLCompositeObject, DMLNamedMember, DMLObject, + DMLShallowObject, DMLShallowObjectVariant, StructureContainer, +}; +use crate::analysis::DeclarationSpan; +use crate::analysis::LocationSpan; +use crate::analysis::DeviceAnalysis; +use crate::backends::scip::SpanSymbolMap; +use crate::Span as ZeroSpan; + +// --------------------------------------------------------------------------- +// Source text extraction +// --------------------------------------------------------------------------- + +/// Cache of file contents for extracting source text from spans. +struct SourceTextCache { + files: HashMap>, +} + +impl SourceTextCache { + fn new() -> Self { + SourceTextCache { + files: HashMap::new(), + } + } + + /// Load a file's lines into the cache if not already present. + fn ensure_loaded(&mut self, path: &Path) { + if !self.files.contains_key(path) { + match std::fs::read_to_string(path) { + Ok(content) => { + let lines: Vec = + content.lines().map(String::from).collect(); + self.files.insert(path.to_path_buf(), lines); + } + Err(e) => { + trace!("Failed to read file {:?}: {}", path, e); + self.files.insert(path.to_path_buf(), vec![]); + } + } + } + } + + /// Extract the source text covered by a zero-indexed span. + fn text_from_span(&mut self, span: &ZeroSpan) -> Option { + let path = span.path(); + self.ensure_loaded(&path); + let lines = self.files.get(&path)?; + + let r = &span.range; + let row_start = r.row_start.0 as usize; + let row_end = r.row_end.0 as usize; + let col_start = r.col_start.0 as usize; + let col_end = r.col_end.0 as usize; + + if row_start >= lines.len() { + return None; + } + + if row_start == row_end { + // Single-line span + let line = &lines[row_start]; + let end = col_end.min(line.len()); + let start = col_start.min(end); + Some(line[start..end].to_string()) + } else { + // Multi-line span + let mut result = String::new(); + for row in row_start..=row_end.min(lines.len() - 1) { + let line = &lines[row]; + if row == row_start { + let start = col_start.min(line.len()); + result.push_str(&line[start..]); + } else if row == row_end { + let end = col_end.min(line.len()); + result.push(' '); + result.push_str(&line[..end]); + } else { + result.push(' '); + result.push_str(line); + } + } + Some(result.trim().to_string()) + } + } + + /// Extract text for a `DMLResolvedType` span, returning None for + /// dummy types. + fn type_text( + &mut self, + resolved: &crate::analysis::templating::types::DMLResolvedType, + ) -> Option { + if resolved.is_dummy() { + return None; + } + self.text_from_span(resolved.span()) + } +} + +// --------------------------------------------------------------------------- +// JSON output types +// --------------------------------------------------------------------------- + +/// A parameter in the hierarchy. +#[derive(Debug, Clone, Serialize)] +pub struct HierarchyParameter { + pub scip_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub value_expression: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_name: Option, +} + +/// A single argument in a method signature. +#[derive(Debug, Clone, Serialize)] +pub struct HierarchyMethodArg { + pub name: String, + #[serde(rename = "type")] + pub type_name: String, +} + +/// A method in the hierarchy. +#[derive(Debug, Clone, Serialize)] +pub struct HierarchyMethod { + pub scip_name_of_method_used: String, + pub arg_list: Vec, + pub return_types: Vec, + pub modifiers: Vec, +} + +/// A composite object (device, bank, register, ...) in the hierarchy. +#[derive(Debug, Clone, Serialize)] +pub struct HierarchyObject { + pub scip_name: String, + pub kind: String, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub parameters: BTreeMap, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub methods: BTreeMap, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub objects: BTreeMap, +} + +/// Top-level hierarchy: maps device short names to their hierarchy. +pub type DeviceHierarchy = BTreeMap; + +// --------------------------------------------------------------------------- +// Building the hierarchy +// --------------------------------------------------------------------------- + +fn modifier_to_string(modifier: MethodModifier) -> &'static str { + match modifier { + MethodModifier::None => "none", + MethodModifier::Shared => "shared", + MethodModifier::Inline => "inline", + } +} + +fn build_method_entry( + method: &crate::analysis::templating::methods::DMLMethodRef, + span_map: &SpanSymbolMap, + source: &mut SourceTextCache, +) -> HierarchyMethod { + let decl = method.get_decl(); + + let scip_name = span_map + .get(method.location()) + .cloned() + .unwrap_or_else(|| method.identity().to_string()); + + let arg_list: Vec = method.args().iter().map(|arg| { + match arg { + DMLMethodArg::Typed(declaration) => { + let type_text = source.type_text(&declaration.type_ref) + .unwrap_or_else(|| "".to_string()); + HierarchyMethodArg { + name: declaration.name.val.clone(), + type_name: type_text, + } + } + DMLMethodArg::Inline(name) => HierarchyMethodArg { + name: name.val.clone(), + type_name: "inline".to_string(), + }, + } + }).collect(); + + let return_types: Vec = method.returns().iter().map(|rt| { + source.type_text(rt) + .unwrap_or_else(|| "".to_string()) + }).collect(); + + let mut modifiers = Vec::new(); + let modifier_str = modifier_to_string(decl.modifier); + if modifier_str != "none" { + modifiers.push(modifier_str.to_string()); + } + if decl.independent { + modifiers.push("independent".to_string()); + } + if decl.default { + modifiers.push("default".to_string()); + } + if decl.throws { + modifiers.push("throws".to_string()); + } + + HierarchyMethod { + scip_name_of_method_used: scip_name, + arg_list, + return_types, + modifiers, + } +} + +fn build_param_entry( + param: &crate::analysis::templating::objects::DMLParameter, + parent_span: &ZeroSpan, + span_map: &SpanSymbolMap, + source: &mut SourceTextCache, +) -> HierarchyParameter { + let def = param.get_likely_definition(); + + // Look up the SCIP symbol for this parameter by scanning the + // definition/declaration chain. Auto-generated parameters borrow + // the parent object's name span, so we skip any entry whose span + // collides with the parent. Walking used_definitions first, then + // overridden definitions, then declarations means we pick the + // nearest concrete declaration — and when built-in template + // sources are available their declaration will be found here too. + let scip_name = param.used_definitions.iter() + .chain(param.definitions.iter()) + .chain(param.declarations.iter()) + .filter_map(|(_, p)| { + let span = p.loc_span(); + if span != parent_span { + span_map.get(span).cloned() + } else { + None + } + }) + .next() + .unwrap_or_else(|| param.identity.clone()); + + let value_expression = def.value.as_ref().and_then(|v| match v { + ParamValue::Set(expr) => source.text_from_span(expr.span()), + ParamValue::Auto(_) => Some("auto".to_string()), + }); + + let type_name = def.typed.as_ref().and_then(|t| { + // DMLType is a ZeroSpan; extract the actual type text + source.text_from_span(t) + }); + + HierarchyParameter { + scip_name, + value_expression, + type_name, + } +} + +fn build_object_hierarchy( + comp_obj: &DMLCompositeObject, + container: &StructureContainer, + span_map: &SpanSymbolMap, + source: &mut SourceTextCache, +) -> HierarchyObject { + let mut parameters = BTreeMap::new(); + let mut methods = BTreeMap::new(); + let mut objects = BTreeMap::new(); + + let scip_name = span_map + .get(comp_obj.location()) + .cloned() + .unwrap_or_else(|| comp_obj.identity().to_string()); + + for (name, dml_obj) in &comp_obj.components { + match dml_obj { + DMLObject::CompObject(key) => { + if let Some(child) = container.get(*key) { + let short_name = child.identity().to_string(); + objects.insert( + short_name, + build_object_hierarchy( + child, container, span_map, source), + ); + } + } + DMLObject::ShallowObject(DMLShallowObject { + variant: DMLShallowObjectVariant::Parameter(param), + .. + }) => { + parameters.insert( + name.clone(), + build_param_entry( + param, comp_obj.location(), span_map, source), + ); + } + DMLObject::ShallowObject(DMLShallowObject { + variant: DMLShallowObjectVariant::Method(method_ref), + .. + }) => { + methods.insert( + name.clone(), + build_method_entry(method_ref, span_map, source), + ); + } + // Sessions, saveds, constants, hooks are not included + // in the hierarchy output per current specification. + _ => {} + } + } + + HierarchyObject { + scip_name, + kind: comp_obj.kind.kind_name().to_string(), + parameters, + methods, + objects, + } +} + +/// Build the device-semantic object hierarchy for the given device analyses. +/// +/// `span_map` provides declaration-span → SCIP-symbol-path lookups, +/// typically built via `crate::backends::scip::build_span_symbol_map`. +/// +/// Returns a map from device short name → hierarchy object. +pub fn build_hierarchy( + devices: &[&DeviceAnalysis], + span_map: &SpanSymbolMap, +) -> DeviceHierarchy { + debug!("Building object hierarchy for {} device(s)", devices.len()); + + let mut source = SourceTextCache::new(); + let mut hierarchy = DeviceHierarchy::new(); + + for device in devices { + let comp_obj = device.get_device_comp_obj(); + let entry = build_object_hierarchy( + comp_obj, &device.objects, span_map, &mut source); + hierarchy.insert(device.name.clone(), entry); + } + + hierarchy +} + +/// Serialize a `DeviceHierarchy` to pretty-printed JSON and write +/// it to the given file path. +pub fn write_hierarchy_to_file( + hierarchy: &DeviceHierarchy, + output_path: &Path, +) -> Result<(), String> { + debug!("Writing object hierarchy to {:?}", output_path); + let json = serde_json::to_string_pretty(hierarchy) + .map_err(|e| format!("Failed to serialize hierarchy: {}", e))?; + std::fs::write(output_path, json) + .map_err(|e| format!("Failed to write hierarchy file: {}", e)) +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 index 00000000..9cd84dda --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1,9 @@ +// © 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 and MIT +//! Backend modules for exporting DML analysis results. +//! +//! - `scip`: SCIP (Source Code Intelligence Protocol) index export. +//! - `hierarchy`: Device-semantic object hierarchy export (JSON). + +pub mod scip; +pub mod hierarchy; diff --git a/src/scip/mod.rs b/src/backends/scip.rs similarity index 98% rename from src/scip/mod.rs rename to src/backends/scip.rs index b35b6342..88a45229 100644 --- a/src/scip/mod.rs +++ b/src/backends/scip.rs @@ -991,6 +991,21 @@ pub fn build_scip_index( index } +/// Build just the span→SCIP-symbol map without producing the full index. +/// +/// This is useful for other backends (e.g. object hierarchy) that need +/// to look up SCIP symbol paths for declarations. +pub fn build_span_symbol_map( + analyses: &HashMap, + project_root: &Path, +) -> SpanSymbolMap { + let mut span_map = SpanSymbolMap::new(); + for (_, analysis) in analyses { + process_file(analysis, project_root, None, &mut span_map); + } + span_map +} + /// Write a SCIP index to a file. pub fn write_scip_to_file(index: Index, output_path: &Path) -> Result<(), String> { diff --git a/src/cmd.rs b/src/cmd.rs index f066e8e2..c9fb007b 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -340,6 +340,26 @@ pub fn export_scip(devices: Vec, output_path: String) -> Request, output_path: String) + -> Request +{ + Request { + params: requests::ExportObjectHierarchyParams { + devices: if devices.is_empty() { + None + } else { + Some(devices.into_iter() + .map(|p| parse_uri(&p).unwrap()) + .collect()) + }, + output_path, + }, + action: PhantomData, + id: next_id(), + received: Instant::now(), + } +} + fn next_id() -> RequestId { static ID: AtomicU64 = AtomicU64::new(1); RequestId::Num(ID.fetch_add(1, Ordering::SeqCst)) diff --git a/src/dfa/client.rs b/src/dfa/client.rs index 36e0c637..583a63f8 100644 --- a/src/dfa/client.rs +++ b/src/dfa/client.rs @@ -445,4 +445,35 @@ impl ClientInterface { } } } + + pub fn export_object_hierarchy(&mut self, + device_paths: Vec, + output_path: String) + -> anyhow::Result { + debug!("Sending object hierarchy export request for {:?} -> {}", device_paths, output_path); + self.send( + cmd::export_object_hierarchy(device_paths, output_path).to_string() + )?; + loop { + match self.receive_maybe() { + Ok(ServerMessage::Response(value)) => { + let result: crate::actions::requests::ExportObjectHierarchyResult + = serde_json::from_value(value) + .map_err(|e| RpcErrorKind::from(e.to_string()))?; + return Ok(result); + }, + Ok(ServerMessage::Error(e)) => { + return Err(anyhow::anyhow!( + "Server exited during object hierarchy export: {:?}", e)); + }, + Ok(_) => { + continue; + }, + Err(e) => { + trace!("Skipping message during hierarchy export wait: {:?}", e); + continue; + } + } + } + } } diff --git a/src/dfa/main.rs b/src/dfa/main.rs index 63b174bc..f973f7e5 100644 --- a/src/dfa/main.rs +++ b/src/dfa/main.rs @@ -37,6 +37,7 @@ struct Args { test: bool, quiet: bool, scip_output: Option, + object_hierarchy_output: Option, } fn parse_args() -> Args { @@ -97,6 +98,11 @@ fn parse_args() -> Args { .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) .required(false)) + .arg(Arg::new("object-hierarchy").long("object-hierarchy") + .help("Export device object hierarchy as JSON to the specified file after analysis") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .required(false)) .arg(arg!( ... "DML files to analyze") .value_parser(clap::value_parser!(PathBuf))) .arg_required_else_help(false) @@ -123,6 +129,8 @@ fn parse_args() -> Args { .cloned(), scip_output: args.get_one::("scip-output") .cloned(), + object_hierarchy_output: args.get_one::("object-hierarchy") + .cloned(), } } @@ -211,6 +219,36 @@ fn main_inner() -> Result<(), i32> { } } } + + // Export object hierarchy if requested + if let Some(hierarchy_path) = &arg.object_hierarchy_output { + println!("Exporting object hierarchy to {:?}", hierarchy_path); + let hierarchy_output_str = hierarchy_path.to_string_lossy().to_string(); + let device_paths: Vec = arg.files.iter() + .filter_map(|f| f.canonicalize().ok()) + .map(|p| p.to_string_lossy().to_string()) + .collect(); + match dlsclient.export_object_hierarchy( + device_paths, hierarchy_output_str) + { + Ok(result) => { + if result.success { + println!("Object hierarchy export complete: \ + {} device(s) written", + result.device_count); + } else { + let err_msg = result.error.unwrap_or_else( + || "Unknown error".to_string()); + eprintln!("Object hierarchy export failed: {}", err_msg); + exit_code = Err(1); + } + }, + Err(e) => { + eprintln!("Object hierarchy export request failed: {}", e); + exit_code = Err(1); + } + } + } } // Disregard this result, we dont _really_ care about shutting down diff --git a/src/lib.rs b/src/lib.rs index e6d7c8d4..b1f7888f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,9 +36,9 @@ pub mod concurrency; pub mod config; pub mod dfa; pub mod file_management; +pub mod backends; pub mod lint; pub mod lsp_data; -pub mod scip; pub mod server; pub mod span; pub mod utility; diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 7eceaec5..b367431e 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -114,6 +114,7 @@ define_dispatch_request_enum!( CodeLensRequest, GetKnownContextsRequest, ExportScipRequest, + ExportObjectHierarchyRequest, ); /// Provides ability to dispatch requests to a worker thread that will diff --git a/src/server/mod.rs b/src/server/mod.rs index 6ba9f718..9bc4fd68 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -595,7 +595,8 @@ impl LsService { requests::Completion, requests::CodeLensRequest, requests::GetKnownContextsRequest, - requests::ExportScipRequest; + requests::ExportScipRequest, + requests::ExportObjectHierarchyRequest; ); Ok(()) } From 1d75a3cebf4117522edf47a6b9e895ea5138ee42 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 8 Apr 2026 13:42:16 +0200 Subject: [PATCH 22/32] Add hack for references from interfaces and implements to their types --- USAGE.md | 4 -- src/backends/hierarchy.rs | 6 ++- src/backends/scip.rs | 109 ++++++++++++++++++++++++-------------- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/USAGE.md b/USAGE.md index b6a7624c..819f9b6f 100644 --- a/USAGE.md +++ b/USAGE.md @@ -240,10 +240,6 @@ Composite objects that instantiate templates (via `is some_template`) emit SCIP `Relationship` entries with `is_implementation = true` pointing to the template symbol. -Connect objects additionally emit `is_implementation` relationships pointing to -each interface symbol nested under their implement children. This captures the -semantic link between a connect and the interfaces it provides. - Methods that override a default or abstract method from a template also emit `is_implementation` relationships pointing to the overridden method's symbol. For example, if template `foo` defines `method a` as `default` and object `b` diff --git a/src/backends/hierarchy.rs b/src/backends/hierarchy.rs index 8e784b72..96db2bf7 100644 --- a/src/backends/hierarchy.rs +++ b/src/backends/hierarchy.rs @@ -92,8 +92,10 @@ impl SourceTextCache { } else { // Multi-line span let mut result = String::new(); - for row in row_start..=row_end.min(lines.len() - 1) { - let line = &lines[row]; + for (row, line) in lines.iter().enumerate() + .take(row_end + 1) + .skip(row_start) + { if row == row_start { let start = col_start.min(line.len()); result.push_str(&line[start..]); diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 88a45229..d23e1e5b 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -32,9 +32,7 @@ use crate::analysis::structure::objects::{ }; use crate::analysis::structure::toplevel::{ObjectDecl, StatementSpec}; use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; -use crate::analysis::templating::objects::{ - DMLNamedMember, DMLObject, -}; +use crate::analysis::templating::objects::DMLNamedMember; use crate::analysis::DeviceAnalysis; use crate::analysis::IsolatedAnalysis; use crate::Span as ZeroSpan; @@ -58,6 +56,10 @@ pub type FileImportData = HashMap>; /// both source and target symbols from actual emitted data. pub type SpanSymbolMap = HashMap; +/// Map from extern typedef name to its SCIP symbol string. +/// Used to resolve implement/interface → underlying type references. +type ExternTypedefMap = HashMap; + /// Extract relationship data from DeviceAnalysis semantic models. /// /// Uses the `span_map` (built during the structural walk) to resolve @@ -91,7 +93,7 @@ fn extract_relationships( let mut rels: Vec = Vec::new(); // Template instantiation relationships - for (_templ_name, templ_arc) in &comp_obj.templates { + for templ_arc in comp_obj.templates.values() { if let Some(loc) = &templ_arc.location { // Look up the template's emitted SCIP symbol if let Some(target_scip) = span_map.get(loc) { @@ -103,34 +105,6 @@ fn extract_relationships( } } - // Connect → Interface relationships - if comp_obj.kind == CompObjectKind::Connect { - for child_obj in comp_obj.components.values() { - if let DMLObject::CompObject(impl_key) = child_obj { - if let Some(impl_obj) = container.get(*impl_key) { - if impl_obj.kind != CompObjectKind::Implement { - continue; - } - for grandchild in impl_obj.components.values() { - if let DMLObject::CompObject(iface_key) = grandchild { - if let Some(iface_obj) = container.get(*iface_key) { - if iface_obj.kind == CompObjectKind::Interface { - let iface_span = &iface_obj.declloc; - if let Some(target_scip) = span_map.get(iface_span) { - let mut rel = Relationship::new(); - rel.symbol = target_scip.clone(); - rel.is_implementation = true; - rels.push(rel); - } - } - } - } - } - } - } - } - } - if !rels.is_empty() { rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); rels.dedup_by(|a, b| a.symbol == b.symbol); @@ -761,6 +735,7 @@ fn process_file( project_root: &Path, import_data: Option<&FileImportData>, span_map: &mut SpanSymbolMap, + extern_typedef_map: &mut ExternTypedefMap, ) -> (PathBuf, FileData) { let file_path: PathBuf = analysis.path.clone().into(); let mut file_data = FileData::default(); @@ -844,6 +819,11 @@ fn process_file( occ.enclosing_range = span_to_scip_range(&typedef.object.span); file_data.add_occurrence(occ); + if typedef.is_extern { + extern_typedef_map.insert( + typedef.object.name.val.clone(), sym.clone()); + } + let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = ScipSymbolKind::TypeAlias.into(); @@ -889,6 +869,33 @@ fn process_file( (file_path, file_data) } +// --------------------------------------------------------------------------- +// Implement/Interface → extern typedef resolution +// --------------------------------------------------------------------------- + +/// Recursively walk a `StatementSpec` tree and collect the name span +/// and object name of every `implement` or `interface` composite object. +fn collect_impl_iface_from_spec( + spec: &StatementSpec, + results: &mut Vec<(ZeroSpan, String)>, +) { + for obj_decl in &spec.objects { + let comp = &obj_decl.obj; + if comp.comp_kind() == CompObjectKind::Implement + || comp.comp_kind() == CompObjectKind::Interface + { + results.push(( + comp.object.name.span, + comp.object.name.val.clone(), + )); + } + collect_impl_iface_from_spec(&obj_decl.spec, results); + } + for template_decl in &spec.templates { + collect_impl_iface_from_spec(&template_decl.spec, results); + } +} + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- @@ -934,12 +941,34 @@ pub fn build_scip_index( // --- Pass 1: walk all files, emit symbols, build span→symbol map --- let mut span_map = SpanSymbolMap::new(); - let mut file_results: Vec<(PathBuf, FileData)> = Vec::new(); + let mut extern_typedef_map = ExternTypedefMap::new(); + let mut file_results: HashMap = HashMap::new(); - for (_, analysis) in analyses { + for analysis in analyses.values() { let (path, data) = process_file( - analysis, project_root, import_data, &mut span_map); - file_results.push((path, data)); + analysis, project_root, import_data, &mut span_map, + &mut extern_typedef_map); + file_results.insert(path, data); + } + + // --- Pass 1b: emit references from implement/interface to extern typedefs --- + if !extern_typedef_map.is_empty() { + for analysis in analyses.values() { + let file_path: PathBuf = analysis.path.clone().into(); + let mut refs = Vec::new(); + collect_impl_iface_from_spec(&analysis.toplevel.spec, &mut refs); + if let Some(file_data) = file_results.get_mut(&file_path) { + for (name_span, obj_name) in refs { + let typedef_name = format!("{}_interface_t", obj_name); + if let Some(typedef_sym) = extern_typedef_map.get(&typedef_name) { + let mut ref_occ = Occurrence::new(); + ref_occ.range = span_to_scip_range(&name_span); + ref_occ.symbol = typedef_sym.clone(); + file_data.add_occurrence(ref_occ); + } + } + } + } } // --- Pass 2: extract relationships from DeviceAnalysis using span_map --- @@ -949,7 +978,7 @@ pub fn build_scip_index( let mut documents = Vec::new(); let mut external_symbols = Vec::new(); - for (path, mut data) in file_results { + for (path, mut data) in file_results.drain() { // Apply relationships to SymbolInformation entries for sym_info in data.symbols.values_mut() { if let Some(rels) = sym_rels.get(&sym_info.symbol) { @@ -1000,8 +1029,10 @@ pub fn build_span_symbol_map( project_root: &Path, ) -> SpanSymbolMap { let mut span_map = SpanSymbolMap::new(); - for (_, analysis) in analyses { - process_file(analysis, project_root, None, &mut span_map); + let mut extern_typedefs = ExternTypedefMap::new(); + for analysis in analyses.values() { + process_file(analysis, project_root, None, &mut span_map, + &mut extern_typedefs); } span_map } From 9ee5144b3354bee77591d98030852952580dbc58 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 14 Apr 2026 15:32:14 +0200 Subject: [PATCH 23/32] Re-add reference resolutions --- src/backends/scip.rs | 75 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index d23e1e5b..3fe7cd9f 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -33,7 +33,8 @@ use crate::analysis::structure::objects::{ use crate::analysis::structure::toplevel::{ObjectDecl, StatementSpec}; use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; use crate::analysis::templating::objects::DMLNamedMember; -use crate::analysis::DeviceAnalysis; +use crate::analysis::reference::Reference; +use crate::analysis::{DeviceAnalysis, LocationSpan}; use crate::analysis::IsolatedAnalysis; use crate::Span as ZeroSpan; use crate::file_management::CanonPath; @@ -896,6 +897,29 @@ fn collect_impl_iface_from_spec( } } +// --------------------------------------------------------------------------- +// AST reference collection +// --------------------------------------------------------------------------- + +/// Recursively walk a `StatementSpec` tree and collect all `Reference` +/// objects from templates, composite objects, methods, and in-each blocks. +fn collect_refs_from_spec(spec: &StatementSpec, results: &mut Vec) { + for template_decl in &spec.templates { + results.extend_from_slice(&template_decl.obj.references); + collect_refs_from_spec(&template_decl.spec, results); + } + for obj_decl in &spec.objects { + results.extend_from_slice(&obj_decl.obj.references); + collect_refs_from_spec(&obj_decl.spec, results); + } + for method_decl in &spec.methods { + results.extend_from_slice(&method_decl.obj.references); + } + for ineach_decl in &spec.ineachs { + results.extend_from_slice(&ineach_decl.obj.references); + } +} + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- @@ -974,6 +998,55 @@ pub fn build_scip_index( // --- Pass 2: extract relationships from DeviceAnalysis using span_map --- let sym_rels = extract_relationships(devices, &span_map); + // --- Pass 2b: emit reference occurrences from AST references --- + // Build a reverse map: reference_span → SCIP symbols, by walking + // each device's symbols and their reference/implementation sets. + // This is O(symbols × avg_refs) which is much faster than calling + // symbols_of_ref (O(refs × symbols)) for each AST reference. + let mut ref_span_to_scip: HashMap> = HashMap::new(); + for device in devices { + for sym_ref in device.symbol_info.all_symbols() { + let sym = sym_ref.symbol.lock().unwrap(); + let Some(scip_sym) = span_map.get(&sym.loc) else { + continue; + }; + let scip_sym = scip_sym.clone(); + for ref_span in sym.references.iter() + .chain(sym.implementations.iter()) + { + ref_span_to_scip.entry(*ref_span) + .or_default() + .push(scip_sym.clone()); + } + } + } + // Dedup within each entry + for syms in ref_span_to_scip.values_mut() { + syms.sort(); + syms.dedup(); + } + // Now walk the AST to find reference sites and emit occurrences + // using the pre-built map. + for analysis in analyses.values() { + let file_path: PathBuf = analysis.path.clone().into(); + let mut refs = Vec::new(); + refs.extend_from_slice(&analysis.toplevel.references); + collect_refs_from_spec(&analysis.toplevel.spec, &mut refs); + if let Some(file_data) = file_results.get_mut(&file_path) { + for ast_ref in &refs { + let ref_span = *ast_ref.loc_span(); + if let Some(scip_syms) = ref_span_to_scip.get(&ref_span) { + for scip_sym in scip_syms { + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(&ref_span); + occ.symbol = scip_sym.clone(); + file_data.add_occurrence(occ); + } + } + } + } + } + // --- Pass 3: assemble documents, injecting relationships --- let mut documents = Vec::new(); let mut external_symbols = Vec::new(); From 2f268ca3deffb7b173da39bec38e9412f2db716f Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 22 Apr 2026 10:28:13 +0200 Subject: [PATCH 24/32] Allow and adjust index files for muti-root --- USAGE.md | 86 +++++++++---- src/actions/requests.rs | 115 ++++++++++++----- src/backends/scip.rs | 266 ++++++++++++++++++++++++++++++---------- src/dfa/client.rs | 3 +- src/dfa/main.rs | 12 +- 5 files changed, 358 insertions(+), 124 deletions(-) diff --git a/USAGE.md b/USAGE.md index 819f9b6f..f6d723c2 100644 --- a/USAGE.md +++ b/USAGE.md @@ -137,12 +137,35 @@ Sourcegraph for cross-repository navigation and code search. ### Invocation SCIP export is available through the DFA (DML File Analyzer) binary via the -`--scip-output ` flag: +`--scip-output ` flag: ``` -dfa --compile-info --workspace --scip-output [list of devices to analyze, ] +dfa --compile-info --workspace --scip-output [list of devices to analyze, ] ``` -It is worth noting that SCIP format specifies that symbols from documents that are not under the project root (which we define as the workspace) get slotted under external symbols with no occurances tracked. +The output directory is created if it does not exist. Each workspace root +produces a separate SCIP index file named `.scip` inside the +output directory. + +#### Multi-root workspaces + +Multiple `--workspace` (`-w`) flags can be specified to cover source trees that +live under different root directories (e.g. project code under one root and +Simics built-in DML files under another): + +``` +dfa -w /home/user/project -w /opt/simics/linux64 --scip-output ./scip-out device.dml +``` + +This produces: +``` +./scip-out/project.scip +./scip-out/linux64.scip +``` + +Each index contains full `Document` entries (with occurrences) only for files +that fall under that workspace root. Symbols defined under no root appear as `external_symbols` in files that reference them — providing their `SymbolInformation` +(kind, documentation, relationships) so that consumers can resolve references +to them. ### SCIP schema details Here we list how we have mapped DML specifically to the SCIP format. @@ -182,37 +205,47 @@ signature that disambiguates: SCIP symbols follow the format: ` ' ' ' ' ' ' ' ' ` -For DML, the scheme is `dml`, the manager is `simics`, version is `.` (currently we cannot extract simics version here), and the -package is the device name. Descriptors are built from the fully qualified path -through the device hierarchy: +For DML, the scheme is `dml`, the manager is `simics`, and both the package and +version are `.` (empty). Descriptors are built by concatenating *file-path +components* (from the file's path relative to its project root) with +*code-level namespace segments* (the nested DML object names in the source +file): + +``` +dml simics . . src. `sample_device.dml`. regs. r1. offset. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + file path components code-level names +``` + +Path components that contain special characters (such as `.` in file +extensions) are backtick-escaped per the SCIP spec. +More examples: ``` -dml simics sample_device . sample_device.regs.r1.offset. - ^ term (parameter) -dml simics sample_device . sample_device.regs.r1.read(). - ^ method -dml simics sample_device . bank# - ^ 'type' (template) +dml simics . . src. `sample_device.dml`. regs. r1. read(). + ^ method +dml simics . . src. `sample_device.dml`. bank# + ^ type (template) ``` Descriptor suffixes follow the SCIP standard: - `.` (term) — used for composite objects, parameters, and other named values -- `#` (type) — used only for templates +- `#` (type) — used for templates and typedefs - `().` (method) — used for methods #### Local Symbols Method arguments and method-local variables use SCIP local symbols of the form -`local _`, where `` is the internal symbol identifier. Local -symbols are scoped to a single document and are not navigable across files. +`local _`, where `` is a sequential counter scoped to each +source file (starting at 0). Local symbols are scoped to a single document +and are not navigable across files. #### Occurrence Roles DML definitions (including the primary symbol location) are emitted with the -SCIP `Definition` role. Declarations that also appear as definitions share -this role. Declarations that do _not_ define a value (e.g. abstract method -declarations, or `default` parameter declarations that are overridden) are -emitted with the `ForwardDefinition` role. +SCIP `Definition` role. All declarations — including abstract method +declarations and `default` parameter declarations — currently also receive the +`Definition` role. References (including template instantiation sites from `is` statements) are emitted as plain references with no additional role flags. Access-kind @@ -220,11 +253,11 @@ refinement (`ReadAccess` / `WriteAccess`) is not yet tracked. #### Enclosing Ranges -For composite object definitions and method declarations, each `Definition` -or `ForwardDefinition` occurrence includes an `enclosing_range` that spans -the full AST node (e.g. the complete `register r1 is ... { ... }` block or -the full method body). This allows consumers to associate the definition site -with the extent of the construct it names. +For all symbol definitions — composite objects, methods, templates, parameters, +sessions, saved variables, constants, hooks, typedefs, and method arguments — +each `Definition` occurrence includes an `enclosing_range` that spans the full +AST node. This allows consumers to associate the definition site with the +extent of the construct it names. #### Deduplication and Determinism @@ -261,9 +294,10 @@ dependency tracking without needing to scan occurrences. File symbols use the format: ``` -dml simics . . path/to/file_dml. +dml simics . . path. to. `file.dml`. ``` -where path segments are separated by term descriptors (`.`). +where each path component becomes a separate term descriptor (`.`), and +components containing special characters are backtick-escaped. ## Device Object Hierarchy Export diff --git a/src/actions/requests.rs b/src/actions/requests.rs index 56495294..9f1e7a87 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -968,7 +968,8 @@ pub struct ExportScipRequest; pub struct ExportScipParams { /// Device paths to export SCIP for. If empty, exports all known devices. pub devices: Option>, - /// The file path where the SCIP index should be written. + /// Directory where SCIP index files should be written. + /// Each workspace root produces a `.scip` file. pub output_path: String, } @@ -976,8 +977,11 @@ pub struct ExportScipParams { pub struct ExportScipResult { /// Whether the export succeeded. pub success: bool, - /// Number of documents in the exported index. + /// Total number of documents across all index files. pub document_count: usize, + /// Paths of all SCIP index files written. + #[serde(default)] + pub index_files: Vec, /// Error message, if any. pub error: Option, } @@ -1000,6 +1004,7 @@ impl RequestAction for ExportScipRequest { Ok(ExportScipResult { success: false, document_count: 0, + index_files: vec![], error: Some("Request timed out".to_string()), }) } @@ -1058,6 +1063,7 @@ impl RequestAction for ExportScipRequest { return Ok(ExportScipResult { success: false, document_count: 0, + index_files: vec![], error: Some("No device analyses found".to_string()), }); } @@ -1084,38 +1090,91 @@ impl RequestAction for ExportScipRequest { } } - // Determine project root from workspaces - let project_root = ctx.workspace_roots + // Build the list of project roots from all workspace roots. + let project_roots: Vec = ctx.workspace_roots .lock() .unwrap() - .first() - .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) - .unwrap_or_else(|| std::path::PathBuf::from(".")); + .iter() + .filter_map(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) + .collect(); + let project_roots = if project_roots.is_empty() { + vec![std::path::PathBuf::from(".")] + } else { + project_roots + }; - let index = crate::backends::scip::build_scip_index( - &isolated_map, &project_root, Some(&import_data), &devices); - let doc_count = index.documents.len(); + let indices = crate::backends::scip::build_scip_indices( + &isolated_map, &project_roots, Some(&import_data), &devices); - let output = std::path::Path::new(¶ms.output_path); - match crate::backends::scip::write_scip_to_file(index, output) { - Ok(()) => { - info!("SCIP export complete: {} documents written to {}", - doc_count, params.output_path); - Ok(ExportScipResult { - success: true, - document_count: doc_count, - error: None, - }) - }, - Err(e) => { - error!("SCIP export failed: {}", e); - Ok(ExportScipResult { - success: false, - document_count: 0, - error: Some(e), - }) + // Write each index into the output directory, named + // after the last component of its project root. + let output_dir = std::path::Path::new(¶ms.output_path); + if let Err(e) = std::fs::create_dir_all(output_dir) { + error!("Failed to create SCIP output directory {:?}: {}", output_dir, e); + return Ok(ExportScipResult { + success: false, + document_count: 0, + index_files: vec![], + error: Some(format!( + "Failed to create output directory {:?}: {}", output_dir, e)), + }); + } + + let mut total_docs = 0usize; + let mut written_files = Vec::new(); + + let mut used_names: std::collections::HashSet = + std::collections::HashSet::new(); + + for (root, index) in indices.into_iter() { + let doc_count = index.documents.len(); + total_docs += doc_count; + + let base_name = root.file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "index".to_string()); + let mut name = base_name.clone(); + let mut suffix = 1u32; + while used_names.contains(&name) { + name = format!("{}.{}", base_name, suffix); + suffix += 1; + } + used_names.insert(name.clone()); + let file_path = output_dir.join(format!("{}.scip", name)); + + match crate::backends::scip::write_scip_to_file( + index, &file_path, + ) { + Ok(()) => { + info!( + "SCIP index written: {} documents to {:?}", + doc_count, file_path + ); + written_files.push( + file_path.to_string_lossy().to_string()); + } + Err(e) => { + error!("SCIP export failed for {:?}: {}", file_path, e); + return Ok(ExportScipResult { + success: false, + document_count: 0, + index_files: written_files, + error: Some(e), + }); + } } } + + info!( + "SCIP export complete: {} total documents in {} file(s)", + total_docs, written_files.len() + ); + Ok(ExportScipResult { + success: true, + document_count: total_docs, + index_files: written_files, + error: None, + }) } } diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 3fe7cd9f..7b1e7b22 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -15,7 +15,7 @@ //! The symbol path is: file path segments (as term descriptors) → //! template/object nesting (as type/term descriptors) → leaf symbol. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use protobuf::Enum; @@ -152,6 +152,13 @@ fn extract_relationships( } } + // Deduplicate relationships across devices (multiple devices + // may share source files and produce identical relationships). + for rels in result.values_mut() { + rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + rels.dedup_by(|a, b| a.symbol == b.symbol); + } + result } @@ -288,7 +295,7 @@ pub fn extract_import_data( /// /// Uses HashMaps keyed by dedup keys so that duplicate entries /// from multiple device analyses are naturally collapsed. -#[derive(Default)] +#[derive(Default, Clone)] struct FileData { /// Occurrences keyed by (symbol, range, roles) to avoid duplicates. occurrences: HashMap<(String, Vec, i32), Occurrence>, @@ -734,6 +741,7 @@ fn emit_method( fn process_file( analysis: &IsolatedAnalysis, project_root: &Path, + project_roots: &[PathBuf], import_data: Option<&FileImportData>, span_map: &mut SpanSymbolMap, extern_typedef_map: &mut ExternTypedefMap, @@ -768,7 +776,11 @@ fn process_file( if let Some(imports) = import_data.and_then(|id| id.get(canon)) { for (import_span, resolved_path) in imports { let target_pathbuf: PathBuf = resolved_path.clone().into(); - let target_sym = make_file_symbol(&target_pathbuf, project_root); + let target_root_idx = closest_root(&target_pathbuf, project_roots); + let target_root = target_root_idx + .map(|i| project_roots[i].as_path()) + .unwrap_or(project_root); + let target_sym = make_file_symbol(&target_pathbuf, target_root); let mut imp_occ = Occurrence::new(); imp_occ.range = span_to_scip_range(import_span); @@ -924,53 +936,73 @@ fn collect_refs_from_spec(spec: &StatementSpec, results: &mut Vec) { // Public API // --------------------------------------------------------------------------- -/// Build a complete SCIP Index from a set of isolated (per-file) -/// analyses, using code-namespace-based symbol names. +/// Assign a file path to its closest matching project root. +/// +/// Returns the index into `roots` of the longest-prefix match, +/// or `None` if the path doesn't fall under any root. +fn closest_root(path: &Path, roots: &[PathBuf]) -> Option { + let mut best: Option<(usize, usize)> = None; // (root_idx, component_count) + for (i, root) in roots.iter().enumerate() { + if path.starts_with(root) { + let depth = root.components().count(); + if best.is_none_or(|(_, d)| depth > d) { + best = Some((i, depth)); + } + } + } + best.map(|(i, _)| i) +} + +/// Build one or more SCIP indices from per-file analyses, partitioned +/// by project root. +/// +/// Each file is assigned to its closest matching root (longest prefix +/// match). A separate `Index` is produced for every root. Files +/// under *other* roots appear as `external_symbols` in each index, +/// preserving cross-root relationship targets. Files that don't fall +/// under any root are placed in `external_symbols` of every index. /// -/// Each `IsolatedAnalysis` contributes one Document in the resulting -/// index. Symbols are named after their position in the structural -/// tree (file → template → object → method, etc.) rather than the -/// merged device hierarchy. +/// Symbol strings are consistent across all indices: they use the +/// file's *own* root to build the path-based prefix. This means a +/// consumer can merge the indices and the symbol identifiers will +/// agree. /// /// # Arguments -/// * `analyses` – map from canonical path to the per-file analysis -/// * `project_root` – workspace root, used to compute relative paths -/// * `import_data` – optional pre-resolved import data for emitting -/// import occurrences -pub fn build_scip_index( +/// * `analyses` – map from canonical path to per-file analysis +/// * `project_roots` – one or more workspace roots; each produces +/// its own `Index` +/// * `import_data` – optional pre-resolved import data +/// * `devices` – device analyses for relationship extraction +/// +/// # Returns +/// A vec of `(root_path, Index)` pairs, one per project root that +/// had at least one file assigned to it. +pub fn build_scip_indices( analyses: &HashMap, - project_root: &Path, + project_roots: &[PathBuf], import_data: Option<&FileImportData>, devices: &[&DeviceAnalysis], -) -> Index { +) -> Vec<(PathBuf, Index)> { + assert!(!project_roots.is_empty(), "need at least one project root"); + debug!( - "Building namespace-based SCIP index for {} file(s) rooted at {:?}", + "Building namespace-based SCIP index for {} file(s) with {} root(s)", analyses.len(), - project_root + project_roots.len() ); - let mut tool_info = ToolInfo::new(); - tool_info.name = "dls".to_string(); - tool_info.version = crate::version(); - - let mut metadata = Metadata::new(); - metadata.tool_info = MessageField::some(tool_info); - let root_str = project_root.to_string_lossy(); - metadata.project_root = if root_str.ends_with('/') { - format!("file://{root_str}") - } else { - format!("file://{root_str}/") - }; - metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); - // --- Pass 1: walk all files, emit symbols, build span→symbol map --- let mut span_map = SpanSymbolMap::new(); let mut extern_typedef_map = ExternTypedefMap::new(); let mut file_results: HashMap = HashMap::new(); for analysis in analyses.values() { + let file_path: PathBuf = analysis.path.clone().into(); + let root_idx = closest_root(&file_path, project_roots); + let root = root_idx.map(|i| &project_roots[i]) + .unwrap_or(&project_roots[0]); let (path, data) = process_file( - analysis, project_root, import_data, &mut span_map, + analysis, root, project_roots, import_data, &mut span_map, &mut extern_typedef_map); file_results.insert(path, data); } @@ -999,10 +1031,6 @@ pub fn build_scip_index( let sym_rels = extract_relationships(devices, &span_map); // --- Pass 2b: emit reference occurrences from AST references --- - // Build a reverse map: reference_span → SCIP symbols, by walking - // each device's symbols and their reference/implementation sets. - // This is O(symbols × avg_refs) which is much faster than calling - // symbols_of_ref (O(refs × symbols)) for each AST reference. let mut ref_span_to_scip: HashMap> = HashMap::new(); for device in devices { for sym_ref in device.symbol_info.all_symbols() { @@ -1020,13 +1048,10 @@ pub fn build_scip_index( } } } - // Dedup within each entry for syms in ref_span_to_scip.values_mut() { syms.sort(); syms.dedup(); } - // Now walk the AST to find reference sites and emit occurrences - // using the pre-built map. for analysis in analyses.values() { let file_path: PathBuf = analysis.path.clone().into(); let mut refs = Vec::new(); @@ -1047,22 +1072,60 @@ pub fn build_scip_index( } } - // --- Pass 3: assemble documents, injecting relationships --- - let mut documents = Vec::new(); - let mut external_symbols = Vec::new(); + // --- Pass 3: inject relationships into SymbolInformation, partition by root --- - for (path, mut data) in file_results.drain() { - // Apply relationships to SymbolInformation entries + // Apply relationships first (before partitioning). + // Use extend rather than replace so that pre-existing + // relationships (e.g. import relationships from process_file) + // are preserved. + for data in file_results.values_mut() { for sym_info in data.symbols.values_mut() { if let Some(rels) = sym_rels.get(&sym_info.symbol) { - sym_info.relationships = rels.clone(); + sym_info.relationships.extend(rels.iter().cloned()); } } + } + + // Partition files into buckets by closest root. + // root_idx → Vec<(path, FileData)>. + // None-bucket = files not under any root; these "orphan" files + // never get a Document in any index (their occurrences are dropped). + // Their SymbolInformation is selectively included in each root's + // external_symbols if referenced from that root's documents. + let mut root_buckets: HashMap, Vec<(PathBuf, FileData)>> = + HashMap::new(); + for (path, data) in file_results.drain() { + let bucket = closest_root(&path, project_roots); + root_buckets.entry(bucket).or_default().push((path, data)); + } - let (occs, syms) = data.into_vecs(); + // For each root, build an Index. + let mut results: Vec<(PathBuf, Index)> = Vec::new(); - match path.strip_prefix(project_root) { - Ok(rel) => { + for (root_idx, root) in project_roots.iter().enumerate() { + let mut tool_info = ToolInfo::new(); + tool_info.name = "dls".to_string(); + tool_info.version = crate::version(); + + let mut metadata = Metadata::new(); + metadata.tool_info = MessageField::some(tool_info); + let root_str = root.to_string_lossy(); + metadata.project_root = if root_str.ends_with('/') { + format!("file://{root_str}") + } else { + format!("file://{root_str}/") + }; + metadata.text_document_encoding = + scip::types::TextEncoding::UTF8.into(); + + let mut documents = Vec::new(); + let mut external_symbols = Vec::new(); + + // Files belonging to this root → Documents. + if let Some(files) = root_buckets.get(&Some(root_idx)) { + for (path, data) in files { + let (occs, syms) = data.clone().into_vecs(); + let rel = path.strip_prefix(root).unwrap(); let mut doc = Document::new(); doc.relative_path = rel.to_string_lossy().to_string(); doc.language = "dml".to_string(); @@ -1072,25 +1135,98 @@ pub fn build_scip_index( doc.symbols = syms; documents.push(doc); } - Err(_) => { - external_symbols.extend(syms); + } + + // Collect the set of symbol strings referenced from this + // root's documents and the set already defined locally. + let mut referenced: HashSet = HashSet::new(); + let mut defined_locally: HashSet = HashSet::new(); + for doc in &documents { + for occ in &doc.occurrences { + if !occ.symbol.is_empty() { + referenced.insert(occ.symbol.clone()); + } + } + for sym in &doc.symbols { + defined_locally.insert(sym.symbol.clone()); + } + } + + // Only emit external_symbols for symbols from files that + // are NOT under any project root (orphan files). Files + // under other roots get their own index, so per the SCIP + // spec we leave those out ("the external package will get + // indexed separately"). + // Additionally, only include symbols that are actually + // referenced from this root's documents but not already + // defined locally. + if let Some(orphan_files) = root_buckets.get(&None) { + for (_path, data) in orphan_files { + let (_, syms) = data.clone().into_vecs(); + for sym in syms { + if referenced.contains(&sym.symbol) + && !defined_locally.contains(&sym.symbol) + { + external_symbols.push(sym); + } + } } } - } - documents.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); - external_symbols.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + documents.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); + external_symbols.sort_by(|a, b| a.symbol.cmp(&b.symbol)); + external_symbols.dedup_by(|a, b| a.symbol == b.symbol); - let mut index = Index::new(); - index.metadata = MessageField::some(metadata); - index.documents = documents; - index.external_symbols = external_symbols; + if documents.is_empty() && external_symbols.is_empty() { + continue; + } - debug!( - "Namespace-based SCIP index built with {} document(s)", - index.documents.len() - ); - index + let mut index = Index::new(); + index.metadata = MessageField::some(metadata); + index.documents = documents; + index.external_symbols = external_symbols; + + debug!( + "SCIP index for root {:?}: {} document(s), {} external symbol(s)", + root, + index.documents.len(), + index.external_symbols.len() + ); + results.push((root.clone(), index)); + } + + results +} + +/// Build a single SCIP Index using one project root. +/// +/// Convenience wrapper around `build_scip_indices` for the +/// single-root case. +pub fn build_scip_index( + analyses: &HashMap, + project_root: &Path, + import_data: Option<&FileImportData>, + devices: &[&DeviceAnalysis], +) -> Index { + let roots = vec![project_root.to_path_buf()]; + let mut indices = build_scip_indices( + analyses, &roots, import_data, devices); + indices.pop().map(|(_, idx)| idx).unwrap_or_else(|| { + let mut index = Index::new(); + let mut tool_info = ToolInfo::new(); + tool_info.name = "dls".to_string(); + tool_info.version = crate::version(); + let mut metadata = Metadata::new(); + metadata.tool_info = MessageField::some(tool_info); + let root_str = project_root.to_string_lossy(); + metadata.project_root = if root_str.ends_with('/') { + format!("file://{root_str}") + } else { + format!("file://{root_str}/") + }; + index.metadata = MessageField::some(metadata); + index + }) } /// Build just the span→SCIP-symbol map without producing the full index. @@ -1104,8 +1240,8 @@ pub fn build_span_symbol_map( let mut span_map = SpanSymbolMap::new(); let mut extern_typedefs = ExternTypedefMap::new(); for analysis in analyses.values() { - process_file(analysis, project_root, None, &mut span_map, - &mut extern_typedefs); + process_file(analysis, project_root, &[project_root.to_path_buf()], + None, &mut span_map, &mut extern_typedefs); } span_map } diff --git a/src/dfa/client.rs b/src/dfa/client.rs index 583a63f8..4e3ba83c 100644 --- a/src/dfa/client.rs +++ b/src/dfa/client.rs @@ -417,7 +417,8 @@ impl ClientInterface { device_paths: Vec, output_path: String) -> anyhow::Result { - debug!("Sending SCIP export request for {:?} -> {}", device_paths, output_path); + debug!("Sending SCIP export request for {:?} -> {}", + device_paths, output_path); self.send( cmd::export_scip(device_paths, output_path).to_string() )?; diff --git a/src/dfa/main.rs b/src/dfa/main.rs index f973f7e5..a410638c 100644 --- a/src/dfa/main.rs +++ b/src/dfa/main.rs @@ -94,7 +94,8 @@ fn parse_args() -> Args { .value_parser(clap::value_parser!(PathBuf)) .required(false)) .arg(Arg::new("scip-output").long("scip-output") - .help("Export SCIP index to the specified file after analysis") + .help("Export SCIP index files to the specified directory after analysis. \ + Each workspace root produces a .scip file.") .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) .required(false)) @@ -195,7 +196,7 @@ fn main_inner() -> Result<(), i32> { // Export SCIP if requested if let Some(scip_path) = &arg.scip_output { - println!("Exporting SCIP index to {:?}", scip_path); + println!("Exporting SCIP indices to {:?}", scip_path); let scip_output_str = scip_path.to_string_lossy().to_string(); let device_paths: Vec = arg.files.iter() .filter_map(|f| f.canonicalize().ok()) @@ -204,8 +205,11 @@ fn main_inner() -> Result<(), i32> { match dlsclient.export_scip(device_paths, scip_output_str) { Ok(result) => { if result.success { - println!("SCIP export complete: {} document(s) written", - result.document_count); + println!("SCIP export complete: {} document(s) in {} index file(s)", + result.document_count, result.index_files.len()); + for f in &result.index_files { + println!(" -> {}", f); + } } else { let err_msg = result.error.unwrap_or_else( || "Unknown error".to_string()); From d040cd8eaac30a54fec384aa549a5c21c98d4935 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 22 Apr 2026 10:41:27 +0200 Subject: [PATCH 25/32] Cleanup --- src/actions/requests.rs | 3 +-- src/backends/scip.rs | 28 ++++++++++++---------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/actions/requests.rs b/src/actions/requests.rs index 9f1e7a87..9be2c419 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -1123,8 +1123,7 @@ impl RequestAction for ExportScipRequest { let mut total_docs = 0usize; let mut written_files = Vec::new(); - let mut used_names: std::collections::HashSet = - std::collections::HashSet::new(); + let mut used_names: HashSet = HashSet::new(); for (root, index) in indices.into_iter() { let doc_count = index.documents.len(); diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 7b1e7b22..7a5bc887 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -107,8 +107,6 @@ fn extract_relationships( } if !rels.is_empty() { - rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); - rels.dedup_by(|a, b| a.symbol == b.symbol); result.entry(source_scip.clone()) .or_default() .extend(rels); @@ -140,8 +138,6 @@ fn extract_relationships( } } if !rels.is_empty() { - rels.sort_by(|a, b| a.symbol.cmp(&b.symbol)); - rels.dedup_by(|a, b| a.symbol == b.symbol); result.entry(source_scip.clone()) .or_default() .extend(rels); @@ -162,6 +158,16 @@ fn extract_relationships( result } +/// Build a `file://`-prefixed project root URI with a trailing slash. +fn make_project_root_uri(root: &Path) -> String { + let s = root.to_string_lossy(); + if s.ends_with('/') { + format!("file://{s}") + } else { + format!("file://{s}/") + } +} + /// Convert a ZeroSpan range into the SCIP occurrence range format. /// /// SCIP uses `[startLine, startChar, endLine, endChar]` (4 elements) @@ -1109,12 +1115,7 @@ pub fn build_scip_indices( let mut metadata = Metadata::new(); metadata.tool_info = MessageField::some(tool_info); - let root_str = root.to_string_lossy(); - metadata.project_root = if root_str.ends_with('/') { - format!("file://{root_str}") - } else { - format!("file://{root_str}/") - }; + metadata.project_root = make_project_root_uri(root); metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); @@ -1218,12 +1219,7 @@ pub fn build_scip_index( tool_info.version = crate::version(); let mut metadata = Metadata::new(); metadata.tool_info = MessageField::some(tool_info); - let root_str = project_root.to_string_lossy(); - metadata.project_root = if root_str.ends_with('/') { - format!("file://{root_str}") - } else { - format!("file://{root_str}/") - }; + metadata.project_root = make_project_root_uri(project_root); index.metadata = MessageField::some(metadata); index }) From fd2abeada9fedf1ce8b86bb6d5a00cfdffc80b75 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Fri, 8 May 2026 15:40:10 +0200 Subject: [PATCH 26/32] Canonicalize workspace path comparisons --- src/backends/scip.rs | 71 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 7a5bc887..dbcce06d 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -747,7 +747,7 @@ fn emit_method( fn process_file( analysis: &IsolatedAnalysis, project_root: &Path, - project_roots: &[PathBuf], + canon_map: &HashMap<&Path, PathBuf>, import_data: Option<&FileImportData>, span_map: &mut SpanSymbolMap, extern_typedef_map: &mut ExternTypedefMap, @@ -782,9 +782,7 @@ fn process_file( if let Some(imports) = import_data.and_then(|id| id.get(canon)) { for (import_span, resolved_path) in imports { let target_pathbuf: PathBuf = resolved_path.clone().into(); - let target_root_idx = closest_root(&target_pathbuf, project_roots); - let target_root = target_root_idx - .map(|i| project_roots[i].as_path()) + let target_root = closest_root(&target_pathbuf, canon_map) .unwrap_or(project_root); let target_sym = make_file_symbol(&target_pathbuf, target_root); @@ -944,19 +942,22 @@ fn collect_refs_from_spec(spec: &StatementSpec, results: &mut Vec) { /// Assign a file path to its closest matching project root. /// -/// Returns the index into `roots` of the longest-prefix match, +/// Returns the canonical root path with the longest prefix match, /// or `None` if the path doesn't fall under any root. -fn closest_root(path: &Path, roots: &[PathBuf]) -> Option { - let mut best: Option<(usize, usize)> = None; // (root_idx, component_count) - for (i, root) in roots.iter().enumerate() { - if path.starts_with(root) { - let depth = root.components().count(); +fn closest_root<'a>( + path: &Path, + canon_map: &'a HashMap<&Path, PathBuf>, +) -> Option<&'a Path> { + let mut best: Option<(&'a Path, usize)> = None; + for canon_root in canon_map.values() { + if path.starts_with(canon_root) { + let depth = canon_root.components().count(); if best.is_none_or(|(_, d)| depth > d) { - best = Some((i, depth)); + best = Some((canon_root.as_path(), depth)); } } } - best.map(|(i, _)| i) + best.map(|(r, _)| r) } /// Build one or more SCIP indices from per-file analyses, partitioned @@ -997,6 +998,15 @@ pub fn build_scip_indices( project_roots.len() ); + // Pre-canonicalize all roots once to avoid repeated filesystem + // calls during matching. The original `project_roots` are kept + // for metadata/output so user-facing paths remain unchanged. + let canon_map: HashMap<&Path, PathBuf> = project_roots + .iter() + .map(|r| (r.as_path(), r.canonicalize().unwrap_or_else(|_| r.clone()))) + .collect(); + let default_canon = &canon_map[project_roots[0].as_path()]; + // --- Pass 1: walk all files, emit symbols, build span→symbol map --- let mut span_map = SpanSymbolMap::new(); let mut extern_typedef_map = ExternTypedefMap::new(); @@ -1004,11 +1014,10 @@ pub fn build_scip_indices( for analysis in analyses.values() { let file_path: PathBuf = analysis.path.clone().into(); - let root_idx = closest_root(&file_path, project_roots); - let root = root_idx.map(|i| &project_roots[i]) - .unwrap_or(&project_roots[0]); + let root = closest_root(&file_path, &canon_map) + .unwrap_or(default_canon); let (path, data) = process_file( - analysis, root, project_roots, import_data, &mut span_map, + analysis, root, &canon_map, import_data, &mut span_map, &mut extern_typedef_map); file_results.insert(path, data); } @@ -1092,23 +1101,23 @@ pub fn build_scip_indices( } } - // Partition files into buckets by closest root. - // root_idx → Vec<(path, FileData)>. - // None-bucket = files not under any root; these "orphan" files - // never get a Document in any index (their occurrences are dropped). - // Their SymbolInformation is selectively included in each root's - // external_symbols if referenced from that root's documents. - let mut root_buckets: HashMap, Vec<(PathBuf, FileData)>> = + // Partition files into buckets by closest canonical root. + // None-bucket = files not under any root ("orphan" files); + // they never get a Document but their SymbolInformation may + // appear in external_symbols if referenced. + let mut root_buckets: HashMap, Vec<(PathBuf, FileData)>> = HashMap::new(); for (path, data) in file_results.drain() { - let bucket = closest_root(&path, project_roots); + let bucket = closest_root(&path, &canon_map); root_buckets.entry(bucket).or_default().push((path, data)); } // For each root, build an Index. let mut results: Vec<(PathBuf, Index)> = Vec::new(); - for (root_idx, root) in project_roots.iter().enumerate() { + for root in project_roots { + let canon_root = canon_map[root.as_path()].as_path(); + let mut tool_info = ToolInfo::new(); tool_info.name = "dls".to_string(); tool_info.version = crate::version(); @@ -1123,10 +1132,10 @@ pub fn build_scip_indices( let mut external_symbols = Vec::new(); // Files belonging to this root → Documents. - if let Some(files) = root_buckets.get(&Some(root_idx)) { + if let Some(files) = root_buckets.get(&Some(canon_root)) { for (path, data) in files { let (occs, syms) = data.clone().into_vecs(); - let rel = path.strip_prefix(root).unwrap(); + let rel = path.strip_prefix(canon_root).unwrap_or(path); let mut doc = Document::new(); doc.relative_path = rel.to_string_lossy().to_string(); doc.language = "dml".to_string(); @@ -1233,11 +1242,15 @@ pub fn build_span_symbol_map( analyses: &HashMap, project_root: &Path, ) -> SpanSymbolMap { + let canon_root = project_root.canonicalize() + .unwrap_or_else(|_| project_root.to_path_buf()); + let canon_map: HashMap<&Path, PathBuf> = + HashMap::from([(project_root, canon_root)]); let mut span_map = SpanSymbolMap::new(); let mut extern_typedefs = ExternTypedefMap::new(); for analysis in analyses.values() { - process_file(analysis, project_root, &[project_root.to_path_buf()], - None, &mut span_map, &mut extern_typedefs); + process_file(analysis, &canon_map[project_root], + &canon_map, None, &mut span_map, &mut extern_typedefs); } span_map } From 881c8b7c18a492a4080037e482deb7043c24b2f4 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 12 May 2026 10:57:09 +0200 Subject: [PATCH 27/32] Emit interface->type relationshis --- USAGE.md | 6 ++++++ src/backends/scip.rs | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/USAGE.md b/USAGE.md index f6d723c2..f762bc9a 100644 --- a/USAGE.md +++ b/USAGE.md @@ -280,6 +280,12 @@ implements `foo` with its own definition of `method a`, then `b.a` will carry an `is_implementation` relationship to `foo.a`. This lets consumers identify which method version is the active override and navigate the override chain. +`implement` and `interface` objects whose name matches an extern typedef +(following the DML convention `_interface_t`) emit an +`is_type_definition` relationship on the object's `SymbolInformation` pointing +to the typedef symbol. A reference occurrence at the object's name span is also +emitted, linking the declaration site to the underlying interface type. + #### File Symbols and Imports Each source file involved in the analysis gets a dedicated SCIP symbol of kind diff --git a/src/backends/scip.rs b/src/backends/scip.rs index dbcce06d..557543f5 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -1032,10 +1032,24 @@ pub fn build_scip_indices( for (name_span, obj_name) in refs { let typedef_name = format!("{}_interface_t", obj_name); if let Some(typedef_sym) = extern_typedef_map.get(&typedef_name) { + // Reference occurrence at the object name span let mut ref_occ = Occurrence::new(); ref_occ.range = span_to_scip_range(&name_span); ref_occ.symbol = typedef_sym.clone(); file_data.add_occurrence(ref_occ); + + // is_type_definition relationship on the object's + // SymbolInformation → the extern typedef + if let Some(obj_sym) = span_map.get(&name_span) { + if let Some(sym_info) = + file_data.symbols.get_mut(obj_sym) + { + let mut rel = Relationship::new(); + rel.symbol = typedef_sym.clone(); + rel.is_type_definition = true; + sym_info.relationships.push(rel); + } + } } } } From 3933d59f96385f710733d8540caaef7d9805eb00 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Tue, 12 May 2026 11:17:12 +0200 Subject: [PATCH 28/32] Misc fixes - We failed to recurse into in-eachs in various cases, now fixed - Corrected code comment --- src/backends/scip.rs | 49 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 557543f5..31e64509 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -539,6 +539,39 @@ fn walk_spec( file_path, project_root, namespace, file_data, span_map, ); } + + // --- In-each blocks --- + // + // An `in each (foo, bar) { ... }` block applies its contents as + // template additions, not as a true child namespace. We still + // need to give nested declarations *some* unique scope to avoid + // collisions with siblings of the in-each. We synthesize a + // namespace segment from the target template list, formatted + // with characters that are not valid in DML identifiers (`<`, + // `>`, `:`, `,`, ` `). `sanitize_name` will backtick-escape + // the whole segment, guaranteeing it cannot collide with a + // real DML object name. + for ineach_decl in &spec.ineachs { + let targets = ineach_decl.obj.spec.iter() + .map(|t| t.val.as_str()) + .collect::>() + .join(", "); + let scope_name = format!(""); + namespace.push(NamespaceSegment { + name: scope_name, + suffix: DescriptorSuffix::Term, + }); + walk_spec( + &ineach_decl.spec, + file_path, + project_root, + namespace, + file_data, + local_counter, + span_map, + ); + namespace.pop(); + } } // --------------------------------------------------------------------------- @@ -911,6 +944,11 @@ fn collect_impl_iface_from_spec( for template_decl in &spec.templates { collect_impl_iface_from_spec(&template_decl.spec, results); } + // Recurse into in-each blocks: a connect inside an in-each + // may contain interface declarations. + for ineach_decl in &spec.ineachs { + collect_impl_iface_from_spec(&ineach_decl.spec, results); + } } // --------------------------------------------------------------------------- @@ -933,6 +971,7 @@ fn collect_refs_from_spec(spec: &StatementSpec, results: &mut Vec) { } for ineach_decl in &spec.ineachs { results.extend_from_slice(&ineach_decl.obj.references); + collect_refs_from_spec(&ineach_decl.spec, results); } } @@ -964,10 +1003,12 @@ fn closest_root<'a>( /// by project root. /// /// Each file is assigned to its closest matching root (longest prefix -/// match). A separate `Index` is produced for every root. Files -/// under *other* roots appear as `external_symbols` in each index, -/// preserving cross-root relationship targets. Files that don't fall -/// under any root are placed in `external_symbols` of every index. +/// match). A separate `Index` is produced for every root. Files that +/// don't fall under any root ("orphan" files) do not get a Document; +/// instead, their `SymbolInformation` may appear in `external_symbols` +/// of any index that references them but does not define them locally. +/// Files under *other* roots are not included as external symbols +/// because they get their own dedicated index, per the SCIP spec. /// /// Symbol strings are consistent across all indices: they use the /// file's *own* root to build the path-based prefix. This means a From 69546d3f1c3de27db4bd61bd545c97290db4791f Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 18 May 2026 09:40:33 +0200 Subject: [PATCH 29/32] More small fixes --- src/backends/hierarchy.rs | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/backends/hierarchy.rs b/src/backends/hierarchy.rs index 96db2bf7..24e1165b 100644 --- a/src/backends/hierarchy.rs +++ b/src/backends/hierarchy.rs @@ -26,7 +26,7 @@ use crate::analysis::structure::objects::{ use crate::analysis::templating::methods::{DMLMethodArg, MethodDeclaration}; use crate::analysis::templating::objects::{ DMLCompositeObject, DMLNamedMember, DMLObject, - DMLShallowObject, DMLShallowObjectVariant, StructureContainer, + DMLShallowObjectVariant, StructureContainer, }; use crate::analysis::DeclarationSpan; use crate::analysis::LocationSpan; @@ -314,28 +314,22 @@ fn build_object_hierarchy( ); } } - DMLObject::ShallowObject(DMLShallowObject { - variant: DMLShallowObjectVariant::Parameter(param), - .. - }) => { - parameters.insert( - name.clone(), - build_param_entry( - param, comp_obj.location(), span_map, source), - ); - } - DMLObject::ShallowObject(DMLShallowObject { - variant: DMLShallowObjectVariant::Method(method_ref), - .. - }) => { - methods.insert( - name.clone(), - build_method_entry(method_ref, span_map, source), - ); + DMLObject::ShallowObject(shallow) => match &shallow.variant { + DMLShallowObjectVariant::Parameter(param) => { + parameters.insert( + name.clone(), + build_param_entry( + param, comp_obj.location(), span_map, source), + ); + } + DMLShallowObjectVariant::Method(method_ref) => { + methods.insert( + name.clone(), + build_method_entry(method_ref, span_map, source), + ); + } + _ => {} } - // Sessions, saveds, constants, hooks are not included - // in the hierarchy output per current specification. - _ => {} } } From 8f4e8e6ee32b6d079e535d7caf4dc638f1ed80e5 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 20 May 2026 09:25:24 +0200 Subject: [PATCH 30/32] Output implements relations between template symbols --- src/backends/scip.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index 31e64509..e5500454 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -146,6 +146,37 @@ fn extract_relationships( } } } + + // --- Template → template inheritance (is) --- + for sym_ref in device.symbol_info.template_symbols.values() { + let sym = sym_ref.symbol.lock().unwrap(); + let source_span = &sym.loc; + + let Some(source_scip) = span_map.get(source_span) else { + continue; + }; + + if let SymbolSource::Template(templ_arc) = &sym.source { + let mut rels: Vec = Vec::new(); + + for parent_trait in templ_arc.traitspec.parents.values() { + if let Some(loc) = &parent_trait.loc { + if let Some(target_scip) = span_map.get(loc) { + let mut rel = Relationship::new(); + rel.symbol = target_scip.clone(); + rel.is_implementation = true; + rels.push(rel); + } + } + } + + if !rels.is_empty() { + result.entry(source_scip.clone()) + .or_default() + .extend(rels); + } + } + } } // Deduplicate relationships across devices (multiple devices From d032b11de1659e0cfe8fd0b0d2ede5e18416afbb Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 1 Jun 2026 10:30:40 +0200 Subject: [PATCH 31/32] Fix potential conflicts between in-eachs with same spec in same scope --- src/backends/scip.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index e5500454..b9fda924 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -582,12 +582,20 @@ fn walk_spec( // `>`, `:`, `,`, ` `). `sanitize_name` will backtick-escape // the whole segment, guaranteeing it cannot collide with a // real DML object name. + // + // A per-spec counter is folded into the segment so that two + // in-each blocks with the *same* target template list in the + // same enclosing scope still produce distinct namespace + // segments — otherwise equally-named members would collide in + // `file_data.symbols` and silently overwrite one another. for ineach_decl in &spec.ineachs { + *local_counter += 1; + let ineach_idx = *local_counter; let targets = ineach_decl.obj.spec.iter() .map(|t| t.val.as_str()) .collect::>() .join(", "); - let scope_name = format!(""); + let scope_name = format!(""); namespace.push(NamespaceSegment { name: scope_name, suffix: DescriptorSuffix::Term, From ef6e74b9d17e8a71deb6171bf93ed4c12449b856 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 1 Jun 2026 10:39:22 +0200 Subject: [PATCH 32/32] Speculative optimizations --- src/backends/scip.rs | 237 ++++++++++++++++++++----------------------- 1 file changed, 111 insertions(+), 126 deletions(-) diff --git a/src/backends/scip.rs b/src/backends/scip.rs index b9fda924..84bf0957 100644 --- a/src/backends/scip.rs +++ b/src/backends/scip.rs @@ -245,17 +245,20 @@ fn is_scip_identifier_char(c: char) -> bool { c.is_ascii_alphanumeric() || matches!(c, '_' | '+' | '-' | '$') } -/// Encode a name as a SCIP descriptor identifier. +/// Append a name to `out` as a SCIP descriptor identifier. /// /// Names consisting entirely of SCIP identifier characters are emitted /// as-is (a "simple identifier"). Names that contain other characters /// (e.g. dots, spaces) are backtick-escaped, with interior backticks /// doubled. -fn sanitize_name(name: &str) -> String { +/// +/// Writing directly into a caller-provided buffer avoids the +/// intermediate `String` allocation that `sanitize_name` would +/// otherwise produce on every emission. +fn push_sanitized(out: &mut String, name: &str) { if !name.is_empty() && name.chars().all(is_scip_identifier_char) { - name.to_string() + out.push_str(name); } else { - let mut out = String::new(); out.push('`'); for c in name.chars() { if c == '`' { @@ -265,24 +268,37 @@ fn sanitize_name(name: &str) -> String { } } out.push('`'); - out } } +/// Encode a name as a SCIP descriptor identifier (allocating variant). +fn sanitize_name(name: &str) -> String { + let mut out = String::new(); + push_sanitized(&mut out, name); + out +} + /// Build a SCIP symbol string representing a DML source file. /// /// File symbols use the path relative to the project root (or the /// full path for external files) as the descriptor. Each path /// component becomes a term descriptor with proper SCIP escaping. fn make_file_symbol(path: &Path, project_root: &Path) -> String { + let mut out = String::from("dml simics . . "); + push_file_descriptors(&mut out, path, project_root); + out +} + +/// Append the file-path descriptor block (each path component as a +/// term descriptor) to `out`. +fn push_file_descriptors(out: &mut String, path: &Path, project_root: &Path) { let rel = path.strip_prefix(project_root).unwrap_or(path); - let descriptors: String = rel.components() - .filter_map(|c| { - let s = c.as_os_str().to_str()?; - Some(format!("{}.", sanitize_name(s))) - }) - .collect(); - format!("dml simics . . {}", descriptors) + for component in rel.components() { + if let Some(s) = component.as_os_str().to_str() { + push_sanitized(out, s); + out.push('.'); + } + } } /// Extract import resolution data from an AnalysisStorage for a set @@ -390,41 +406,52 @@ impl DescriptorSuffix { } } -/// A single segment of the code-namespace path embedded in a SCIP -/// symbol string. -struct NamespaceSegment { - name: String, - suffix: DescriptorSuffix, -} - -/// Build a global SCIP symbol string from file-relative path and -/// a chain of namespace segments. +/// Incremental builder for the SCIP symbol string of nested +/// declarations within a single file. /// -/// Format: -/// `dml simics . . ` -fn make_namespace_symbol( - file_path: &Path, - project_root: &Path, - namespace: &[NamespaceSegment], -) -> String { - let rel = file_path.strip_prefix(project_root).unwrap_or(file_path); - let mut descriptors = String::new(); +/// Maintains the full symbol prefix +/// `dml simics . . ...` +/// in a single `String` that grows on `push` and shrinks on `pop`. +/// This avoids rebuilding the entire prefix (file path components + +/// every ancestor segment, each re-sanitized) for every emitted +/// declaration, which was previously the dominant cost of the walk +/// for deep object trees. +struct NamespaceBuilder { + /// Current accumulated symbol string. + buf: String, + /// Stack of buffer lengths so `pop` can truncate back to the + /// pre-push state in O(1). + lengths: Vec, +} - // File path components as term descriptors - for component in rel.components() { - if let Some(s) = component.as_os_str().to_str() { - descriptors.push_str(&sanitize_name(s)); - descriptors.push('.'); +impl NamespaceBuilder { + fn new(file_path: &Path, project_root: &Path) -> Self { + let mut buf = String::from("dml simics . . "); + push_file_descriptors(&mut buf, file_path, project_root); + Self { + buf, + lengths: Vec::new(), } } - // Code-level namespace descriptors - for seg in namespace { - descriptors.push_str(&sanitize_name(&seg.name)); - descriptors.push_str(seg.suffix.as_str()); + /// Push a code-namespace segment onto the builder, appending its + /// sanitized name and descriptor suffix to the buffer. + fn push(&mut self, name: &str, suffix: DescriptorSuffix) { + self.lengths.push(self.buf.len()); + push_sanitized(&mut self.buf, name); + self.buf.push_str(suffix.as_str()); + } + + /// Pop the most recently pushed segment, truncating the buffer. + fn pop(&mut self) { + let len = self.lengths.pop().expect("unbalanced NamespaceBuilder pop"); + self.buf.truncate(len); } - format!("dml simics . . {descriptors}") + /// The current full SCIP symbol string for the active namespace. + fn current(&self) -> &str { + &self.buf + } } /// Build a document-local SCIP symbol for a method argument or local. @@ -442,22 +469,16 @@ fn emit_term_symbol( object: &DMLObjectCommon, scip_kind: ScipSymbolKind, doc_text: &str, - file_path: &Path, - project_root: &Path, - namespace: &mut Vec, + namespace: &mut NamespaceBuilder, file_data: &mut FileData, span_map: &mut SpanSymbolMap, ) { - let name = object.name.val.clone(); + let name = &object.name.val; let name_span = &object.name.span; let full_span = &object.span; - namespace.push(NamespaceSegment { - name: name.clone(), - suffix: DescriptorSuffix::Term, - }); - - let sym = make_namespace_symbol(file_path, project_root, namespace); + namespace.push(name, DescriptorSuffix::Term); + let sym: String = namespace.current().to_string(); span_map.insert(*name_span, sym.clone()); @@ -471,7 +492,7 @@ fn emit_term_symbol( let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = scip_kind.into(); - sym_info.display_name = name; + sym_info.display_name = name.clone(); sym_info.documentation = vec![doc_text.to_string()]; file_data.add_symbol_info(sym_info); @@ -486,29 +507,27 @@ fn emit_term_symbol( /// occurrences and SymbolInformation entries for every declaration. fn walk_spec( spec: &StatementSpec, - file_path: &Path, - project_root: &Path, - namespace: &mut Vec, + namespace: &mut NamespaceBuilder, file_data: &mut FileData, local_counter: &mut u64, span_map: &mut SpanSymbolMap, ) { // --- Templates --- for template_decl in &spec.templates { - emit_template(template_decl, file_path, project_root, - namespace, file_data, local_counter, span_map); + emit_template(template_decl, namespace, file_data, + local_counter, span_map); } // --- Composite objects (bank, register, group, …) --- for obj_decl in &spec.objects { - emit_composite_object(obj_decl, file_path, project_root, - namespace, file_data, local_counter, span_map); + emit_composite_object(obj_decl, namespace, file_data, + local_counter, span_map); } // --- Methods --- for method_decl in &spec.methods { - emit_method(method_decl, file_path, project_root, - namespace, file_data, local_counter, span_map); + emit_method(method_decl, namespace, file_data, + local_counter, span_map); } // --- Parameters --- @@ -522,7 +541,7 @@ fn walk_spec( ¶m_decl.obj.object, ScipSymbolKind::Constant, doc, - file_path, project_root, namespace, file_data, span_map, + namespace, file_data, span_map, ); } @@ -533,7 +552,7 @@ fn walk_spec( &var_decl.object, ScipSymbolKind::Variable, "session", - file_path, project_root, namespace, file_data, span_map, + namespace, file_data, span_map, ); } } @@ -545,7 +564,7 @@ fn walk_spec( &var_decl.object, ScipSymbolKind::Variable, "saved", - file_path, project_root, namespace, file_data, span_map, + namespace, file_data, span_map, ); } } @@ -557,7 +576,7 @@ fn walk_spec( &hook_decl.obj.object, ScipSymbolKind::Event, doc, - file_path, project_root, namespace, file_data, span_map, + namespace, file_data, span_map, ); } @@ -567,7 +586,7 @@ fn walk_spec( &const_decl.obj.object, ScipSymbolKind::Constant, "constant", - file_path, project_root, namespace, file_data, span_map, + namespace, file_data, span_map, ); } @@ -596,14 +615,9 @@ fn walk_spec( .collect::>() .join(", "); let scope_name = format!(""); - namespace.push(NamespaceSegment { - name: scope_name, - suffix: DescriptorSuffix::Term, - }); + namespace.push(&scope_name, DescriptorSuffix::Term); walk_spec( &ineach_decl.spec, - file_path, - project_root, namespace, file_data, local_counter, @@ -620,24 +634,18 @@ fn walk_spec( /// Emit SCIP data for a template declaration and recurse into its body. fn emit_template( template_decl: &ObjectDecl, - file_path: &Path, - project_root: &Path, - namespace: &mut Vec, + namespace: &mut NamespaceBuilder, file_data: &mut FileData, local_counter: &mut u64, span_map: &mut SpanSymbolMap, ) { let tmpl = &template_decl.obj; - let name = tmpl.object.name.val.clone(); + let name = &tmpl.object.name.val; let name_span = &tmpl.object.name.span; let full_span = &tmpl.object.span; - namespace.push(NamespaceSegment { - name: name.clone(), - suffix: DescriptorSuffix::Type, - }); - - let sym = make_namespace_symbol(file_path, project_root, namespace); + namespace.push(name, DescriptorSuffix::Type); + let sym: String = namespace.current().to_string(); span_map.insert(*name_span, sym.clone()); @@ -653,15 +661,13 @@ fn emit_template( let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = ScipSymbolKind::Class.into(); - sym_info.display_name = name; + sym_info.display_name = name.clone(); sym_info.documentation = vec!["template".to_string()]; file_data.add_symbol_info(sym_info); // Recurse into the template's flattened spec walk_spec( &template_decl.spec, - file_path, - project_root, namespace, file_data, local_counter, @@ -674,26 +680,20 @@ fn emit_template( /// Emit SCIP data for a composite object declaration and recurse. fn emit_composite_object( obj_decl: &ObjectDecl, - file_path: &Path, - project_root: &Path, - namespace: &mut Vec, + namespace: &mut NamespaceBuilder, file_data: &mut FileData, local_counter: &mut u64, span_map: &mut SpanSymbolMap, ) { let comp = &obj_decl.obj; - let name = comp.object.name.val.clone(); + let name = &comp.object.name.val; let name_span = &comp.object.name.span; let full_span = &comp.object.span; let scip_kind = dml_kind_to_scip_kind( &DMLSymbolKind::CompObject(comp.kind.kind)); - namespace.push(NamespaceSegment { - name: name.clone(), - suffix: DescriptorSuffix::Term, - }); - - let sym = make_namespace_symbol(file_path, project_root, namespace); + namespace.push(name, DescriptorSuffix::Term); + let sym: String = namespace.current().to_string(); span_map.insert(*name_span, sym.clone()); @@ -707,15 +707,13 @@ fn emit_composite_object( let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = scip_kind.into(); - sym_info.display_name = name; + sym_info.display_name = name.clone(); sym_info.documentation = vec![comp.kind.kind.kind_name().to_string()]; file_data.add_symbol_info(sym_info); // Recurse into nested declarations walk_spec( &obj_decl.spec, - file_path, - project_root, namespace, file_data, local_counter, @@ -728,24 +726,18 @@ fn emit_composite_object( /// Emit SCIP data for a method declaration, including its arguments. fn emit_method( method_decl: &ObjectDecl, - file_path: &Path, - project_root: &Path, - namespace: &mut Vec, + namespace: &mut NamespaceBuilder, file_data: &mut FileData, local_counter: &mut u64, span_map: &mut SpanSymbolMap, ) { let meth = &method_decl.obj; - let name = meth.object.name.val.clone(); + let name = &meth.object.name.val; let name_span = &meth.object.name.span; let full_span = &meth.object.span; - namespace.push(NamespaceSegment { - name: name.clone(), - suffix: DescriptorSuffix::Method, - }); - - let sym = make_namespace_symbol(file_path, project_root, namespace); + namespace.push(name, DescriptorSuffix::Method); + let sym: String = namespace.current().to_string(); span_map.insert(*name_span, sym.clone()); @@ -778,7 +770,7 @@ fn emit_method( let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = ScipSymbolKind::Method.into(); - sym_info.display_name = name; + sym_info.display_name = name.clone(); sym_info.documentation = vec![doc_parts.join(" ")]; file_data.add_symbol_info(sym_info); @@ -827,7 +819,7 @@ fn process_file( let file_path: PathBuf = analysis.path.clone().into(); let mut file_data = FileData::default(); let mut local_counter: u64 = 0; - let mut namespace = Vec::new(); + let mut namespace = NamespaceBuilder::new(&file_path, project_root); // File-level symbol (definition at line 0) let file_sym = make_file_symbol(&file_path, project_root); @@ -883,8 +875,6 @@ fn process_file( &var_decl.object, ScipSymbolKind::Variable, "extern", - &file_path, - project_root, &mut namespace, &mut file_data, span_map, @@ -895,11 +885,9 @@ fn process_file( // Top-level typedefs (not in StatementSpec) for typedef in &tl.typedefs { // Typedefs are type-like; use a type descriptor. - namespace.push(NamespaceSegment { - name: typedef.object.name.val.clone(), - suffix: DescriptorSuffix::Type, - }); - let sym = make_namespace_symbol(&file_path, project_root, &namespace); + let typedef_name = &typedef.object.name.val; + namespace.push(typedef_name, DescriptorSuffix::Type); + let sym: String = namespace.current().to_string(); let mut occ = Occurrence::new(); occ.range = span_to_scip_range(&typedef.object.name.span); @@ -909,14 +897,13 @@ fn process_file( file_data.add_occurrence(occ); if typedef.is_extern { - extern_typedef_map.insert( - typedef.object.name.val.clone(), sym.clone()); + extern_typedef_map.insert(typedef_name.clone(), sym.clone()); } let mut sym_info = SymbolInformation::new(); sym_info.symbol = sym; sym_info.kind = ScipSymbolKind::TypeAlias.into(); - sym_info.display_name = typedef.object.name.val.clone(); + sym_info.display_name = typedef_name.clone(); sym_info.documentation = vec![if typedef.is_extern { "extern typedef".to_string() } else { @@ -936,8 +923,6 @@ fn process_file( }, ScipSymbolKind::Constant, "loggroup", - &file_path, - project_root, &mut namespace, &mut file_data, span_map, @@ -947,8 +932,6 @@ fn process_file( // Walk the main StatementSpec (templates, objects, methods, …) walk_spec( &tl.spec, - &file_path, - project_root, &mut namespace, &mut file_data, &mut local_counter, @@ -1226,10 +1209,12 @@ pub fn build_scip_indices( let mut external_symbols = Vec::new(); // Files belonging to this root → Documents. - if let Some(files) = root_buckets.get(&Some(canon_root)) { + // Move the bucket out so we don't have to clone each + // FileData (which carries hashmaps full of String keys). + if let Some(files) = root_buckets.remove(&Some(canon_root)) { for (path, data) in files { - let (occs, syms) = data.clone().into_vecs(); - let rel = path.strip_prefix(canon_root).unwrap_or(path); + let (occs, syms) = data.into_vecs(); + let rel = path.strip_prefix(canon_root).unwrap_or(&path); let mut doc = Document::new(); doc.relative_path = rel.to_string_lossy().to_string(); doc.language = "dml".to_string();