-
Notifications
You must be signed in to change notification settings - Fork 1
/
history-tree.lisp
884 lines (776 loc) · 36 KB
/
history-tree.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
;;;; SPDX-FileCopyrightText: Atlas Engineer LLC
;;;; SPDX-License-Identifier: BSD-3-Clause
(in-package :history-tree)
(eval-when (:compile-toplevel :load-toplevel :execute)
(trivial-package-local-nicknames:add-package-local-nickname :alex :alexandria))
;; TODO: Thread safe?
;; TODO: Use fast sets for unique lookups? Turns out hash-tables are overkill
;; with SBCL on modern hardware, for less than 10.000.000 entries.
;; Always use lists then?
;; See TODO notes below mentioning "fast sets".
;; See https://old.reddit.com/r/Common_Lisp/comments/l1z7ei/fast_set_library_or_gethash_vs_findmember/.
;; TODO: Should we have different functions for finding nodes vs. "owned nodes",
;; or pass an option as key argument?
;; TODO: Is "Shared history tree" a better name than "Global history tree"?
;; TODO: Turn unique defmethod to defuns.
(defmacro export-always (symbols &optional (package nil package-supplied?)) ; From serapeum.
"Like `export', but also evaluated at compile time."
`(eval-when (:compile-toplevel :load-toplevel :execute)
(export ,symbols ,@(and package-supplied? (list package)))))
(deftype function-symbol ()
`(and symbol (satisfies fboundp)))
(define-class entry ()
((history nil
:type (or null history-tree)
:documentation "Required.
This is gives access to the custom hash functions, see the corresponding
`history-tree' slots.
We allow null values for easier deserialization.")
(data nil
:type t
:documentation "Arbitrary data.")
(last-access (local-time:now)
:type (or local-time:timestamp string) ; Support `string' for easier deserialization.
:writer t
:documentation "The last access to the corresponding entry by
any owner. It's useful to keep this access stored here so that when an entry
goes owner-less, we can still consult the last time it was accessed.")
(nodes '()
:type list
:documentation "The list of nodes that access an entry."))
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer)
(:documentation "Wrapped data as stored in `history-tree''s `entries'.
Each entry has a unique datum. Each `node' points to one entry. Multiple nodes
may point to the same entry. Entries may also be node-less; they are kept
around so that we can remember the data that was visited since the beginning of
time. Node-less entries are available for manual deletion with
`delete-data'."))
(defun ensure-timestamp (string-or-timestamp)
(if (stringp string-or-timestamp)
(or (ignore-errors (local-time:parse-timestring string-or-timestamp))
(local-time:now))
string-or-timestamp))
(defmethod last-access ((entry entry))
"Ensure we return last-access as a timestamp, in case it was a string."
(setf (slot-value entry 'last-access) (ensure-timestamp
(slot-value entry 'last-access))))
(defun make-entry (history data &optional last-access)
"Return an `entry' wrapping DATA and suitable for HISTORY."
(make-instance 'entry :data data
:history history
:last-access (or last-access (local-time:now))))
(define-class node ()
((parent nil
:type (or null node)
:documentation "If nil, it means the node is a root node.
(The first of the parents.)")
(children '()
:type (list node)
:documentation "Order does not matter.")
(bindings (make-hash-table)
:documentation "The key is an `owner', the value is a
`binding'. This slot also allows us to know to which owner a node belongs.")
(entry nil
:type (or null entry)
:documentation "Required.
(Null entry is accepted only to ease deserialization.)
Arbitrary data (wrapped in an `entry' object) carried
by the node. `history-tree''s `entries' holds `entry'-`node' associations."))
(:export-class-name-p t)
(:export-accessor-names-p t)
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer)
(:documentation "Node structure of the history tree.
Each node has one parent (unless it's a root node) and zero or multiple
children. Nodes may have zero or multiple owners."))
(export 'data)
(defmethod data ((node node))
(data (entry node)))
(defmethod root ((node node))
(if (parent node)
(root (parent node))
node))
(defun make-node (&key parent entry)
(let ((node (make-instance 'node :parent parent :entry entry)))
(cl-custom-hash-table:with-custom-hash-table
(pushnew node (nodes entry)))
node))
(define-class binding ()
((forward-child nil
:type (or null node)
:documentation "Which of the `children' (in the bound `node')
is the child to go forward to for the bound owner.")
(last-access (local-time:now)
:type (or local-time:timestamp string) ; Support `string' for easier deserialization.
:writer t
:documentation "Timestamp of the last access to this node by the
owner."))
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer)
(:documentation "The relationship between an owner and one of its nodes.
In particular, it encodes the forward child and the date of last access to the
node for a given owner."))
(export-always 'last-access)
(defmethod last-access ((binding binding))
"Ensure we return last-access as a timestamp, in case it was a string."
(setf (slot-value binding 'last-access) (ensure-timestamp
(slot-value binding 'last-access))))
(defmethod last-access ((node node))
"Return node's last access across all its owners.
If the node has no owner, return Epoch."
(if (< 0 (hash-table-count (bindings node)))
(apply #'local-time:timestamp-maximum
(mapcar #'last-access
(alex:hash-table-values (bindings node))))
(local-time:unix-to-timestamp 0)))
(define-class owner ()
;; REVIEW: Add slot pointing to history an owner belongs to? As long as the
;; owner has at least one node, the history can be accessed via the entry.
((origin nil
:type (or null node)
:documentation "The first node created for this owner.
Not to be confused with the root, since the owner be go back to a parent of `origin'.")
(data nil
:type t
:documentation "Arbitrary data.
Use it to persist extra owner information to history.")
(creator-id nil
:type t
:documentation "The owner-id in `origin's parent node that
created this owner. May be nil, in which case `origin' is a root node.
Unless the parent was disowned by this `creator-id',
(gethash (owner history CREATOR-ID) (bindings (parent (origin OWNER))))
should return non-nil.
We store the owner-id instead of the `owner' object so that we keep the
information of who created this owner even after the creator object has been
deleted.")
(creator-node nil
:export nil
:type (or null node)
:documentation "The current node of the creator when this owner
is created. This is useful since the owner corresponding to `creator-id' may be
deleted before the `origin' node is added.")
(current nil
:type (or null node)
:reader current
:export t
:documentation "The current node.
It's updated every time a node is visited.")
(nodes '()
:type (or null (cons node))
:documentation "The list of all owned nodes."))
(:export-class-name-p t)
(:export-accessor-names-p t)
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer)
(:documentation "The high-level information about an owner.
Each owner is identified by a unique identifier, which is arbitrary data (may
even be NIL)."))
(defmethod (setf current) (value (owner owner))
"This setter protects against setting OWNER's `current' slot to an invalid object."
(if (node-p value)
(setf (slot-value owner 'current) value)
(error "Attempted to set current node to a non-node for owner ~a." owner)))
(defmethod last-access ((owner owner))
"Return owner current node last access."
(last-access (gethash owner (bindings (current owner)))))
(declaim (ftype (function (owner) function) owned-children-lister))
(defun owned-children-lister (owner)
"Return a function which lists the OWNER's owned children of the node argument."
(lambda (node)
(remove-if (complement (alex:curry #'owned-p owner))
(children node))))
(export-always 'owned-children)
(defun owned-children (owner)
"Return the OWNER's owned children for the current node."
(funcall (owned-children-lister owner) (current owner) ))
(export-always 'owned-parent)
(defun owned-parent (owner node)
"Return OWNER's parent if it's owned, nil otherwise."
(let ((parent (and node (parent node))))
(when (owned-p owner parent)
parent)))
(export-always 'current-binding)
(declaim (ftype (function (owner &optional (or null node)) (or null binding)) current-binding))
(defun current-binding (owner &optional (node (current owner)))
(and node
(gethash owner (bindings node))))
(export-always 'owned-p)
(declaim (ftype (function (owner (or null node)) (or null binding)) owned-p))
(defun owned-p (owner node)
(and node
(bindings node)
(gethash owner (bindings node))))
(declaim (ftype (function (node) boolean) disowned-p))
(defun disowned-p (node)
(= 0 (hash-table-count (bindings node))))
(declaim (ftype (function (owner node) boolean) disown))
(defun disown (owner node)
"Remove binding between OWNER and NODE.
Return true if NODE was owned by OWNER, nil otherwise."
(alex:deletef (nodes owner) node)
(remhash owner (bindings node)))
(defun entry-equal-p (a b)
(let ((h (history a)))
(funcall (test h)
(funcall (key h) (data a))
(funcall (key h) (data b)))))
(defun entry-hash (a)
(let ((h (history a)))
(funcall (hash-function h) (funcall (key h) (data a)))))
(cl-custom-hash-table:define-custom-hash-table-constructor make-entry-hash-table
:test entry-equal-p
:hash-function entry-hash)
(defun data-equal-entry-p (data entry)
(let ((h (history entry)))
(funcall (test h)
(funcall (key h) (data entry))
(funcall (key h) data))))
(export-always 'add-entry)
(defun add-entry (history data &optional last-access)
"Add DATA to an `entry' in HISTORY `entries'.
If DATA is already there, don't alter the entry.
Return the new or existing `entry'.
The higher-level functions take care of adding entries for you, so you normally
need not call this function. See `add-child' instead.
One case in which this function might be useful is when you want to import flat
history data, e.g. a list of visited URLs that's not bound to any owner."
(cl-custom-hash-table:with-custom-hash-table
(let ((new-entry (make-entry history data last-access)))
(multiple-value-bind (existing-entry found?)
(gethash new-entry (entries history))
(if found?
(progn
(when last-access
(setf (last-access existing-entry) last-access))
existing-entry)
(progn
(setf (gethash new-entry (entries history))
new-entry)
new-entry))))))
(define-class history-tree () ; TODO: Rename `history'?
((owners (make-hash-table :test #'equalp)
:type hash-table
:documentation "The key is an owner identifier (an artitrary value),
the value is an `owner'.")
(entries (make-entry-hash-table)
:type hash-table
:documentation "Both the key and the value are an `entry', so that
we can access the actual object from a given piece of data.
Indeed, with custom hash table the key that is store (here the entry) is not
necessarily identical to the one used in `gethash'. So storing the entry as a
value gives us access to to the actual object.")
(key 'identity
:type function-symbol
:documentation "The result of this function is passed to `test'
and `hash-function'. It is useful to uniquely identify (i.e. avoid
duplications) objects from one of their slots.
It is a `function-symbol' so that the history can be more easily serialized than
if if were a function.")
(test 'equalp
:type function-symbol
:documentation "Function that tests if the two results of `key' called
over two entries are equal.
Also see `hash-function'.
It is a `function-symbol' so that the history can be more easily serialized than
if if were a function.")
(hash-function 'sxhash
:type function-symbol
:documentation "Function that returns the hash of the result
of `key' called over an `entry'.
Also see `test'.
It is a `function-symbol' so that the history can be more easily serialized than
if if were a function."))
(:export-class-name-p t)
(:export-accessor-names-p t)
(:predicate-name-transformer 'nclasses:always-dashed-predicate-name-transformer)
(:documentation "Starting point of the global history tree data structure."))
(export 'make)
(defun make (&rest args
&key key
test
hash-function
initial-owners)
"Return a new `history-tree'."
(declare (ignore key test hash-function))
(alex:remove-from-plistf args :initial-owners)
(let ((history (apply #'make-instance 'history-tree args)))
(dolist (owner initial-owners)
(setf (gethash owner (htree:owners history))
(make-instance 'htree:owner)))
history))
(export-always 'data-last-access)
(declaim (ftype (function (history-tree t) local-time:timestamp) data-last-access))
(defun data-last-access (history data)
"Return data last access across all its nodes, regardless of the owner.
Return Epoch if DATA is not found or if entry has no timestamp."
(let* ((entry (find-entry history data))
(nodes (when entry (nodes entry))))
(the (values local-time:timestamp &optional)
(if nodes
(let ((new-last-access
(apply #'local-time:timestamp-maximum
(mapcar #'last-access nodes))))
(setf (last-access entry) new-last-access)
new-last-access)
(if entry
(last-access entry)
(local-time:unix-to-timestamp 0))))))
(export-always 'owner)
(defun owner (history owner-spec)
"Return the `owner' object identified by OWNER-SPEC in HISTORY.
OWNER may be an owner ID or owner object."
(if (owner-p owner-spec)
owner-spec
(gethash owner-spec (owners history))))
(export-always 'add-owner)
(declaim (ftype (function (history-tree t &key (:creator-id t)
(:data t))
(values owner &optional))
add-owner))
(defun add-owner (history owner-id &key creator-id data)
"Create and register owner object for OWNER-IDENTIFIER.
CREATOR-ID is the optional identifier of the parent owner.
DATA is the optional, arbitrary payload associated to the owner.
Return the newly created owner. If the owner with such identifier already
exists, return it and raise a warning."
(let ((owner (owner history owner-id)))
(if owner
(progn
(warn "Owner with identifier ~s already exists" owner-id)
owner)
(let ((creator-owner (owner history creator-id)))
(when (and creator-id
(or (not creator-owner)
(not (current creator-owner))))
(error "Cannot make owner a child of the node-less parent ~s"
creator-id))
(let ((owner (make-instance 'owner
:creator-id creator-id
:data data
:creator-node (when creator-id
(current creator-owner)))))
(setf (gethash owner-id (owners history))
owner)
owner)))))
(export-always 'owner-node)
(defun owner-node (history owner-spec)
(current (owner history owner-spec)))
(defmethod visit ((history history-tree) owner-spec node)
"Visit NODE with HISTORY's OWNER-SPEC.
Return (values HISTORY OWNER)."
(let ((owner (owner history owner-spec)))
(when owner
(pushnew node (nodes owner)) ; TODO: See TODO note on "fast sets".
(setf (current owner) node)
(let ((binding (gethash owner (bindings node))))
(if binding
(setf (last-access binding) (local-time:now))
(setf (gethash owner (bindings node))
(make-instance 'binding)))))
(values history owner)))
(export-always 'visit-all)
(defmethod visit-all ((history history-tree) owner-spec node)
"Like `visit' but on all nodes between the current node and NODE.
This is only possible if the current node and NODE are on the same branch.
If they are not, an error is raised.
Return (values HISTORY OWNER)."
(let ((owner (owner history owner-spec)))
(when (and owner (not (eq node (owner-node history owner-spec))))
(labels ((nodes-with-common-parent (node current-node-parents)
(unless node
(error "Node ~s and owner ~s node must be on the same branch" node owner-spec))
(if (find node current-node-parents)
(list node)
(cons node (nodes-with-common-parent (parent node) current-node-parents)))))
(let* ((current-node (owner-node history owner-spec))
(current-node-with-parents (cons current-node (all-parents current-node)))
(node-parents-until-common-parent (nreverse (nodes-with-common-parent
node
current-node-with-parents)))
(common-parent (first node-parents-until-common-parent)))
(loop :until (eq common-parent (owner-node history owner))
:do (backward history owner))
(loop :until (eq node (owner-node history owner))
;; Skip the first node since it's the common-parent and it's already visited.
:do (setf node-parents-until-common-parent (rest node-parents-until-common-parent))
:do (go-to-child (data (first node-parents-until-common-parent)) history owner)))))
(values history owner)))
(deftype positive-integer ()
`(integer 1 ,most-positive-fixnum))
(export-always 'backward)
(defmethod backward ((history history-tree) owner-spec &optional (count 1))
"Go COUNT parent up from the OWNER-SPEC current node, if possible.
Return (VALUES HISTORY OWNER)."
(check-type count positive-integer)
(let ((owner (owner history owner-spec)))
(when (and owner (current owner) (parent (current owner)))
(let ((former-current (current owner)))
(visit history owner (parent (current owner)))
;; Put former current node back as forward-child if it is not already
;; the case, e.g. if current node was set manually.
(setf (forward-child (current-binding owner))
former-current))
(when (< 1 count)
(backward history owner (1- count))))
(values history owner)))
(export-always 'backward-owned-parents)
(defmethod backward-owned-parents ((history history-tree) owner-spec &optional (count 1))
"Go COUNT parent up from the OWNER-SPEC current node, if possible.
Only contiguous owned parents are considered.
Return (VALUES HISTORY OWNER)."
(let ((owner (owner history owner-spec)))
(when (and owner (current owner) (owned-parent owner (current owner)))
(backward history count))
(values history owner)))
(export-always 'forward)
(defmethod forward ((history history-tree) owner-spec &optional (count 1))
"Go COUNT forward-children down from OWNER-SPEC current node, if possible.
Return (values HISTORY CURRENT-NODE)) so that `backward' and `forward' calls can be
chained."
(check-type count positive-integer)
(let ((owner (owner history owner-spec)))
(when (and owner
(current-binding owner)
(forward-child (current-binding owner)))
(visit history owner (forward-child (current-binding owner)))
(when (< 1 count)
(forward history owner (1- count))))
(values history (current owner))))
(defun find-entry (history data)
"Return the nodes matching DATA."
(cl-custom-hash-table:with-custom-hash-table
(let ((new-entry (make-entry history data)))
(gethash new-entry (entries history)))))
(export 'find-nodes)
(defun find-nodes (history data)
"Return the nodes matching DATA."
(cl-custom-hash-table:with-custom-hash-table
(let ((entry (find-entry history data)))
(when entry
(nodes entry)))))
(declaim (ftype (function (t (or null (cons node *))) (or null node)) find-node))
(defun find-node (data nodes)
"Return the node owned by OWNER which matches DATA."
(find data
nodes
:key #'entry
:test #'data-equal-entry-p))
(declaim (ftype (function (t owner) (or null node)) find-child))
(defun find-child (data owner)
"Return the direct child node of OWNER which matches DATA. "
(find-node data (children (current owner))))
(declaim (ftype (function (t owner) (or null node))
find-owned-child))
(defun find-owned-child (data owner)
"Return the direct child node owned by OWNER which matches DATA."
(find-node data (owned-children owner)))
(export-always 'go-to-child)
(defmethod go-to-child (data (history history-tree) owner-spec &key (child-finder #'find-child)) ; TODO: Should take a node instead?
"Go to direct current node's child matching DATA.
Return (values HISTORY OWNER)."
(let* ((owner (owner history owner-spec))
(match (when owner (funcall child-finder data owner))))
(if match
(visit history owner match)
(values history owner))))
(export-always 'go-to-owned-child)
(defmethod go-to-owned-child (data (history history-tree) owner-spec)
"Go to current node's direct owned child matching DATA.
A child is owned if it has a binding with OWNER.
Return (values OWNER (current OWNER))."
(go-to-child data history owner-spec :child-finder #'find-owned-child))
(defun make-origin-node (history owner-spec data)
(let* ((owner (owner history owner-spec))
(new-node (make-node :entry (add-entry history data)
:parent (creator-node owner))))
(when (creator-node owner)
(push new-node (children (creator-node owner))))
(setf (origin owner) new-node)
(visit history owner-spec new-node)))
(export-always 'add-child)
(defmethod add-child (data (history history-tree) owner-spec)
"Create or find a node holding DATA and set current node to it.
Return the (possibly new) current node.
Return NIL if OWNER-SPEC does not refer to an existing owner.
If current node matches DATA (which may be non-identical since the
`history-tree''s `key' and `test' functions may identify two non-identical
pieces of data as equal), do nothing.
If DATA is found among the children, OWNER-SPEC current node `forward-child' is
set to the matching child, the owner current node is set to this child.
If there is no current node, this creates the `origin' node of OWNER-SPEC
and also sets `current' to it. If the owner has a `creator-id' set,
the new node is added to the children of the current node of the creator."
(let* ((owner (owner history owner-spec)))
(when owner
(cond
((null (current owner))
(make-origin-node history (owner history owner-spec) data))
((not (data-equal-entry-p data (entry (current owner))))
(let ((node (find-child data owner)))
(unless node
(let ((maybe-new-entry (add-entry history data)))
(push (setf node (make-node :entry maybe-new-entry
:parent (current owner)))
(children (current owner)))))
(let ((binding (gethash owner (bindings (current owner)))))
(setf (forward-child binding) node))
(forward history owner)))
(t
;; Current node matches data, do nothing.
nil))
(current owner))))
(export 'add-children)
(defmethod add-children (children-data (history history-tree) owner-spec)
"Add CHILDREN-DATA to the HISTORY OWNER-SPEC current node.
Each child is added with `add-child' to the current node.
If the owner does not have any node yet, then first element of CHILDREN-DATA
forms the new root, while the rest of the elements form the `children' of this
root.
Return the (maybe new) current node, which holds the last piece of data in
`children-data'."
(let ((owner (owner history owner-spec)))
(when owner
(add-child (first children-data) history owner)
(if (rest children-data)
(progn (backward history owner)
(add-children (rest children-data) history owner))
(current owner)))))
(export-always 'map-tree)
(defun map-tree (function tree &key owner flatten include-root
(collect-function #'cons)
(children-function #'children))
"Map the FUNCTION over the TREE.
If TREE is a `htree:history-tree', start from its OWNER root.
If TREE is a `htree:node', start from it.
OWNER can be an ID or an `owner' object.
Include results of applying FUNCTION over ROOT if INCLUDE-ROOT is
non-nil.
Return results as cons cells tree if FLATTEN is nil and as a flat
list otherwise.
COLLECT-FUNCTION is the function of two arguments that glues the
current node result to the result of further traversal."
(labels ((collect (result further-results)
(funcall collect-function result further-results))
(traverse (node)
(when node
(collect (funcall function node)
;; This lambda is here because (apply #'identity ...) fails on empty arglist.
(apply (if flatten #'append #'(lambda (&rest args) args))
(mapcar #'traverse (funcall children-function node)))))))
(let ((root (typecase tree
(node tree)
(history-tree (root (owner-node tree owner))))))
(when root
(if include-root
(traverse root)
(apply #'append (mapcar #'traverse (children root))))))))
(export-always 'map-owned-tree)
(defun map-owned-tree (function tree owner &key flatten include-root
(collect-function #'cons))
"Like `map-tree' but restrict traversal to OWNER's nodes.
TREE is unused."
(declare (ignore tree))
(map-tree function (owned-root owner)
:owner owner
:flatten flatten
:include-root include-root
:collect-function collect-function
:children-function (owned-children-lister owner)))
(export-always 'do-tree)
(defmacro do-tree ((var tree) &body body) ; TODO: Edit? Unexport?
"Apply actions in BODY to all the nodes in a tree.
Nodes are bound to VAR.
If TREE is a node, it's passed right away,
if it is a tree, then the root is taken.
Always return nil, as it is an explicitly imperative macro."
`(progn
(map-tree (lambda (,var) ,@body) ,tree :include-root t)
;; Explicitly return nil
nil))
(export-always 'all-children)
(defmethod all-children ((node node) &key &allow-other-keys)
"Return a list of all the children of NODE, recursively."
(map-tree #'identity node :flatten t))
(defmethod all-children ((history history-tree) &key (owner (error "Owner required.")))
"Return a list of all the children of HISTORY's OWNER-SPEC current node.
Children may not all be owned by OWNER-SPEC."
(alex:when-let ((node (owner-node history owner)))
(all-children node)))
(export-always 'all-contiguous-owned-children)
(defmethod all-contiguous-owned-children ((history history-tree) owner-spec &optional node)
"Return a list of all the children of HISTORY's OWNER-SPEC current node,
recursively."
(let ((owner (owner history owner-spec)))
(map-tree #'identity (or node (current owner))
:flatten t
:children-function (owned-children-lister owner))))
(export-always 'all-parents)
(defmethod all-parents ((node node) &key &allow-other-keys)
"Return a list of parents of NODE, recursively.
First parent comes first in the resulting list."
(when (parent node)
(cons (parent node)
(all-parents (parent node)))))
(defmethod all-parents ((history history-tree) &key (owner (error "Owner required.")) &allow-other-keys)
"Return a list of all parents of the current node.
Parents may not be owned by the current owner.
First parent comes first in the resulting list."
(alex:when-let ((node (owner-node history owner)))
(all-parents node)))
(defun node-contiguous-owned-parents (owner node)
"Return a list of parents of owned by NODE, recursively.
First parent comes first in the resulting list."
(labels ((contiguous-owned-parents (node)
(when (owned-parent owner node)
(cons (parent node)
(contiguous-owned-parents (parent node))))))
(contiguous-owned-parents node)))
(export-always 'all-contiguous-owned-parents)
(defmethod all-contiguous-owned-parents ((history history-tree) owner-spec)
"Return a list of parents of owned by HISTORY OWNER-SPEC current node, recursively.
First parent comes first in the resulting list."
(alex:when-let ((owner (owner history owner-spec)))
(node-contiguous-owned-parents owner
(current owner))))
(export-always 'all-forward-children)
(defmethod all-forward-children ((history history-tree)
owner-spec
&optional (node (owner-node history owner-spec)))
"Return a list of the forward children of NODE, recursively.
First child comes first in the resulting list."
(alex:when-let ((owner (owner history owner-spec)))
(let ((binding (current-binding owner node)))
(when (and binding (forward-child binding))
(cons (forward-child binding)
(all-forward-children history
owner-spec
(forward-child binding)))))))
(export 'all-owner-nodes)
(defmethod all-owner-nodes ((history history-tree) owner-spec)
"Return a list of all OWNER nodes, in unspecified order."
(alex:when-let ((owner (owner history owner-spec)))
(nodes owner)))
(export 'all-branch-nodes)
(defmethod all-branch-nodes ((history history-tree) owner-spec)
"Return a list of all nodes that belong to the branch OWNER-SPEC node is on.
These nodes do not necessarily belong to OWNER-SPEC.
See `all-contiguous-owned-nodes'."
(alex:when-let ((node (owner-node history owner-spec)))
(let ((root (root node)))
(cons root (all-children root)))))
(export 'owned-root)
(defun owned-root (owner)
"Return the first parent among the contiguous owned parents of NODE."
(or
(first (last (node-contiguous-owned-parents owner (current owner))))
(current owner)))
(export 'all-contiguous-owned-nodes)
(defmethod all-contiguous-owned-nodes ((history history-tree) owner-spec)
"Return a list of all nodes contiguous to OWNER-SPEC node, starting
from the top-most parent, in depth-first order."
(alex:when-let ((owned-root (owned-root (owner history owner-spec))))
(cons owned-root (all-contiguous-owned-children history owner-spec owned-root))))
(export-always 'all-data)
(defmethod all-data ((history history-tree))
"Return a list of all entries data, in unspecified order."
(mapcar #'data (alex:hash-table-keys (entries history))))
(defun map-data (arg)
(mapcar #'data arg))
(defun branch-owners (node)
"Return the list of all NODE's children (including NODE) owners."
(let ((owners '()))
(do-tree (child-node node)
(alexandria:appendf owners (alexandria:hash-table-keys (bindings child-node))))
(delete-duplicates owners)))
(defun disowned-branch-nodes (node)
"Return true if all NODE's children (including NODE) are disowned.
Return nil otherwise.
As a second value, return the list of all NODE's children, including NODE."
(let ((disowned? t)
(children '()))
(do-tree (child-node node)
(unless (disowned-p child-node)
(setf disowned? nil))
(push child-node children))
(values disowned? children)))
(defun delete-node (history node)
(cl-custom-hash-table:with-custom-hash-table
(let ((entry (gethash (entry node) (entries history))))
(when entry
(alex:deletef (nodes entry) node)))))
(defun delete-disowned-branch-nodes (history nodes)
(labels ((garbage-collect (list-of-roots)
(when list-of-roots
(let ((node (first list-of-roots)))
(multiple-value-bind (disowned-branch? nodes)
(disowned-branch-nodes node)
(when disowned-branch?
(mapc (alex:curry #'delete-node history) nodes))))
(garbage-collect (rest list-of-roots)))))
;; `delete-duplicates' ensures that no node is processed twice.
(garbage-collect (delete-duplicates (mapcar #'root nodes)))))
(defun first-hash-table-key (hash-table)
(cl-custom-hash-table:with-custom-hash-table
(with-hash-table-iterator (next-entry hash-table)
(nth-value 1 (next-entry)))))
(defun first-hash-table-value (hash-table) ; TODO: Unused
(cl-custom-hash-table:with-custom-hash-table
(with-hash-table-iterator (next-entry hash-table)
(nth-value 2 (next-entry)))))
(defun disown-all (history owner)
(when (owner-p owner)
(let ((nodes (nodes owner)))
(mapc (alex:curry #'disown owner) (nodes owner))
;; Delete nodes only when whole branch is owner-less. Indeed, otherwise
;; we would lose information for other owners. It's better to be as
;; "immutable" as possible.
;;
;; If we want to "free" disowned nodes from a branch with still owned
;; nodes, the less confusing approach (at least from a user perspective)
;; is delete all remaining owners, possibly by duplicating elsewhere
;; beforehand.
(delete-disowned-branch-nodes history nodes))
(setf (creator-node owner) nil)))
(export-always 'delete-owner)
(declaim (ftype (function (history-tree t) (or null owner)) delete-owner))
(defun delete-owner (history owner-id)
"Delete `owner' corresponding to OWNER-ID from HISTORY.
For every branch `owner' has nodes on, remove all its nodes if the branch is
without any owner.
Return owner, or nil if there is no owner corresponding to OWNER-ID."
(alex:when-let ((owner (owner history owner-id)))
(remhash owner-id (owners history))
(disown-all history owner)
owner))
(export-always 'reset-owner)
(defun reset-owner (history owner-id)
"Disown all OWNER's nodes and create a new root node with the previous current
node entry."
(alex:when-let* ((owner (owner history owner-id))
(old-current-entry (entry (current owner))))
(disown-all history owner)
(make-origin-node history owner-id (data old-current-entry))
owner))
(export-always 'delete-data)
(defun delete-data (history data)
"Delete entry matching DATA from HISTORY.
If nodes are still associated to entry, do nothing."
(cl-custom-hash-table:with-custom-hash-table
(let ((nodes (find-nodes history data)))
(unless nodes
(let ((matching-entry (make-entry history data)))
(remhash matching-entry (entries history)))))))
(deftype non-negative-integer ()
`(integer 0 ,most-positive-fixnum))
(declaim (ftype (function (history-tree (or string owner)) non-negative-integer) depth))
(defun depth (history owner-spec)
"Return the number of (possibly unowned) parents of OWNER-SPEC cutrent node."
(length (all-parents history :owner owner-spec)))
(defmethod size ((owner owner) &key &allow-other-keys)
"Return the total number of nodes owned by OWNER."
(length (nodes owner)))
(defmethod contiguous-size ((history history-tree) (owner owner))
"Return the total number of owned nodes contiguous to the current OWNER node."
(length (all-contiguous-owned-nodes history owner)))
(defmethod size ((history history-tree) &key (owner "Owner required.") &allow-other-keys)
"Return the total number of nodes for the branch OWNER's current node sits on."
;; TODO: This could be optimized with a SIZE slot, but is it worth it?
(length (all-branch-nodes history owner)))