-
Notifications
You must be signed in to change notification settings - Fork 3
/
Leanwuzla.lean
455 lines (400 loc) · 16.3 KB
/
Leanwuzla.lean
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Henrik Böving
-/
import Lean.Elab.Tactic.BVDecide.Frontend.BVDecide
/--
Invoke Bitwuzla on an SMT-fied version of a bitvector goal to see if it holds or not.
Does not generate a proof term.
-/
syntax (name := bvBitwuzla) "bv_bitwuzla" (str)? : tactic
/--
Compare the performance of `bv_decide` and `bv_bitwuzla`.
-/
syntax (name := bvCompare) "bv_compare" (str)? : tactic
namespace Lean.Elab.Tactic.BVDecide
namespace Frontend
open Std.Sat
open Std.Tactic.BVDecide
open Std.Tactic.BVDecide.Reflect
open Lean.Meta
partial def toSMT (expr : BVLogicalExpr) (atomsAssignment : Std.HashMap Nat (Nat × Expr × Bool)) : String :=
let (_, buffer) := StateT.run (go expr atomsAssignment) ""
buffer
where
@[inline]
push (str : String) : StateM String Unit := modify fun buf => buf ++ str
@[specialize]
withParens (arg : StateM String Unit) : StateM String Unit := do
push "("
arg
push ")"
@[inline]
pushUnaryOp (opName : String) (arg : StateM String Unit) : StateM String Unit := do
withParens do
push s!"{opName} "
arg
@[inline]
pushBinaryOp (opName : String) (lhs rhs : StateM String Unit) : StateM String Unit := do
withParens do
push s!"{opName} "
lhs
push " "
rhs
@[inline]
pushIteOp (c e1 e2 : StateM String Unit) : StateM String Unit := do
withParens do
push s!"ite "
c
push " "
e1
push " "
e2
go (expr : BVLogicalExpr) (atomsAssignment : Std.HashMap Nat (Nat × Expr × Bool)) : StateM String Unit := do
push "(set-logic QF_BV)\n"
declareConsts atomsAssignment
pushUnaryOp "assert" (goBVLogical expr)
push "\n"
push "(check-sat)\n"
push "(exit)\n"
declareConsts (atomsAssignment : Std.HashMap Nat (Nat × Expr × Bool)) : StateM String Unit := do
for (atom, (width, _, _)) in atomsAssignment do
push s!"(declare-const x_{atom} (_ BitVec {width}))\n"
goBVLogical (expr : BVLogicalExpr) : StateM String Unit := do
match expr with
| .literal pred => goBVPred pred
| .const b =>
match b with
| true => push "true"
| false => push "false"
| .not expr => pushUnaryOp "not" (goBVLogical expr)
| .ite c e1 e2 => pushIteOp (goBVLogical c) (goBVLogical e1) (goBVLogical e2)
| .gate gate lhs rhs =>
let gateStr :=
match gate with
| .and => "and"
| .xor => "xor"
| .beq => "="
| .imp => "=>"
pushBinaryOp gateStr (goBVLogical lhs) (goBVLogical rhs)
goBVPred (pred : BVPred) : StateM String Unit := do
match pred with
| .bin lhs op rhs =>
let opStr :=
match op with
| .eq => "="
| .ult => "bvult"
pushBinaryOp opStr (goBVExpr lhs) (goBVExpr rhs)
| .getLsbD expr idx =>
push s!"(= #b1 ((_ extract {idx} {idx}) "
goBVExpr expr
push "))"
goBVExpr {w : Nat} (expr : BVExpr w) : StateM String Unit := do
match expr with
| .var idx => push s!"x_{idx}"
| .const val =>
let s := (Nat.toDigits 2 val.toNat).asString
let t := (List.replicate (w - s.length) '0').asString
let binStr := t ++ s
push "#b"
push binStr
| .extract start len expr => pushUnaryOp s!"(_ extract {len - 1 + start} {start})" (goBVExpr expr)
| .bin lhs op rhs =>
let lhs := goBVExpr lhs
let rhs := goBVExpr rhs
match op with
| .and => pushBinaryOp "bvand" lhs rhs
| .or => pushBinaryOp "bvor" lhs rhs
| .xor => pushBinaryOp "bvxor" lhs rhs
| .add => pushBinaryOp "bvadd" lhs rhs
| .mul => pushBinaryOp "bvmul" lhs rhs
| .udiv =>
let zero := goBVExpr <| .const (w := w) 0
withParens do
push "ite "
pushBinaryOp "=" zero rhs
push " "
zero
push " "
pushBinaryOp "bvudiv" lhs rhs
| .umod =>
let zero := goBVExpr <| .const (w := w) 0
withParens do
push "ite "
pushBinaryOp "=" zero rhs
push " "
zero
push " "
pushBinaryOp "bvurem" lhs rhs
| .un op operand =>
match op with
| .not => pushUnaryOp "bvnot" (goBVExpr operand)
| .shiftLeftConst n => pushBinaryOp "bvshl" (goBVExpr operand) (goBVExpr (bvConst w n))
| .shiftRightConst n => pushBinaryOp "bvlshr" (goBVExpr operand) (goBVExpr (bvConst w n))
| .rotateLeft n => pushUnaryOp s!"(_ rotate_left {n})" (goBVExpr operand)
| .rotateRight n => pushUnaryOp s!"(_ rotate_right {n})" (goBVExpr operand)
| .arithShiftRightConst n => pushBinaryOp s!"bvashr" (goBVExpr operand) (goBVExpr (bvConst w n))
| .append lhs rhs => pushBinaryOp "concat" (goBVExpr lhs) (goBVExpr rhs)
| .replicate n expr => pushUnaryOp s!"(_ repeat {n})" (goBVExpr expr)
| .zeroExtend (w := w) v expr =>
if v ≤ w then
emitTruncate expr v
else
pushUnaryOp s!"(_ zero_extend {v - w})" (goBVExpr expr)
| .signExtend (w := w) v expr =>
if v ≤ w then
emitTruncate expr v
else
pushUnaryOp s!"(_ sign_extend {v - w})" (goBVExpr expr)
| .shiftLeft lhs rhs => pushBinaryOp "bvshl" (goBVExpr lhs) (goBVExpr rhs)
| .shiftRight lhs rhs => pushBinaryOp "bvlshr" (goBVExpr lhs) (goBVExpr rhs)
emitTruncate {w : Nat} (expr : BVExpr w) (targetWidth : Nat) : StateM String Unit := do
pushUnaryOp s!"(_ extract {targetWidth - 1} 0)" (goBVExpr expr)
@[inline]
bvConst (w : Nat) (n : Nat) : BVExpr w := .const (BitVec.ofNat w n)
def smtQuery (solverPath : System.FilePath) (problemPath : System.FilePath) (timeout : Nat) :
CoreM External.SolverResult := do
let cmd := solverPath.toString
let opts ← getOptions
let verbose := diagnostics.get opts
let mut args := #[
problemPath.toString,
"-v=1"
]
if verbose then
args := args.push "-p"
let out? ← External.runInterruptible timeout { cmd, args, stdin := .piped, stdout := .piped, stderr := .null }
match out? with
| .timeout =>
throwError "The SMT solver timed out while solving the problem."
| .success { exitCode := exitCode, stdout := stdout, stderr := stderr} =>
if exitCode == 255 then
throwError s!"Failed to execute external prover:\n{stderr}"
else
let stdoutLines := stdout.splitOn "\n"
let solvingLine := stdoutLines[stdoutLines.length - 2]!
assert! solvingLine.startsWith "solving_context::time_solve:"
trace[Meta.Tactic.bv] solvingLine
if stdoutLines.contains "sat" then
return .sat #[]
else if stdoutLines.contains "unsat" then
return .unsat
else
throwError s!"The external prover produced unexpected output, stdout:\n{stdout}stderr:\n{stderr}"
private axiom bitwuzlaCorrect (expr : BVLogicalExpr) : expr.Unsat
def bitwuzlaCounterExample : String := "Bitwuzla found a counter example"
def bitwuzlaSuccess : String := "Bitwuzla thinks it's right but can't trust the wuzla!"
def bitwuzla (g : MVarId) (reflectionResult : ReflectionResult) (atomsAssignment : Std.HashMap Nat (Nat × Expr × Bool))
(solverPath : System.FilePath) :
MetaM (Except CounterExample UnsatProver.Result) := do
let smt := toSMT reflectionResult.bvExpr atomsAssignment
trace[Meta.Tactic.bv] s!"Encoded as SMT: {smt}"
let res ←
IO.FS.withTempFile fun handle path => do
handle.putStr smt
handle.flush
withTraceNode `bv (fun _ => return "Proving with bitwuzla") do
let opts ← getOptions
let timeout := sat.timeout.get opts
smtQuery solverPath path timeout
match res with
| .sat .. => return .error ⟨g, {}, #[]⟩
| .unsat => return .ok ⟨mkApp (mkConst ``bitwuzlaCorrect) (toExpr reflectionResult.bvExpr), ""⟩
def bvBitwuzla (g : MVarId) (solverPath : System.FilePath) : MetaM (Except CounterExample Unit) := do
let some g ← Normalize.bvNormalize g | return .ok ()
let unsatProver : UnsatProver := fun g reflectionResult atomsAssignment => do
withTraceNode `bv (fun _ => return "Preparing LRAT reflection term") do
bitwuzla g reflectionResult atomsAssignment solverPath
match ← closeWithBVReflection g unsatProver with
| .ok .. => return .ok ()
| .error err => return .error err
@[tactic bvBitwuzla]
def evalBvBitwuzla : Tactic := fun
| `(tactic| bv_bitwuzla $solverPath:str) => do
liftMetaFinishingTactic fun g => do
match ← bvBitwuzla g solverPath.getString with
| .ok .. => throwError bitwuzlaSuccess
| .error .. => throwError bitwuzlaCounterExample
| `(tactic| bv_bitwuzla) => do
liftMetaFinishingTactic fun g => do
match ← bvBitwuzla g "bitwuzla" with
| .ok .. => throwError bitwuzlaSuccess
| .error .. => throwError bitwuzlaCounterExample
| _ => throwUnsupportedSyntax
structure BitwuzlaPerf where
success : Bool
overallTime : Float
solvingContextTime : Float
structure LeansatSuccessTimings where
timeRewrite: Float
timeBitBlasting : Float
timeSatSolving : Float
timeLratTrimming : Float
timeLratChecking : Float
structure LeansatFailureTimings where
timeRewrite : Float
timeSatSolving : Float
inductive LeansatPerf where
| success (overallTime : Float) (timings : LeansatSuccessTimings)
| failure (overallTime : Float) (timings : LeansatFailureTimings)
structure Comparision where
bitwuzlaPerf : Option BitwuzlaPerf
leansatPerf : Option LeansatPerf
def BitwuzlaPerf.toString (perf : BitwuzlaPerf) : String :=
if perf.success then
s!"Bitwuzla proved the goal after {perf.overallTime}ms, solving context: {perf.solvingContextTime}ms"
else
s!"Bitwuzla provided a counter example after {perf.overallTime}ms, solving context: {perf.solvingContextTime}ms"
instance : ToString BitwuzlaPerf where
toString := BitwuzlaPerf.toString
def LeansatSuccessTimings.toString (timings : LeansatSuccessTimings) : String :=
let { timeRewrite, timeBitBlasting, timeSatSolving, timeLratTrimming, timeLratChecking } := timings
s!"rewriting {timeRewrite}ms, bitblasting {timeBitBlasting}ms, SAT solving {timeSatSolving}ms, LRAT trimming {timeLratTrimming}ms, LRAT checking {timeLratChecking}ms"
instance : ToString LeansatSuccessTimings where
toString := LeansatSuccessTimings.toString
def LeansatFailureTimings.toString (timings : LeansatFailureTimings) : String :=
let { timeRewrite, timeSatSolving } := timings
s!"rewriting {timeRewrite} SAT solving {timeSatSolving}ms"
instance : ToString LeansatFailureTimings where
toString := LeansatFailureTimings.toString
def LeansatPerf.toString (perf : LeansatPerf) : String :=
match perf with
| .success overallTime timings =>
s!"LeanSAT proved the goal after {overallTime}ms: {timings}"
| .failure overallTime timings =>
s!"LeanSAT provided a counter example after {overallTime}ms: {timings}"
instance : ToString LeansatPerf where
toString := LeansatPerf.toString
def Comparision.toString (comp : Comparision) : String :=
match comp.bitwuzlaPerf, comp.leansatPerf with
| none, none => "Bitwuzla failed.\nLeanSAT failed."
| some bitwuzla, none => bitwuzla.toString ++ "\nLeanSAT failed."
| none, some leansat => "Bitwuzla failed.\n" ++ leansat.toString
| some bitwuzla, some leansat => bitwuzla.toString ++ "\n" ++ leansat.toString
instance : ToString Comparision where
toString := Comparision.toString
def TraceData.durationMs (data : TraceData) : Float := (data.stopTime - data.startTime) * 1000.0
partial def parseSuccessTrace (traces : PersistentArray TraceElem) : IO LeansatSuccessTimings := do
let traces := traces.toArray.map TraceElem.msg
let (_, time) ← go traces |>.run {
timeRewrite := 0,
timeBitBlasting := 0,
timeSatSolving := 0,
timeLratTrimming := 0,
timeLratChecking := 0,
}
return time
where
go (msgs : Array MessageData) : StateT LeansatSuccessTimings IO Unit := do
for msg in msgs do
match msg with
| .trace data msg children =>
let msg ← msg.toString
match msg with
| "Normalizing goal" =>
modify fun s => { s with timeRewrite := TraceData.durationMs data }
| "Bitblasting BVLogicalExpr to AIG" =>
modify fun s => { s with timeBitBlasting := TraceData.durationMs data }
| "Running SAT solver" =>
modify fun s => { s with timeSatSolving := TraceData.durationMs data }
| "Obtaining LRAT certificate" =>
modify fun s => { s with timeLratTrimming := TraceData.durationMs data }
| "Verifying LRAT certificate" | "Compiling expr term" | "Compiling proof certificate term"
| "Compiling reflection proof term" =>
modify fun s => { s with timeLratChecking := s.timeLratChecking + TraceData.durationMs data }
| _ => pure ()
go children
| .withContext _ msg => go #[msg]
| _ => continue
partial def parseFailureTrace (traces : PersistentArray TraceElem) : IO LeansatFailureTimings := do
let traces := traces.toArray.map TraceElem.msg
let (_, time) ← go traces |>.run { timeRewrite := 0.0, timeSatSolving := 0.0 }
return time
where
go (msgs : Array MessageData) : StateT LeansatFailureTimings IO Unit := do
for msg in msgs do
match msg with
| .trace data msg children =>
let msg ← msg.toString
match msg with
| "Normalizing goal" =>
modify fun s => { s with timeRewrite := TraceData.durationMs data }
| "Running SAT solver" =>
modify fun s => { s with timeSatSolving := TraceData.durationMs data }
| _ => pure ()
go children
| .withContext _ msg => go #[msg]
| _ => continue
partial def evalBitwuzla (g : MVarId) (solverPath : System.FilePath) : MetaM BitwuzlaPerf := do
g.withContext do
let t1 ← IO.monoNanosNow
let res ← bvBitwuzla g solverPath
let t2 ← IO.monoNanosNow
let overallTime := (Float.ofNat <| t2 - t1) / 1000000.0
let (_, solvingContextTime) ← parseBitwuzlaTrace ((← getTraces).toArray.map TraceElem.msg) |>.run 0
return { success := res.isOk, overallTime, solvingContextTime }
where
parseBitwuzlaTrace (msgs : Array MessageData) : StateT Float IO Unit := do
for msg in msgs do
match msg with
| .trace _ msg children =>
let msg ← msg.toString
let pref := "solving_context::time_solve: "
if msg.startsWith pref then
let msg := (msg.splitOn " ")[1]!.dropRight 2
set <| Float.ofInt msg.toNat!
parseBitwuzlaTrace children
| .withContext _ msg => parseBitwuzlaTrace #[msg]
| _ => continue
def evalLeanSat (g : MVarId) (cfg : TacticContext) : MetaM LeansatPerf := do
g.withContext do
let t1 ← IO.monoNanosNow
let result ← bvDecide' g cfg
let t2 ← IO.monoNanosNow
let overallTime := (Float.ofNat <| t2 - t1) / 1000000.0
let traces ← getTraces
match result with
| .ok _ =>
return .success overallTime (← parseSuccessTrace traces)
| .error _ =>
return .failure overallTime (← parseFailureTrace traces)
def bvCompare (g : MVarId) (solverPath : System.FilePath) (cfg : TacticContext) : MetaM Comparision := do
let bitwuzlaPerf ← measure evalBitwuzla g solverPath
let leansatPerf ← measure evalLeanSat g cfg
return { bitwuzlaPerf , leansatPerf }
where
withFreshTraceState {α : Type} (x : MetaM α) : MetaM α := do
let traces ← getTraceState
resetTraceState
try x finally setTraceState traces
measure {α : Type _} {β : Type _} (f : MVarId → α → MetaM β) (g : MVarId) (arg : α) : MetaM (Option β) := do
let setTraceOptions (opt : Options) : Options :=
opt
|>.setBool `trace.profiler true
|>.setBool `trace.Meta.Tactic.bv true
|>.setBool `trace.Meta.Tactic.sat true
|>.setNat `trace.profiler.threshold 1
try
withOptions setTraceOptions <| withoutModifyingEnv <| withoutModifyingState <| withFreshTraceState do
f g arg
catch e =>
logError e.toMessageData
return none
@[tactic bvCompare]
def evalBvCompare : Tactic := fun
| `(tactic| bv_compare $solverPath:str) => do
IO.FS.withTempFile fun _ lratFile => do
let cfg ← TacticContext.new lratFile
let g ← getMainGoal
let res ← bvCompare g solverPath.getString cfg
logInfo <| toString res
| `(tactic| bv_compare) => do
IO.FS.withTempFile fun _ lratFile => do
let cfg ← TacticContext.new lratFile
let g ← getMainGoal
let res ← bvCompare g "bitwuzla" cfg
logInfo <| toString res
| _ => throwUnsupportedSyntax
end Frontend
end Lean.Elab.Tactic.BVDecide