Skip to content

Commit

Permalink
Support all option types in the project file (#4506)
Browse files Browse the repository at this point in the history
### Changes
- Support all option types in the project file by parsing the project
file options in the same way as the CLI parses them

### Testing
- Added a case that uses an enum option to an existing test

<small>By submitting this pull request, I confirm that my contribution
is made under the terms of the [MIT
license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small>
  • Loading branch information
keyboardDrummer committed Sep 12, 2023
1 parent f8b2cb6 commit 27eca33
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 57 deletions.
8 changes: 4 additions & 4 deletions Source/DafnyCore.Test/DafnyProjectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ public class DafnyProjectTest {

Assert.Equal(first, second);

first.Options.Add("k", new[] { 1, 2, 3 });
second.Options.Add("k", new[] { 1, 2, 3 });
first.Options.Add("k", "1, 2, 3");
second.Options.Add("k", "1, 2, 3");

Assert.Equal(first, second);

first.Options.Add("m", new[] { 1, 2, 3 });
second.Options.Add("m", new[] { 3, 2, 1 });
first.Options.Add("m", "1, 2, 3");
second.Options.Add("m", "3, 2, 1");
Assert.NotEqual(first, second);
}
}
9 changes: 2 additions & 7 deletions Source/DafnyCore/DooFile.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Binding;
using System.Configuration;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using DafnyCore.Generic;
using Microsoft.Dafny;
using Microsoft.Dafny.Auditor;
using Tomlyn;
using Tomlyn.Model;
using Type = System.Type;

namespace DafnyCore;

Expand Down Expand Up @@ -137,7 +132,7 @@ public class ManifestData {

object libraryValue = null;
if (Manifest.Options.TryGetValue(option.Name, out var manifestValue)) {
if (!DafnyProject.TryGetValueFromToml(Console.Out, null,
if (!TomlUtil.TryGetValueFromToml(Console.Out, null,
option.Name, option.ValueType, manifestValue, out libraryValue)) {
return false;
}
Expand Down
70 changes: 70 additions & 0 deletions Source/DafnyCore/Generic/TomlUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Tomlyn.Model;

namespace DafnyCore.Generic;

public static class TomlUtil {

public static bool TryGetValueFromToml(TextWriter errorWriter, string sourceDir, string tomlPath, Type type, object tomlValue, out object value) {
if (tomlValue == null) {
value = null;
return false;
}

if (type.IsAssignableFrom(typeof(List<string>))) {
return TryGetListValueFromToml<string>(errorWriter, sourceDir, tomlPath, (TomlArray)tomlValue, out value);
}
if (type.IsAssignableFrom(typeof(List<FileInfo>))) {
return TryGetListValueFromToml<FileInfo>(errorWriter, sourceDir, tomlPath, (TomlArray)tomlValue, out value);
}

if (tomlValue is string tomlString) {
if (type == typeof(FileInfo)) {
// Need to make sure relative paths are interpreted relative to the source of the value,
// not the current directory.
var fullPath = sourceDir != null ? Path.GetFullPath(tomlString, sourceDir) : tomlString;
value = new FileInfo(fullPath);
return true;
}

if (typeof(Enum).IsAssignableFrom(type)) {
try {
value = Enum.Parse(type, tomlString);
return true;
} catch (ArgumentException) {
value = null;
return false;
}
}
}

if (!type.IsInstanceOfType(tomlValue)) {
if (type == typeof(string)) {
value = tomlValue.ToString();
return true;
}
errorWriter.WriteLine(
$"Error: property '{tomlPath}' is of type '{tomlValue.GetType()}' but should be of type '{type}'");
value = null;
return false;
}

value = tomlValue;
return true;
}

private static bool TryGetListValueFromToml<T>(TextWriter errorWriter, string sourceDir, string tomlPath, TomlArray tomlValue, out object value) {
var success = true;
value = tomlValue.Select((e, i) => {
if (TryGetValueFromToml(errorWriter, sourceDir, $"{tomlPath}[{i}]", typeof(T), e, out var elementValue)) {
return (T)elementValue;
}
success = false;
return default(T);
}).ToList();
return success;
}
}
67 changes: 28 additions & 39 deletions Source/DafnyCore/Options/DafnyProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
using System.Collections;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DafnyCore.Options;
Expand Down Expand Up @@ -140,55 +143,41 @@ public class DafnyProject : IEquatable<DafnyProject> {
return false;
}

return TryGetValueFromToml(errorWriter, Path.GetDirectoryName(Uri.LocalPath), option.Name, option.ValueType, tomlValue, out value);
}

public static bool TryGetValueFromToml(TextWriter errorWriter, string sourceDir, string tomlPath, System.Type type, object tomlValue, out object value) {
if (tomlValue == null) {
var printTomlValue = PrintTomlOptionToCliValue(tomlValue, option);
var parseResult = option.Parse(new[] { option.Aliases.First(), printTomlValue });
if (parseResult.Errors.Any()) {
errorWriter.WriteLine($"Error: Could not parse value '{tomlValue}' for option '{option.Name}' that has type '{option.ValueType.Name}'");
value = null;
return false;
}
// By using the dynamic keyword, we can use the generic version of GetValueForOption which does type conversion,
// which is sadly not accessible without generics.
value = parseResult.GetValueForOption((dynamic)option);
return true;
}

if (type.IsAssignableFrom(typeof(List<string>))) {
return TryGetListValueFromToml<string>(errorWriter, sourceDir, tomlPath, (TomlArray)tomlValue, out value);
}
if (type.IsAssignableFrom(typeof(List<FileInfo>))) {
return TryGetListValueFromToml<FileInfo>(errorWriter, sourceDir, tomlPath, (TomlArray)tomlValue, out value);
}
string PrintTomlOptionToCliValue(object value, Option valueType) {
var projectDirectory = Path.GetDirectoryName(Uri.LocalPath);

if (type == typeof(FileInfo) && tomlValue is string tomlString) {
// Need to make sure relative paths are interpreted relative to the source of the value,
// not the current directory.
var fullPath = sourceDir != null ? Path.GetFullPath(tomlString, sourceDir) : tomlString;
value = new FileInfo(fullPath);
return true;
}
if (value is TomlArray array) {
if (valueType.ValueType.IsAssignableTo(typeof(IEnumerable<FileInfo>))) {
return string.Join(" ", array.Select(element => {
if (element is string elementString) {
return Path.GetFullPath(elementString, projectDirectory!);
}
if (!type.IsInstanceOfType(tomlValue)) {
if (type == typeof(string)) {
value = tomlValue.ToString();
return true;
return element.ToString();
}));
}
errorWriter.WriteLine(
$"Error: property '{tomlPath}' is of type '{tomlValue.GetType()}' but should be of type '{type}'");
value = null;
return false;

return string.Join(" ", array);
}

value = tomlValue;
return true;
}
if (value is string stringValue && valueType.ValueType == typeof(FileInfo)) {
value = Path.GetFullPath(stringValue, projectDirectory);
}

private static bool TryGetListValueFromToml<T>(TextWriter errorWriter, string sourceDir, string tomlPath, TomlArray tomlValue, out object value) {
var success = true;
value = tomlValue.Select((e, i) => {
if (TryGetValueFromToml(errorWriter, sourceDir, $"{tomlPath}[{i}]", typeof(T), e, out var elementValue)) {
return (T)elementValue;
}
success = false;
return default(T);
}).ToList();
return success;
return value.ToString();
}

public bool Equals(DafnyProject other) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class DafnyLanguageServerTestBase : LanguageProtocolTestBase {
}
}".TrimStart();

protected string SlowToVerifyNoLimit => SlowToVerify.Replace(" {:rlimit 100}", "");

protected readonly string NeverVerifies = @"
lemma {:neverVerify} HasNeverVerifyAttribute(p: nat, q: nat)
ensures true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class ProjectFilesTest : ClientBasedLanguageServerTest {
public async Task ProjectFileOverridesOptions() {
await SetUp(options => {
options.Set(Function.FunctionSyntaxOption, "3");
options.Set(CommonOptionBag.QuantifierSyntax, QuantifierSyntaxOptions.Version3);
options.Set(CommonOptionBag.WarnShadowing, true);
});
var source = @"
Expand All @@ -110,13 +111,18 @@ public class ProjectFilesTest : ClientBasedLanguageServerTest {
}
}
function Zoo(): set<(int,int)> {
set x: int | 0 <= x < 5, y | 0 <= y < 6 :: (x,y)
}
ghost function Bar(): int { 3 }".TrimStart();

var projectFileSource = @"
includes = [""**/*.dfy""]
[options]
warn-shadowing = false
quantifier-syntax = 4
function-syntax = 4";

var directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Expand All @@ -127,7 +133,9 @@ public class ProjectFilesTest : ClientBasedLanguageServerTest {
Assert.Single(diagnostics1); // Stops after parsing

await File.WriteAllTextAsync(Path.Combine(directory, DafnyProject.FileName), projectFileSource);
await CreateAndOpenTestDocument(source, Path.Combine(directory, "source.dfy"));
var sourceFile = await CreateAndOpenTestDocument(source, Path.Combine(directory, "source.dfy"));
var diagnostics2 = await GetLastDiagnostics(sourceFile, CancellationToken);
Assert.Empty(diagnostics2.Where(d => d.Severity == DiagnosticSeverity.Error));
await AssertNoDiagnosticsAreComing(CancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ method test()
method test() {
assert false;
}
".TrimStart() + SlowToVerify;
".TrimStart() + SlowToVerifyNoLimit;
await SetUp(options => options.Set(BoogieOptionBag.Cores, 1U));
var documentItem = CreateTestDocument(source, "ApplyChangeBeforeVerificationFinishes.dfy");
client.OpenDocument(documentItem);
Expand Down
2 changes: 1 addition & 1 deletion Test/cli/projectFile/projectFile.dfy.expect
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Warning: only Dafny project files named dfyconfig.toml are recognised by the Daf
Warning: option 'does-not-exist' that was specified in the project file, is not a valid Dafny option.

Dafny program verifier did not attempt verification
Error: property 'warn-shadowing' is of type 'System.Int64' but should be of type 'System.Boolean'
Error: Could not parse value '3' for option 'warn-shadowing' that has type 'Boolean'
input.dfy(6,8): Warning: Shadowed local-variable name: x
moreInput.dfy(6,8): Warning: Shadowed local-variable name: x

Expand Down
8 changes: 4 additions & 4 deletions docs/dev/news/4435.feat
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
The Dafny IDE now has improved behavior when working with a Dafny file that's part of a Dafny project. A Dafny file is part of a project if a `dfyconfig.toml` can be found somewhere in the file's path hierarchy, such as in the same folder or in the parent folder. A `dfyconfig.toml` can specify which Dafny options to use for that project, and can specify which Dafny files are part of the project. By default, the project will include all .dfy files reachable from the folder in which the `dfyconfig.toml` resides. Project related features of the IDE are:
- Whenever one file in the project is opened, diagnostics for all files in the Dafny project are shown. When including a file with errors that's part of the same project, the message "the included file contains errors" is no longer shown. Instead, the included file's errors are shown directly.
- If any file in the project in changed, diagnostics for all files in the project are updated. Without a project, changing an included file will not update diagnostics for the including file until the including file is also changed.
- If any file in the project is resolved, all files in the project are resolved. Opening a file in a project that's already resolved means the opened file is resolved instantly.
- The IDE's memory consumption stays the same regardless of how many files in a project are opened. Without a project, the IDE increases it's memory usage for each open file.
- If any file in the project is changed, diagnostics for all files in the project are updated. Without a project, changing an included file will not update diagnostics for the including file until the including file is also changed.
- The find references feature (also added in this release), works better in files that are part of a project, since only then can it find references that are inside files that include the current file.
- The assisted rename feature (also added in this release), only works for files that are part of a project.
- For files that are part of a project, the Dafny IDE does not require the use of include directives to work, and neither does the CLI if you run it using the project file. This means that Dafny projects can be used without use include directives for files inside the project. When using library files that are outside the project, it's still necessary to use includes.
- When using a project file, it is no longer necessary to use include directives. In the previous version of Dafny, it was already the case that the Dafny CLI, when passed a Dafny project file, does not require include directives to process the Dafny program. The same now holds for the Dafny IDE when working with Dafny files for which a project file can be found.
- If any file in the project is resolved, all files in the project are resolved. Opening a file in a project that's already resolved means the opened file is resolved instantly.
- The IDE's memory consumption stays the same regardless of how many files in a project are opened. Without a project, the IDE increases it's memory usage for each open file.

Try out the IDE's project support now by creating an empty `dfyconfig.toml` file in the root of your project repository.
1 change: 1 addition & 0 deletions docs/dev/news/4506.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support all types of options in the Dafny project file (dfyconfig.toml)

0 comments on commit 27eca33

Please sign in to comment.