-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathExprEquiv.fs
222 lines (210 loc) · 7.59 KB
/
ExprEquiv.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/// <summary>
/// Heavyweight expression equivalence checks.
///
/// <para>
/// Unlike normal expression checks, these produce <c>Equiv</c>
/// values, which must be passed to <c>runEquiv</c> to check.
/// </para>
///
/// <para>
/// These are farmed out to Z3. As such, they are not likely to
/// execute quickly. Use with caution.
/// </para>
/// </summary>
module Starling.Core.ExprEquiv
open Microsoft
open Starling.Core.Expr
open Starling.Core.Var
open Starling.Core.Z3
/// <summary>
/// Type for equivalence checks.
/// </summary>
type Equiv<'var> = ('var -> string) -> Z3.Context -> Z3.BoolExpr
/// <summary>
/// Runs an equivalence check.
/// </summary>
/// <param name="toVar">
/// A function converting variables in the check to <c>Var</c>s.
/// The vars must be unique to their origin variables across the
/// equivalence.
/// </param>
/// <param name="e">
/// The equivalence check to run.
/// </param>
/// <typeparam name="var">
/// Meta-type of variables inside the equivalence-checked expressions.
/// </typeparam>
/// <returns>
/// True if the equivalence check definitely succeeded.
/// False otherwise (including if the check was undecideable).
/// </returns>
let equivHolds
(toVar : 'var -> Var)
(e : Equiv<'var>) =
(* The tactic here is the same as the Starling one:
negate the equivalence and try to falsify it. *)
use ctx = new Z3.Context ()
let term = ctx.MkNot (e toVar ctx)
match (Run.runTerm ctx term) with
| Z3.Status.UNSATISFIABLE -> true
| _ -> false
/// <summary>
/// Or-disjoins two equivalence checks.
/// </summary>
/// <param name="x">
/// The first equivalence check to disjoin.
/// </param>
/// <param name="y">
/// The second equivalence check to disjoin.
/// </param>
/// <typeparam name="var">
/// Meta-type of variables inside the equivalence-checked expressions.
/// </typeparam>
/// <returns>
/// The or-disjunction of the two, which will return true only if
/// (but not necessarily if!) at least one equivalence holds.
/// </returns>
let orEquiv (x : Equiv<'var>) (y : Equiv<'var>) : Equiv<'var> =
fun toVar (ctx : Z3.Context) ->
ctx.MkOr [| x toVar ctx ; y toVar ctx |]
/// <summary>
/// And-conjoins two equivalence checks.
/// </summary>
/// <param name="x">
/// The first equivalence check to conjoin.
/// </param>
/// <param name="y">
/// The second equivalence check to conjoin.
/// </param>
/// <typeparam name="var">
/// Meta-type of variables inside the equivalence-checked expressions.
/// </typeparam>
/// <returns>
/// The and-conjunction of the two, which will return true only if
/// (but not necessarily if!) both equivalences hold.
/// </returns>
let andEquiv (x : Equiv<'var>) (y : Equiv<'var>) : Equiv<'var> =
fun toVar (ctx : Z3.Context) ->
ctx.MkAnd [| x toVar ctx ; y toVar ctx |]
/// <summary>
/// Returns true if two expressions are definitely equivalent to each
/// other.
///
/// <para>
/// This is sound, but not complete. It should only be used for
/// optimisations.
/// </para>
/// </summary>
/// <param name="x">
/// The first expression to check.
/// </param>
/// <param name="y">
/// The second expression to check.
/// </param>
/// <typeparam name="var">
/// Meta-type of variables inside the equivalence-checked expressions.
/// </typeparam>
/// <returns>
/// An equivalence check returning true only if (but not if!)
/// <paramref name="x" /> and <paramref name="y" /> are equivalent.
/// </returns>
/// <remarks>
/// This function calls into Z3, and is thus likely to be slow.
/// Use with caution.
/// </remarks>
let equiv (x : BoolExpr<'var>) (y : BoolExpr<'var>) : Equiv<'var> =
fun toVar ctx ->
let sx = Expr.boolToZ3 false toVar ctx (normalBool (simp x))
let sy = Expr.boolToZ3 false toVar ctx (normalBool (simp y))
ctx.MkIff (sx, sy)
/// <summary>
/// Returns true if two expressions are definitely negations of each
/// other.
///
/// <para>
/// This is sound, but not complete. It should only be used for
/// optimisations.
/// </para>
/// </summary>
/// <param name="x">
/// The first expression to check.
/// </param>
/// <param name="y">
/// The second expression to check.
/// </param>
/// <typeparam name="var">
/// Meta-type of variables inside the equivalence-checked expressions.
/// </typeparam>
/// <returns>
/// An equivalence check returning true only if (but not if!)
/// <paramref name="x" /> and <paramref name="y" /> negate each other.
/// </returns>
/// <remarks>
/// This function calls into Z3, and is thus likely to be slow.
/// Use with caution.
/// </remarks>
let negates (x : BoolExpr<'var>) (y : BoolExpr<'var>) : Equiv<'var> =
fun toVar ctx -> equiv x (BNot y) toVar ctx
/// <summary>
/// Tests for <c>ExprEquiv</c>.
/// </summary>
module Tests =
open NUnit.Framework
open Starling.Utils.Testing
open Starling.Core.Pretty
open Starling.Core.Var.Pretty
/// <summary>
/// NUnit tests for <c>ExprEquiv</c>.
/// </summary>
type NUnit () =
/// Test cases for negation checking.
static member ObviousNegations =
[ (tcd [| (BTrue : VBoolExpr)
(BFalse : VBoolExpr) |])
.Returns(true)
(tcd [| (BTrue : VBoolExpr)
(BTrue : VBoolExpr) |])
.Returns(false)
(tcd [| (BFalse : VBoolExpr)
(BFalse : VBoolExpr) |])
.Returns(false)
(tcd [| (BTrue : VBoolExpr)
(iEq (IInt 5L) (IInt 6L) : VBoolExpr) |])
.Returns(true)
(tcd [| (iEq (IVar "x") (IInt 2L))
(BNot (iEq (IVar "x") (IInt 2L))) |])
.Returns(true)
(tcd [| (iEq (IVar "x") (IInt 2L))
(BNot (iEq (IVar "y") (IInt 2L))) |])
.Returns(false)
// De Morgan
(tcd [| (BAnd [ BVar "x" ; BVar "y" ])
(BOr [ BNot (BVar "x")
BNot (BVar "y") ] ) |] )
.Returns(true)
(tcd [| (BAnd [ BVar "x" ; BVar "y" ])
(BOr [ BNot (BVar "y")
BNot (BVar "x") ] ) |] )
.Returns(true)
(tcd [| (BOr [ BVar "x" ; BVar "y" ])
(BAnd [ BNot (BVar "x")
BNot (BVar "y") ] ) |] )
.Returns(true)
(tcd [| (BOr [ BVar "x" ; BVar "y" ])
(BAnd [ BNot (BVar "y")
BNot (BVar "x") ] ) |] )
.Returns(true) ]
|> List.map (
fun d -> d.SetName(sprintf "%s and %s are %s negation"
(((d.OriginalArguments.[1])
:?> VBoolExpr)
|> printVBoolExpr |> print)
(((d.OriginalArguments.[0])
:?> VBoolExpr)
|> printVBoolExpr |> print)
(if (d.ExpectedResult :?> bool)
then "a" else "not a")))
/// Checks whether negation checking is sound and sufficiently complete.
[<TestCaseSource("ObviousNegations")>]
member x.``negates is sound and sufficiently complete`` a b =
equivHolds id (negates a b)