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

Improve Z3 detection #3233

Merged
merged 14 commits into from
Jan 9, 2023
Merged
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
13 changes: 13 additions & 0 deletions Source/DafnyCore/DafnyMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ private static string ParseFile(DafnyFile dafnyFile, Include include, ModuleDecl
string moduleName,
Microsoft.Boogie.Program boogieProgram, string programId) {
var moduleId = (programId ?? "main_program_id") + "_" + moduleName;
var z3NotFoundMessage = @"
Z3 not found. Please either provide a path to the `z3` executable using
the `--solver-path <path>` option, manually place the `z3` directory
next to the `dafny` executable you are using (this directory should
contain `bin/z3` or `bin/z3.exe`), or set the PATH environment variable
to also include a directory containing the `z3` executable.
";

var proverPath = DafnyOptions.O.ProverOptions.Find(o => o.StartsWith("PROVER_PATH="));
if (proverPath is null && DafnyOptions.O.Verify) {
Console.WriteLine(z3NotFoundMessage);
return (PipelineOutcome.FatalError, new PipelineStatistics());
}

string bplFilename;
if (DafnyOptions.O.PrintFile != null) {
Expand Down
93 changes: 75 additions & 18 deletions Source/DafnyCore/DafnyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.CommandLine;
using System.CommandLine.Binding;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Linq;
using System.Diagnostics.Contracts;
using System.IO;
Expand Down Expand Up @@ -813,8 +814,10 @@ public void ApplyDefaultOptionsWithoutSettingsDefault() {

// expand macros in filenames, now that LogPrefix is fully determined

SetZ3ExecutablePath();
SetZ3Options();
if (!ProverOptions.Any(x => x.StartsWith("SOLVER=") && !x.EndsWith("=z3"))) {
var z3Version = SetZ3ExecutablePath();
SetZ3Options(z3Version);
}

// Ask Boogie to perform abstract interpretation
UseAbstractInterpretation = true;
Expand Down Expand Up @@ -1057,35 +1060,85 @@ in such cases in the verifier being faster.

/// <summary>
/// Dafny releases come with their own copy of Z3, to save users the trouble of having to install extra dependencies.
/// For this to work, Dafny looks for Z3 at the location where it is put by our release script (i.e., z3/bin/z3[.exe]).
/// If Z3 is not found there, Dafny relies on Boogie to locate Z3 (which also supports setting a path explicitly on the command line).
/// Developers (and people getting Dafny from source) need to install an appropriate version of Z3 themselves.
/// For this to work, Dafny first tries any prover path explicitly provided by the user, then looks for for the copy
/// distributed with Dafny, and finally looks in any directory in the system PATH environment variable.
/// </summary>
private void SetZ3ExecutablePath() {
private Version SetZ3ExecutablePath() {
string confirmedProverPath = null;

// Try an explicitly provided prover path, if there is one.
var pp = "PROVER_PATH=";
var proverPathOption = ProverOptions.Find(o => o.StartsWith(pp));
if (proverPathOption != null) {
var proverPath = proverPathOption.Substring(pp.Length);
// Boogie will perform the ultimate test to see if "proverPath" is real--it will attempt to run it.
// However, by at least checking if the file exists, we can produce a better error message in common scenarios.
// Unfortunately, there doesn't seem to be a portable way of checking whether it's executable.
if (!File.Exists(proverPath)) {
throw new Bpl.ProverException($"Requested prover not found: '{proverPath}'");
return null;
}
} else {
var platform = (int)System.Environment.OSVersion.Platform;

var isUnix = platform == 4 || platform == 6 || platform == 128;
confirmedProverPath = proverPath;
}

var z3binName = isUnix ? "z3" : "z3.exe";
var platform = System.Environment.OSVersion.Platform;
var isUnix = platform == PlatformID.Unix || platform == PlatformID.MacOSX;
var z3binName = isUnix ? "z3" : "z3.exe";

// Next, try looking in a directory relative to Dafny itself.
if (confirmedProverPath is null) {
var dafnyBinDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var z3BinDir = Path.Combine(dafnyBinDir, "z3", "bin");
var z3BinPath = Path.Combine(z3BinDir, z3binName);
var z3BinPath = Path.Combine(dafnyBinDir, "z3", "bin", z3binName);

if (File.Exists(z3BinPath)) {
// Let's use z3BinPath
ProverOptions.Add($"{pp}{z3BinPath}");
confirmedProverPath = z3BinPath;
}
}

// Finally, try looking in the system PATH variable.
if (confirmedProverPath is null) {
confirmedProverPath = System.Environment
.GetEnvironmentVariable("PATH")?
.Split(isUnix ? ':' : ';')
.Select(s => Path.Combine(s, z3binName))
.FirstOrDefault(File.Exists);
}

if (confirmedProverPath is not null) {
ProverOptions.Add($"{pp}{confirmedProverPath}");
return GetZ3Version(confirmedProverPath);
}

return null;
}

private static readonly Regex Z3VersionRegex = new Regex(@"Z3 version (?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)");

[CanBeNull]
public static Version GetZ3Version(string proverPath) {
var z3Process = new ProcessStartInfo(proverPath, "-version") {
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
};
var run = Process.Start(z3Process);
if (run == null) {
return null;
}

var actualOutput = run.StandardOutput.ReadToEnd();
run.WaitForExit();
var versionMatch = Z3VersionRegex.Match(actualOutput);
if (!versionMatch.Success) {
// Might be another solver.
return null;
}

var major = int.Parse(versionMatch.Groups["major"].Value);
var minor = int.Parse(versionMatch.Groups["minor"].Value);
var patch = int.Parse(versionMatch.Groups["patch"].Value);
return new Version(major, minor, patch);
}

// Set a Z3 option, but only if it is not overwriting an existing option.
Expand All @@ -1095,7 +1148,7 @@ private void SetZ3Option(string name, string value) {
}
}

private void SetZ3Options() {
private void SetZ3Options(Version z3Version) {
// Boogie sets the following Z3 options by default:
// smt.mbqi = false
// model.compact = false
Expand All @@ -1105,10 +1158,14 @@ private void SetZ3Options() {
// Boogie also used to set the following options, but does not anymore.
SetZ3Option("auto_config", "false");
SetZ3Option("type_check", "true");
SetZ3Option("smt.case_split", "3"); // TODO: try removing
SetZ3Option("smt.qi.eager_threshold", "100"); // TODO: try lowering
SetZ3Option("smt.delay_units", "true");
SetZ3Option("smt.arith.solver", "2");

if (z3Version is not null && z3Version.CompareTo(new Version(4, 8, 5)) <= 0) {
// These options tend to help with Z3 4.8.5 but hurt with newer versions of Z3.
SetZ3Option("smt.case_split", "3");
SetZ3Option("smt.arith.solver", "2");
Copy link
Member

Choose a reason for hiding this comment

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

How much are they hurting? Can you please describe the problem in a separate issue?

Also consider describing a range of versions in the conditional, rather than an exact version. Perhaps refactor this:
https://github.com/dafny-lang/dafny/pull/3233/files#diff-b3d55f76ae78d57880964c42ed16a37e9e6a8a3a7ca4022cb96367bccda8d667R80

Copy link
Member Author

Choose a reason for hiding this comment

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

My experience is that any version newer than 4.8.5 works better with those options removed. I considered adding code to produce an error if it detects a version older than 4.8.5. If I do that, then the conditional above really should just be for a single version. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

If you add this error if we detect a version older than 4.8.5, then yes that's fine. Where would you add this error?
If you do not add it in this PR, then I would prefer to see a version "<=" instead of just "=".

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 went for changing it to <= for now, since it's a little awkward to a signal an error from that location and I haven't actually seen evidence that versions < 4.8.5 don't work. Before we release 4.0 we may want to test those early versions and have a more complete strategy for how to handle the entire range of available versions.

}

if (DisableNLarith || 3 <= ArithMode) {
SetZ3Option("smt.arith.nl", "false");
Expand Down
6 changes: 6 additions & 0 deletions Source/DafnyDriver.Test/DafnyDriver.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@
<ProjectReference Include="..\DafnyDriver\DafnyDriver.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="..\..\Binaries\z3\**\*.*" LinkBase="z3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
28 changes: 7 additions & 21 deletions Source/DafnyLanguageServer/DafnyLanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,19 @@ private static void PublishSolverPath(ILanguageServer server) {
}

private static void HandleZ3Version(ITelemetryPublisher telemetryPublisher, SMTLibSolverOptions proverOptions) {
var z3Process = new ProcessStartInfo(proverOptions.ProverPath, "-version") {
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
};
var run = Process.Start(z3Process);
if (run == null) {
return;
}

var actualOutput = run.StandardOutput.ReadToEnd();
run.WaitForExit();
var versionMatch = Z3VersionRegex.Match(actualOutput);
if (!versionMatch.Success) {
// Might be another solver.
var z3Version = DafnyOptions.GetZ3Version(proverOptions.ProverPath);
if (z3Version is null) {
return;
}

telemetryPublisher.PublishZ3Version(versionMatch.Value);
var major = int.Parse(versionMatch.Groups["major"].Value);
var minor = int.Parse(versionMatch.Groups["minor"].Value);
var patch = int.Parse(versionMatch.Groups["patch"].Value);
var major = z3Version.Major;
var minor = z3Version.Minor;
var patch = z3Version.Build;
if (major <= 4 && (major < 4 || minor <= 8) && (minor < 8 || patch <= 6)) {
return;
}

telemetryPublisher.PublishZ3Version("Z3 version {major}.{minor}.{patch}");

var toReplace = "O:model_compress=false";
var i = DafnyOptions.O.ProverOptions.IndexOf(toReplace);
if (i == -1) {
Expand Down
6 changes: 6 additions & 0 deletions Source/TestDafny/TestDafny.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
<ProjectReference Include="..\XUnitExtensions\XUnitExtensions.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="..\..\Binaries\z3\**\*.*" LinkBase="z3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions Test/cli/badProverPath.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// RUN: %exits-with 4 %baredafny verify %args --solver-path=doesNotExist "%s" > "%t"
// RUN: %OutputCheck --file-to-check "%t" "%s"
// CHECK: Fatal Error: ProverException: Cannot find specified prover:.*
method m() {
assert 1 + 1 == 2;
}
2 changes: 1 addition & 1 deletion Test/git-issues/git-issue-401a.dfy.expect
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*** ProverException: Requested prover not found: 'Output/binz/z3'
Fatal Error: ProverException: Cannot find specified prover: Output/binz/z3