forked from google-deepmind/deepmind-research
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rewrite-tree.rkt
870 lines (802 loc) · 33.3 KB
/
rewrite-tree.rkt
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
#lang racket/base
;***************************************************************************************;
;**** Binary Rewrite Tree ****;
;***************************************************************************************;
(require bazaar/cond-else
bazaar/mutation
(except-in bazaar/order atom<=>)
define2
define2/define-wrapper
global
racket/file
racket/list
racket/string
satore/Clause
satore/clause-format
satore/clause
satore/misc
satore/trie
satore/unification)
(provide (all-defined-out)
(all-from-out satore/trie))
;===============;
;=== Globals ===;
;===============;
(define-counter n-rule-added 0)
(define-counter n-binary-rewrites 0)
(define-global:boolean *bounded-confluence?* #true
'("When performing confluence, should the size of the critical pairs "
"be bounded by the size of parent rules?"))
(define-global *confluence-max-steps* 10
"Maximum number of confluence steps"
number?
string->number)
;====================;
;=== Rewrite tree ===;
;====================;
;; atom<=> : comparator?
;; dynamic-ok? : boolean? ; whether we keep dynamic rules
;; rules-file : file? ; if not #false, loads rules from the given file
(struct rewrite-tree trie (atom<=> dynamic?))
;; rewrite-tree constructor.
;;
;; constructor: procedure?
;; atom<=> : comparator?
;; dynamic-ok? : boolean?
;; rules-file : file?
;; other-args : list?
;; -> rewrite-tree?
(define (make-rewrite-tree #:? [constructor rewrite-tree]
#:! atom<=>
#:? [dynamic-ok? #true]
#:? [rules-file #false]
. other-args)
(define rwtree
(apply make-trie
#:constructor constructor
#:variable? Var?
atom<=>
dynamic-ok?
other-args))
(when rules-file
(load-rules! rwtree #:rules-file rules-file #:rewrite? #true))
rwtree)
;; Returns a new rewrite-tree.
;; Duplicates the structure of rwtree, but the Clauses and rules are shared.
;; This means that the Clause ancestors of the rules are preserved.
;;
;; rwtree : rewrite-tree?
;; -> rewrite-tree?
(define (rewrite-tree-shallow-copy rwtree)
(define new-rwtree (make-rewrite-tree #:atom<=> (rewrite-tree-atom<=> rwtree)
#:dynamic-ok? (rewrite-tree-dynamic? rwtree)))
(for ([rl (in-list (rewrite-tree-rules rwtree))])
(add-rule! new-rwtree rl))
new-rwtree)
;; Returns the list of values that match the literal t.
;;
;; rwtree : rewrite-tree?
;; t : any/c
;; -> list?
(define (rewrite-tree-ref rwtree t)
; Node values are lists of rules, and trie-ref returns a list of node-values,
; hence the append*.
(append* (trie-ref rwtree t)))
;; Clause : The Clause from which this rule originates
;; NOTICE: Clause may subsume the rule, and can be *more* general if the rule is asymmetric.
;; from-literal : heavy-side
;; to-literal : light side
;; to-fresh : variables of to-literal not in from-literal that need to be freshed
;; after applying the rule
;;
;; Clause : Clause?
;; rule-group : rule-group?
;; from-literal? : literal?
;; to-literal? : literal?
;; to-fresh : unused
;; static? : boolean?
(struct rule (Clause rule-group from-literal to-literal to-fresh static?)
#:prefab)
;; A rule group holds all the rules that have been created by a single call to
;; Clause->rules.
;; A rule also has a reference to its rule group, so it makes it easy to find
;; the other rules of the same group from a given group.
;; A single group can have 2 or 4 rules: 2 for static rules or dynamic self-converse rules,
;; and 4 for dynamic non-self-converse rules.
;;
;; Clause : (or/c false/c Clause?)
;; Converse : Clause?
;; rules: (listof rule?)
(struct rule-group (Clause Converse [rules #:mutable]) #:prefab)
;; Rule constructor.
;;
;; C : Clause?
;; rule-gp : rule-group?
;; from : literal?
;; to : literal?
;; static? : boolean?
;; -> rule?
(define (make-rule C rule-gp from to static?)
(define-values (lit1 lit2) (apply values (fresh (list from to))))
(rule C rule-gp lit1 lit2 (variables-minus lit2 lit1) static?))
;; Returns whether rule is a unit rule, that is, if the `to` literal
;; is either ltrue or lfalse.
;;
;; rl : rule?
;; -> any/c
(define (unit-rule? rl)
(memq (rule-to-literal rl) (list lfalse ltrue)))
;; Returns whether rl is a 'tautology', that is, if its `to` and `from`
;; literals are syntactically equal.
;;
;; rl : rule?
;; -> boolean?
(define (rule-tautology? rl)
(equal? (rule-from-literal rl) (rule-to-literal rl)))
;; Returns whether the rule is left linear, that is, if each variable
;; occurs at most once in the `from` literal.
;;
;; rl : rule?
;; -> boolean?
(define (left-linear? rl)
(define occs (var-occs (rule-from-literal rl)))
(for/and ([(v n) (in-hash occs)])
(<= n 1)))
;; Returns whether rl1 subsumes rl2.
;;
;; rl1 : rule?
;; rl2 : rule?
;; -> any/c
(define (rule-subsumes rl1 rl2)
(define subst (left-unify (rule-from-literal rl1) (rule-from-literal rl2)))
(and subst (left-unify (rule-to-literal rl1) (rule-to-literal rl2) subst)))
;; A binary Clause [lit1 lit2] is treated as an *equivalence* (4 implications)
;; rather than two implications,
;; which means that a converse Clause or one that subsumes the latter MUST have been proven too
;; before adding the rules.
;; Thus: (Clause->rules C <=>) == (Clause->rules (make-converse-Clause C) <=>).
;; That is, if C = ~q | p, the converse C' = q | ~p MUST have been proven.
;; We MUST add the four rules at the same time, because the if we add just the (max 2) rules
;; corresponding to the implications of C only, and then add the rules for C',
;; then C' is going to be rewritten to a tautology before it has a chance to propose its rules
;; (since resolution(C, converse(C)) = tautology).
;; The number of returned rules is either 2 or 4.
;; Conv is either the converse Clause of C, or a Clause that subsumes it, and is used
;; as a parent reference (for proofs), even if converse(C) is more specific than Conv.
;; Conv is *not* used to generate the rules themselves (only C), so if Conv is more general than C
;; (as for asymmetric rules) the generated rules will *not* be more general than those of C.
;;
;; C : Clause?
;; Conv : Clause?
;; atom<=> : comparator?
;; dynamic-ok? : boolean?
;; -> (listof rule?)
(define (Clause->rules C Conv #:! atom<=> #:? [dynamic-ok? #true])
(define cl (Clause-clause C))
(define unit? (unit-Clause? C))
(define lit1 (first cl))
(define lit2 (if unit? lfalse (second cl)))
(define rg (rule-group C Conv #false))
; Not the most efficient. The last 2 cases can be deduced from the first 2,
; as long as atom<=> does not count polarity (which SHOULD be the case).
(define rules
(for/list ([parent
(in-list (cond [unit?
(list C #false C)]
[(eq? C Conv) ; self-converse, no need to duplicate the rules
(list C C)]
[else
(list C C Conv Conv)]))]
[from (in-list (list (lnot lit1) (lnot lit2) lit1 lit2))]
[to (in-list (list lit2 lit1 (lnot lit2) (lnot lit1)))]
; if Conv is #false for example, skip it (see force-add-binary-Clause!)
; also useful for unit?
#:when parent
[c (in-value (atom<=> from to))]
#:when (and (not (order<? c)) ; wrong direction
; either we keep dynamic rules or the rule is static
; TODO: Tests
(or dynamic-ok? (order>? c))))
; rule cannot be oriented to → from,
; so the rule from → to is valid.
; It is 'static' if it can be oriented from → to, 'dynamic' otherwise.
(make-rule parent rg from to (order>? c))))
(set-rule-group-rules! rg rules)
rules)
;; a is left-unify< to b if a left-unifies with b.
;;
;; comparator?
;; literal? literal? -> (one-of '< '> '= #false)
(define left-unify<=> (make<=> left-unify) ; left-unify is <=?
; equivalent to:
#;(if (left-unify c1 c2)
(if (left-unify c2 c1)
'=
'<)
(if (left-unify c2 c1)
'>
#false)))
;;; What we want to assess is whether
;;; when applying rl1 to any clause c to get c1,
;;; c1 is always necessarily better than c2 when applying rl2 to c.
;; Compares 2 rules. May help decide if one should be discarded.
;; rl1 <= rl2 if the heavyside of rl1 subsumes that of rl2,
;; and its light side is no heavier than that of rl2.
;; NOTICE: A return value of '= does not mean that rl1 and rl2 are equal or equivalent;
;; it only means that either can be used. It does mean that their heavy sides are equivalent though.
;;
;; rl1 : rule?
;; rl2 : rule?
;; atom<=> : comparator?
;; -> (one-of '< '> '= #false)
(define (rule<=> rl1 rl2 atom<=>)
(chain-comparisons
(left-unify<=> (rule-from-literal rl1) (rule-from-literal rl2))
(atom<=> (rule-to-literal rl1) (rule-to-literal rl2))))
;; Returns a rule of rwtree that subsumes rl if any.
;;
;; rwtree : rewrite-tree?
;; rl : rule?
;; -> (or/c #false rule?)
(define (find-subsuming-rule rwtree rl)
(for/or ([rl2 (in-list (rewrite-tree-ref rwtree (rule-from-literal rl)))])
(and (rule-subsumes rl2 rl) rl2)))
;; If there is a substitution α such that rule-from α = lit, then rule-to α is returned,
;; otherwise #false is returned.
;; If the rule introduces variables, these are freshed.
;;
;; rl : rule?
;; lit : literal?
;; -> literal?
(define (rule-rewrite-literal rl lit)
(define subst (left-unify (rule-from-literal rl) lit))
(cond [subst
; NOTICE: We need to fresh the variables
; that are introduced by the rule.
; Ex:
; clause: [(p a) (p b)] rule: (p X) -> (q Y)
; Then this must be rewritten to:
; [(q A) (q B)] and NOT [(q Y) (q Y)]
(for ([v (in-list (rule-to-fresh rl))])
(subst-set!/name subst v (new-Var)))
(left-substitute (rule-to-literal rl) subst)]
[else #false]))
;; Recursively rewrites the given literal lit from the rules in rwtree.
;; Returns the new literal and the sequence of rules used to rewrite it
;; (may contain duplicate rules).
;; lit: literal?
;; C: The Clause containing the literal lit. Used to avoid backward rewriting C to a tautology.
;;
;; rwtree : rewrite-tree?
;; lit : literal?
;; C : Clause?
(define (binary-rewrite-literal rwtree lit C)
(define atom<=> (rewrite-tree-atom<=> rwtree))
(let loop ([lit lit] [rules '()])
(define candidate-rules (rewrite-tree-ref rwtree lit))
; Find the best rewrite of lit according to atom<=>.
; Q: Can we rewrite more greedily, that is, applied each rewrite asap?
; A: if the set of rules is confluent, yes, since they are all equivalent (although
; choosing the one that leads to the smallest literal may still be faster)
; But if the rules are not confluent then we may have a problem.
(for/fold ([best-lit lit]
[best-rule #false]
#:result (if best-rule
(loop best-lit (cons best-rule rules)) ; try once more
(values best-lit (reverse rules)))) ; no more rewrites
([r (in-list candidate-rules)]
#:unless (let ([g (rule-rule-group r)])
; Don't rewrite yourself or your converse!
(or (eq? C (rule-group-Clause g))
(eq? C (rule-group-Converse g)))))
(define new-lit (rule-rewrite-literal r lit))
(if (and new-lit (order<? (atom<=> new-lit best-lit)))
(values new-lit r)
(values best-lit best-rule)))))
;; Returns a rewritten clause and the set (without duplicates) of rules used for rewriting.
;; Rewriting a clause is a simple as rewriting each literal, because
;; only left-unification is used, so the substitution cannot apply to the rest of the clause.
;; NOTICE: The variables of the resulting clause are not freshed.
;;
;; rwtree : rewrite-tree?
;; cl : clause?
;; C : Clause?
(define (binary-rewrite-clause rwtree cl C)
(let/ec return
(for/fold ([lits '()]
[rules '()]
#:result (values (sort-clause lits)
(remove-duplicates rules eq?)))
([lit (in-list cl)])
(define-values (new-lit new-rules) (binary-rewrite-literal rwtree lit C))
(values (cond [(ltrue? new-lit) (return (list new-lit) new-rules)] ; tautology shortcut
[(lfalse? new-lit) lits]
[else (cons new-lit lits)])
(append new-rules rules)))))
(define Clause-type-rw 'rw)
;; Clause? -> boolean?
(define (Clause-type-rw? C)
(eq? (Clause-type C) Clause-type-rw))
;; Returns a new Clause if C can be rewritten, C otherwise.
;; The parents of the new Clause are C followed by the set of clauses from which the rules
;; used for rewriting C originate.
;;
;; rwtree : rewrite-tree?
;; C : Clause?
;; -> Clause?
(define (binary-rewrite-Clause rwtree C)
(define-values (new-cl rules) (binary-rewrite-clause rwtree (Clause-clause C) C))
(cond [(empty? rules)
C]
[else
(++n-binary-rewrites)
(make-Clause (clause-normalize new-cl)
(cons C (remove-duplicates (map rule-Clause rules) eq?))
#:type Clause-type-rw)]))
;; Returns whether the clause cl would be rewritten. Does not perform the rewriting.
;;
;; rwtree : rewrite-tree?
;; cl : clause?
;; C : Clause?
;; -> boolean?
(define (binary-rewrite-clause? rwtree cl C)
(for/or ([lit (in-list cl)])
; We need to perform the literal rewriting anyway, because
; we need to check if the result is order<?
(define-values (new-lit new-rules) (binary-rewrite-literal rwtree lit C))
(not (empty? new-rules))))
;; Returns whether the Clause C would be rewritten. Does not perform the rewriting.
;;
;; rwtree : rewrite-tree?
;; C : Clause?
;; -> boolean?
(define (binary-rewrite-Clause? rwtree C)
(binary-rewrite-clause? rwtree (Clause-clause C) C))
;; Unconditionally adds the rule rl to the rewrite-tree rwtree,
;; and removes from rwtree the rules that are subsumed by rl
;;
;; rwtree : rewrite-tree?
;; rl : rule?
;; -> void?
(define (add-rule! rwtree rl)
(define atom<=> (rewrite-tree-atom<=> rwtree))
(unless (or (rule-tautology? rl)
(find-subsuming-rule rwtree rl))
;; Remove existing rules that are subsumed by rl.
(define from-lit (rule-from-literal rl))
(trie-inverse-find rwtree from-lit
(λ (nd)
(define matches (trie-node-value nd))
(when (list? matches) ; o.w. no-value
(define new-value
(for/list ([rl2 (in-list matches)]
#:unless (rule-subsumes rl rl2))
rl2))
;; TODO: If new-value is '(), the node should be removed from the trie,
;; TODO: along with any similar parent. (this could be made automatic?)
(set-trie-node-value! nd new-value))))
;; NOTICE: No need to backward-rewrite the rules.
;; Resolution (in the saturation loop) will take care of generating new rules
;; and rewriting both their heavy sides and the light sides,
;; while add-rule! takes care of subsumption on the heavy side.
;; Letting the resolution loop take care of this is much safer, as it ensures
;; that rwtree and utree are in sync, and that rwtree is not going to rewrite
;; clauses before utree has a chance to generate new candidates via resolution.
;; That is, if we only *remove* stuff from rwtree, and not do the job of resolution
;; (apart from forward rewriting), then we should be fine?
(++n-rule-added)
(trie-insert! rwtree from-lit rl)))
;; Unconditionally removes a single (oriented) rule from the tree.
;; Use with caution and see instead remove-rule-group!.
;;
;; rwtree : rewrite-tree?
;; rl : rule?
;; -> void?
(define (remove-rule! rwtree rl)
(trie-find rwtree (rule-from-literal rl)
(λ (nd)
(define old-value (trie-node-value nd))
(when (list? old-value) ; o.w. no-value
;; TODO: If new-value is '(), the node should be removed from the trie,
;; TODO: along with any similar parent. (this could be made automatic?)
(set-trie-node-value! nd (remove rl old-value eq?))))))
;; Removes a group of rules that were added at the same time via Clause->rules
;; (via add-binary-Clause!).
;;
;; rwtree : rewrite-tree?
;; gp : rule-group?
;; -> void?
(define (remove-rule-group! rwtree gp)
(for ([rl (in-list (rule-group-rules gp))])
(remove-rule! rwtree rl)))
;; Turn the unit Clause into a binary Clause before adding it.
;; As for self-converses, we say that C is its own converse but that's a lie;
;; This is to avoid problems and specific cases when loading/saving rules.
;; A self-converse rule is necessarily dynamic (unless commutativity can be handled statically?),.
;; A unit rule is necessarily static (since $false is the bottom element).
;; Hence a 'self-converse' static rule is necessarily a unit rule (for now).
;;
;; rwtree : rewrite-tree?
;; C : Clause?
;; rewrite? : boolean?
;; -> void?
(define (rewrite-tree-add-unit-Clause! rwtree C #:? rewrite?)
(unless (unit-Clause? C) (error "Non-unit Clause v" C))
(rewrite-tree-add-binary-Clause! rwtree C C #:rewrite? rewrite?))
;; Rewriting of the clause C must be done prior to calling this function.
;; Conv: Converse of C. See Clause->rules.
;; Returns the new rules (use these to obtain the rewritten Clauses).
;;
;; rwtree : rewrite-tree?
;; C : Clause?
;; Conv : Clause?
;; rewrite? : boolean?
;; -> void?
(define (rewrite-tree-add-binary-Clause! rwtree C Conv #:? [rewrite? #true])
(cond
[(Clause-binary-rewrite-rule? C) ; already added as a rule in the past?
(when-debug>= steps
(displayln "Clause has already been added before. Skipping."))
'()]
[else
(let-values
([(C Conv)
(cond/else
[(not rewrite?) (values C Conv)]
#:else ; Rewriting
(define C2 (binary-rewrite-Clause rwtree C))
(when-debug>= steps
(displayln "Considering binary Clause for a rule (before rewriting):")
(display-Clause-ancestor-graph C #:depth 0)
(displayln "After rewriting:")
(display-Clause-ancestor-graph C2)
(when (Clause-tautology? C2)
(displayln "...but tautology.")))
#:cond
[(Clause-tautology? C2)
(values #false #false)]
#:else
; Rewritten rule can still be used, rewrite the converse too. Not very efficient.
(define Conv2 (if (eq? C Conv) Conv (binary-rewrite-Clause rwtree Conv)))
(values C2 Conv2))])
(cond
[(not C) '()]
[(empty-clause? (Clause-clause C))
; Refutation found!
(when-debug>= steps
(displayln "Refutation found while adding rules!")
(display-Clause-ancestor-graph C))
; TODO: Let's just discard it for now. A refutation will probably be found very early
; TODO: at the next saturation iteration.
'()]
[else
(define atom<=> (rewrite-tree-atom<=> rwtree))
(define dynamic-ok? (rewrite-tree-dynamic? rwtree))
(define rls (Clause->rules C Conv #:atom<=> atom<=> #:dynamic-ok? dynamic-ok?))
(when-debug>= steps
(displayln "Adding the following rules:")
(display-rules rls))
(for ([rl (in-list rls)])
(add-rule! rwtree rl))
; We set the bit to #true, *even if* no rule has been added,
; because the purpose of this bit is to avoid considering
; C later again to save time.
; TODO: We could set Conv as a rewrite-rule too but ONLY if C also subsumes the converse
; TODO: of conv.
(set-Clause-binary-rewrite-rule?! C #true)
rls]))]))
;; Adds the binary Clauses Cs as rules and returns the corresponding rules.
;; If rewrite? is not #false, the clauses are rewritten beforehand using the rules in the tree
;; (the order of the rules in Cs does matter as of now)
;; and tautologies are filtered out.
;; The default rewrite = #true is 'safe' because when considering A->B, Clause->rules also considers
;; B->A because we provide it with the converse equivalence (that is, the proof that B->A is valid).
;; Hence even converse implications can safely be rewritten to tautologies without losing rules.
;; Conv: MUST Subsumes the converse clause of each of Cs.
;;
;; rwtree : rewrite-tree?
;; Cs : (listof Clause?)
;; Conv : Clause?
;; rewrite? : boolean?
;; -> void?
(define (rewrite-tree-add-binary-Clauses! rwtree Cs Conv #:? rewrite?)
(for/fold ([rules '()])
([C (in-list Cs)])
(define rls (rewrite-tree-add-binary-Clause! rwtree C Conv #:rewrite? rewrite?))
(append rls rules)))
;; Returns the list of rules (without duplicates) of rwtree.
;;
;; rwtree : rewrite-tree?
;; -> (listof rule?)
(define (rewrite-tree-rules rwtree)
(remove-duplicates (append* (trie-values rwtree)) eq?))
;; Returns the list of rule groups (without duplicates) of rwtree.
;;
;; rwtree : rewrite-tree?
;; -> (listof rule-group?)
(define (rewrite-tree-rule-groups rwtree)
(remove-duplicates (map rule-rule-group (append* (trie-values rwtree))) eq?))
;; Returns the list of unique Clauses that have been used to create the rules
;; held by the given rule groups.
;;
;; groups : (listof rule-group?)
;; -> (listof Clause?)
(define (rule-groups-original-Clauses groups)
(remove-duplicates (append (map rule-group-Clause groups)
(map rule-group-Converse groups))
eq?))
;; Returns the list of unique Clauses that have been used to create the rules.
;;
;; rules : (listof rule?)
;; -> (listof Clause?)
(define (rules-original-Clauses rules)
(rule-groups-original-Clauses (map rule-rule-group rules)))
;; Returns the list of unique Clauses that have been used to create the rules
;; of the rewrite tree.
;;
;; rwtree : rewrite-tree?
;; -> (listof Clause?)
(define (rewrite-tree-original-Clauses rwtree)
(rule-groups-original-Clauses (rewrite-tree-rule-groups rwtree)))
;; Returns the number of rules in the rewrite tree.
;;
;; rwtree : rewrite-tree?
;; -> exact-nonnegative-integer?
(define (rewrite-tree-count rwtree)
(length (rewrite-tree-rules rwtree))) ; not efficient
;; Returns a dictionary of statistics about the rewrite tree.
;;
;; rwtree : rewrite-tree?
;; -> list?
(define (rewrite-tree-stats rwtree)
(define rules (rewrite-tree-rules rwtree))
(define n-rules (length rules))
(define n-dyn (count (λ (r) (not (rule-static? r))) rules))
(define n-unit (count unit-rule? rules))
(define n-bin (- n-rules n-unit))
`((rules . ,n-rules)
(unit-rules . ,n-unit)
(binary-rules . ,n-bin)
(binary-rules-static . ,(- n-bin n-dyn))
(binary-rules-dynamic . ,n-dyn)))
;; Attempts to simplify the rules by successively removing each rule,
;; rewrite its underlying Clause and add the rule again—checking for subsumption.
;; Since all rules are processed, the new set of rules can be obtained via rewrite-tree-rules.
;; Notice: Not (currently) suitable for use *inside* the saturation loop because the new Clauses
;; (as per `eq?` are not part of the active set or candidates.
;;
;; rwtree : rewrite-tree?
;; -> void?
(define (re-add-rules! rwtree)
(define groups (rewrite-tree-rule-groups rwtree))
(for ([gp (in-list groups)])
(remove-rule-group! rwtree gp)
(define C (rule-group-Clause gp))
(set-Clause-binary-rewrite-rule?! C #false) ; otherwise will not be added
(rewrite-tree-add-binary-Clause! rwtree C (rule-group-Converse gp) #:rewrite? #true)))
;===================;
;=== Save / load ===;
;===================;
;; Save the rules to file f (as clauses).
;;
;; rwtree : rewrite-tree?
;; rules-file : file?
;; exists : symbol? ; see `display-to-file`.
;; -> void?
(define (save-rules! rwtree #:! rules-file #:? [exists 'replace])
(define groups (rewrite-tree-rule-groups rwtree))
; We use hash to avoid duplicating clauses, so that we also avoid loading
; the same clause under different names.
(define-counter idx 0)
(define h (make-hasheq))
(define (get-clause C)
(if (hash-has-key? h C)
(hash-ref h C)
(begin0 (Clause-clause C)
(++idx)
(hash-set! h C idx))))
(make-parent-directory* rules-file) ; ensure the path exists
(with-output-to-file rules-file #:exists exists
(λ () (for ([gp (in-list groups)])
(define C (rule-group-Clause gp))
(define Conv (rule-group-Converse gp))
(writeln (if (eq? C Conv)
(list (get-clause C)) ; self-converse
(list (get-clause C)
(get-clause Conv))))))))
;; Private.
;;
;; rules-file : file?
;; -> (listof Clause?)
(define (load-rule-Clause-lists #:! rules-file)
(define-counter idx 0)
; Each clause has a number (local to the file) and if a number appears while reading
; then we must load the saved clause of the same number.
(define h (make-hasheqv))
(define (get-Clause! x)
(cond [(number? x)
(hash-ref h x)]
[else
(++idx)
(define C (make-Clause x #:type 'load))
(hash-set! h idx C)
C]))
(for/list ([cls (in-list (file->list rules-file))])
(map get-Clause! cls)))
;; Load the rules from file into the rewrite-tree.
;; Optionally: rewrites the rules before adding them
;; Returns the set of new rules.
;;
;; rwtree : rewrite-tree?
;; rules-file : file?
;; rewrite? : boolean?
;; -> (listof rule?)
(define (load-rules! rwtree #:! rules-file #:? [rewrite? #true])
(define Crules (load-rule-Clause-lists #:rules-file rules-file))
(for/fold ([rules '()])
([Cs (in-list Crules)])
(define C (first Cs))
(define Conv
(if (= (length Cs) 1)
C ; self-converse
(second Cs)))
(define new-rules (rewrite-tree-add-binary-Clause! rwtree C Conv #:rewrite? rewrite?))
(append new-rules rules)))
;=======================;
;=== Filtering rules ===;
;=======================;
;; Returns a new rwtree containing only the filtered rules
;;
;; rwtree : rewrite-tree?
;; proc : rule-group? -> boolean?
;; -> rewrite-tree?
(define (filter-rule-groups rwtree proc)
(define rwtree2 (make-rewrite-tree #:atom<=> (rewrite-tree-atom<=> rwtree)
#:dynamic-ok? (rewrite-tree-dynamic? rwtree)))
(define groups (rewrite-tree-rule-groups rwtree))
(for ([gp (in-list groups)])
(when (proc gp)
; Copy the clauses (shallow)
(define C (make-Clause (Clause-clause (rule-group-Clause gp)) #:type 'filter-rule-groups))
(define Conv (make-Clause (Clause-clause (rule-group-Converse gp)) #:type 'filter-rule-groups))
(rewrite-tree-add-binary-Clause! rwtree2 C Conv #:rewrite? #true)))
rwtree2)
;==================;
;=== Confluence ===;
;==================;
;; Unifies (resolves) only with the lhs of rules and returns
;; the new set of Clauses (and converse Clauses).
;; If bounded? is not #false, then Clauses which have a literal
;; that is heavier (in literal-size) than a `from' literal of its parent rules
;; are discarded.
;;
;; rwtree : rewrite-tree?
;; rl : rule?
;; bounded? : boolean?
;; -> (listof Clause?)
(define (find-critical-pairs rwtree rl #:? [bounded? (*bounded-confluence?*)])
(define lnot-from-lit1 (lnot (rule-from-literal rl)))
(define to-lit1 (rule-to-literal rl))
(define new-Clauses '())
; Resolution (like unification with the converse)
; to ensure that the Clause parents are correct
(trie-both-find rwtree lnot-from-lit1
(λ (nd)
(define rls (trie-node-value nd))
(when (list? rls)
(for ([rl2 (in-list rls)]
#:unless (eq? rl rl2))
(define from-lit2 (rule-from-literal rl2))
(define to-lit2 (rule-to-literal rl2))
(define s (unify lnot-from-lit1 from-lit2))
(when s
(define cl (clause-normalize (substitute (list to-lit1 to-lit2) s)))
(define C (make-Clause cl (list (rule-Clause rl) (rule-Clause rl2))
#:type 'c-p)) ; critical-pair
(define max-size (max (literal-size (rule-from-literal rl))
(literal-size from-lit2)))
(unless (or (Clause-tautology? C)
(and bounded?
(> (max (literal-size (first cl))
(literal-size (second cl)))
max-size)))
(cons! C new-Clauses)))))))
new-Clauses)
;; This is a bad fix. Instead we should find the correct converse Clause (via rule groups)
;; Returns the list of rules that were added, or '() if none.
;;
;; rwtree : rewrite-tree?
;; C : Clause?
;; -> void?
(define (force-add-binary-Clause! rwtree C)
(define Conv (make-converse-Clause C))
(rewrite-tree-add-binary-Clause! rwtree C (if (Clause-equivalence? C Conv) C Conv)))
;; Add all critical pairs between existing rules.
;; This procedure may need to be called several times, but such a loop may not terminate
;; as some rules can be inductive.
;; It is recommended to call `simplify-rewrite-tree!` before and after calling this procedure.
;; Returns #true if any rules has been added.
;;
;; rwtree : rewrite-tree?
;; bounded? : boolean?
;; -> boolean?
(define (rewrite-tree-confluence-step! rwtree #:? bounded?)
(define rules (rewrite-tree-rules rwtree))
(for/fold ([any-change? #false])
([rl (in-list rules)])
(define pairs (find-critical-pairs rwtree rl #:bounded? bounded?))
(for/fold ([any-change? any-change?])
([C (in-list pairs)])
(define change? (force-add-binary-Clause! rwtree C))
(or any-change? (and change? #true)))))
;; Performs several steps of confluence until no more change happens.
;; Returns whether any change has been made.
;; Notice: if bounded? is #false, this may loop forever.
;; If it does halt however, the system is confluent, but may not be minimal;
;; call re-add-rules! to remove unnecessary rules and simplify rhs of rules
;; (this should help make rewriting faster).
;; It is advised to call re-add-rules! once before and once after rewrite-tree-confluence!.
;;
;; rwtree : rewrite-tree?
;; bounded? : boolean?
;; max-steps : exact-nonnegative-integer?
;; -> boolean?
(define (rewrite-tree-confluence! rwtree #:? bounded? #:? [max-steps (*confluence-max-steps*)])
(let loop ([step 1] [any-change? #false])
(define changed? (rewrite-tree-confluence-step! rwtree #:bounded? bounded?))
(cond [(or (>= step max-steps)
(not changed?))
any-change?]
[else (loop (+ step 1) #true)])))
;================;
;=== Printing ===;
;================;
;; Returns a list representing the rule.
;;
;; rule? -> list?
(define (rule->list rl)
(cons (Clause-idx (rule-Clause rl))
(Vars->symbols (list (rule-from-literal rl)
(rule-to-literal rl)))))
;; A comparator to sort the rules for printing
(define display-rule<=>
(make-chain<=> boolean<=> rule-static?
boolean<=> unit-rule?
boolean<=> (λ (r) (lnot? (rule-from-literal r)))
number<=> (λ (r) (+ (tree-size (rule-from-literal r))
(tree-size (rule-to-literal r))))
number<=> (λ (r) (tree-size (rule-to-literal r)))
length<=> rule-to-fresh))
;; (listof rule?) -> (listof rule?)
(define (sort-rules rls)
(sort rls (λ (a b) (order>? (display-rule<=> a b)))))
;; Human-readable output
;; Notice: Since unit rules are asymmetric (only one rule per unit clause),
;; if positive-only is not #false it may not display some unit rules.
;;
;; rls : (listof rule?)
;; sort? : boolean?
;; positive-only? : boolean?
;; unit? : boolean?
(define (rules->string rls #:? [sort? #true] #:? [positive-only? #false] #:? [unit? #true])
(string-join
(for/list ([rl (in-list (if sort? (sort-rules rls) rls))]
#:unless (or (and (not unit?) (unit-rule? rl))
(and positive-only? (lnot? (rule-from-literal rl)))))
; abusing clause->string:
(clause->string (list (if (rule-static? rl) "static " "dynamic")
(map Var (rule-to-fresh rl))
':
(rule-from-literal rl)
'→
(rule-to-literal rl))))
"\n"))
;; Like rules->string but directly displays the result.
;;
;; rls : (listof rule?)
;; sort? : boolean?
;; positive-only? : boolean?
;; unit? : boolean?
(define-wrapper (display-rules (rules->string rls #:? sort? #:? positive-only? #:? unit?))
#:call-wrapped call
(displayln (call)))