-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdoc.lisp
More file actions
1753 lines (1453 loc) · 80.4 KB
/
doc.lisp
File metadata and controls
1753 lines (1453 loc) · 80.4 KB
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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
; FGL - A Symbolic Simulation Framework for ACL2
; Copyright (C) 2008-2013 Centaur Technology
;
; Contact:
; Centaur Technology Formal Verification Group
; 7600-C N. Capital of Texas Highway, Suite 300, Austin, TX 78731, USA.
; http://www.centtech.com/
;
; License: (An MIT/X11-style license)
;
; Permission is hereby granted, free of charge, to any person obtaining a
; copy of this software and associated documentation files (the "Software"),
; to deal in the Software without restriction, including without limitation
; the rights to use, copy, modify, merge, publish, distribute, sublicense,
; and/or sell copies of the Software, and to permit persons to whom the
; Software is furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
; DEALINGS IN THE SOFTWARE.
;
; Original author: Sol Swords <sswords@centtech.com>
(in-package "FGL")
(include-book "xdoc/top" :dir :system)
(defxdoc fgl
:parents (acl2::proof-automation acl2::hardware-verification)
:short "A prover framework that supports bit-blasting."
:long "
<p>FGL is the successor to <see topic='@(url fgl::fgl)'>GL</see>. It mainly
consists of a clause processor that calls on a custom rewriter/term interpreter
which features support for efficient representations of Boolean functions.
Compared to GL, FGL offers the following new features:</p>
<ul>
<li>Support for representing Boolean functions using the @(see
aignet::aignet) package.</li>
<li>Support for calling incremental SAT during rewriting/symbolic execution.</li>
<li>Less functionality included in built-in primitives, but better able to be
programmed using rewrite rules and user-provided extensions.</li>
<li>Explicit representation of the entire interpreter state,
allowing global simplifications (e.g. combinational-equivalence-preserving
AIGNET transformations).</li>
<li>The symbolic object representation includes a new primitive kind
representing a fast alist or array.</li>
<li>Better debugging capabilities, including a tracing facility for the
rewriter and the ability to examine the full interpreter stack at any
point.</li>
</ul>
<p>FGL is currently missing some important features of GL. In particular, BDD
and hons-AIG modes are not complete. Shape specifiers don't exist yet. Many
of the usual ways of doing things in FGL are done differently in FGL.</p>
<p>To get started with FGL in the default configuration:</p>
@({
;; include FGL
(include-book \"path/to/fgl/top-plus\")
;; attach to an incremental SAT backend library.
;; Note: must have environment variable IPASIR_SHARED_LIBRARY set to the path to
;; an IPASIR-compliant shared library
(include-book \"centaur/ipasir/ipasir-backend\" :dir :system)
;; test a proof
(fgl::fgl-thm
:hyp (and (unsigned-byte-p 5 x) (unsigned-byte-p 5 y))
:concl (equal (+ x y) (+ y x)))
(fgl::def-fgl-thm my-silly-theorem
:hyp (unsigned-byte-p 3 x)
:concl (not (logbitp 4 x)))
})
<p>To learn more about FGL, here are some places to get started:</p>
<ul>
<li>@(see fgl-object)</li>
<li>@(see fgl-getting-bits-from-objects)</li>
<li>@(see fgl-rewrite-rules)</li>
<li>@(see fgl-debugging)</li>
<li>@(see fgl-handling-if-then-elses)</li>
<li>@(see fgl-interpreter-overview)</li>
<li>@(see fgl-primitive-and-meta-rules)</li>
<li>@(see fgl-sat-check)</li>
<li>@(see fgl-solving)</li>
<li>@(see fgl-counterexamples)</li>
</ul>
")
(defxdoc fgl-getting-bits-from-objects
:parents (fgl)
:short "How to ensure that FGL can reduce your conjecture to a Boolean formula"
:long " <p>FGL is intended to reduce ACL2 conjectures to Boolean formulas
that can be solved with a SAT solver. The first step in this process is to
reduce the variables of the theorem -- which could represent any object at all
-- into a bit-blasted representation of some sort. In GL, this was typically
done by providing a shape specifier for each variable, and providing a coverage
proof to show that the shape specifier was sufficient to cover all the values
of the variable that satisfy the hypotheses of the theorem. In FGL, shape
specifiers are not supported (yet); instead, theorem variables are just
variables, but rewrite rules can be used to extract a Boolean representation
from them.</p>
<h3>Generating Boolean variables</h3>
<p>When the FGL interpreter process an IF term, it interprets the test of the
IF, resulting in a symbolic object. If that symbolic object is a function call
or variable object, then we check the Boolean variable database of the
interpreter state to see whether that object has already been bound to a
Boolean variable. If not, we add a new Boolean variable representing that
object's truth value. This is the only way that Boolean variables are
generated in FGL. An example of how this can be used can be seen in the
following rule (from \"fgl/bitops.lisp\") for expanding the @('loghead') of
some object:</p>
@({
(def-fgl-rewrite loghead-const-width
(implies (syntaxp (integerp n))
(equal (loghead n x)
(if (or (zp n)
(and (check-int-endp x (syntax-bind xsyn (g-concrete x)))
(not (intcar x))))
0
(intcons (and (intcar x) t)
(loghead (1- n) (intcdr x)))))))
})
<p>Applying this rule to @('(loghead 3 z)') results in three new Boolean
variables being generated due to the subterm @('(and (intcar x) t)'), which is
really @('(if (intcar x) t nil)'). The full expression reduces to a
@('g-integer') form whose bits are the variables generated from @('(intcar
z)'), @('(intcar (intcdr z))'), and @('(intcar (intcdr (intcdr z)))').</p>
<p>So one way of getting from free variables to Boolean formulas that FGL can
use is to access them through functions like @('loghead'), and to have rules
like @('loghead-const-width') that rewrite those in a way that expose the
Boolean variables that we want to reason about.</p>
<h3>Normalizing Variables to Bit-blasted Objects</h3>
<p>Often instead of accessing a variable only through accessors like
@('loghead'), we'll have a hyp that describes the type of the variable. We can
use such hyps to reduce the variables to bit-blasted objects. For example, we
have the following rule in \"fgl/bitops.lisp\":</p>
@({
(def-fgl-rewrite unsigned-byte-p-const-width
(implies (syntaxp (integerp n))
(equal (unsigned-byte-p n x)
(and (natp n)
(equal x (loghead n x))
t))))
})
<p>When we assume @('(unsigned-byte-p 3 z)'), we end up with the formula
@('(equal z (loghead 3 z))'), but @('(loghead 3 z)') gets rewritten to a
symbolic integer as described above. Since this @('equal') term is in an IF
test @('(and (equal z ...) t)'), we add a Boolean variable for it. When we add
a Boolean variable for an @('equal') term, we also can add an entry in a
database that maps terms to Boolean variables that equate them to other terms.
Whenever the FGL interpreter produces a symbolic object that has an entry in
this database, we check whether any of those Boolean variables are currently
assumed to be true, and replace that symbolic object with the object it is
equated to. In this case, we create an entry for @('z') in the database
mapping it to the Boolean variable representing the @('equal') term. Since
this occurred as a hypothesis in the theorem, we're going to be assuming that
to be true in the conclusion, so whenever we see the variable @('z') there
we'll then replace it with the symbolic integer generated from the
@('loghead').</p>
<h3>Pitfalls and Suggestions</h3>
<p>Make sure the terms that are assigned Boolean variables don't alias each
other -- otherwise proofs may end up having false counterexamples. For
example, if you generate a Boolean variable for both @('(intcar (intcdr x))')
and @('(logbitp 1 x)'), then you're likely to have false counterexamples where
these two variables are assigned opposite values. Choose a single normal form
and rewrite other accessors to that -- e.g., use a rule like
@('logbitp-const-index') to ensure that @('logbitp') calls are normalized
correctly to @('(intcar (intcdr ...))') form.</p>
<p>When taking the approach of normalizing typed variables to bit-blasted
objects, it is important that the variable's correct type be assumed at each
use of the variable. That is, the assumption should be in a top-level
hypothesis or else an @('if') test guarding each use of the variable (aside
from the type assumption itself). For example, the following probably
works (depending what rules are installed):</p>
@({
(fgl-thm
(if (and (unsigned-byte-p 3 x) (unsigned-byte-p 3 y))
(equal (+ x y) (+ y x))
t))
})
<p>But the following probably doesn't, or at least not by bitblasting:</p>
@({
(fgl-thm
(if (not (equal (+ x y) (+ y x)))
(not (and (unsigned-byte-p 3 x) (unsigned-byte-p 3 y)))
t))
})
")
(defxdoc fgl-correctness-of-binding-free-variables
:parents (fgl)
:short "Discussion of the logical justification for the @(see bind-var) feature."
:long "<p>FGL's @(see bind-var) feature can be used to bind a free variable
in a rewrite rule to some arbitrary value. Here we discuss how the correctness
of that feature may be formalized. Note we discuss this with respect to
@('bind-var'), but it applies to binder functions generally.</p>
<h3>Requirements and Basic Idea</h3>
<p>First we describe the requirements for using the @('bind-var') feature. We
must first have a rewrite rule containing a call of bind-var in either the RHS
or a hyp. Then, if and when symbolic execution reaches the call of bind-var,
it must be the case that the first argument of @('bind-var') is a variable that
is not yet bound in the current frame (this is checked in the interpreter). If
this is the case, the interpreter may add a binding for that variable to any
object; in particular, it interprets the second argument of @('bind-var') under
the trivial equivalence @(see unequiv) (meaning that the value returned is
not important for soundness) and binds it to that value.</p>
<p>The justification for this is essentially that all free variables in a
theorem are universally quantified. When we apply a rewrite rule, we start
with the free variables of the LHS bound to the unifying substitution. We are
free to bind any other variables that are mentioned in the theorem to whatever
values we want, because the unifying substitution contains all variables of the
LHS, so extending the unifying substitution to include additional variables
won't change the value of the LHS under that substitution.</p>
<h3>Technical Problem and Solutions</h3>
<p>There is a technical problem with this when these free variables appear
inside lambda bodies. ACL2 term translation ensures that all lambdas bind all
the free variables of their bodies -- when it translates a lambda that has free
variables in the body, it adds bindings for those variables to themselves. So
if we have a rewrite rule like this:</p>
@({
(equal (foo x)
(let* ((a (bar x))
(c (bind-var b (baz x a))))
...))
})
<p>then the outside of the RHS looks like this:</p>
@({
((lambda (a x b) ...)
(bar x) x b)
})
<p>We interpret the arguments of a lambda or function call before the body, and
normally if we encounter an unbound variable during symbolic execution we
produce an error. Therefore, we would run into an error when we process the
argument @('b') of the lambda call. To avoid this and get the intended
behavior of binding @('b') when we arrive at its @('bind-var') form, we
preprocess lambdas to remove bindings of variables to themselves, instead
carrying forward the existing bindings from the outer context into the lambda
body. (See ACL2 community book \"centaur/meta/bindinglist.lisp\" for the
implementation.)</p>
<p>A further complication is that when we bind a free variable due to
@('bind-var'), it must be bound in the context of the current rewrite rule
application, not just the current lambda body. To do otherwise would allow
that variable to be bound to different objects in a single application of the
rule. To deal with this, we effectively have two levels of variable bindings
at any time: the <em>major frame</em> produced by the current rewrite rule
application, which has a wholly new namespace for variables, and a <em>minor
frame</em>produced by each nesting of lambdas, which carries forward all
variable bindings from the previous minor frame and may shadow variable
bindings from the major frame. When we add a variable binding with
@('bind-var'), that binding goes into the bindings of the major frame.</p>
<h3>Statement of Correctness</h3>
<p>Finally we discuss how the @('bind-var') feature affects how we state the
correctness of the FGL symbolic interpreter.</p>
<p>The correctness of a typical rewriter that takes an alist of variable
bindings is along the lines of:</p>
@({
(equivalent-under-relation equiv-relation-context
(eval-term input-term (eval-bindings bindings env))
(eval-term result-term env))
})
<p>Here equiv-relation-context, input-term, and bindings are inputs to the
rewriter, which produces result-term.</p>
<p>The exact such correctness statement for FGL involves various complications
that we won't discuss here, such as the distinction between input ACL2 terms
and output symbolic objects and various invariants of the interpreter state.
The main complication that we will discuss is that, rather than just an input
to the rewriter as would be the case in a typical rewriter, the relevant
bindings are changed (extended) by the rewriter as it (potentially) adds
variable bindings to the major frame due to @('bind-var'). So the statement we
want (again, very approximately) is:</p>
@({
(equivalent-under-relation
equiv-relation-context
(eval-term input-term (append (eval-bindings output-minor-bindings env)
(eval-bindings output-major-bindings env)))
(eval-term result-term env))
})
<p>(To simplify this slightly, we do show that the evaluation of the output and
input minor bindings is equivalent.)</p>
<p>This is reasonable and true, but it isn't sufficiently strong to be
inductively proved. The problem is that if we successively rewrite two terms,
the output major bindings from the second term may be different from those from
the first, so the inductive assumption from the first term doesn't help us
anymore.</p>
<p>To fix this, we instead prove, approximately:</p>
@({
(forall ext
(implies (eval-alist-extension-p
ext
(append (eval-bindings output-minor-bindings env)
(eval-bindings output-major-bindings env)))
(equivalent-under-relation equiv-relation-context
(eval-term input-term ext)
(eval-term result-term env))))
})
<p>Where @('(eval-alist-extension-p a b)') holds when @('a') binds all keys
present in @('b') to the same values. Since the rewriter only adds pairs to
the major bindings whose keys are not yet bound in either the major or minor
bindings, the resulting appended, evaluated minor and major bindings is such an
extension of the input appended, evaluated major and minor bindings. This
allows the inductive assumption about the first term to be applied to the
bindings resulting from rewriting the second term.</p>")
;; (defxdoc fgl-interpreter-correctness
;; :parents (fgl)
;; :short "Discussion of the correctness criterion for the FGL interpreter and its
;; relation to the @(see bind-var) feature."
;; :long
;; " <p>The correctness theorems for the core FGL interpreter are somewhat more
;; complicated than might be expected due to our support for binding free
;; variables using @(see bind-var). For example, the top correctness theorem for
;; @('fgl-interp-term') is the following mess:</p>
;; @(def fgl-interp-term-correct)
;; <p>We'll skip past most of the hypotheses and focus on one particular conclusion:</p>
;; @({
;; (fgl-ev-context-equiv-forall-extensions
;; (interp-st->equiv-contexts interp-st)
;; (fgl-object-eval xobj env new-logicman)
;; x
;; eval-alist)
;; })
;; <p>Here the @('xobj') is the symbolic object returned from @('fgl-interp-term'),
;; @('new-logicman') is the logicman field of the resulting interpreter state
;; after executing @('fgl-interp-term'), @('x') is the input term, and
;; @('eval-alist') is the combined evaluated bindings for the current major and minor stack
;; frame from the resulting interpreter state. (Actually, the minor frame
;; bindings are pulled from the input interpreter state, but it is a theorem that
;; the evaluation of the minor frame bindings is preserved by @('fgl-interp-term'),
;; so that is immaterial.)</p>
;; <p>The
;; <ul>
;; <li>
;; @({
;; (logicman-pathcond-eval (fgl-env->bfr-vals env)
;; (interp-st->constraint interp-st)
;; (interp-st->logicman interp-st))
;; })
;; This says that the constraint field of the interpreter state, which is supposed
;; to hold Boolean constraints stating facts that are known to be true
;; universally, is true under the evaluation environment.</li>
;; <li>@('(interp-st-bvar-db-ok new-interp-st env)') says that the evaluation
;; environment is consistent in its evaluation of all the Boolean
;; variable/symbolic object pairs stored in the Boolean variable database of the
;; final interpreter state. In order to apply this theorem to prove the
;; correctness of the FGL clause processor, we construct a special environment
;; that satisfies this condition.</li>
;; <li>@('(fgl-ev-meta-extract-global-facts :state st)'), @('(equal (w st) (w
;; state))') says, essentially, that for some state @('st') whose world field is
;; equal to the one in the state that we're running on, the facts available
;; through the @('meta-extract') facility are correct with respect to the term
;; evaluator @('fgl-ev').</li>
;; <li>@('(fgl-primitive-formula-checks-stub st)') says that all the formulas that
;; must be stored in the logical world in order for our symbolic interpretation of
;; primitives to be correct (see @(see fgl-primitives)) are correctly stored in
;; @('st').</li>
;; <li>@('(interp-st-bfrs-ok interp-st)') is a syntactic invariant on the
;; interpreter state saying essentially that no malformed Boolean function objects
;; exist in the interpreter state.</li>
;; <li>With the above hyps we can then show:
;; @({
;; (logicman-pathcond-eval (fgl-env->bfr-vals env)
;; (interp-st->constraint new-interp-st)
;; (interp-st->logicman new-interp-st))
;; })
;; which says that the constraint field of the new interpreter state after running
;; @('fgl-interp-term') remains true.</li>
;; <li>
;; @({
;; (logicman-pathcond-eval (fgl-env->bfr-vals env)
;; (interp-st->pathcond interp-st)
;; logicman)
;; })
;; says that the path condition, which is the same type of object as the
;; constraint but used differently, is true under the environment.
;; ")
(defxdoc fgl-interpreter-overview
:parents (fgl)
:short "Outline of the way the FGL interpreter works."
:long
"
<p>The FGL interpreter is called by the FGL clause processor in order to try
and turn the theorem term into an equivalent Boolean formula, which then can
perhaps be solved by SAT. In order to do this, it recursively interprets terms
turning them into symbolic objects (see @(see fgl-object)) containing Boolean formula
objects. In this doc topic we outline the operation of the interpreter.</p>
<p>The interpreter consists of a 31-way mutual recursion. We won't detail each
of the functions in the mutual recursion, but we will try to cover in general
terms all of the things that happen in them.</p>
<h3>Interpreting Terms -- Overview</h3>
<p>The highest-level entry point into the interpreter when trying to compute a
Boolean formula from a term is @('fgl-interp-test'). This first interprets the
term using another possible high-level entry point, @('fgl-interp-term-equivs'),
which calls @('fgl-interp-term') to produce a symbolic object (we'll cover its
operation shortly), then checks a database of equivalences that have been
assumed in the current path condition and replaces that symbolic object with an
equivalent if there are any that have been assumed (and which some heuristics
say are a good replacement). Finally, it calls @('fgl-interp-simplify-if-test')
which is the subroutine for coercing a symbolic object into a Boolean
formula.</p>
<p>@('fGl-interp-term') is very similar to a classical interpreter or rewriter.
It examines the term and treats different kinds of terms differently:</p>
<ul>
<li>When it is a constant (quote), it returns the quoted value, coerced to a @(see
g-concrete) symbolic object.</li>
<li>When it is a variable, it looks up the variable in the current
bindings (alist). There are actually two sets of bindings for our interpreter:
\"minor\" bindings, which come from lambdas, and \"major\" bindings, which come
from outermost contexts not inside any lambdas; these are checked in that
order.</li>
<li>When it is a lambda application, it processes the term into an equivalent
pair @('(bindinglist body)'). The bindinglist is a list of pairs @('(formals
actuals)') from the nest of lambdas, omitting formal/actual pairs that are the
same. This may include the bindings of more than one lambda: if the body of a
lambda is itself a lambda application, it recursively adds that to the
bindinglist, stopping when it finds a lambda body that is not itself a lambda.
The interpreter interprets the bindinglist by recurring through the list,
recursively interpreting each of the actuals of each pair with
@('fgl-interp-term-equivs'). When a pair is done it adds the bindings formed by
pairing the formals with the symbolic object results from the actuals to the
minor bindings of the interpreter. When done with all the @('(formals
actuals)') pairs, it then recursively interprets the body, then pops off the
bindings produced by the bindinglist, returning the symbolic object resulting
from interpreting the body.</li>
<li>When it is a function call, it deals with a few special cases, described
next, and then the generic function call case. In the generic case, it first
recursively interprets the arguments of the function, then calls
@('fgl-interp-fncall'), described below, on the function and symbolic objects
resulting from the arguments.</li>
</ul>
<h3>Interpreting Function Calls -- Special Cases</h3>
<p>The special cases of function calls include @('if'), @('return-last'),
@('bind-var'), @('abort-rewrite'), @('fgl-sat-check'), @('syntax-interp-fn'),
@('assume'), @('narrow-equiv'), and @('fgl-interp-obj'). Each of these requires
special treatment of the arguments, rather than just recursively interpreting
them:</p>
<ul>
<li>For @('if') terms, the test is recursively interpreted and coerced to a
Boolean function using @('fgl-interp-test'). Then, unless a syntactic analysis
shows that the path condition implies the test's negation, we recursively
interpret the \"then\" branch with @('fgl-interp-term-equivs') and with the test
conjoined to the path condition, and unless the syntactic analysis shows the
path condition implies the test, we recursively interpret the \"else\" branch
with the negated test conjoined to the path condition. If both branches were
interpreted, we then attempt to merge the results from the two branches into a
single symbolic object using @('fgl-interp-merge-branches'), described
below.</li>
<li>For @('return-last'), we provide special support for @('time$'), allowing
it to time the symbolic interpretation of the enclosed term. Otherwise, we
simply interpret the last argument. Note this means that @('prog2$') might not
function as expected -- if you intend the first argument to @('prog2$') to be
interpreted in FGL for side effects, you should instead bind it to an ignored
variable or else use an equivalent two-argument function instead.</li>
<li>For @('bind-var'), we bind the first argument, which must be a variable
that is not yet bound in the current major or minor frame, to the result of
interpreting the second argument under the @('unequiv') equivalence relation.
Under this equivalence relation, every object is equivalent to every other
object; this is sort of an \"anything goes\" mode which allows certain behavior
that would normally be unsound. This is allowable in this context because
@('bind-var') logically just returns its first argument (the variable), so the
second argument is irrelevant. It is allowable to bind anything to the
variable if it is not yet bound **** </li>
<li>For @('binder'), the argument must be a function call whose first argument
is a variable, and that variable must be free in the current stack frame. The
rest of that function's arguments are recursively interpreted, and then binder
rewrite/meta rules are applied to choose a binding for the free variable based
on the rest of the arguments.</li>
<li>For @('abort-rewrite'), the interpreter returns @('nil') and inserts a
special error keyword in the interpreter state's @('errmsg') field, which will
get caught when returning from the current major stack frame.</li>
<li>For @('fgl-sat-check'), we use @('fgl-interp-test') to coerce the second
argument (the condition to be tested) to a Boolean function, and
@('fgl-interp-term-equivs') to interpret the first argument (params). We then
call @('interp-st-sat-check'), an attachable function which calls SAT and
returns NIL if the input Boolean formula is unsat.</li>
<li>For @('syntax-interp-fn'), we check that the second argument is a quoted
term (this is true for calls generated by the @('syntax-interp') and
@('syntax-bind') macros) and that we're in an @('unequiv') equivalence
context. If so, we evaluate the first argument using @('fancy-ev'), with the
current variable bindings from the symbolic interpreter passed in as the
evaluation environment. For example, if a variable @('x') is bound to a
symbolic integer in our current interpreter frame, then @('x') will be bound to
the @(see fgl-object) representation of that symbolic integer when evaluating
the @('syntax-interp') term. This is similar to ACL2's @(see syntaxp)
behavior, but the syntaxp term operates on FGL object syntax rather than ACL2
term syntax.</li>
<li>For @('assume'), we first check that we're in an @('unequiv') equivalence
context. Then we interpret the first and second arguments as if they were the
@('test') and @('then') branches of an @('if'); in particular, the Boolean
formula resulting from the @('test') is conjoined onto the path condition (that
is, assumed) when symbolically executing the @('then') branch. We then simply
return the result from the @('then') branch.</li>
<li>For @('narrow-equiv'), we interpret the first argument. If its result is a
concrete object whose value satisfies @('equiv-contexts-p') and those
equivalence contexts are a refinement (narrowing) of the interpreter state's
current equiv-contexts, then we set the interpreter state's equiv-contexts
field to that object while interpreting the second argument. This can be used
to (conditionally) exit an @('unequiv') context.</li>
<li>For @('fgl-time-fn'), we interpret the first argument and use that as a
@('time$') format specifier for timing the interpretation of the second
argument, returning the result from the second argument.</li>
<li>For @('fgl-prog2'), we interpret the first argument under the
@('unequiv') equivalence context, discarding the result; then interpret the
second argument under the current equivalence context and return its result.</li>
<li>For @('fgl-interp-obj'), we first check that we're in an @('unequiv')
equivalence context. Then we interpret the first argument. If its result is a
concrete object whose value satisfies @('pseudo-term-p'), then we interpret
that term and return its result.</li>
</ul>
<h3>Interpreting Function Calls -- Generic Case</h3>
<p>Generic function calls are run by @('fgl-interp-fncall') after reducing the
arguments to a list of symbolic objects. This looks up the <see topic='@(url
fgl-function-mode)'>function mode</see> of the function and, depending on the
restrictions encoded in that mode, may do some or all of the following:</p>
<ul>
<li>If all the arguments are explicit, i.e. symbolic objects of the @(see
g-concrete) kind, then try to execute the function using @(see
magic-ev-fncall). If it runs successfuly, return the result as a
@('g-concrete') object.</li>
<li>Otherwise try applying each of the rules enabled for that function in the
@('fgl-rewrite-rules') table using @('fgl-rewrite-try-rule'). These may be
rewrite, meta, or primitive rules. If any of those rules succeeds, return the
symbolic object produced by recursively interpreting the RHS of the rule under
the unifying substitution.</li>
<li>Otherwise try executing a primitive associated with that function; see
@(see fgl-primitive-and-meta-rules). If successful, return the value from that primitive.</li>
<li>Otherwise, if there exists a rule with rune @('(:definition fnname)'), or
if there are rewrite/definition rules for that function listed in the
@('fgl-definition-rules') table, then try rewriting the call using those
rules.</li>
<li>Finally, if none of the above were successful, produce the object
@('(g-apply fnname args)').</li>
</ul>
<p>This completes the overview of how a term is interpreted and turned into
either a symbolic object (@(see fgl-object)) or Boolean formula. Next we
describe three subroutines that we skipped describing above:
@('fgl-rewrite-try-rule'), which attempts to apply a rewrite rule;
@('fgl-interp-simplify-if-test'), which coerces a symbolic object into a Boolean
formula; and @('fgl-interp-merge-branches'), which merges two branches of an IF
test. This will also lead us to discuss @('fgl-interp-add-constraints'), which
adds Boolean constraints according to a set of rules activated when introducing
a new Boolean variable representing some term.</p>
<h3>Applying Rewrite Rules</h3>
<p>@('fgl-rewrite-try-rule') takes a rule, a function name, and a list of
arguments which are FGL symbolic objects. The rule may be a rewrite, meta, or
primitive rule. For a primitive rule, the primitive function indicated by the
rule is run on the function and arguments, and we return the object it produces
if successful. For a meta rule, the metafunction indicated is run, and if
successful we recursively interpret the term returned under the bindings
returned. For a rewrite rule, we first try to unify the LHS of the rule with
the input function/arguments. If successful, we then try to relieve the hyps
of the rule by calling @('fgl-interp-test') on each one and checking that the
result is (syntactically) constant-true. We also check @('syntaxp') and
@('bind-free') hyps, the latter of which might extend the unifying substitution
with some free variable bindings.</p>
<p>If the hypotheses are all relieved, then we recurs on the conclusion using
@('fgl-interp-term') and return the result unless there were errors recorded in
the interpreter state. Errors are passed up to the caller except for the
special error produced by @('abort-rewrite'), which only causes the current
rule application to fail.</p>
<p>During this process, various helper functions are also called to support
tracing (see @(see fgl-rewrite-tracing)) and accumulated-persistence-style
profiling, but none of these are logically relevant.</p>
<p>Application of binder rules is similar, but with slightly different logical
contracts for the rules and very slightly different behavior. For binder
rules, we unify the argument list with the argument list of the LHS of the
rule, omitting the initial argument of the LHS which must be the variable to be
bound. Binder rules also specify the equivalence context under which the RHS
must be rewritten.</p>
<h3>Simplifying IF Tests</h3>
<p>@('fgl-interp-simplify-if-test') takes a symbolic object and attempts to
reduce it to an IFF-equivalent Boolean formula. For some varieties of symbolic
object, this is trivial: @(':g-concrete') objects' truth value is just the
truth value of the quoted value, @('g-integer') and @('g-cons') objects are
always nonnil, @('g-map') objects are nonnil unless their alist field is
exactly NIL, and @('g-boolean') objects' truth values are given by their
Boolean formulas. This leaves @('g-var'), @('g-ite'), and @('g-apply') objects.</p>
<p>For @('g-ite') objects, we coerce the @('test') sub-object into a Boolean
formula using @('fgl-interp-simplify-if-test') recursively. Then, similar to
symbolic interpretation of IF, we recur on the @('then') object unless the test
formula is syntactically falsified by the path condition, we recur on the
@('else') branch unless the test formula is syntactically true under the path
condition, and we then merge the branches by creating the if-then-else of the
Boolean formulas if both branches were run.</p>
<p>For @('g-var') objects, we assign a fresh Boolean variable to the object,
storing the association in the Boolean variable database (@('bvar-db')) of the
interpreter state, unless the object already has an associated Boolean
variable, in which case we return it.</p>
<p>For @('g-apply') objects, we first rewrite the function call under an IFF
context. In many cases this is redundant, but in some cases it may produce
reductions. If rewriting is successful, we recursively apply
@('fgl-simplify-if-test') to the result. Otherwise, we look up the function
call object in the @('bvar-db') and return the associated Boolean variable, if
any, or else introduce a fresh one and record that association. Finally, if a
new Boolean variable was introduced, we process the object with
@('fgl-interp-add-constraints') (see below) to record any constraints on the new
Boolean variable.</p>
<h3>Merging IF Branches</h3>
<p>@('fgl-interp-merge-branches') takes a Boolean formula for an IF test and
symbolic objects for the then and else branch values, and returns a new
symbolic object encapsulating the if-then-else.</p>
<p>It first checks for trivial cases -- test constant true, test constant false,
or branches equal -- and returns the obvious results in those cases.
Otherwise, if either branch is a function call (@(see g-apply)) object, then it
tries applying branch merge rules for those functions using
@('fgl-rewrite-try-rule') applied to the IF. If any of these are successful, it
returns the result.</p>
<p>Otherwise, if both branches are calls of the same function, it recursively
merges the argument lists and returns the function applied to the merged
arguments. Otherwise, it calls helper function
@('interp-st-fgl-object-basic-merge'), which merges basic symbolic objects
together when their types match, otherwise either producing an
if-then-else (@(see g-ite)) object or an error, depending on the configuration
setting of @('make-ites'). (See also @(see fgl-handling-if-then-elses).)</p>
<h3>Introducing Boolean Constraints</h3>
<p>To do.</p>
")
(defxdoc fgl-solving
:parents (fgl)
:short "Proving SAT instances in FGL"
:long "
<p>When finished symbolically executing the theorem, FGL automatically tries to
solve the resulting Boolean formula, proving it valid. It may also call SAT
during symbolic execution if directed to by user rules; see @(see
fgl-sat-check). Both of these satisfiability checks are done by calling an
attachable function @('interp-st-sat-check'). By default the attachment for
this is @('fgl-default-sat-check-impl'), which takes a @(see fgl-sat-config)
object. This object may either be a configuration object for calling <see
topic='@(url ipasir::ipasir)'>IPASIR</see> incremental SAT or <see topic='@(url
satlink::satlink)'>SATLINK</see> monolithic SAT (perhaps with AIGNET transforms
in the latter case). The IPASIR solver is suitable for numerous easy SAT
checks where it is beneficial to save lemmas from previous SAT checks in order
to help with solving subsequent ones, whereas the SATLINK solver is suitable
for more difficult problems.</p>
<p>The attachment for @('interp-st-sat-check') may be changed in order to
drastically reconfigure the way FGL calls SAT. However, it is usually better
to tweak the configuration object passed into the SAT solver. The top-level
validity check is configured using the attachable function
@('fgl-toplevel-sat-check-config'); this may be overridden by either attaching
a different function or setting the @(':sat-config') field of the FGL
configuration object. The type of the SAT configuration object (for the
default attachment of @('interp-st-sat-check')) is @(see fgl-sat-config).</p>
")
(defxdoc fgl-rewrite-rules
:parents (fgl)
:short "Differences between FGL and ACL2 rewrite rules"
:long "<p>FGL rewrite rules are really just ACL2 rewrite rules. But this
doesn't mean that any good ACL2 rewrite rule is a good FGL rewrite rule, or
vice versa. A particularly important difference is that @(see syntaxp) and
@(see bind-free) forms receive <see topic='@(url fgl-object)'>FGL symbolic
objects</see> as their inputs, rather than ACL2 terms. FGL rewrite rules also
allow special constructs @(see bind-var), @(see binder), and @(see syntax-bind)
which allow free variables to be bound as with @(see bind-free), but in the RHS
of the rewrite rule rather than in the hyps. They additionally support a form
@(see abort-rewrite) which causes the application of the rule to be aborted
when encountered in the RHS, similarly to if a hypothesis was not relieved.</p>
<h3>Creating and Removing FGL Rewrite Rules</h3>
<p>An FGL rewrite rule is an ACL2 rewrite rule. You can register an existing
ACL2 rewrite rule for use in FGL using:</p>
@({
(fgl::add-fgl-rewrite my-rule)
})
<p>And you can disable that rule for use by FGL using:</p>
@({
(fgl::remove-fgl-rewrite my-rule)
})
<p>To create a new rewrite rule and enable it for FGL, you may use:</p>
@({
(fgl::def-fgl-rewrite my-rule
body
:hints ...)
})
<p>This just expands to:</p>
@({
(defthmd my-rule ...)
(fgl::add-fgl-rewrite my-rule)
})
<p>FGL also supports rewrite rules that are triggered not on the leading
function symbol of the LHS, but on the leading function symbol of an
if-then-else branch. These rules can be added using
@('(fgl::add-fgl-branch-merge my-rule)') and they can be created using:</p>
@({
(fgl::def-fgl-branch-merge my-rule
body
...)
})
<p>A suitable branch merge rule has a left-hand side of the form @('(if test
then else)') where @('then') is a function call. Generally, @('test') and
@('else') should be free variables, but this isn't a hard and fast rule.</p>
<h3>Advanced Features</h3>
<p>FGL rewrite rules support ACL2's @(see syntaxp) and @(see bind-free)
hypothesis forms. However, it generally won't work to use the same rewrite
rule in both the ACL2 and FGL rewriters if it has syntaxp or bind-free
hypotheses, because in FGL the variables in the syntaxp/bind-free forms will be
bound to symbolic objects, whereas in ACL2 they will be bound to
terms. Therefore to use syntaxp, bind-free, and bind-var (discussed below),
one needs to be familiar with FGL symbolic objects -- see @(see fgl-object).</p>
<p>Two additional features support a new style of programming rewrite rules.
@('Bind-var') and @('syntax-bind') allow functionality similar to bind-free,
but available within the right-hand side of a rewrite rule. @('Abort-rewrite')
allows the rewrite operation to be cancelled partway through interpreting the
right-hand side.</p>
<p>Here is an example that uses both syntax-bind and abort-rewrite, from
\"bitops.lisp\":</p>
@({
(def-fgl-rewrite logtail-to-logtail-helper
(implies (syntaxp (not (fgl-object-case n :g-concrete)))
(equal (logtail n x)
(b* ((x (int x))
(n (nfix (int n)))
((when (syntax-bind n-concrete (fgl-object-case n :g-concrete)))
(logtail n x))
(n-width (syntax-bind n-width (fgl-object-case n
:g-integer (max 0 (1- (len n.bits)))
:otherwise nil)))
((unless (and n-width
(check-unsigned-byte-p n-width n)))
(abort-rewrite (logtail n x)))
(n-rev (int-revapp n-width n 0)))
(logtail-helper n-rev n-width x)))))
})
<p>When attempting to apply this rewrite rule, the FGL rewriter starts in much
the same way as the ACL2 rewriter. As a precondition for attempting to apply
this rule, the term we are rewriting must be a call of logtail, and that call
must unify with the LHS of this rule -- in this case any call of logtail will
do so.</p>
<p>Next, we relieve the hyps. This rule only has one hyp, which is a
syntaxp call; we check that @('n') is not a @(':g-concrete') object.</p>
<p>Then we proceed by interpreting the RHS. We bind @('x') to the result of
rewriting @('(int x)') (where @('int') is an alias for @('ifix') and rebind
@('n') to the result of rewriting @('(nfix (int n))'), and then we encounter
our first syntax-bind form. The syntax-bind form has the free variable
@('n-concrete'), which is here to logically represent the result we get from
evaluating the syntax-bind form. A syntax-bind form is logically equivalent to
its first argument, so when we prove @('logtail-to-logtail-helper') correct we
prove it with the free variable @('n-concrete') in place of the syntax-bind.
That means we are logically justified in returning anything we want from the
syntax-bind form -- since n-concrete is a free variable not previously bound,
the rule is applicable for any value of n-concrete. In this case we evaluate
@('(fgl-object-case n :g-concrete)'). (Note: @('syntax-bind') is a macro that
uses the primitive forms @(see bind-var) and @(see syntax-interp) to implement
this behavior; see their documentation for more general usage.) This will
produce a Boolean value, which is a concrete FGL object representing itself. If
true, then n is concrete and we will produce the result of again rewriting
@('(logtail n x)') -- note that we haven't created a loop here because the
syntaxp hyp required that the original @('n') was not concrete. Otherwise, we
proceed with the next syntax-bind form.</p>
<p>This next syntax-bind form (note it must use a different free variable than
the other form!) returns either some natural number value or else NIL. Again,
either way it is a concrete object representing itself. We bind this to
@('n-width'). We then require that n-width is nonnil and that we can show
@('n') satisfies @('(unsigned-byte-p n-width n)') (note: check-unsigned-byte-p
is an alias for unsigned-byte-p which has rules that try to prove it cheaply).
If not, then we come to an @('abort-rewrite') form. When FGL arrives at an
abort-rewrite form, it aborts the current rewrite rule attempt, ignoring the
form inside the abort-rewrite. (In this case the form @('(logtail n x)')
inside the abort-rewrite was selected to make it easy to prove
@('logtail-to-logtail-helper') correct.) Otherwise, we go on with our rewrite.</p>
<p>Note we needed to know @('(unsigned-byte-p n-width n)') in order to prove
@('logtail-to-logtail-helper'). The @('syntax-bind') form produces the correct
number, but while proving @('logtail-to-logtail-helper') we don't know that.
Fortunately, it's fairly efficient to verify that after the fact using
@('check-unsigned-byte-p').</p>
<h3>Examining the Interpreter State</h3>
<p>FGL's implementation of syntaxp, bind-free, and syntax-bind interpret the
syntactic term using a custom evaluator, @(see fancy-ev), that can be
instrumented to call functions that examine the ACL2 state and the FGL
interpreter state, and even make limited modifications to them. See the
documentation for @(see fancy-ev) for how to use it, and see @(see
fgl-internals) for documentation of the contents of the interpreter state. One
main use of this is to examine counterexamples produced from incremental SAT
calls. By default, after loading @('fgl/top'), the rewrite rule
@('show-counterexample-rw') rewrites the constant-nil function
@('(show-counterexample msg)') such that a syntax-bind form fetches the latest
incremental SAT counterexample and prints it.</p>
")
(defxdoc fgl-handling-if-then-elses
:parents (fgl)
:short "Discussion of how if-then-else objects are dealt with in FGL."
:long "
<h3>Why If-Then-Elses Are A Problem</h3>
<p>One of the most powerful and advantageous things about FGL is that it can
avoid case splits in proofs by encoding the case splits into Boolean functions
that are later handled by fast solvers. For example, if a function might
return either 3 or 5, then instead of considering the two cases separately we
can encode both cases into a symbolic integer value. But for some case splits
the cases aren't so easily merged into a single symbolic value, except by
directly encoding the case split. For example, if we have @('((a . b) . c)')
in one case but @('(d . (e . f))') in the other case, it isn't clear that
there's a good way to represent that.</p>
<p>Such cases can cause dilemmas and problems for FGL's symbolic
interpretation. For example, suppose we have the following rewrite rules:</p>
@({
(foo (bar x) y) = (baz x y)
(foo (zar x) y) = (fuz x y)
})
<p>But suppose that when we encounter a call of @('foo'), we have @('(if c (bar
3) (zar 5))') as the first argument. What do we do? We could always split
into cases for all the IFs in all the arguments of a function we're processing,
but this could easily get stuck in exponential case splits. But otherwise, we
might miss rewrites that the user might expect to happen. The user could fix
this case by adding the following rewrite rule:</p>
@({
(foo (if c a b) y) = (if c (foo a y) (foo b y))
})
<p>But this means the user needs to know all of the places this might happen
and make a rewrite rule for each of them, and by that time it might be just as
catastrophic for performance.</p>
<h3>Disallowing If-Then-Elses</h3>
<p>Because of the dilemma above, the default approach in FGL is to cause an
error whenever we are unable to merge the two branches of an if-then-else into
a single symbolic form automatically. When this happens, the FGL clause processor will
produce an error saying that an if-then-else failed to merge and to look at the
debug object. This behavior can be disabled as follows:</p>
@({
(assign :fgl-make-ites t)
})
<p>It is also possible to use rewrite rules to allow if-then-else objects to be
created in certain cases; we discuss how this is used to handle a couple of
common cases in the next section. The FGL interpreter includes support for
making @('ifs') transparent to certain functions, so that rewrite rules for
that purpose needn't proliferate. But often the best option is to encode data
so that you don't need to represent it using if-then-elses; some examples of
how to do that follow.</p>
<h3>Merge Rules</h3>
<p>FGL uses a function @('gl-object-basic-merge') to merge certain combinations
of objects: it can merge two symbolic or concrete integers into a symbolic
integer, or merge two symbolic or concrete Boolean values into a symbolic
Boolean. It also comes with some merging rules that allow a couple of common
idioms: the use of @('nil') as a \"bottom\" element for various types, and the
use of symbols as enum types.</p>
<p>To force the creation of an if-then-else, overriding the setting of
@(':fgl-make-ites'), use the function @('if!') instead of @('if') in the
right-hand side. For example, one of the rules that allows the use of @('nil')
as bottom follows:</p>
@({
(def-gl-branch-merge if-with-if-with-nil-nil
(equal (if test (if test2 then nil) nil)
(if! (and test test2) then nil)))
})
<p>This will ensure that a @(':g-ite') object will be created without checking
whether @('then') can be merged with @('nil') (presumably this was already
tried since @('(if test2 then nil)') was matched in the left-hand side.</p>
<h3>Making Functions Transparent to If-Then-Elses</h3>
<p>The FGL interpreter can be told to distribute calls of a given function over