-
Notifications
You must be signed in to change notification settings - Fork 261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Verification diagnostics hover #1946
Conversation
dae48ba
to
5b89eea
Compare
5b89eea
to
6bb8800
Compare
076b5f4
to
fb4b98a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is ready for review.
// When hovering the failing path, it does not display the position of the failing postcondition | ||
// because the IDE extension already does it. | ||
await AssertHoverMatches(documentItem, (5, 4), | ||
@"assertion #1/2 of [batch](???) #1/1 checked in ???ms with ??? resource count: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question to reviewers: should I display batch #1/1
when there is only one batch?
It's useful to have the word batch so that the link is available, but I could as well put the link on assertion and remove of batch #1/1
. I currently favored consistency with other messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it's reasonable to always say what batch it's in, partly because it clarifies that the time and resource counts listed are for batches, not individual assertions (and only apply to individual assertions if they're 1/1 in the current batch).
@@ -123,7 +123,7 @@ IDiagnosticPublisher diagnosticPublisher | |||
/// </summary> | |||
/// <param name="implementations">The implementations to be verified</param> | |||
public void ReportImplementationsBeforeVerification(Implementation[] implementations) { | |||
if (document.LoadCanceled || implementations.Length == 0) { | |||
if (document.LoadCanceled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About this: It happenes that in the past somewhere, for some time, this function was called twice, once without any implementations, and the other one with all the implementations. This started to happen after I had merged once with master, so I had this guard to prevent this. However, it would not enable to consider files without really any implementation to verify.
This "bug" does not happen anymore (probably thanks to recent's refactoring with Boogie) so I remove this check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only one of my comments that I think it's important to address before merging is the one about assertion batch numbering/ordering.
|
||
public AssertionBatchVerificationTree? GetLongestAssertionBatch() => | ||
!AssertionBatches.Any() ? null : | ||
AssertionBatches.MaxBy(assertionBatch => assertionBatch.TimeSpent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be AssertionBatchTimes.Max()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only if I only wanted the time, but I want the entire assertion batch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, of course! 🤦
} while (lastResult.Status != CompilationStatus.VerificationFailed && | ||
lastResult.Status != CompilationStatus.VerificationSucceeded && | ||
lastResult.Status != CompilationStatus.ParsingFailed && | ||
lastResult.Status != CompilationStatus.ResolutionFailed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worth having a separate definition for "done" that encapsulates these? It would make the code a little less messy, and would make it clear what the meaning of that big conjunction is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worth having a separate definition for "done" that encapsulates these? It would make the code a little less messy, and would make it clear what the meaning of that big conjunction is.
Refactored, thanks for pointing this out.
|
||
private string? GetDiagnosticsHover(DafnyDocument document, Position position, out bool areMethodStatistics) { | ||
areMethodStatistics = false; | ||
foreach (var node in document.VerificationTree.Children.OfType<TopLevelDeclMemberVerificationTree>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eventually it might be possible to use a more efficient data structure for this, but that's probably only worth it if we find that this repeated iteration is too expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the past I've done this type of thing by first finding a particular AST element, in this case it would be finding the assertion statement, which you do by checks of the node.Range.Contains(position)
starting at the root node of the tree. That's similar to what's being done here although it would include modules and classes as filters. I think for finding elements in lists, like finding a particular statement in a list of statements, you can also do a binary search instead of a linear traversal.
Then given a particular AST node, compilation results relating to that AST node, such as resolved declarations for nodes that are references, or in this case verification performance data, should be attached to it using some form of a dictionary.
@MikaelMayer have you considered storing the verification results on the AST instead of on a separate tree that mimics parts of the AST?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't considered changing the AST to store verification results there, not just because I think it is better if we modified the AST as little as possible,
but especially because the AST does not have the notion of implementations and assertion batches like what we get from verification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
especially because the AST does not have the notion of implementations and assertion batches like what we get from verification.
Good point. Food for thought :-)
var assertionBatchIndex = -1; | ||
var information = ""; | ||
foreach (var assertionBatch in node.AssertionBatches) { | ||
assertionBatchIndex += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might not always be the case that the assertion batches are listed in order. This should probably use VCResult.vcNum
, piped through appropriately. The assertions in each batch should always be in the same order, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not know about vcNum
, that's a great insight ! I refactored the code to make use of it instead of a counter that is incrementing. It will even make the migration of assertion batch diagnostics easier down the road since this counter is probably more deterministic than mine.
# Conflicts: # Source/DafnyLanguageServer.Test/Util/ClientBasedLanguageServerTest.cs
For the UX, how would diagnostic hints (they show up as ... under the first bit of the range they cover) compare to hover? I'm wondering if hints will provide greater discoverability of this feature. |
return CreateMarkdownHover(hoverContent); | ||
} | ||
|
||
class TupleComparer : IComparer<(int, int)> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the default way of comparing tuples? https://stackoverflow.com/a/27781538/93197
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After I changes tuples to a record, I kept this class since the tuple comparison wouldn't work anymore, but thanks for this interesting finding !
|
||
private string? GetDiagnosticsHover(DafnyDocument document, Position position, out bool areMethodStatistics) { | ||
areMethodStatistics = false; | ||
foreach (var node in document.VerificationTree.Children.OfType<TopLevelDeclMemberVerificationTree>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the past I've done this type of thing by first finding a particular AST element, in this case it would be finding the assertion statement, which you do by checks of the node.Range.Contains(position)
starting at the root node of the tree. That's similar to what's being done here although it would include modules and classes as filters. I think for finding elements in lists, like finding a particular statement in a list of statements, you can also do a binary search instead of a linear traversal.
Then given a particular AST node, compilation results relating to that AST node, such as resolved declarations for nodes that are references, or in this case verification performance data, should be attached to it using some form of a dictionary.
@MikaelMayer have you considered storing the verification results on the AST instead of on a separate tree that mimics parts of the AST?
} | ||
} | ||
|
||
private string? GetDiagnosticsHover(DafnyDocument document, Position position, out bool areMethodStatistics) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method seems fairly long. Is there are way to cut it up into smaller parts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing it out ! I refactored it in 3 smaller parts.
@@ -64,5 +64,9 @@ public record DafnyDocument( | |||
} | |||
|
|||
public int LinesCount => VerificationTree.Range.End.Line; | |||
|
|||
public static int NumberOfLines(string text) { | |||
return text.Count(c => c == '\n') + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should compute a number of lines from the full text anywhere besides in TextChangeProcessor
. Instead of working with TextDocumentItem
we can work with our own datatype (like DocumentTextBuffer
) that besides storing the text as a string
also stores the number of lines.
In the future we'll change TextChangeProcessor
so it'll update DocumentTextBuffer
with a cost linear to the amount of changes (instead of the the current cost which is linear in the document size). I'm not sure what the text buffer data-structure will look like, but maybe this can inspire us: https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented your suggestion, I like it ! Makes more sense than in the DafnyDocument.
// Recomputed from the children which are ImplementationVerificationTree | ||
public List<AssertionBatchVerificationTree> AssertionBatches { get; set; } = new(); | ||
public ImmutableDictionary<(int, int), AssertionBatchVerificationTree> AssertionBatches { get; private set; } = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you replace the anonymous tuple in this field with a record to make it easier to understand what the field contains?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I replaced it with the record ASsertionBatchIndex
for now
Here my quick and probably incomplete thinking about hints vs hovering:
|
AssertionBatchIndex record
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great feature! And I think all of the previous comments from @keyboardDrummer and myself have been addressed.
|
||
namespace Microsoft.Dafny.LanguageServer.Handlers { | ||
public class DafnyHoverHandler : HoverHandlerBase { | ||
// TODO add the range of the name to the hover. | ||
private readonly ILogger logger; | ||
private readonly IDocumentDatabase documents; | ||
|
||
private const int RuLimitToBeOverCostly = 10000000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure this is the right value, but I think we'll only find out by using it in practice.
// When hovering the failing path, it does not display the position of the failing postcondition | ||
// because the IDE extension already does it. | ||
await AssertHoverMatches(documentItem, (5, 4), | ||
@"assertion #1/2 of [batch](???) #1/1 checked in ???ms with ??? resource count: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it's reasonable to always say what batch it's in, partly because it clarifies that the time and resource counts listed are for batches, not individual assertions (and only apply to individual assertions if they're 1/1 in the current batch).
// https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocumentContentChangeEvent | ||
return change.Text; | ||
} | ||
int absoluteStart = change.Range.Start.ToAbsolutePosition(text, cancellationToken); | ||
int absoluteEnd = change.Range.End.ToAbsolutePosition(text, cancellationToken); | ||
numberOfLines -= | ||
DocumentTextBuffer.ComputeNumberOfNewlines(text.Substring(absoluteStart, absoluteEnd - absoluteStart)); | ||
numberOfLines += DocumentTextBuffer.ComputeNumberOfNewlines(change.Text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like you can combine line 39 and 31 by putting line 39 before line 30.
|
||
public int NumberOfLines { get; init; } // | ||
|
||
public static int ComputeNumberOfLines(string text) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put this function and the next in TextChangeProcessor and make NumberOfLines
a parameter of DocumentTextBuffer
}; | ||
} | ||
|
||
public int NumberOfLines { get; init; } // |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is init
used? //
can be removed.
}; | ||
} | ||
|
||
private static string ApplyTextChanges(string originalText, Container<TextDocumentContentChangeEvent> changes, CancellationToken cancellationToken) { | ||
private static string ApplyTextChanges(string originalText, int originalLines, Container<TextDocumentContentChangeEvent> changes, out int numberOfLines, CancellationToken cancellationToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can clean up these APIs by letting DafnyDocument not contain a TextDocumentItem
but instead a VersionedTextDocumentIdentifier
(containing the Version and Uri) and a DocumentTextBuffer
(containing just the text and the # of lines), and then these APIs could take and return a DocumentTextBuffer
.
But we might want to do this in a follow-up PR since these changes are all a bit out of scope of hover.
At some point we should also move PositionsExtensions.ToAbsolutePosition
to DocumentTextBuffer
and implement it in constant time.
@@ -45,7 +45,7 @@ public record DafnyDocument( | |||
/// </summary> | |||
public VerificationTree VerificationTree { get; init; } = new DocumentVerificationTree( | |||
TextDocumentItem.Uri.ToString(), | |||
TextDocumentItem.Text.Count(c => c == '\n') + 1 | |||
TextDocumentItem.NumberOfLines |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice :)
|
||
private string? GetDiagnosticsHover(DafnyDocument document, Position position, out bool areMethodStatistics) { | ||
areMethodStatistics = false; | ||
foreach (var node in document.VerificationTree.Children.OfType<TopLevelDeclMemberVerificationTree>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
especially because the AST does not have the notion of implementations and assertion batches like what we get from verification.
Good point. Food for thought :-)
Good callout. Let's start with hover and see how that works. |
This PR adds assertion information when hovering them (without needing to change the VSCode plugin).
Hovering over f (empty body):
Hovering over f (one failing assertion), "assertion batch" points to this link.
Hovering over false, "Error" points to this link.
Hovering over a failing postcondition shows where the return path is failing
Hovering over
x == 2
Hovering over
true
, I tried to put it in green but it does not work.Hovering over
f
but this time there are 3 assertions. It indicates the longest batch and where it started:Hovering over a lemma very expensive to verify before it starts verifying
Hovering over a lemma very expensive to verify before it finishes verifying
Hovering over a lemma very expensive to verify, the warning icon points to this link.
Hovering over a lemma very expensive to verify with multiple assertion batches:
Formal tests
One way to discover the effect of this PR is by visiting the first hover test
Implementation
Everything in this feature is in
DafnyHoverHandler.cs
, the methodHandle
symbolHoverContent
(for type information) anddiagnosticGoverContent
(for verification performance and insights). If the diagnostics are only about statistics of an entire method (not individual assertions), it should not be displayed if there is a symbol under the caret, to prevent distractions, andareMethodStatistics
is set to true in that case.diagnosticHoverContent
is assigned using the new methodGetDiagnosticsHover
node.AssertionBatches
which are of typeAssertionBatchVerificationTree
. I'm keeping track of the assertion batch index withassertionBatchIndex
to be able to display it to users.Range
, I can only traverse node that contain the hovering position, which is efficient. I can't do a binary search however because I don't require verification trees to be ordered by range.ImmediatelyRelatedRange
(i.e. the postcondition for return AssertCmd).assertionNode.ImmediatelyRelatedRanges.Any(range => range.Contains(position))
information
: The assertion number and total number of assertions in this batch (only if it's more than 1), the batch number over the total number of batches and its timing and resource count.Description
to generate an even better message about its status. (i.e.SetDescription(returnCounterexample.FailingReturn.Description);
)By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.