Skip to content
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

Separate UI code and business logic for the resolve and verify commands #4798

Merged
merged 239 commits into from
Jan 23, 2024

Conversation

keyboardDrummer
Copy link
Member

@keyboardDrummer keyboardDrummer commented Nov 17, 2023

Changes

At the heart of this PR is an architectural change: to lay the foundation for separating UI code from business logic. Part of this is to use Boogie purely as an API, instead of as a library that produces textual output for the CLI.

This change is not necessary, but I believe it is a good step towards giving us more control over the UI experience of our CLI, and to make it easier to work with our codebase. In the future section down below are some ideas of what this PR enables.

Because there is a lot of UI code intermingled with a lot of business code, making this architectural change produces a lot of code changes, so this PR only takes the first step: the pathway where UI and logic are separated is only used when using dafny resolve or dafny verify. I will update the remaining commands in follow-up PRs.

Refactoring

  • Move Compilation, a UI agnostic back-end for driving the Dafny compiler pipeline, from the project LanguageServer to DafnyCore, so it can be used by the CLI as well. Reusing this code means more IDE code is tested using the CLI test, and you can see the effect of this additional testing from the IDE fixes down below, yay!
  • Introduce CliCompilation which uses the CLI as a user interface to track the results of Compilation.
  • Let the commands dafny resolve and dafny verify use CliCompilation instead of CompilerDriver .
  • Due to the introduction of CliCompilation, rename CompilerDriver to SynchronousCliCompilation, even though there are many features CliCompilation does not support yet.
  • Remove DafnySymbolKind from DafnyCore, which was a duplicate of an LSP enum that is now directly accessible in DafnyCore

Behavior

  • IDE fix: enable comma's in the library flag to specify multiple libraries, as the CLI does

  • IDE fix: enable having the same file as input multiple times, as the CLI does

  • IDE fix: support verification for some code generating Dafny constructors, like iterators

  • IDE fix: the option solver-path is respected by the IDE

  • IDE fix: when verification can not start due to a technical issue, the IDE tells the user what happened

  • Make it easy to update .expect files by setting the flag DiffCommand.UpdateExpectFile to true. This was broken before this PR.

  • Configure littish tests so they time out after running for 120 minutes. They normally complete in less than 30m on all platforms, so 120m should be ample.

  • Fix a bug in the cloning code, that was discovered because the CLI tests are using the server back-end code, which clones the program after parsing. (Is this good or bad? In the future I hope we'll have an immutable AST so then the question becomes redundant)

  • Make the order in which verification error are shown on the CLI consistent. Now errors are always shown in the order in which they occur in the file

    Example diff:

- git-issue-4074.dfy(24,11): Error: assertion might not hold
git-issue-4074.dfy(5,9): Error: assertion might not hold
git-issue-4074.dfy(14,11): Error: assertion might not hold
+ git-issue-4074.dfy(24,11): Error: assertion might not hold
  • Consistently let CLI diagnostics have a Warning/Error label. Example diff:
- github-issue-4804.dfy(19,8): Verification out of resource (Power.LemmaPowSubAddCancel (correctness))
+ github-issue-4804.dfy(19,8): Error: Verification out of resource (Power.LemmaPowSubAddCancel (correctness))
  • The way in which custom error messages is shown is slightly different. Example diff:
CustomErrorMesage.dfy(42,63): Error: this loop invariant could not be proved on entry
- CustomErrorMesage.dfy(42,63): Related message: position variable out of range
+  Related message: position variable out of range

Future

This PR paves the way for:

  • Adding a --filter option that operates on Dafny symbols/files, and possibly also filters parsing and resolution
  • Update the CLI output.
    • Removed useless "related message" output. Example below this list.
    • Improve summary output. Dafny currently reports Dafny program verifier finished with 21 verified, 0 errors but the number 21 does not refer to anything visible to the user. It would be better to report the number of Dafny symbols that were verified.
    • Do not report Dafny program verifier did not attempt verification when running dafny resolve
Definedness.dfy(175,27): Error: this loop invariant could not be proved on entry
- Definedness.dfy(175,27): Related message: loop invariant violation

How has this been tested?

  • Updated existing tests to account for behavioral changes

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed most of the rest, just need to have a final look at the core changes in a third batch.

docs/HowToFAQ/Errors-CommandLine.md Show resolved Hide resolved
Source/DafnyCore/Pipeline/Compilation.cs Outdated Show resolved Hide resolved
Source/DafnyDriver/DafnyBackwardsCompatibleCli.cs Outdated Show resolved Hide resolved
Comment on lines -10 to -18
/// <summary>
/// Initializes a new symbol table with the given compilation unit.
/// </summary>
/// <param name="program">The parsed dafny program.</param>
/// <param name="compilationUnit">The compilation to create the symbol table of.</param>
/// <param name="cancellationToken">A token to cancel the update operation before its completion.</param>
/// <returns>A symbol table representing the provided compilation unit.</returns>
/// <exception cref="System.OperationCanceledException">Thrown when the cancellation was requested before completion.</exception>
/// <exception cref="System.ObjectDisposedException">Thrown if the cancellation token was disposed before the completion.</exception>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why delete this comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it useful? I would argue it's not. Even the interface ISymbolTableFactory is unused.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the amount of refactoring going on I just wanted to make sure it wasn't dropped by accident. If you removed it because it wasn't useful that's just fine (and looking closer I agree it seems like "I'm being forced to document every method" rather than "I have something useful to say beyond the signature here" :)

Comment on lines 10 to 13
/// <summary>
/// Publishes the diagnostics of the specified dafny document to the connected LSP client.
/// </summary>
/// <param name="state">The document whose diagnostics should be published.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add it back.

Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more for today. :)

Source/DafnyCore/AST/Grammar/Printer.cs Show resolved Hide resolved
Comment on lines +145 to +148
if (Options.UseStdin) {
var uri = new Uri("stdin:///");
result.Add(DafnyFile.CreateAndValidate(errorReporter, fileSystem, Options, uri, Token.Cli));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OoC what happens if you specify this in the IDE?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't. StdIn is not an IDE option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'm just pointing out that technically you could add --stdin to the Language Server Launch Args, and now it seems like it would actually do something rather than being ignored. I assume it's harmless but asking just in case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use the CLI to specify an option to a command to which it doesn't apply, then the CLI will return an error. If you pass it through the project file, the CLI won't error but it'll be ignored.

Comment on lines 225 to 226
} catch (TaskCanceledException) {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does this happen? Why not catch OperationCanceledException instead?

Copy link
Member Author

@keyboardDrummer keyboardDrummer Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment and changed to OperationCanceledException. Happens when there's a parsing error so it can not resolve. I could change the interface of Compiler so it uses null instead of OperationCanceledException when it can't return a Task<ResolutionResult due to a user program error. This means inside Compilation there's a need for more if (resultIDependOn == null) { return null } since we don't coast on the monadic behavior of async/await, but it might be more readable.

I'd prefer doing that in a follow-up PR, this one is huge as is.

Comment on lines +191 to +193
if (HasErrors) {
throw new OperationCanceledException();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this exception always bothered/confused me a little, and it feels worse using it in the CLI context now: it sounds like the user cancelled the pipeline, when it's really that the pipeline stoped early because of errors.

Is this usage idiomatic for C# in general when hitting an "early return" as opposed to a parallel cancellation request?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above answer

Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last batch!


public Task<ResolutionResult> Resolution => Compilation.Resolution;

public void Start() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth raising an explicit error if Start is called more than once?

new DafnyProgramVerifier(factory.CreateLogger<DafnyProgramVerifier>()), engine, input);
}

public async Task VerifyAllAndPrintSummary() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, can we raise an error if Start wasn't called yet? Otherwise I assume you just get deadlock.

Copy link
Member Author

@keyboardDrummer keyboardDrummer Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory you can first call VerifyAllAndPrintSummary but not await the result, then call Start, and then await the task, although I don't see why you would do that.

Comment on lines +191 to +194
foreach (var canVerify in orderedCanVerifies) {
canVerifyResults[canVerify] = new CliCanVerifyResults();
await Compilation.VerifyCanVerify(canVerify, false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more efficient/cleaner to do await Task.WhenAll(...) instead in general, rather than awaiting inside a loop like this?

Copy link
Member Author

@keyboardDrummer keyboardDrummer Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to await before I do the next call, to ensure that the previous verification work has finished being queued. Otherwise, the order in which things get verified becomes non-deterministic.

this.logger = logger;
this.innerLogger = innerLogger;
this.telemetryPublisher = telemetryPublisher;
}

private readonly ResolutionCache resolutionCache = new();
public CompilationUnit ResolveSymbols(DafnyProject project, Program program, CancellationToken cancellationToken) {
public void ResolveSymbols(Compilation compilation, Program program, CancellationToken cancellationToken) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels redundant to be passing in a program separately from the compilation here (and probably other similar places). Does it have to be the same as compilation.programAfterParsing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mutating operation, and program is the program to mutate. It's a bit much for this code to assume which Program field that Compilation exposes it may mutate. For example, compilation.programAfterParsing would be the wrong one to mutate ! That's an instance of Program which is just the parsed program and which won't be changed afterwards.

var compilationUnit = resolutionResult.HasErrors
? new CompilationUnit(input.Project.Uri, resolutionResult.ResolvedProgram)
: new SymbolDeclarationResolver(logger, cancellationToken).ProcessProgram(input.Project.Uri, resolutionResult.ResolvedProgram);
// telemetryPublisher.PublishTime("LegacyServerResolution", project.Uri.ToString(), DateTime.Now - beforeLegacyServerResolution); TODO
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

@@ -139,4 +161,379 @@ public record IdeState(
public IEnumerable<Uri> GetDiagnosticUris() {
return StaticDiagnostics.Keys.Concat(VerificationResults.Keys);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume all the code from this point on is relocated rather than brand new? If not, how's the testing coverage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is relocated, yes

@@ -111,7 +111,7 @@
// CHECK: ProofDependencies.dfy\(337,12\)-\(337,16\): requires clause
// CHECK: ProofDependencies.dfy\(338,11\)-\(338,15\): ensures clause
// CHECK: ProofDependencies.dfy\(341,3\)-\(341,39\): function call result
// CHECK: ProofDependencies.dfy\(341,3\)-\(341,3\): function precondition satisfied
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not seeing why this location changed, was it actually incorrect before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we have enough consistency in our reporting to say what's correct or not, but previously it reported a "range" which was just twice the starting position of the call, and now the range is the entire call expression.

Comment on lines +1 to +4
- Fix: verification in the IDE no longer fails for iterators
- Fix: the IDE now provides feedback when verification fails to run, for example due to a bad solver path
- Fix: let the IDE correctly use the solver-path option when it's specified in a project file
- Feat: improve the order of verification diagnostics emitted by the Dafny CLI, so that they now always follow the line order of the program.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Member

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of work but well worth it, thanks!

@keyboardDrummer keyboardDrummer merged commit 9d85655 into dafny-lang:master Jan 23, 2024
20 checks passed
@keyboardDrummer keyboardDrummer deleted the mergedCLiAndServer branch January 23, 2024 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants