Skip to content

Commit bd2d4ec

Browse files
fix: Select alternative default calc operator more carefully (#3963)
Fixes #3962 <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> --------- Co-authored-by: Remy Willems <[email protected]>
1 parent e803796 commit bd2d4ec

File tree

5 files changed

+96
-34
lines changed

5 files changed

+96
-34
lines changed

Source/DafnyCore/AST/Statements/CalcStmt.cs

+57-14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Diagnostics.Contracts;
44
using System.Linq;
5+
using JetBrains.Annotations;
56

67
namespace Microsoft.Dafny;
78

@@ -11,13 +12,13 @@ public abstract class CalcOp {
1112
/// Resulting operator "x op z" if "x this y" and "y other z".
1213
/// Returns null if this and other are incompatible.
1314
/// </summary>
14-
[Pure]
15+
[System.Diagnostics.Contracts.Pure]
1516
public abstract CalcOp ResultOp(CalcOp other);
1617

1718
/// <summary>
1819
/// Returns an expression "line0 this line1".
1920
/// </summary>
20-
[Pure]
21+
[System.Diagnostics.Contracts.Pure]
2122
public abstract Expression StepExpr(Expression line0, Expression line1);
2223
}
2324

@@ -32,7 +33,7 @@ void ObjectInvariant() {
3233
/// <summary>
3334
/// Is op a valid calculation operator?
3435
/// </summary>
35-
[Pure]
36+
[System.Diagnostics.Contracts.Pure]
3637
public static bool ValidOp(BinaryExpr.Opcode op) {
3738
return
3839
op == BinaryExpr.Opcode.Eq || op == BinaryExpr.Opcode.Neq
@@ -44,7 +45,7 @@ public static bool ValidOp(BinaryExpr.Opcode op) {
4445
/// <summary>
4546
/// Is op a valid operator only for Boolean lines?
4647
/// </summary>
47-
[Pure]
48+
[System.Diagnostics.Contracts.Pure]
4849
public static bool LogicOp(BinaryExpr.Opcode op) {
4950
return op == BinaryExpr.Opcode.Iff || op == BinaryExpr.Opcode.Imp || op == BinaryExpr.Opcode.Exp;
5051
}
@@ -84,7 +85,7 @@ private bool Subsumes(BinaryCalcOp other) {
8485
public override CalcOp ResultOp(CalcOp other) {
8586
if (other is BinaryCalcOp) {
8687
var o = (BinaryCalcOp)other;
87-
if (this.Subsumes(o)) {
88+
if (Subsumes(o)) {
8889
return this;
8990
} else if (o.Subsumes(this)) {
9091
return other;
@@ -153,6 +154,48 @@ public override string ToString() {
153154

154155
}
155156

157+
/// <summary>
158+
/// This method infers a default operator to be used between the steps.
159+
/// Usually, we'd use == as the default operator. However, if the calculation
160+
/// begins or ends with a boolean literal, then we can do better by selecting ==>
161+
/// or <==. Also, if the calculation begins or ends with an empty set, then we can
162+
/// do better by selecting <= or >=.
163+
/// Note, these alternative operators are chosen only if they don't clash with something
164+
/// supplied by the user.
165+
/// If the rules come up with a good inferred default operator, then that default operator
166+
/// is returned; otherwise, null is returned.
167+
/// </summary>
168+
[CanBeNull]
169+
public CalcOp GetInferredDefaultOp() {
170+
CalcOp alternativeOp = null;
171+
if (Lines.Count == 0) {
172+
return null;
173+
}
174+
175+
if (Expression.IsBoolLiteral(Lines.First(), out var firstOperatorIsBoolLiteral)) {
176+
alternativeOp = new BinaryCalcOp(firstOperatorIsBoolLiteral ? BinaryExpr.Opcode.Imp : BinaryExpr.Opcode.Exp);
177+
} else if (Expression.IsBoolLiteral(Lines.Last(), out var lastOperatorIsBoolLiteral)) {
178+
alternativeOp = new BinaryCalcOp(lastOperatorIsBoolLiteral ? BinaryExpr.Opcode.Exp : BinaryExpr.Opcode.Imp);
179+
} else if (Expression.IsEmptySetOrMultiset(Lines.First())) {
180+
alternativeOp = new BinaryCalcOp(BinaryExpr.Opcode.Ge);
181+
} else if (Expression.IsEmptySetOrMultiset(Lines.Last())) {
182+
alternativeOp = new BinaryCalcOp(BinaryExpr.Opcode.Le);
183+
} else {
184+
return null;
185+
}
186+
187+
// Check that the alternative operator is compatible with anything supplied by the user.
188+
var resultOp = alternativeOp;
189+
foreach (var stepOp in StepOps.Where(stepOp => stepOp != null)) {
190+
resultOp = resultOp.ResultOp(stepOp);
191+
if (resultOp == null) {
192+
// no go
193+
return null;
194+
}
195+
}
196+
return alternativeOp;
197+
}
198+
156199
public readonly List<Expression> Lines; // Last line is dummy, in order to form a proper step with the dangling hint
157200
public readonly List<BlockStmt> Hints; // Hints[i] comes after line i; block statement is used as a container for multiple sub-hints
158201
public readonly CalcOp UserSuppliedOp; // may be null, if omitted by the user
@@ -189,13 +232,13 @@ public CalcStmt(RangeToken rangeToken, CalcOp userSuppliedOp, List<Expression> l
189232
Contract.Requires(cce.NonNullElements(hints));
190233
Contract.Requires(hints.Count == Math.Max(lines.Count - 1, 0));
191234
Contract.Requires(stepOps.Count == hints.Count);
192-
this.UserSuppliedOp = userSuppliedOp;
193-
this.Lines = lines;
194-
this.Hints = hints;
235+
UserSuppliedOp = userSuppliedOp;
236+
Lines = lines;
237+
Hints = hints;
195238
Steps = new List<Expression>();
196-
this.StepOps = stepOps;
197-
this.Result = null;
198-
this.Attributes = attrs;
239+
StepOps = stepOps;
240+
Result = null;
241+
Attributes = attrs;
199242
}
200243

201244
public CalcStmt Clone(Cloner cloner) {
@@ -291,7 +334,7 @@ public bool SetIndent(int indentBefore, TokenNewIndentCollector formatter) {
291334
var inOrdinal = false;
292335
var innerCalcIndent = indentBefore + formatter.SpaceTab;
293336
var extraHintIndent = 0;
294-
var ownedTokens = this.OwnedTokens;
337+
var ownedTokens = OwnedTokens;
295338
// First phase: We get the alignment
296339
foreach (var token in ownedTokens) {
297340
if (formatter.SetIndentLabelTokens(token, indentBefore)) {
@@ -366,7 +409,7 @@ public bool SetIndent(int indentBefore, TokenNewIndentCollector formatter) {
366409
}
367410
}
368411

369-
foreach (var hint in this.Hints) {
412+
foreach (var hint in Hints) {
370413
// This block
371414
if (hint.Tok.pos != hint.EndToken.pos) {
372415
foreach (var hintStep in hint.Body) {
@@ -375,7 +418,7 @@ public bool SetIndent(int indentBefore, TokenNewIndentCollector formatter) {
375418
}
376419
}
377420

378-
foreach (var expression in this.Lines) {
421+
foreach (var expression in Lines) {
379422
formatter.SetIndentations(expression.StartToken, innerCalcIndent, innerCalcIndent, innerCalcIndent);
380423
}
381424

Source/DafnyCore/Resolver/NameResolutionAndTypeInference.cs

+1-20
Original file line numberDiff line numberDiff line change
@@ -4521,26 +4521,7 @@ public void ResolveStatement(Statement stmt, ResolutionContext resolutionContext
45214521
if (s.UserSuppliedOp != null) {
45224522
s.Op = s.UserSuppliedOp;
45234523
} else {
4524-
// Usually, we'd use == as the default main operator. However, if the calculation
4525-
// begins or ends with a boolean literal, then we can do better by selecting ==>
4526-
// or <==. Also, if the calculation begins or ends with an empty set, then we can
4527-
// do better by selecting <= or >=.
4528-
if (s.Lines.Count == 0) {
4529-
s.Op = CalcStmt.DefaultOp;
4530-
} else {
4531-
bool b;
4532-
if (Expression.IsBoolLiteral(s.Lines.First(), out b)) {
4533-
s.Op = new CalcStmt.BinaryCalcOp(b ? BinaryExpr.Opcode.Imp : BinaryExpr.Opcode.Exp);
4534-
} else if (Expression.IsBoolLiteral(s.Lines.Last(), out b)) {
4535-
s.Op = new CalcStmt.BinaryCalcOp(b ? BinaryExpr.Opcode.Exp : BinaryExpr.Opcode.Imp);
4536-
} else if (Expression.IsEmptySetOrMultiset(s.Lines.First())) {
4537-
s.Op = new CalcStmt.BinaryCalcOp(BinaryExpr.Opcode.Ge);
4538-
} else if (Expression.IsEmptySetOrMultiset(s.Lines.Last())) {
4539-
s.Op = new CalcStmt.BinaryCalcOp(BinaryExpr.Opcode.Le);
4540-
} else {
4541-
s.Op = CalcStmt.DefaultOp;
4542-
}
4543-
}
4524+
s.Op = s.GetInferredDefaultOp() ?? CalcStmt.DefaultOp;
45444525
reporter.Info(MessageSource.Resolver, s.Tok, s.Op.ToString());
45454526
}
45464527

Test/git-issues/git-issue-3962.dfy

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// RUN: %exits-with 4 %dafny /printTooltips "%s" > "%t"
2+
// RUN: %diff "%s.expect" "%t"
3+
4+
method Issues(a: bool, s: set) {
5+
// Each one of the following 8 calc statements used to cause a crash
6+
7+
if
8+
case true =>
9+
calc { a; <== false; }
10+
case true =>
11+
calc { true; <== a; }
12+
case true =>
13+
calc { a; ==> true; }
14+
case true =>
15+
calc { false; ==> a; }
16+
17+
case true =>
18+
calc { s; >= ({}); }
19+
case true =>
20+
calc { s; > ({}); } // error: cannot prove calc step
21+
case true =>
22+
calc { {}; <= s; }
23+
case true =>
24+
calc { {}; < s; } // error: cannot prove calc step
25+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
git-issue-3962.dfy(9,4): Info: ==
2+
git-issue-3962.dfy(11,4): Info: ==
3+
git-issue-3962.dfy(13,4): Info: ==
4+
git-issue-3962.dfy(15,4): Info: ==
5+
git-issue-3962.dfy(18,4): Info: ==
6+
git-issue-3962.dfy(20,4): Info: ==
7+
git-issue-3962.dfy(22,4): Info: ==
8+
git-issue-3962.dfy(24,4): Info: ==
9+
git-issue-3962.dfy(20,16): Error: the calculation step between the previous line and this line might not hold
10+
git-issue-3962.dfy(24,17): Error: the calculation step between the previous line and this line might not hold
11+
12+
Dafny program verifier finished with 0 verified, 2 errors

docs/dev/news/3963.fix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Select alternative default calc operator only if it doesn't clash with given step operators

0 commit comments

Comments
 (0)