Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,7 @@ subprocess = "=0.2.9"
thiserror = "2.0"
urlencoding = "2.1"
utf8-read = "0.4"
scip = "0.6.1"
protobuf = "3"
walkdir = "2"
heck = "0.5"
77 changes: 77 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,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 <path>` flag:
```
dfa --compile-info <compile_commands.json> --workspace <project root> --scip-output <scip_file_name> [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:
`<scheme> ' ' <manager> ' ' <package> ' ' <version> ' ' <descriptors>`

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 <name>_<id>`, where `<id>` 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.
139 changes: 139 additions & 0 deletions src/actions/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,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<Vec<lsp_types::Uri>>,
/// 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<String>,
}

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<Self::Response, ResponseError> {
Ok(ExportScipResult {
success: false,
document_count: 0,
error: Some("Request timed out".to_string()),
})
}

fn get_identifier(params: &Self::Params) -> String {
Self::request_identifier(&params.output_path)
}

fn handle<O: Output>(
ctx: InitActionContext<O>,
params: Self::Params,
) -> Result<Self::Response, ResponseError> {
info!("Handling SCIP export request to {}", params.output_path);

// Determine which device paths to export
let device_paths: Vec<CanonPath> =
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(&params.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 = <Self as lsp_data::request::Request>::Result;
Expand Down
18 changes: 18 additions & 0 deletions src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ pub fn set_contexts(paths: Vec<String>) -> Notification<notifications::ChangeAct
}
}

pub fn export_scip(devices: Vec<String>, output_path: String) -> Request<requests::ExportScipRequest> {
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))
Expand Down
33 changes: 33 additions & 0 deletions src/dfa/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,37 @@ impl ClientInterface {
self.server.wait_timeout(Duration::from_millis(1000))?;
Ok(())
}

pub fn export_scip(&mut self,
device_paths: Vec<String>,
output_path: String)
-> anyhow::Result<crate::actions::requests::ExportScipResult> {
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;
}
}
}
}
}
37 changes: 36 additions & 1 deletion src/dfa/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Args {
lint_cfg_path: Option<PathBuf>,
test: bool,
quiet: bool,
scip_output: Option<PathBuf>,
}

fn parse_args() -> Args {
Expand Down Expand Up @@ -95,6 +96,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!(<PATH> ... "DML files to analyze")
.value_parser(clap::value_parser!(PathBuf)))
.arg_required_else_help(false)
Expand All @@ -118,7 +124,9 @@ fn parse_args() -> Args {
linting_enabled: args.get_one::<bool>("linting-enabled")
.cloned(),
lint_cfg_path: args.get_one::<PathBuf>("lint-cfg-path")
.cloned()
.cloned(),
scip_output: args.get_one::<PathBuf>("scip-output")
.cloned(),
}
}

Expand Down Expand Up @@ -182,6 +190,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<String> = 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
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading