Skip to content

Commit 90679c3

Browse files
Dargonesrobin-aws
andauthored
Counterexamples from Command Line (#1511)
* Move position extraction to DafnyModelState * Move DafnyModel parsing to DafnyModel class * Add /extractCounterExample option * Add a lit Test * Update option description * Change source location reporting Co-authored-by: Robin Salkeld <[email protected]>
1 parent 72ed4e7 commit 90679c3

File tree

9 files changed

+148
-41
lines changed

9 files changed

+148
-41
lines changed

Source/Dafny/DafnyOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public enum IncludesModes { None, Immediate, Transitive }
102102
public bool ShowSnippets = false;
103103
public bool WarningsAsErrors = false;
104104
[CanBeNull] private TestGenerationOptions testGenOptions = null;
105+
public bool ExtractCounterExample = false;
105106

106107
public virtual TestGenerationOptions TestGenOptions =>
107108
testGenOptions ??= new TestGenerationOptions();
@@ -415,6 +416,10 @@ protected override bool ParseOption(string name, Bpl.CommandLineOptionEngine.Com
415416
case "warningsAsErrors":
416417
WarningsAsErrors = true;
417418
return true;
419+
420+
case "extractCounterExample":
421+
ExtractCounterExample = true;
422+
return true;
418423
}
419424

420425
// Unless this is an option for test generation, defer to superclass
@@ -867,6 +872,10 @@ or type definitions during translation.
867872
Read standard input and treat it as an input .dfy file.
868873
/warningsAsErrors
869874
Treat warnings as errors.
875+
/extractCounterExample
876+
If verification fails, report a detailed counterexample for the first
877+
failing assertion. Requires specifying the /mv option as well as
878+
/proverOpt:0:model_compress=false and /proverOpt:0:model.completion=true.
870879
{TestGenOptions.Help}
871880
872881
Dafny generally accepts Boogie options and passes these on to Boogie. However,

Source/DafnyDriver/DafnyDriver.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//---------------------------------------------------------------------------------------------
1212

1313
using System.Security;
14+
using DafnyServer.CounterExampleGeneration;
1415
using DafnyTestGeneration;
1516

1617
namespace Microsoft.Dafny {
@@ -214,6 +215,12 @@ public static CommandLineArgumentsResult ProcessCommandLineArguments(string[] ar
214215
"*** Error: Only one .dfy file can be specified for testing");
215216
return CommandLineArgumentsResult.PREPROCESSING_ERROR;
216217
}
218+
219+
if (DafnyOptions.O.ExtractCounterExample && DafnyOptions.O.ModelViewFile == null) {
220+
ExecutionEngine.printer.ErrorWriteLine(Console.Out,
221+
"*** Error: ModelView file must be specified when attempting counterexample extraction");
222+
return CommandLineArgumentsResult.PREPROCESSING_ERROR;
223+
}
217224
return CommandLineArgumentsResult.OK;
218225
}
219226

@@ -290,9 +297,32 @@ static ExitValue ProcessFiles(IList<DafnyFile/*!*/>/*!*/ dafnyFiles, ReadOnlyCol
290297
if (err == null && dafnyProgram != null && DafnyOptions.O.PrintFunctionCallGraph) {
291298
Util.PrintFunctionCallGraph(dafnyProgram);
292299
}
300+
if (dafnyProgram != null && DafnyOptions.O.ExtractCounterExample && exitValue == ExitValue.VERIFICATION_ERROR) {
301+
PrintCounterexample(DafnyOptions.O.ModelViewFile);
302+
}
293303
return exitValue;
294304
}
295305

306+
/// <summary>
307+
/// Extract the counterexample corresponding to the first failing
308+
/// assertion and print it to the console
309+
/// </summary>
310+
/// <param name="modelViewFile"> Name of the file from which to read
311+
/// the counterexample </param>
312+
private static void PrintCounterexample(string modelViewFile) {
313+
var model = DafnyModel.ExtractModel(File.ReadAllText(modelViewFile));
314+
Console.WriteLine("Counterexample for first failing assertion: ");
315+
foreach (var state in model.States.Where(state => !state.IsInitialState)) {
316+
Console.WriteLine(state.FullStateName + ":");
317+
var vars = state.ExpandedVariableSet(-1);
318+
foreach (var variable in vars) {
319+
Console.WriteLine($"\t{variable.ShortName} : " +
320+
$"{variable.Type.InDafnyFormat()} = " +
321+
$"{variable.Value}");
322+
}
323+
}
324+
}
325+
296326
private static string BoogieProgramSuffix(string printFile, string suffix) {
297327
var baseName = Path.GetFileNameWithoutExtension(printFile);
298328
var dirName = Path.GetDirectoryName(printFile);

Source/DafnyDriver/DafnyDriver.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<ProjectReference Include="..\Dafny\DafnyPipeline.csproj" />
1919
<ProjectReference Include="..\DafnyTestGeneration\DafnyTestGeneration.csproj" />
20+
<ProjectReference Include="..\DafnyServer\DafnyServer.csproj" />
2021
</ItemGroup>
2122

2223
<ItemGroup>

Source/DafnyLanguageServer/Handlers/Custom/DafnyCounterExampleHandler.cs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.Dafny.LanguageServer.Workspace;
55
using Microsoft.Extensions.Logging;
66
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
7-
using System;
87
using System.Collections.Generic;
98
using System.IO;
109
using System.Linq;
@@ -76,39 +75,19 @@ private IEnumerable<CounterExampleItem> GetCounterExamples(DafnyModel model) {
7675
return model.States
7776
.WithCancellation(cancellationToken)
7877
.OfType<DafnyModelState>()
79-
.Where(state => !IsInitialState(state))
78+
.Where(state => !state.IsInitialState)
8079
.Select(GetCounterExample);
8180
}
8281

83-
private static bool IsInitialState(DafnyModelState state) {
84-
return state.ShortenedStateName.Equals(InitialStateName);
85-
}
86-
8782
private CounterExampleItem GetCounterExample(DafnyModelState state) {
83+
HashSet<DafnyModelVariable> vars = state.ExpandedVariableSet(counterExampleDepth);
8884
return new(
89-
GetPositionFromInitialState(state),
90-
GetVariablesFromState(state, counterExampleDepth)
91-
);
92-
}
93-
94-
private static Position GetPositionFromInitialState(DafnyModelState state) {
95-
var match = StatePositionRegex.Match(state.ShortenedStateName);
96-
if (!match.Success) {
97-
throw new ArgumentException($"state does not contain position: {state.ShortenedStateName}");
98-
}
99-
// Note: lines in a model start with 1, characters/columns with 0.
100-
return new Position(
101-
int.Parse(match.Groups["line"].Value) - 1,
102-
int.Parse(match.Groups["character"].Value)
103-
);
104-
}
105-
106-
private IDictionary<string, string> GetVariablesFromState(DafnyModelState state, int maxDepth) {
107-
HashSet<DafnyModelVariable> vars = state.ExpandedVariableSet(maxDepth);
108-
return vars.WithCancellation(cancellationToken).ToDictionary(
85+
new Position(state.GetLineId() - 1, state.GetCharId()),
86+
vars.WithCancellation(cancellationToken).ToDictionary(
10987
variable => variable.ShortName + ":" + variable.Type.InDafnyFormat(),
11088
variable => variable.Value
111-
);
89+
)
90+
);
11291
}
11392
}
11493
}

Source/DafnyServer/CounterExampleGeneration/DafnyModel.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics.Contracts;
7+
using System.IO;
78
using System.Linq;
89
using System.Text.RegularExpressions;
910
using Microsoft.Boogie;
@@ -80,6 +81,19 @@ public DafnyModel(Model model) {
8081
}
8182
}
8283

84+
/// <summary>
85+
/// Extract and parse the first Dafny model recorded in the model view file.
86+
/// </summary>
87+
public static DafnyModel ExtractModel(string mv) {
88+
const string begin = "*** MODEL";
89+
const string end = "*** END_MODEL";
90+
var beginIndex = mv.IndexOf(begin, StringComparison.Ordinal);
91+
var endIndex = mv.IndexOf(end, StringComparison.Ordinal);
92+
var modelString = mv.Substring(beginIndex, endIndex + end.Length - beginIndex);
93+
var model = Model.ParseModels(new StringReader(modelString)).First();
94+
return new DafnyModel(model);
95+
}
96+
8397
/// <summary>
8498
/// Collect the array dimensions from the various array.Length functions,
8599
/// and collect all known datatype values

Source/DafnyServer/CounterExampleGeneration/DafnyModelState.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text.RegularExpressions;
78
using Microsoft.Boogie;
89

910
namespace DafnyServer.CounterExampleGeneration {
@@ -24,6 +25,12 @@ public class DafnyModelState {
2425
private readonly Dictionary<Model.Element, DafnyModelVariable> varMap;
2526
private readonly Dictionary<string, int> varNamesMap;
2627

28+
private const string InitialStateName = "<initial>";
29+
private static Regex statePositionRegex = new(
30+
@".*\((?<line>\d+),(?<character>\d+)\)",
31+
RegexOptions.IgnoreCase | RegexOptions.Singleline
32+
);
33+
2734
public DafnyModelState(DafnyModel model, Model.CapturedState state) {
2835
Model = model;
2936
State = state;
@@ -97,6 +104,24 @@ public bool VarNameIsShared(string name) {
97104

98105
public string ShortenedStateName => ShortenName(State.Name, 20);
99106

107+
public bool IsInitialState => FullStateName.Equals(InitialStateName);
108+
109+
public int GetLineId() {
110+
var match = statePositionRegex.Match(ShortenedStateName);
111+
if (!match.Success) {
112+
throw new ArgumentException($"state does not contain position: {ShortenedStateName}");
113+
}
114+
return int.Parse(match.Groups["line"].Value);
115+
}
116+
117+
public int GetCharId() {
118+
var match = statePositionRegex.Match(ShortenedStateName);
119+
if (!match.Success) {
120+
throw new ArgumentException($"state does not contain position: {ShortenedStateName}");
121+
}
122+
return int.Parse(match.Groups["character"].Value);
123+
}
124+
100125
/// <summary>
101126
/// Initialize the vars list, which stores all variables relevant to
102127
/// the counterexample except for Skolem constants

Source/DafnyTestGeneration/TestMethod.cs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public TestMethod(DafnyInfo dafnyInfo, string log) {
3535
DafnyInfo = dafnyInfo;
3636
var typeNames = ExtractPrintedInfo(log, "Types | ");
3737
var argumentNames = ExtractPrintedInfo(log, "Impl | ");
38-
var dafnyModel = ExtractModel(log);
38+
var dafnyModel = DafnyModel.ExtractModel(log);
3939
MethodName = Utils.GetDafnyMethodName(argumentNames.First());
4040
argumentNames.RemoveAt(0);
4141
RegisterReservedValues(dafnyModel.Model);
@@ -314,19 +314,6 @@ private static List<string> ExtractPrintedInfo(string log, string prefix) {
314314
return new List<string>();
315315
}
316316

317-
/// <summary>
318-
/// Extract and parse the first Dafny model recorded in the log file.
319-
/// </summary>
320-
private static DafnyModel ExtractModel(string log) {
321-
const string begin = "*** MODEL";
322-
const string end = "*** END_MODEL";
323-
var beginIndex = log.IndexOf(begin, StringComparison.Ordinal);
324-
var endIndex = log.IndexOf(end, StringComparison.Ordinal);
325-
var modelString = log.Substring(beginIndex, endIndex + end.Length - beginIndex);
326-
var model = Model.ParseModels(new StringReader(modelString)).First();
327-
return new DafnyModel(model);
328-
}
329-
330317
/// <summary> Return the test method as a list of lines of code </summary>
331318
private List<string> TestMethodLines() {
332319

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// RUN: %dafny /compile:0 /proverOpt:O:model_compress=false /proverOpt:O:model.completion=true /warnShadowing /extractCounterExample /errorTrace:0 /mv:%t.model "%s" > "%t"
2+
// RUN: %diff "%s.expect" "%t"
3+
4+
// The following method performs string equality comparison whereas its
5+
// specification states that it must perform pattern matching (the difference
6+
// being that the '?' metacharacter in the pattern is supposed to match
7+
// any character in the string).
8+
module Patterns {
9+
10+
class Simple {
11+
12+
var p:string;
13+
14+
method Match(s: string) returns (b: bool)
15+
requires |p| == 1 // this is to make counterexample deterministic
16+
requires |p| == |s|
17+
ensures b <==> forall n :: 0 <= n < |s| ==>
18+
s[n] == p[n] ||
19+
p[n] == '?'
20+
{
21+
var i := 0;
22+
while (i < |s|)
23+
invariant i <= |s|
24+
invariant forall n :: 0 <= n < i ==>
25+
s[n] == p[n] ||
26+
p[n] == '?'
27+
{
28+
if (s[i] != p[i]) { // must add && (p[i] != '?') to verify
29+
return false;
30+
}
31+
i := i + 1;
32+
}
33+
return true;
34+
}
35+
}
36+
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
counterexample_commandline.dfy(29,20): Error: A postcondition might not hold on this return path.
2+
counterexample_commandline.dfy(17,22): Related location: This is the postcondition that might not hold.
3+
4+
Dafny program verifier finished with 1 verified, 1 error
5+
Counterexample for first failing assertion:
6+
counterexample_commandline.dfy(20,8): initial state:
7+
s : seq<char> = [?#0]
8+
this : Patterns.Simple? = (p := @2)
9+
@2 : seq<char> = ['?']
10+
counterexample_commandline.dfy(21,22):
11+
s : seq<char> = [?#0]
12+
this : Patterns.Simple? = (p := @2)
13+
i : int = 0
14+
@2 : seq<char> = ['?']
15+
counterexample_commandline.dfy(22,12): after some loop iterations:
16+
s : seq<char> = [?#0]
17+
this : Patterns.Simple? = (p := @2)
18+
i : int = 0
19+
@2 : seq<char> = ['?']
20+
counterexample_commandline.dfy(29,32):
21+
s : seq<char> = [?#0]
22+
this : Patterns.Simple? = (p := @2)
23+
i : int = 0
24+
b : bool = false
25+
@2 : seq<char> = ['?']

0 commit comments

Comments
 (0)