Skip to content

Commit def17b5

Browse files
feggeclaude
andcommitted
Add diagnostics when decompilation fails for a procedure
Emit warning diagnostics when pseudocode generation fails, explaining why decompilation failed and optionally pointing to related locations (e.g., a called procedure that couldn't be decompiled). These diagnostics only appear when decompilation inlay hints are enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fc55281 commit def17b5

File tree

5 files changed

+122
-17
lines changed

5 files changed

+122
-17
lines changed

src/decompiler/collector.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use miden_assembly_syntax::ast::visit::{self, Visit};
99
use miden_assembly_syntax::ast::{Block, Module, Op, Procedure};
1010
use miden_debug_types::{DefaultSourceManager, SourceSpan, Spanned};
1111
use tower_lsp::lsp_types::{
12-
Diagnostic, DiagnosticSeverity, InlayHint, InlayHintKind, InlayHintLabel, Position, Range,
12+
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, InlayHint, InlayHintKind,
13+
InlayHintLabel, Location, Position, Range, Url,
1314
};
1415

1516
use crate::analysis::{
@@ -28,11 +29,25 @@ use super::state::DecompilerState;
2829
// Hint Collection Types
2930
// ═══════════════════════════════════════════════════════════════════════════
3031

32+
/// Related information for a tracking failure diagnostic.
33+
#[derive(Clone)]
34+
pub struct RelatedInfo {
35+
/// The span of the related location
36+
pub span: SourceSpan,
37+
/// Message explaining the related location
38+
pub message: String,
39+
}
40+
3141
/// A tracking failure that should be reported as a diagnostic.
3242
pub(crate) struct TrackingFailure {
43+
/// The span where decompilation failed
3344
pub span: SourceSpan,
45+
/// The reason for the failure
3446
pub reason: String,
47+
/// The name of the procedure where failure occurred
3548
pub proc_name: String,
49+
/// Optional related information (e.g., the root cause in another procedure)
50+
pub related: Option<RelatedInfo>,
3651
}
3752

3853
/// Collector that visits procedures and instructions.
@@ -466,10 +481,16 @@ impl<'a> Visit for DecompilationCollector<'a> {
466481
if let Some(ref state) = self.state {
467482
if state.tracking_failed {
468483
if let (Some(span), Some(reason)) = (state.failure_span, state.failure_reason.clone()) {
484+
// Convert state's related info to TrackingFailure's related info
485+
let related = state.failure_related.as_ref().map(|r| RelatedInfo {
486+
span: r.span,
487+
message: r.message.clone(),
488+
});
469489
self.failures.push(TrackingFailure {
470490
span,
471491
reason,
472492
proc_name: proc_name.clone(),
493+
related,
473494
});
474495
}
475496
// Remove all hints for this procedure when decompilation fails
@@ -558,6 +579,7 @@ pub struct DecompilationResult {
558579
pub fn collect_decompilation_hints(
559580
module: &Module,
560581
sources: &DefaultSourceManager,
582+
uri: &Url,
561583
visible_range: &Range,
562584
tab_count: usize,
563585
source_text: &str,
@@ -621,17 +643,30 @@ pub fn collect_decompilation_hints(
621643
.into_iter()
622644
.filter_map(|failure| {
623645
let range = span_to_range(sources, failure.span)?;
646+
647+
// Convert related info to DiagnosticRelatedInformation
648+
let related_information = failure.related.and_then(|related| {
649+
let related_range = span_to_range(sources, related.span)?;
650+
Some(vec![DiagnosticRelatedInformation {
651+
location: Location {
652+
uri: uri.clone(),
653+
range: related_range,
654+
},
655+
message: related.message,
656+
}])
657+
});
658+
624659
Some(Diagnostic {
625660
range,
626-
severity: Some(DiagnosticSeverity::HINT),
661+
severity: Some(DiagnosticSeverity::WARNING),
627662
code: None,
628663
code_description: None,
629-
source: Some("masm-decompiler".to_string()),
664+
source: Some("masm-lsp".to_string()),
630665
message: format!(
631-
"Pseudocode unavailable beyond this point in '{}': {}",
666+
"Pseudocode unavailable in `{}`: {}",
632667
failure.proc_name, failure.reason
633668
),
634-
related_information: None,
669+
related_information,
635670
tags: None,
636671
data: None,
637672
})

src/decompiler/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub use pseudocode::{
1919
apply_counter_indexing, binary_op_pseudocode, extract_declaration_prefix,
2020
format_invocation_target, format_procedure_signature, generate_pseudocode, rename_variable,
2121
};
22-
pub use state::{DecompilerState, NamedValue, SavedStackState, LOOP_COUNTER_NAMES};
22+
pub use state::{DecompilerState, FailureRelatedInfo, NamedValue, SavedStackState, LOOP_COUNTER_NAMES};
2323

2424
#[cfg(test)]
2525
mod tests {

src/decompiler/pseudocode.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ pub fn generate_pseudocode(
762762
}
763763
_ => {
764764
// Unknown stack effect - fail tracking
765-
state.fail_tracking(span, &format!("procedure call to '{}' has unknown stack effect", name));
765+
state.fail_tracking(span, &format!("procedure call to `{}` has unknown stack effect", name));
766766
Some(format!("call {}", name))
767767
}
768768
}
@@ -773,7 +773,7 @@ pub fn generate_pseudocode(
773773
for _ in 0..4 {
774774
state.pop_name();
775775
}
776-
state.fail_tracking(span, "dynamic call has unknown stack effect");
776+
state.fail_tracking(span, "dynamic call `dyn` has unknown stack effect");
777777
Some("call <dynamic>".to_string())
778778
}
779779

@@ -910,7 +910,7 @@ pub fn generate_pseudocode(
910910
// Complex STARK operations - fail tracking (unknown effects)
911911
Instruction::FriExt2Fold4 | Instruction::HornerBase | Instruction::HornerExt |
912912
Instruction::EvalCircuit | Instruction::LogPrecompile => {
913-
state.fail_tracking(span, &format!("complex STARK operation: {}", inst));
913+
state.fail_tracking(span, &format!("complex STARK operation `{}`", inst));
914914
Some(format!("{}", inst))
915915
}
916916
}

src/decompiler/state.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ pub struct SavedStackState {
2424
pub stack: Vec<NamedValue>,
2525
}
2626

27+
/// Related information for a decompilation failure.
28+
#[derive(Debug, Clone)]
29+
pub struct FailureRelatedInfo {
30+
/// The span of the related location
31+
pub span: SourceSpan,
32+
/// Message explaining the related location
33+
pub message: String,
34+
}
35+
2736
/// State for decompiling a procedure.
2837
///
2938
/// Supports dynamic input discovery: when an operation tries to access a stack
@@ -43,6 +52,8 @@ pub struct DecompilerState {
4352
pub failure_span: Option<SourceSpan>,
4453
/// The reason tracking failed
4554
pub failure_reason: Option<String>,
55+
/// Related information for the failure (e.g., root cause in called procedure)
56+
pub failure_related: Option<FailureRelatedInfo>,
4657
/// Counter for generating loop counter names (c_1, c_2, ...)
4758
pub next_counter_id: usize,
4859
/// Span of a loop that produced a dynamic/unknown number of stack items.
@@ -73,6 +84,7 @@ impl DecompilerState {
7384
tracking_failed: false,
7485
failure_span: None,
7586
failure_reason: None,
87+
failure_related: None,
7688
next_counter_id: 0,
7789
dynamic_stack_source: None,
7890
}
@@ -150,11 +162,25 @@ impl DecompilerState {
150162

151163
/// Mark tracking as failed (e.g., after unknown procedure call).
152164
pub fn fail_tracking(&mut self, span: SourceSpan, reason: &str) {
165+
self.fail_tracking_with_related(span, reason, None);
166+
}
167+
168+
/// Mark tracking as failed with optional related information.
169+
///
170+
/// The related information can point to the root cause of the failure,
171+
/// such as a called procedure that couldn't be decompiled.
172+
pub fn fail_tracking_with_related(
173+
&mut self,
174+
span: SourceSpan,
175+
reason: &str,
176+
related: Option<FailureRelatedInfo>,
177+
) {
153178
if !self.tracking_failed {
154179
// Only record the first failure
155180
self.tracking_failed = true;
156181
self.failure_span = Some(span);
157182
self.failure_reason = Some(reason.to_string());
183+
self.failure_related = related;
158184
}
159185
self.stack.clear();
160186
}

src/server/mod.rs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use tower_lsp::{
1919
request::{GotoImplementationParams, GotoImplementationResponse},
2020
Diagnostic, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents,
2121
HoverParams, InitializeParams, InitializeResult, InitializedParams, InlayHint,
22-
InlayHintParams, Location, MarkupContent, MarkupKind, ReferenceParams, ServerCapabilities,
23-
SymbolInformation, SymbolKind, TextDocumentContentChangeEvent, TextDocumentSyncCapability,
24-
TextDocumentSyncKind, Url, WorkspaceSymbolParams,
22+
InlayHintParams, Location, MarkupContent, MarkupKind, Position, Range, ReferenceParams,
23+
ServerCapabilities, SymbolInformation, SymbolKind, TextDocumentContentChangeEvent,
24+
TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkspaceSymbolParams,
2525
},
2626
LanguageServer,
2727
};
@@ -246,8 +246,11 @@ where
246246
None
247247
};
248248

249-
// Check if taint analysis is enabled
250-
let taint_enabled = self.config.read().await.taint_analysis_enabled;
249+
// Check configuration
250+
let config = self.config.read().await;
251+
let taint_enabled = config.taint_analysis_enabled;
252+
let decompilation_enabled = config.inlay_hint_type == InlayHintType::Decompilation;
253+
drop(config);
251254

252255
// Then acquire a read lock on the workspace after parsing is done
253256
let workspace = self.workspace.read().await;
@@ -267,6 +270,27 @@ where
267270
diags.extend(taint_diags);
268271
}
269272

273+
// Run decompilation and collect failure diagnostics when decompilation hints are enabled
274+
if decompilation_enabled {
275+
if let Some(source) = self.sources.get_by_uri(&to_miden_uri(&uri)) {
276+
// Use full document range for diagnostics
277+
let full_range = Range {
278+
start: Position { line: 0, character: 0 },
279+
end: Position { line: u32::MAX, character: 0 },
280+
};
281+
let decompilation_result = crate::decompiler::collect_decompilation_hints(
282+
&doc.module,
283+
self.sources.as_ref(),
284+
&uri,
285+
&full_range,
286+
0, // tab_count doesn't matter for diagnostics
287+
source.as_str(),
288+
Some(workspace.contracts()),
289+
);
290+
diags.extend(decompilation_result.diagnostics);
291+
}
292+
}
293+
270294
diags
271295
}
272296
Err(report) => {
@@ -284,6 +308,26 @@ where
284308
);
285309
diags.extend(taint_diags);
286310
}
311+
312+
// Run decompilation on fallback doc if enabled
313+
if decompilation_enabled {
314+
if let Some(source) = self.sources.get_by_uri(&to_miden_uri(&uri)) {
315+
let full_range = Range {
316+
start: Position { line: 0, character: 0 },
317+
end: Position { line: u32::MAX, character: 0 },
318+
};
319+
let decompilation_result = crate::decompiler::collect_decompilation_hints(
320+
&doc.module,
321+
self.sources.as_ref(),
322+
&uri,
323+
&full_range,
324+
0,
325+
source.as_str(),
326+
Some(workspace.contracts()),
327+
);
328+
diags.extend(decompilation_result.diagnostics);
329+
}
330+
}
287331
}
288332
diags
289333
}
@@ -876,15 +920,15 @@ where
876920
let result = crate::decompiler::collect_decompilation_hints(
877921
&doc.module,
878922
self.sources.as_ref(),
923+
&uri,
879924
&params.range,
880925
config.inlay_hint_tabs,
881926
source.as_str(),
882927
Some(contracts),
883928
);
884929
drop(workspace);
885-
// Note: We could publish diagnostics here, but that would require
886-
// storing them and publishing on document open/change. For now,
887-
// the hints simply stop when tracking fails.
930+
// Note: Decompilation failure diagnostics are published via
931+
// publish_diagnostics when decompilation hints are enabled.
888932
result.hints
889933
}
890934
InlayHintType::None => vec![],

0 commit comments

Comments
 (0)