forked from acl2/acl2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
apply-raw.lisp
4258 lines (3462 loc) · 186 KB
/
apply-raw.lisp
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
; ACL2 Version 8.4 -- A Computational Logic for Applicative Common Lisp
; Copyright (C) 2022, Regents of the University of Texas
; This version of ACL2 is a descendent of ACL2 Version 1.9, Copyright
; (C) 1997 Computational Logic, Inc. See the documentation topic NOTE-2-0.
; This program is free software; you can redistribute it and/or modify
; it under the terms of the LICENSE file distributed with ACL2.
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; LICENSE for more details.
; Written by: Matt Kaufmann and J Strother Moore
; email: [email protected] and [email protected]
; Department of Computer Science
; University of Texas at Austin
; Austin, TX 78712 U.S.A.
; Many thanks to ForrestHunt, Inc. for supporting the preponderance of this
; work, and for permission to include it here.
(in-package "ACL2")
; See the Essay on the APPLY$ Integration in apply-prim.lisp for an overview.
; This file supports top-level execution of apply$ by implementing the crucial
; executable functions attached to badge-userfn and apply$-userfn. We proceed
; in four main steps:
; * define doppelganger-badge-userfn, which will be attached to badge-userfn
; * define doppelganger-apply$-userfn, which will be attached to apply$-userfn
; * optimize apply$-lambda with compilation and caching
; * Essay on Admitting a Model for Apply$ and the Functions that Use It, the
; full version of the proof sketched in the paper ``Limited Second Order
; Functionality in a First Order Setting''
; The two doppelganger- functions mentioned above are actually partially
; constrained in other-events.lisp; the definitions here are their raw Lisp
; implementations. The model described in the paper (and justified in the
; essay here) is relevant because, in addition to making the warrants valid, it
; is executable and thus literally serves as our guide for implementing the
; attachments to badge-userfn and apply$-userfn.
; Historical Note: Prior to Version_8.0, apply$ was introduced in a user book
; and had no built-in support. Therefore, it was impossible to execute it in
; general. However, to facilitate experimentation, we ``secretly'' supported
; it and allowed the user to activate that support -- and render moot any
; soundness guarantees of ACL2! -- by executing ``The Rubric''. That code
; changed ``ACL2'' into the possibly unsound ``ACL2(a)''. Here is the (now
; obsolete) comment introducing The Rubric:
; The Rubric
; If you want to convert ACL2 into ACL2(a) evaluate each of the forms below
; immediately after starting your ACL2 session.
; (include-book "projects/apply/apply" :dir :system)
; (defattach
; (badge-userfn doppelganger-badge-userfn)
; (apply$-userfn doppelganger-apply$-userfn)
; :hints
; (("Goal" :use (doppelganger-badge-userfn-type
; doppelganger-apply$-userfn-takes-arity-args))))
; (value :q)
; (defun apply$-lambda (fn args) (concrete-apply$-lambda fn args))
; (setq *allow-doppelganger-execution-of-apply-stubs* t)
; (lp)
; (quote (end of rubric))
; Because The Rubric requires you execute a form in raw Lisp, The Rubric
; eliminates the soundness guarantees provided by the ACL2 implementors!
; End of Historical Note
; -----------------------------------------------------------------
; In the days when The Rubric was necessary, the raw lisp variable
; *allow-concrete-execution-of-apply-stubs* (which we would now call
; *allow-doppelganger-execution-of-apply-stubs*) told us whether it had been
; executed. Later, we just set that variable to t. Since then (after
; Version_8.0) we have eliminated that variable.
(defun query-badge-userfn-structure (fn wrld)
; We determine whether fn is badged and whether it is warranted. We return (mv
; bad-fn-msg badge warrantp) where bad-fn-msg is either nil or a message that
; explains that fn is not a known function or else has not been badged. If
; bad-fn-msg is nil, then badge is the badge and warrantp indicates whether fn
; has a warrant.
; This function is a gatekeeper for the execution of badge-userfn and
; apply$-userfn (q.v.).
; It is important that if this function returns a non-nil badge a world then it
; returns that same answer for all extensions of the world! The guard on the
; badge table, badge-table-guard, guarantees this invariant.
; Programming Note: This function assumes fn is being used as a ``userfn,''
; i.e., by badge-userfn or apply$-userfn. It fails to answer correctly if, for
; example, fn is CONS or APPLY$. Use executable-badge if you want to answer
; the question ``does fn have a badge?'' This function differs from the more
; primitive get-badge, get-warrant, and get-warrant-and-badge by virtue of
; checking that fn is a known function symbol.
(cond
((not (symbolp fn))
(mv (msg "~x0 is not a symbol" fn)
nil nil))
((not (function-symbolp fn wrld))
(mv (msg "~x0 is not a known function symbol" fn)
nil nil))
(t
(mv-let (badge warrantp)
(get-badge-and-warrantp fn wrld)
(cond
((null badge) ; fn is a function symbol with no badge assigned
(mv (msg "~x0 has not been badged" fn)
nil nil))
(t (mv nil badge warrantp)))))))
; The (extensible) attachments for badge-userfn and apply$-userfn are
; doppelganger-badge-userfn and doppelganger-apply$-userfn. They will be
; attached to badge-userfn and apply$-userfn to extend the evaluation theory
; appropriately. See the defattach event at the end of apply.lisp. We define
; the two doppelganger- functions below.
; Because we want to implement their bodies in raw Lisp, we would like to
; introduce them with defun-overrides commands like
; (defun-overrides doppelganger-badge-userfn (fn) ...)
; (defun-overrides doppelganger-apply$-userfn (fn args) ...)
; But the defun-overrides macro requires that STATE be among the formals of the
; function introduced and it is not. So we can't use defun-overrides per se.
; Instead, we use the code that defun-overrides would have introduced, but
; delete the parts about state! We believe this is sound because (a) the
; concrete implementations cause the same kind of error that calling an
; undefined function causes if applied to arguments for which the answer is
; unspecified, and (b) once the answer is specified in a world, the answer is
; the same for all future extensions of the world. Important to observation
; (b) is that we cannot apply$ functions to stobjs or state.
; Here is the STATE-free expansion of
; (defun-overrides doppelganger-badge-userfn (fn) ...)
; ==>
; (assert (member 'state formals :test 'eq))
(progn (push 'doppelganger-badge-userfn *defun-overrides*) ; see add-trip
; The next two items are pushed to the left margin so they get picked up by
; etags. But they're really part of the progn!
; The following defun has two notes in it which are given afterwards.
(defun doppelganger-badge-userfn (fn)
; See the Essay on Evaluation of Apply$ and Loop$ Calls During Proofs.
(cond
((and (null *aokp*) ; See Note 1.
(null *warrant-reqs*))
(throw-raw-ev-fncall ; See Note 2.
(list* 'ev-fncall-null-body-er
nil
'doppelganger-badge-userfn
(replace-live-stobjs-in-list
(list fn)))))
; Recall the Note on Strengthening the Constraint in badge-userfn-type found in
; apply-constraints.lisp. There we discussed a bootstrapping problem arising
; from adding the additional constraint that badge-userfn returns nil on
; primitives and boot functions. That strengthened constraint would have to be
; implemented here too, perhaps with code like the following.
; ((or (apply$-primp fn)
; (eq fn 'badge)
; (eq fn 'tamep)
; (eq fn 'tamep-functionp)
; (eq fn 'suitably-tamep-listp)
; (eq fn 'apply$)
; (eq fn 'ev$))
; nil)
(t
; We know either *aokp* = t (meaning we are in the evaluation theory) or else
; *aokp*=nil and *warrant-reqs* is non-nil (meaning we're in the prover and
; warrants are required).
(mv-let (bad-fn-msg badge warrantp)
(query-badge-userfn-structure fn (w *the-live-state*))
; Note: If bad-fn-msg is nil then we know fn has a badge and badge is it.
(cond
((and (null bad-fn-msg) ; there is a badge and either we're in the
(or *aokp* warrantp)); evaluation theory or fn has a warrant
(or (not warrantp)
(maybe-extend-warrant-reqs fn nil 'badge-userfn))
badge)
(t (throw-raw-ev-fncall
(list* 'ev-fncall-null-body-er
nil
; See the comment under the second throw-raw-ev-fncall in
; doppelganger-apply$-userfn, for why we assume here that this call has come
; from badge-userfn.
(msg "The value of ~x0 is not specified on ~x1 because ~
~@2."
'BADGE-USERFN
fn
(cond (bad-fn-msg bad-fn-msg)
(t (msg "~x0 has not been warranted"
fn))))
(replace-live-stobjs-in-list
(list fn))))))))))
; Notes on DOPPELGANGER-BADGE-USERFN
; Note 1. on the test of *aokp*: We once thought that it was unnecessary to
; test *aokp* in doppelganger-badge-userfn. The (faulty) reasoning was that
; doppelganger-badge-userfn is the attachment for badge-userfn. We wouldn't be
; running doppelganger-badge-userfn if attachments weren't ok. The flaw in
; that reasoning is that doppelganger-badge-userfn is itself a :logic mode
; function and might be called directly by the user at the top level of the
; ACL2 loop, or used in some other function used in proofs or hints. So we
; might find ourselves executing doppelganger-badge-userfn even though *aokp*
; is nil. We need for it to act undefined when *aokp* is nil. This same
; reasoning applies to doppelganger-apply$-userfn. More recently, however, we
; have made doppelganger-badge-userfn untouchable; thus, we could now remove
; the *aokp* test. (The issue for the *warrant-reqs* test is similar.) But
; for now, at least, we'll leave this *aokp* test, mainly to protect against
; inappropriate execution if untouchability is removed, but with the bonus that
; this test provides extra protection in case our thinking here is flawed!
; Note 2. on throw-raw-ev-fncall: Throughout this function we cause errors
; when the answer is not determined by the known warrants. The various errors
; are all equivalent to ``ACL2 cannot evaluate a call to the undefined
; function....'' Once upon a time we signaled the errors by calling
; (throw-without-attach nil fn formals) which expands in raw Lisp to
; `(throw-raw-ev-fncall
; (list* 'ev-fncall-null-body-er
; nil
; ',fn
; (replace-live-stobjs-in-list (list ,@formals))))
; When fn is a symbol, throw-raw-ev-fncall uses the standard undefined function
; error msg, reporting fn as the culprit. If fn is a consp,
; throw-raw-ev-fncall uses fn as the message. But as shown above,
; throw-without-attach puts a quote on fn when it expands. So using
; throw-without-attach prevents us from creating our own messages with msg, as
; we do above. So instead of throw-without-attach we use its expansion,
; without the quote on the ``fn'' arg.
; End of Notes on DOPPELGANGER-BADGE-USERFN
(defun-*1* doppelganger-badge-userfn (fn)
(doppelganger-badge-userfn fn))
; End of progn from ``defun-overrides''
)
; Essay on a Misguided Desire for Erroneous APPLY$s to Print Exactly the
; Same Error Messages whether Evaluation of APPLY$ Stubs is Supported or Not
; One possible objection to our handling of errors in doppelganger-badge-userfn
; arises with the question: If we attempt an evaluation of apply$ that is bound
; to fail, do we get exactly the same error message regardless of whether
; evaluation of the critical apply$ constrained functions is supported or not?
; The answer is "No." In fact, the answer is "No, there's no reason to expect
; that!"
; Here is an example.
; In ACL2, prior to the integration of apply$, we could include the distributed
; apply.lisp book and introduce these two functions:
; (defun$ sq (x) (* x x))
; (defun$ foo (fn1 fn2 x)
; (cons (apply$ fn1 (list x))
; (apply$ fn2 (list x))))
; Then trying to evaluate (foo 'sq 'cube 3) would cause the error:
; ACL2 Error in TOP-LEVEL: ACL2 cannot ev the call of undefined function
; APPLY$-USERFN on argument list: (SQ (5))
; because we can't apply$ 'SQ.
; But if we repeat that experiment today, we get a different error:
; ACL2 Error in TOP-LEVEL: The value of APPLY$-USERFN is not specified on CUBE
; because CUBE is not a known function symbol.
; We got past the (apply$ 'SQ ...) but now failed on (apply$ 'CUBE ...).
; So we can't expect unchanged erroneous behavior because the computation paths
; are just different in the two scenarios.
; End of Essay on A Misguided Desire...
(defun concrete-check-apply$-hyp-tamep-hyp (ilks args wrld)
; Compare to tameness-conditions in apply.lisp.
(declare (ftype (function (t t) (values t))
executable-tamep
executable-tamep-functionp))
(cond ((endp ilks) t)
((eq (car ilks) :fn)
(and (executable-tamep-functionp (car args) wrld)
(concrete-check-apply$-hyp-tamep-hyp (cdr ilks) (cdr args) wrld)))
((eq (car ilks) :expr)
(and (executable-tamep (car args) wrld)
(concrete-check-apply$-hyp-tamep-hyp (cdr ilks) (cdr args) wrld)))
(t (concrete-check-apply$-hyp-tamep-hyp (cdr ilks) (cdr args) wrld))))
(defun maybe-extend-warrant-reqs (fn args caller)
; See the Essay on Evaluation of Apply$ and Loop$ Calls During Proofs.
; This function is evaluated only for side effect, to update *warrant-reqs* as
; appropriate to reflect the need for a true warrant on fn when applying fn to
; args. Caller is used only in an error message (which quite possibly nobody
; will see), to reflect that caller, which is apply$-userfn or badge-userfn as
; of this writing.
; See *warrant-reqs* for a description of the values of that variable, which
; should serve to explain the code below.
(let ((warrant-reqs *warrant-reqs*)) ; bind the special, for efficiency
(cond ((null warrant-reqs) nil)
((eq t warrant-reqs)
(setq *warrant-reqs* (list fn)))
((eq :nil! warrant-reqs)
(setq *warrant-reqs* fn) ; the function responsible for the abort
(throw-raw-ev-fncall
(list* 'ev-fncall-null-body-er
nil
(msg "The value of ~x0 is not specified on ~x1 because the ~
use of warrants is not permitted in this context."
caller fn)
(replace-live-stobjs-in-list
(list fn args)))))
((symbolp warrant-reqs) ; invalid value for *warrant-reqs*
(er hard! 'maybe-extend-warrant-reqs
"Implementation error: *warrant-reqs* has an input value of ~
~x0."
warrant-reqs))
((member fn warrant-reqs :test #'eq) nil)
(t (push fn *warrant-reqs*)))))
; Here is the STATE-free expansion of
; (defun-overrides doppelganger-apply$-userfn (fn args) ...)
; ==>
; (assert (member 'state formals :test 'eq))
(progn (push 'doppelganger-apply$-userfn *defun-overrides*) ; see add-trip
; The next two items are pushed to the left margin so they get picked up by
; etags. But they're really part of the progn!
(defun raw-apply-for-badged-fn (fn arity args)
(cond
((and (eq (symbol-class fn (w *the-live-state*)) :program)
(not (f-get-global 'safe-mode *the-live-state*)))
(state-free-global-let*
((safe-mode t))
(apply (*1*-symbol fn)
(if (= arity (length args))
args
(take arity args)))))
(t (apply (*1*-symbol fn)
(if (= arity (length args))
args
(take arity args))))))
(defun doppelganger-apply$-userfn (fn args)
; See the Essay on Evaluation of Apply$ and Loop$ Calls During Proofs.
(cond
((and (null *aokp*) ; See Note 1.
(null *warrant-reqs*))
(throw-raw-ev-fncall ; See Note 2.
(list* 'ev-fncall-null-body-er
nil
'doppelganger-apply$-userfn
(replace-live-stobjs-in-list
(list fn args)))))
(t (mv-let (bad-fn-msg badge warrantp)
(query-badge-userfn-structure fn (w *the-live-state*))
(cond
((or bad-fn-msg ; no badge for fn, or there is a badge but
; There's a badge, but either
; (a) we're in the prover, or any setting where attachments are not allowed,
; and there's no warrant; or
; (b) attachments are allowed (as in the top-level loop), but fn is in logic
; mode yet fn has no warrant.
(if (null *aokp*)
(not warrantp) ; (a)
(and (logicp fn (w *the-live-state*)) ; (b)
(not warrantp))))
(throw-raw-ev-fncall
(list* 'ev-fncall-null-body-er
nil
; The following message assumes that we got here by way of a call to
; apply$-userfn. Since doppelganger-apply$-userfn is untouchable, that must be
; the case unless the user has changed that, in which case the error message
; below might be confusing -- but surely nobody should remove untouchability of
; doppelganger-apply$-userfn! See the Essay on Memoization with Attachments.
(msg "The value of ~x0 is not specified on ~x1 because ~@2."
'APPLY$-USERFN fn
(cond (bad-fn-msg bad-fn-msg)
(t (msg "~x0 has not been warranted"
fn))))
(replace-live-stobjs-in-list
(list fn args)))))
; If we get here we know that fn has a badge and that either *aokp* is true (so
; no warrants are necessary) or fn has a warrant.
((eq (access apply$-badge badge :ilks) t)
(or (not warrantp)
(maybe-extend-warrant-reqs fn args 'apply$-userfn))
(if (int= (access apply$-badge badge :out-arity) 1)
(raw-apply-for-badged-fn fn
(access apply$-badge badge :arity)
args)
(multiple-value-list
(raw-apply-for-badged-fn fn
(access apply$-badge badge :arity)
args))))
((concrete-check-apply$-hyp-tamep-hyp
(access apply$-badge badge :ilks)
args
(w *the-live-state*))
(or (not warrantp)
(maybe-extend-warrant-reqs fn args 'apply$-userfn))
(if (int= (access apply$-badge badge :out-arity) 1)
(raw-apply-for-badged-fn fn
(access apply$-badge badge :arity)
args)
(multiple-value-list
(raw-apply-for-badged-fn fn
(access apply$-badge badge :arity)
args))))
(t
(throw-raw-ev-fncall
(list* 'ev-fncall-null-body-er
nil
; See a comment above about a corresponding msg in the previous
; throw-raw-ev-fncall.
(msg "The value of ~x0 is not specified when the first ~
argument, fn, is ~x1, and the second argument, args, ~
is ~x2. Fn has badge ~x3 and args is not known to ~
satisfy the tameness requirement of that badge."
'APPLY$-USERFN fn args badge)
(replace-live-stobjs-in-list
(list fn args))))))))))
(defun-*1* doppelganger-apply$-userfn (fn args)
(doppelganger-apply$-userfn fn args))
; End of progn from ``defun-overrides''
)
; What we've described so far is adequate to run APPLY$ and EV$ forms in the
; evaluation theory after attaching the ``doppelgangers'' of badge-userfn and
; apply$-userfn for the current world to those critical functions.
; Now we turn to the optimization of APPLY$-LAMBDA. The following is provable:
; (equal (apply$-lambda fn args)
; (ev$ (lambda-object-body fn)
; (pairlis$ (lambda-object-formals fn)
; args)))
; Indeed, it is apply$-lambda-opener in books/projects/apply/base.lisp.
; The *1* version of apply$-lambda is what we call apply$-lambda-logical,
; essentially defined as the right-hand side of the equation above: interpret
; the body with ev$ under the alist binding the formals to the actuals.
; But we wish to apply certain lambdas more efficiently in the evaluation
; theory by executing compiled code after checking guards. As noted in the
; Essay on Lambda Objects and Lambda$, we have an elaborate cache that stores
; lambda objects seen (or perhaps likely to be seen) by apply$-lambda and
; associates critical information with them like whether they are well-formed
; and Common Lisp compliant (``guard verified'') and the compiled versions of
; them and their guards. Below is the Essay on the CL-Cache Implementation
; Details that is more precise than the earlier essay cited above.
; Essay on the CL-Cache Implementation Details
; The cl-cache (compliant lambda cache) is a raw Lisp, not ACL2, structure that
; stores (by default) the 1000 most recent LAMBDA-expressions applied in this
; session. Technically, *cl-cache* is a defrec triple consisting of a size, a
; circular alist, and a pointer to the ``last'' cell in the alist before it
; closes the loop. But intuitively it is just an alist whose maximum size is
; as given but whose virtual size is the number of entries from the beginning
; to the first nil. The circularity gives us an extremely efficient way to add
; new entries at the front without consing. This abstract view of the
; *cl-cache* is violated in a very important way: the *cl-cache* is intially a
; natural number greater than 2 that is the size of the circular alist. The
; first time we need to add a lambda to the cache we allocate the full
; structure.
; To clear the *cl-cache* just set it to the desired size with (setq *cl-cache*
; k). Initially, k=1000. [It might be better to implement a function for
; initializing the cache to a new size. Better yet, we could make that
; function available to the user in the loop; see the TODO on this, below.]
; The alist basically associates information with lambda objects. Whenever we
; search the alist for a given lambda and exhaust the virtual alist (i.e., hit
; a nil) we can re-use that link in the alist to create a new first cell for
; the lambda we looked for.
; We now confine our discussion to the structure and use of each line.
; Each cl-cache line consists of a defrec with the following fields. WARNING:
; The fields in this raw Lisp defrec are destructively updated!
; - :lambda-object is a lambda object (not necessarily well-formed)
; - :status is a keyword token that explains how to interpret the line;
; the status is one of :GOOD, :BAD, :UGLY, or :UNKNOWN.
; :GOOD means the lambda-object is well-formed and Common Lisp compliant in
; the current world
; :BAD means the lambda-object is not well-formed or not Common Lisp
; compliant in the current world, but that (with high probability) there is
; a world in which it is well-formed and compliant
; :UGLY means the lambda-object is so ill-formed it can never be :GOOD
; :UNKNOWN means that we do not know the status of this object in the
; current world and leave it to apply$-lambda to determine.
; - :absolute-event-number is the max-absolute-event-number of the world in
; which the guards were proved and thus the world in which the line became
; :GOOD. If the line's status is not :GOOD, the :absolute-event-number is
; nil.
; - :extracts is a list of splo-extracts-tuples that allows us to re-check
; well-formedness and guard verification without re-parsing the lambda
; expression
; - :problem is an object that indicates why the :status isn't :GOOD
; - :hits is the number of times we have fetched this line
; - :guard-code is the compiled code for (LAMBDA formals guard) or nil
; - :lambda-code is the compiled code of lambda-object or nil
; Generally speaking, apply$-lambda does the work to correctly set the status
; of a cache line when the line's lambda-object is applied. The resultant
; status set by apply$-lambda is always :GOOD, :BAD, or :UGLY. We give more
; details below. But whenever a lambda object is guard verified, either by
; apply$-lambda (where proofs are limited to Tau), the guard verification of a
; defun containing the object, or because verify-guards was called explicitly
; on the object, a :GOOD cache line is set up. However, every time the world
; is extended, the status of every :BAD line is set to :UNKNOWN because, who
; knows, perhaps the formerly :BAD object is :GOOD in this new world. Every
; time the world is retracted, the status of every :GOOD line is set to
; :UNKNOWN if its :absolute-event-number is greater than the max such number in
; the retracted world, for symmetric reasons.
; We now describe how apply$-lambda handles the lambda object it is given.
; Some objects reaching apply$-lambda carry a special mark, *lambda$-marker*,
; as described in the Essay on Lambda Objects and Lambda$. These objects are
; not actually lambda objects but marked untranslated lambda$ expressions.
; Using the lambda$-alist, apply$-lambda converts these objects to lambda
; objects. Next, apply$-lambda fetches (or creates and adds) the cache line
; for the object. Thereafter, its actions depend on the status of the line.
; In the discussion below:
; - w is understood to be the current world for the apply$-lambda
; - ``Run *1*'' means use *1*apply$-lambda to apply the :lambda-object to the
; actuals
; - ``Run code'' means use raw Lisp's apply to run the compiled :guard-code on
; the actuals to the apply$-lambda. If the guard-code succeeds, use raw
; Lisp's apply to run the :lambda-code on the actuals. If the :guard-code
; fails, run *1*.
; :GOOD - the :lambda-object is well-formed and guard verified in w: run code.
; :BAD - the :lambda-object was (probably) once known to be well-formed and
; Common Lisp compliant in some world but not in w; (the ``probably''
; comes from the possibility that guard obligations are unprovable,
; i.e., contradictory, in which case it would have been better to assign
; status :UGLY but we don't try to do that!): run *1*
; :UGLY - the :lambda-object is hopelessly ill-formed: run *1*.
; :UNKNOWN - work to establish a proper status (:GOOD or :BAD) in w for this
; object and then apply the object as appropriate for that new status.
; The ``work to establish'' :GOOD or :BAD status is as follows. A quick check
; is to see whether the :lambda-object is on common-lisp-compliant-lambdas of
; w. If so, its status is :GOOD and we're done. If not, we first check
; whether the object is well-formed in w. This can be done more efficiently
; than might at first appear since we know the object had status :GOOD or :BAD
; in some other world and hence is basically the right shape. The :extracts of
; the cache line give us the relevant TYPE expressions, guards, and bodies
; without having to re-parse the object. (:Extracts is a list of
; splo-extracts-tuples extracted from the lambda object and from every lambda
; object within it.) We basically make termp checks on these components,
; additionally checking Common Lisp compliant symbol-classes for the functions
; involved in the guard and body, and tameness of the body. Provided these
; checks succeed we generate the guard obligations of the :lambda-object and we
; attempt to prove them with the Tau System. If this succeeds the object is
; :GOOD, otherwise it is :BAD.
; The algorithm sketched above is implemented by fetch-cl-cache-line.
; Through Version-8.1, the cache management generated warnings about whether we
; were running *1* apply$-lambda, i.e., Slow APPLY$ Warning. There was a
; rather subtle use of eq versus equal that allowed us to keep these warnings
; to a minimum when the very same lambda was repeatedly applied as by a mapping
; function. This version does not issue warnings. We decided instead to
; provide the raw Lisp function print-cl-cache (with a :logic mode interface)
; to print out the cache. If the user experiences poor performance he or she
; can inspect the cache for :BAD or :UGLY lambdas with high hit counts and look
; at the :problem field of the relevant lines to see what's wrong with each
; expensive lambda.
; Lines are kept in access order, most recent first. The first line in the
; sequence that is nil indicates that subsequent lines are irrelevant. We
; actually arrange that all subsequent lines are also nil, but that is probably
; not important.
; We handle the possibility of interrupts rendering the cache inconsistent.
; Here is how: Before we modify the cache we render the global cache invalid
; (by setqing it to its size) and restore the global cache only when
; modification is complete.
; Specifically, imagine that a cache manipulation is interrupted and aborted by
; ctrl-c or an error. The cache is left invalid. The next time we attempt to
; manipulate the cache we will re-initialize it. All old caching is lost.
; For efficiency, we often destructively modify the cache.
; History of the cl-cache
; When we first started working on optimizing apply$-lambda in late 2017 for
; release with Version_8.0 in December, we considered four alternatives:
; (a) Do Nothing: Let ACL2 behave normally by running the *1* code for
; APPLY$-LAMBDA, which just interprets the lambda-body with EV$.
; (b) Compile but Don't Cache: recognize and compile unrestricted lambdas every
; time they're applied but do not cache the test or compilation results. Note:
; Version_8.0 did not support declarations in lambda objects and hence all
; lambda objects were considered to have a guard of T, hence the term
; ``unrestricted lambda.'' Clearly, the hope behind this approach was that the
; increased speed of executing compiled code overcomes the cost of recognition
; and compilation.
; (c) Fast-Alist Cache: recognize and compile unrestricted lambdas, caching
; recognized lambdas with their compiled code via a fast-alist. Finding a
; lambda in the cache means it satisfies the ``variety of important
; properties'' and gives us its compiled version. Lambdas without those
; properties are cached separately to avoid having to recognize them again.
; (d) Home-Grown Cache: Like (c) except we rolled our own cache.
; Experiments with all four scenarios are detailed in a long comment below
; under the name Historical Essay on the Performance of APPLY$. An executive
; summary of those experiments is: Our Fast-Alist cache has performed about 50%
; slower than our Home-Grown cache. The Do Nothing and the Compile but Don't
; Cache approaches are much worse. But many things affect performance.
; Choosing the best implementation depends on the expected size of the LAMBDAs,
; whether unrestricted LAMBDAs occur frequently enough to matter, frequency of
; application of a given LAMBDA, etc., how often the world changes
; (invalidating or at least complicating the cache), etc.
; Our tests were with one relatively small LAMBDA,
; (LAMBDA (X) (BINARY-+ '3 (BINARY-* '2 X))) ; ``bad variant''
; and its ``good variant'' with a FIX around the use of X. The bad variant
; fails to satisfy the properties required for compilation (it has a
; non-trivial guard obligation); the good variant is unrestricted. We then
; tested (sum *million* <lambda>), where *million* is a list of the first 1
; million naturals. We focused mainly on the good variant because we are
; interested in the cost of recognizing, compiling, and caching suitable
; lambdas. But note that both Scenario (c) and (d) pay the price of
; recognizing and compiling the good lambda just once in the (sum *million*
; <lambda>) test and then find that same (in fact, EQ) <lambda> in the cache
; 999,999 times while the world remains unchanged.
; Our experiments indicate that if we are going to apply a lambda fewer than
; about 50 times, recognizing and compiling good ones is not worth it: we could
; just interpret the lambda body with EV$ in the same amount of time.
; Our current implementation choice (Home-Grown Cache) is thus skewed toward
; the fast execution of mapping functions, like sum, over large domains. This
; is motivated by the original problem that inspired work on apply$: how to
; provide robust and convenient iterative primitives for interactive use to the
; ACL2 user.
; See the Historical Essay on the Performance of APPLY$ for details of our
; original experiments with designs (a)-(d).
; Two severe disadvantages of our original Home-Grown Cache were that it
; maintained only 3 cache lines (i.e., was capable of remembering only three
; compiled lambdas) and was cleared every time the world changed.
; The following historical note explained our thinking at the time.
; The choice of a small, fixed number of cache lines makes the implementation
; faster because each line is a separate raw Lisp variable, but at the
; expense of more voluminous code as we check and fill or empty each line
; with code that looks much like the code for the line before it. But the
; small, fixed number of lines was considered adequate for executing
; ``typical'' mapping expressions, like (sum (collect ... '(lambda (x) ...))
; '(lambda (y) ...)). Both lambdas would be compiled and cached for the
; duration of the evaluation. We don't anticipate many interactively
; submitted ground mapping expressions to involve more than 3 lambdas. An
; advantage of the Fast-Alist Cache is that it maintains an arbitrary number
; of cache lines. We are content at the moment to recompile such expressions
; every time the world changes.
; After considering design options (a)-(d) mentioned above we moved to a
; structure that contains 1000 cache lines (by default, but any size of at
; least 3 is acceptable) arranged as a ring. The clever thing about the
; ring-based cache is a destructively modified ring of lines. This allowed the
; most recently added/hit entry to always be at the ``front'' and the rest of
; the entries ordered the same as before. But the content of a ``line'' has
; evolved considerably as we added declarations to lambda objects.
; In the earliest implementations, a line was just a pair consisting of an
; unrestricted (implicit :guard of t) lambda object and its compiled
; counterpart. With the introduction of declare into lambda objects it was
; advantageous for the cache to contain parts of the lambda object -- e.g., one
; example TYPE expression, (p X), for each (TYPE (SATISFIES p) ...); the guard
; term; and the body term -- to make it faster to check well-formedness. In
; the first such elaboration, each line contained a status (:GOOD, :BAD, or
; :UGLY) and a logical world, w, with the meaning that the status was known to
; be accurate in that logical world. When apply$-lambda accessed the cache
; line for a lambda object it would check whether the current world was an
; extension of w and, if so, a :GOOD status meant the object was well-formed
; and compliant in the current world. However, if the current world was not an
; extension of w, the status had to be recomputed. The status of :BAD objects
; had to be recomputed every time a world different from w was seen. :GOOD
; objects whose guard was true on the actuals were applied by running compiled
; code; :BAD objects were run by interpreting the body using *1* apply$-lambda
; (i.e., apply$-lambda-logical). :UGLY lambdas were so ill-formed they could
; never be good.
; The main motivation for the :GOOD/:BAD idea was in an example like (map
; '<obj> '(x1 x2 ...)), where <obj> was :GOOD in the line's world, w, but w was
; not an extension of the world in which the apply$-lambda was called. In this
; case, the attempt to apply$-lambda <obj> to x1 would reset the :status to
; :BAD (if indeed, <obj> was not well-formed and compliant in the current
; world) ans set the line's world to the current world; then when <obj> was
; applied to x2, no additional checks were made: <obj> is :BAD in the world of
; the line and *1* apply$-lambda was used.
; But we didn't like this structure because it forced us to save up to 1000
; worlds, possibly hanging on to much garbage; in addition, it required testing
; whether one world is an extension of another every time we applied a lambda
; object. (In truth, we never tested this design extensively. In fact, most
; worlds stored in the cache are almost certainly just tails of the current
; world and the test for extension was pretty fast because we reset the stored
; world to the current world whenever the test succeeded so that subsequent
; extension tests succeeded immediately. But we just thought the design
; inelegant.)
; Thus, we adopted a different scheme in use as of this writing (Fall, 2018).
; There are no worlds stored in lines and the status flag, :GOOD, :BAD, :UGLY,
; and :UNKNOWN, is always accurate with respect to the current world. This
; forces us to manipulate the cache a little bit on every world extension
; (e.g., defthm) or retraction (e.g., :ubt). On extensions, we change every
; :BAD to :UNKNOWN; on retractions we turn some :GOOD lines to :UNKNOWN.
; Whenever apply$-lambda sees an object of :UNKNOWN status is updates the
; status to the current world.
; As of this writing we have not benchmarked our ring-based cache since we
; moved to support declarations in lambda objects and we elaborated cache lines
; from pairs to defrecs. The reason is pretty compelling: we had no choice but
; to move to a more elaborate cache line to support non-trivial guards and
; their apply-time guard verification and guard checking. As the design has
; evolved it got harder to compare it to old designs because, for example,
; we'll see a slowdown on every world extension and retraction whether apply$
; is involved or not. The cache size and the complexity of the guards and
; guard verification proofs also affects timing. So our current thinking is
; simply: build the best cache we can imagine, get LOOP working (it's not part
; of the system as of Fall, 2018) solve the ``LOOP guard verification'' problem
; (see below), and then test to see if the cache is adequate.
; [end of history]
; A Collection of TODOs related to Compilation and Caching
; TODO: Because of the uncertainty regarding how mapping functions will
; actually be used, it might be worthwhile to implement a user-settable flag
; that specifies whether and how APPLY$-LAMBDA is cached. Scenarios (a), (c),
; and (d) above immediately come to mind as optimal depending on usage.
; TODO: Perhaps we should implement a better way to reset or clear the cache.
; Right now, we just tell the user to drop into raw Lisp and set *cl-cache* to
; a number (at least 3) that is the new size. This clears the cache and allows
; it to be reallocated (upon the first fetch from it) to the given size. But
; perhaps it would be better to provide a function for extending or retracting
; the cache size while preserving the contents or clearing it. Such a function
; is akin to cl-cache-init and we left a comment there: ``We considered making
; this function available to the user. However, it seemed a bad idea to allow
; this to be run inside a book (via make-event) and thus affect the rest of the
; session.'' So think about this before giving the user a more sophisticated
; way to grow the cache.
; TODO: As noted in the Essay on Lambda Objects and Lambda$, it is possible for
; a good looking lambda object to have unprovable guard conjectures and thus be
; classed as :BAD, causing us to try to prove the conjectures every time the
; lambda is applied after an extension. It would be cheaper to classify the
; object as :UGLY but that requires solving the decidability problem. However,
; we could change the meaning of :UGLY from ``:GOOD in no possible world'' to
; ``we've given up trying to prove it :GOOD.'' We could then switch :BAD to
; :UGLY on, say, the 10th unsuccessful try to prove the guards. We could set
; the :problem to something informative like, :GIVEN-UP-TRYING-TO-
; VERIFY-GUARDS. This just dooms the object to be applied with *1*
; apply$-lambda. If the user noticed the problem and knew how to prove the
; guards he or she could use verify-guards to set the :status to :GOOD.
; TODO: Solve the LOOP guard verification problem! Under some conditions (SUM
; 'fn lst) could be replaced, under the hood in raw Lisp, by (LOOP FOR X IN lst
; SUM (fn X)). Note that execution of the LOOP version of our (sum lambda
; *million*) test is 10 times faster than Scenario (d).
; But this requires knowing that every element of lst satisfies the guard of fn
; and that under those conditions fn always returns a number. Those two
; statements about fn and lst constitute what might be called the ``proper
; guard for SUM.'' Otherwise, fn has to check its guard on every element of
; lst, and SUM has to check that the output of fn is a number before summing it
; into the answer.
; So perhaps the proper guard for (SUM fn lst) is something like:
; (and (tame-fn-of-n-args fn 1) ; [1]
; (all (guard-of-fn fn) lst) ; [2]
; (implies (apply$ (guard-of-fn fn) (list x)) ; [3]
; (acl2-numberp (apply$ fn (list x)))))
; where [1] says fn is a tame function of 1 argument, [2] says that every
; element of lst satisfies the guard of fn, and [3] says when the guard of fn
; holds on its input, fn returns a number. Of course, the shocking thing about
; this guard is that it presumes we have a function, here named guard-of-fn,
; that can take a function or lambda object and return its guard as a lambda
; object. This is akin to the kind of information about functions only
; accessible via warrants and so suggests an extension of badges to contain
; guard objects. Furthermore, we somehow have to know these guard objects are
; guard verified and themselves have a guard of T.
; But if some instance of (sum fn lst) were known to satisfy the proper guard,
; we could replace (sum fn lst) by the obvious loop statement in raw Lisp.
; Other problems that come up in trying to solve the LOOP guard verification
; problem are
; (a) How do you check a guard like that shown above? [1] is easy, [2] might
; require running the guard on every element of lst, which might not be any
; faster than what we do now, and [3] contains a free variable, x, and is
; probably ``checked'' by proof rather than evaluation.
; (b) We'd need strategies for proving and using these quantified hypotheses.
; The obvious strategy is something like pick-a-point. If you want to prove
; (all p lst), then prove (implies (member e lst) (p e)).
; (c) Not every mapping function visits every element of the domain over which
; it is mapping. There is a sense in which the function named all above is a
; McCarthyesque ``derived function'' from sum. But perhaps if the goal is only
; to replace certain mapping functions by certain LOOP statements we just build
; in the proper guard for each built-in optimized mapping function and forget
; about how to figure out the proper guard in general!
; TODO: Consider adding a cache for apply$ on symbols. What has to happen for
; (apply$ 'sym args) to run in the evaluation theory?
; - We need apply$ to reduce to apply$-userfn.
; - We need to look up the attachment of apply$-userfn, to get
; doppelganger-apply$-userfn, which amounts to evaluating special
; variable (attachment-symbol apply$-userfn) =
; ACL2_*1*_ACL2::APPLY$-USERFN (see throw-or-attach).
; - Apply$-userfn calls doppelganger-apply$-userfn.
; ... then, looking at the defun of doppelganger-apply$-userfn:
; - We need to look up the value of special variable *aokp*.
; - We need to compute query-badge-userfn-structure.
; - We need to call apply.
;
; Maybe we can install a cache on apply$-userfn (or its concrete counterpart?)
; to short-circuit a lot of this?
; [end of todos]
; Here is our cache structure. The value of our global cache variable
; (*cl-cache*, introduced further below) is either such a structure or else is
; an integer, greater than 2, representing the intended size of such a
; structure.
(defrec cl-cache
; Warning: Keep this in sync with access-cl-cache.
; Invariants on the fields include:
; - :Size is a positive integer.
; - :Alist is a circular list that repeats after :size elements; each of its
; elements is a cl-cache-line record (see below) or nil; and for 0 <= i < j <
; size, if (nth i alist) = nil then (nth j alist) = nil.
; - :Last is (nthcdr (1- size) alist).
; Note that :size and :last are constant fields for a given cl-cache. However,
; the cdr of :last can change.
; car cadr cddr
(alist size . last)
t)
(defmacro access-cl-cache (c field)
; Keep this in sync with the defrec for cl-cache.
; We are going to destructively update the fields in the cl-cache. This macro
; allows us to write
; (setf (access-cl-cache c :alist) val)
; It would have been nice to use ACL2's access macro
; (setf (access cl-cache c :alist) val)
; but that expands to a LET with the car/cdr nest inside and setf cannot handle
; it.
(case field
(:alist `(car ,c))
(:size `(cadr ,c))
(:last `(cddr ,c))
(otherwise (er hard 'access-cl-cache "Illegal field name, ~x0." field))))
(defrec cl-cache-line
; Warning: Keep this in sync with access-cl-cache-line!
; Invariants on the fields include
; - :lambda-object is a lambda object (not necessarily well-formed)
; - :status is a keyword token that explains (below) how to interpret the line
; - :absolute-event-number is nil or a number. This entry is a number iff the
; status is :GOOD and the number is the greatest absolute-event-number of
; the world at the time the line was detected as :GOOD.
; - :extracts is a list of splo-extracts-tuples that allows us to re-check
; well-formedness and guard verification without re-parsing the lambda
; expression
; - :problem is an object that indicates why the :status is :BAD
; - :hits is the number of times we have fetched this line
; - :guard-code is the compiled code for (LAMBDA formals guard)
; - :lambda-code is the compiled code of the :lambda-object
; caaar cdaar cadar cddar
(((lambda-object . status) . (absolute-event-number . extracts))
.
((problem . hits) . (guard-code . lambda-code)))
; caadr cdadr caddr . cdddr
t)
(defmacro access-cl-cache-line (line field)
; Warning: Keep this macro in sync with the record declaration cl-cache-line!
; We are going to destructively set the fields in cl-cache-line records.
(case field
(:lambda-object `(caaar ,line))
(:status `(cdaar ,line))
(:absolute-event-number `(cadar ,line))
(:extracts `(cddar ,line))
(:problem `(caadr ,line))
(:hits `(cdadr ,line))
(:guard-code `(caddr ,line))
(:lambda-code `(cdddr ,line))