[rucksack-cvs] CVS rucksack
alemmens
alemmens at common-lisp.net
Fri Aug 4 22:04:43 UTC 2006
Update of /project/rucksack/cvsroot/rucksack
In directory clnet:/tmp/cvs-serv27309
Modified Files:
objects.lisp p-btrees.lisp package.lisp test.lisp
Log Message:
Clean up btree code. Add BTREE-DELETE. (From Edi Weitz.)
--- /project/rucksack/cvsroot/rucksack/objects.lisp 2006/05/24 20:45:09 1.4
+++ /project/rucksack/cvsroot/rucksack/objects.lisp 2006/08/04 22:04:43 1.5
@@ -1,4 +1,4 @@
-;; $Id: objects.lisp,v 1.4 2006/05/24 20:45:09 alemmens Exp $
+;; $Id: objects.lisp,v 1.5 2006/08/04 22:04:43 alemmens Exp $
(in-package :rucksack)
@@ -265,10 +265,10 @@
:end1 end1
:start2 start2
:end2 end2)
- ;; DO: WE MUST TOUCH THE OBJECT HERE!!
+ ;; Touch the vector because it has changed.
+ (cache-touch-object vector-1 (cache vector-1))
vector-1)
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Full fledged persistent objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
--- /project/rucksack/cvsroot/rucksack/p-btrees.lisp 2006/08/04 11:06:04 1.6
+++ /project/rucksack/cvsroot/rucksack/p-btrees.lisp 2006/08/04 22:04:43 1.7
@@ -1,15 +1,16 @@
-;; $Id: p-btrees.lisp,v 1.6 2006/08/04 11:06:04 alemmens Exp $
+;; $Id: p-btrees.lisp,v 1.7 2006/08/04 22:04:43 alemmens Exp $
(in-package :rucksack)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;; b-trees: API
+;;; Btrees: API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#|
;; Btrees
#:btree
- #:btree-key< #:btree-key= #:btree-value=
+ #:btree-key< #:btree-key<= #:btree-key= #:btree-key>= #:btree-key>
+ #:btree-value=
#:btree-max-node-size #:btree-unique-keys-p
#:btree-key-type #:btree-value-type
#:btree-node-class
@@ -18,7 +19,7 @@
#:btree-node
;; Functions
- #:btree-search #:btree-insert #:map-btree
+ #:btree-search #:btree-insert #:btree-delete #:map-btree
;; Conditions
#:btree-error #:btree-search-error #:btree-insertion-error
@@ -82,7 +83,6 @@
(defclass btree ()
((key< :initarg :key< :reader btree-key< :initform '<)
- (key= :initarg :key= :reader btree-key= :initform 'eql)
(value= :initarg :value= :reader btree-value= :initform 'p-eql
:documentation "This is only used for btrees with non-unique keys.")
;;
@@ -109,10 +109,10 @@
(root :accessor btree-root))
(:metaclass persistent-class))
-
+
(defmethod initialize-instance :around ((btree btree)
&rest initargs
- &key key< key= value=
+ &key key< value=
&allow-other-keys)
;; It must be possible to save these btrees in the cache, but
;; that will not work for function objects because they can't be
@@ -120,12 +120,39 @@
;; name a function. For program-independent databases you should
;; only use symbols from the COMMON-LISP package.
(declare (ignore initargs))
- (if (and (symbolp key<) (symbolp key=) (symbolp value=))
+ (if (and (symbolp key<) (symbolp value=))
(call-next-method)
- (error "The :key<, :key= and :value= initargs for persistent btrees
+ (error "The :key< and :value= initargs for persistent btrees
must be symbols naming a function, otherwise they can't be saved on
disk.")))
+;;
+;; Comparison functions that can be deduced from KEY< (because the
+;; btree keys have a total order).
+;;
+
+(defmethod btree-key= ((btree btree))
+ (let ((key< (btree-key< btree)))
+ (lambda (key1 key2)
+ (and (not (funcall key< key1 key2))
+ (not (funcall key< key2 key1))))))
+
+(defmethod btree-key>= ((btree btree))
+ (lambda (key1 key2)
+ (funcall (btree-key< btree) key2 key1)))
+
+(defmethod btree-key<= ((btree btree))
+ (let ((key< (btree-key< btree)))
+ (lambda (key1 key2)
+ (or (funcall key< key1 key2)
+ (not (funcall key< key2 key1))))))
+
+(defmethod btree-key> ((btree btree))
+ (let ((key< (btree-key< btree)))
+ (lambda (key1 key2)
+ (and (not (funcall key< key1 key2))
+ (funcall key< key2 key1)))))
+
;;
;; The next two classes are for internal use only, so we don't bother
@@ -140,7 +167,7 @@
sorted by KEY<. No two keys can be the same. For leaf nodes of btrees
with non-unique-keys, the value part is actually a list of values.
For intermediate nodes, the value is a child node. All keys in the
-child node will be KEY< the child node's key in the parent node.")
+child node will be KEY<= the child node's key in the parent node.")
(index-count :initform 0
:accessor btree-node-index-count
:documentation "The number of key/value pairs in the index vector.")
@@ -187,7 +214,7 @@
(defmethod initialize-instance :after ((node btree-node)
&key btree &allow-other-keys)
(setf (btree-node-index node) (p-make-array (btree-max-node-size btree)
- :initial-element nil)
+ :initial-element nil)
(btree-node-index-count node) 0))
@@ -195,6 +222,24 @@
(print-unreadable-object (node stream :type t :identity t)
(format stream "with ~D pairs" (btree-node-index-count node))))
+;;
+;; Debugging
+;;
+
+(defun display-node (node)
+ (pprint (node-as-cons node)))
+
+(defun node-as-cons (node)
+ (loop with index = (btree-node-index node)
+ with leaf-p = (btree-node-leaf-p node)
+ for i below (btree-node-index-count node)
+ for binding = (p-aref index i)
+ collect (list (binding-key binding)
+ (if leaf-p
+ (binding-value binding)
+ (node-as-cons (binding-value binding))))))
+
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Search
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -211,7 +256,7 @@
(defmethod btree-search (btree key &key (errorp t) (default-value nil))
(restart-case
(if (slot-boundp btree 'root)
- (node-search btree (slot-value btree 'root) key errorp default-value)
+ (node-search btree (btree-root btree) key errorp default-value)
(not-found btree key errorp default-value))
(use-value (value)
:report (lambda (stream)
@@ -239,14 +284,23 @@
;;
;; Node-search
;;
+
+(defun find-binding-in-node (key node btree)
+ (let ((index-count (btree-node-index-count node)))
+ (and (plusp index-count)
+ (loop with array = (btree-node-index node)
+ with btree-key< = (btree-key< btree)
+ for i from 0 below index-count
+ for candidate = (p-aref array i)
+ for candidate-key = (binding-key candidate)
+ while (funcall btree-key< candidate-key key)
+ finally (when (funcall (btree-key= btree) key candidate-key)
+ (return candidate))))))
(defgeneric node-search (btree node key errorp default-value)
(:method ((btree btree) (node btree-node) key errorp default-value)
(if (btree-node-leaf-p node)
- (let ((binding (p-find key (btree-node-index node)
- :key #'binding-key
- :test (btree-key= btree)
- :end (btree-node-index-count node))))
+ (let ((binding (find-binding-in-node key node btree)))
(if binding
(binding-value binding)
(not-found btree key errorp default-value)))
@@ -258,17 +312,15 @@
"Returns the subnode that contains more information for the given key."
;; Find the first binding with a key >= the given key and return
;; the corresponding subnode.
- ;; DO: We should probably use binary search for this.
- (loop for i below (btree-node-index-count node)
- do (let ((binding (node-binding node i)))
- (cond ((= i (1- (btree-node-index-count node)))
- ;; We're at the last binding.
- (return-from find-subnode (binding-value binding)))
- ((funcall (btree-key< btree) key (binding-key binding))
- (let ((next-binding (node-binding node (1+ i))))
- (if (funcall (btree-key= btree) key (binding-key next-binding))
- (return-from find-subnode (binding-value next-binding))
- (return-from find-subnode (binding-value binding))))))))
+ ;; EFFICIENCY: We should probably use binary search for this.
+ (loop with btree-key< = (btree-key< btree)
+ with last-index = (1- (btree-node-index-count node))
+ for i to last-index
+ for binding = (node-binding node i)
+ when (or (= i last-index)
+ (funcall btree-key< key (binding-key binding))
+ (not (funcall btree-key< (binding-key binding) key)))
+ do (return-from find-subnode (binding-value binding)))
(error "This shouldn't happen."))
@@ -292,7 +344,7 @@
:expected-type (btree-value-type btree)))
;; Do the real work.
(if (slot-boundp btree 'root)
- (btree-node-insert btree (slot-value btree 'root) nil key value if-exists)
+ (btree-node-insert btree (btree-root btree) (list nil) key value if-exists)
;; Create a root.
(let ((leaf (make-instance (btree-node-class btree)
:btree btree
@@ -301,7 +353,7 @@
(let* ((empty-leaf (make-instance (btree-node-class btree)
:btree btree
:leaf-p t))
- (root (make-root btree key empty-leaf 'key-irrelevant leaf)))
+ (root (make-root btree key leaf 'key-irrelevant empty-leaf)))
(setf (btree-root btree) root))))
;; Return the inserted value.
value)
@@ -314,6 +366,7 @@
(declare (ignore value))
(when prev-key
(unless (funcall (btree-key< btree) prev-key key)
+ (display-node (btree-root btree))
(error "Btree inconsistency between ~D and ~D" prev-key key)))
(setq prev-key key)))))
@@ -330,17 +383,13 @@
;; Node insert
;;
-(defgeneric btree-node-insert (btree node parent key value if-exists))
+(defgeneric btree-node-insert (btree node parent-stack key value if-exists))
-(defmethod btree-node-insert ((btree btree) (node btree-node) parent key value if-exists)
- (cond ((node-almost-full-p btree node)
- (split-btree-node btree node parent)
- (btree-insert btree key value :if-exists if-exists))
- ((btree-node-leaf-p node)
- (leaf-insert btree node key value if-exists))
+(defmethod btree-node-insert ((btree btree) (node btree-node) parent-stack key value if-exists)
+ (cond ((btree-node-leaf-p node)
+ (leaf-insert btree node parent-stack key value if-exists))
(t (let ((subnode (find-subnode btree node key)))
- (btree-node-insert btree subnode node key value if-exists)))))
-
+ (btree-node-insert btree subnode (cons node parent-stack) key value if-exists)))))
(defun smallest-key (node)
(if (btree-node-leaf-p node)
@@ -353,50 +402,52 @@
(biggest-key (binding-value (node-binding node (1- (btree-node-index-count node)))))))
-(defun split-btree-node (btree node parent)
+(defun split-btree-node (btree node parent-stack key)
;; The node is (almost) full.
;; Create two new nodes and divide the current node-index over
;; these two new nodes.
(let* ((split-pos (floor (btree-node-index-count node) 2))
(left (make-instance (btree-node-class btree)
- :parent parent
:btree btree
:leaf-p (btree-node-leaf-p node)))
(right (make-instance (btree-node-class btree)
- :parent parent
:btree btree
:leaf-p (btree-node-leaf-p node))))
;; Divide the node over the two new nodes.
(p-replace (btree-node-index left) (btree-node-index node)
- :end2 split-pos)
+ :end2 split-pos)
(p-replace (btree-node-index right) (btree-node-index node)
- :start2 split-pos)
+ :start2 split-pos)
(setf (btree-node-index-count left) split-pos
(btree-node-index-count right) (- (btree-node-index-count node) split-pos))
;;
- (let* ((node-pos (and parent (node-position node parent)))
- (parent-binding (and parent (node-binding parent node-pos)))
- (left-key
- ;; The key that splits the two new nodes.
- (smallest-key right))
- (right-key
- (if (null parent)
- 'key-irrelevant
- (binding-key parent-binding))))
- (if (p-eql node (btree-root btree))
- ;; Make a new root.
- (setf (btree-root btree) (make-root btree left-key left right-key right))
- ;; Replace the original subnode by the left-child and
- ;; add a new-binding with new-key & right-child.
- (progn
- (setf (binding-key parent-binding) left-key
- (binding-value parent-binding) left)
- ;; Insert a new binding for the right node.
- (insert-new-binding parent (1+ node-pos) right-key right))))))
-
+ (let ((left-key
+ ;; The key that splits the two new nodes.
+ (biggest-key left)))
+ (cond ((p-eql node (btree-root btree))
+ ;; Make a new root.
+ (setf (btree-root btree)
+ (make-root btree left-key left 'key-irrelevant right)))
+ (t
+ (let* ((parent (first parent-stack))
+ (node-pos (node-position node parent))
+ (parent-binding (node-binding parent node-pos))
+ (old-key (binding-key parent-binding)))
+ (when (node-full-p btree parent)
+ (setq parent (split-btree-node btree parent (rest parent-stack) old-key)
+ node-pos (node-position node parent)))
+ ;; Replace the original subnode by the left-child and
+ ;; add a new-binding with new-key & right-child.
+ (setf (binding-key parent-binding) left-key
+ (binding-value parent-binding) left)
+ ;; Insert a new binding for the right node.
+ (insert-new-binding parent (1+ node-pos) old-key right))))
+ ;; Return the node that's relevant for KEY
+ (if (or (eq key 'key-irrelevant)
+ (funcall (btree-key< btree) left-key key))
+ right
+ left))))
-(defun parent-binding (node parent)
- (node-binding parent (node-position node parent)))
(defun node-position (node parent)
(p-position node (btree-node-index parent)
@@ -405,33 +456,40 @@
(defun insert-new-binding (node position key value)
+ ;; This function must only be called if we know that the index isn't
+ ;; full already
(unless (>= position (btree-node-index-count node))
;; Make room by moving bindings to the right.
(let ((node-index (btree-node-index node))
(length (btree-node-index-count node)))
(p-replace node-index node-index
- :start1 (1+ position) :end1 (1+ length)
- :start2 position :end2 length)))
+ :start1 (1+ position) :end1 (1+ length)
+ :start2 position :end2 length)))
;; Insert new binding.
(setf (node-binding node position) (make-binding key value))
(incf (btree-node-index-count node)))
+;;
+;; Debugging
+;;
+
(defun check-node (btree node)
(loop for i below (1- (btree-node-index-count node))
for left-key = (binding-key (node-binding node i))
for right-key = (binding-key (node-binding node (1+ i)))
do (unless (or (eql right-key 'key-irrelevant)
(funcall (btree-key< btree) left-key right-key))
+ (display-node node)
(error "Inconsistent node ~S" node))))
+;;
+;; Leaf insert
+;;
-(defun leaf-insert (btree leaf key value if-exists)
- (let ((binding (p-find key (btree-node-index leaf)
- :key #'binding-key
- :test (btree-key= btree)
- :end (btree-node-index-count leaf))))
+(defun leaf-insert (btree leaf parent-stack key value if-exists)
+ (let ((binding (find-binding-in-node key leaf btree)))
(if binding
;; Key already exists.
(if (btree-unique-keys-p btree)
@@ -452,21 +510,169 @@
(unless (p-find value (binding-value binding) :test (btree-value= btree))
(setf (binding-value binding)
(p-cons value (binding-value binding)))))
- ;; The key doesn't exist yet. Create a new binding and add it to the
- ;; leaf index in the right position.
- (let ((new-position (p-position key (btree-node-index leaf)
- :test (btree-key< btree)
- :key #'binding-key
- :end (btree-node-index-count leaf))))
- (insert-new-binding leaf
- (or new-position (btree-node-index-count leaf))
- key
- (make-leaf-value btree value))))))
-
-
-(defun node-almost-full-p (btree node)
- (>= (btree-node-index-count node) (1- (btree-max-node-size btree))))
+ ;; The key doesn't exist yet. Create a new binding and add it to the
+ ;; leaf index in the right position.
+ (progn
+ (when (node-full-p btree leaf)
[162 lines skipped]
--- /project/rucksack/cvsroot/rucksack/package.lisp 2006/05/18 12:46:57 1.3
+++ /project/rucksack/cvsroot/rucksack/package.lisp 2006/08/04 22:04:43 1.4
@@ -1,4 +1,4 @@
-;; $Id: package.lisp,v 1.3 2006/05/18 12:46:57 alemmens Exp $
+;; $Id: package.lisp,v 1.4 2006/08/04 22:04:43 alemmens Exp $
#-(or allegro lispworks sbcl openmcl)
(error "Unsupported implementation: ~A" (lisp-implementation-type))
@@ -78,12 +78,13 @@
;; Btrees
#:btree
- #:btree-key< #:btree-key= #:btree-value=
+ #:btree-key< #:btree-key<= #:btree-key= #:btree-key>= #:btree-key>
+ #:btree-value=
#:btree-max-node-size #:btree-unique-keys-p
#:btree-key-type #:btree-value-type
#:btree-node-class #:btree-node
;; Functions
- #:btree-search #:btree-insert #:map-btree
+ #:btree-search #:btree-insert #:btree-delete #:map-btree
;; Conditions
#:btree-error #:btree-search-error #:btree-insertion-error
#:btree-key-already-present-error #:btree-type-error
--- /project/rucksack/cvsroot/rucksack/test.lisp 2006/05/25 13:01:38 1.4
+++ /project/rucksack/cvsroot/rucksack/test.lisp 2006/08/04 22:04:43 1.5
@@ -1,4 +1,4 @@
-;; $Id: test.lisp,v 1.4 2006/05/25 13:01:38 alemmens Exp $
+;; $Id: test.lisp,v 1.5 2006/08/04 22:04:43 alemmens Exp $
(in-package :test-rucksack)
@@ -153,29 +153,102 @@
;; Test btrees as just another persistent data structure.
;;
-(defun test-btree-insert (&key (n 20000) (node-size 100))
- ;; Create a rucksack with btree that maps random integers to the
- ;; equivalent strings in Roman notation.
- (with-rucksack (rucksack *test-suite* :if-exists :supersede)
- (with-transaction ()
- (let ((btree (make-instance 'btree :value= 'string-equal
- :max-node-size node-size)))
- (loop for i from 1 to n
- for key = (random n) do
- (when (zerop (mod i 1000))
- (format t "~D " i))
- (btree-insert btree key (format nil "~R" key)))
- (add-rucksack-root btree rucksack)))))
-
-(defun test-btree-dummy-insert (&key (n 20000))
- ;; This function can be used for timing: subtract the time taken
- ;; by this function from the time taken by TEST-BTREE-INSERT to
- ;; get an estimate of the time needed to manipulate the btrees.
- (loop for i from 1 to n
- for key = (random n)
- when (zerop (mod i 1000)) do (format t "~D " i)
- collect (cons key (format nil "~R" key)))
- t)
+(defun shuffle (array)
+ (loop with n = (array-dimension array 0)
+ repeat n
+ for i = (random n)
+ for j = (random n)
+ when (/= i j)
+ do (rotatef (aref array i) (aref array j))))
+
+(defun check-size (btree expected)
+ (format t "~&Counting~%")
+ (let ((count 0))
+ (map-btree btree
+ (lambda (key value)
+ (declare (ignore key value))
+ (incf count)))
+ (unless (= count expected)
+ (error "Wrong btree size - expected ~A, got ~A."
+ expected count))))
+
+(defun check-order (btree)
+ (format t "~&Checking order~%")
+ (rs::check-btree btree))
+
+(defun check-contents (btree)
+ (format t "~&Checking contents~%")
+ (map-btree btree
+ (lambda (key value)
+ (unless (string= value (format nil "~R" key))
+ (error "Value mismatch: Expected ~S, got ~S."
+ (format nil "~R" key) value)))))
+
+(defmacro with-transaction* ((&rest args) &body body)
+ `(with-transaction ,args
+ (prog1 (progn , at body)
+ (format t "~&Committing..."))))
+
+(defun test-btree (&key (n 20000) (node-size 100) (delete (floor n 10)) check-contents)
+ ;; Create a rucksack with a btree of size N that maps random
+ ;; integers to the equivalent strings as a cardinal English number.
+ ;; Use node size NODE-SIZE for the btree.
+ ;; If DELETE is not NIL, delete and reinsert that number of elements
+ ;; as well.
+ (let ((array (make-array n :initial-contents (loop for i below n collect i))))
+ (shuffle array)
+ (with-rucksack (rucksack *test-suite* :if-exists :supersede)
+ (with-transaction* ()
+ (format t "~&Inserting~%")
+ (let ((btree (make-instance 'btree :value= 'string-equal
+ :max-node-size node-size)))
+ (loop for key across array
+ for i from 1
+ when (zerop (mod i 1000))
+ do (format t "~D " i)
+ do (btree-insert btree key (format nil "~R" key)))
+ (add-rucksack-root btree rucksack))))
+ (with-rucksack (rucksack *test-suite*)
+ (with-transaction ()
+ (let ((btree (first (rucksack-roots rucksack))))
+ (check-order btree)
+ (check-size btree n)
+ (when check-contents
+ (check-contents btree))))
+ (when delete
+ (shuffle array)
+ (setq array (subseq array 0 delete))
+ (shuffle array)
+ (with-transaction* ()
+ (format t "~&Deleting~%")
+ (let ((btree (first (rucksack-roots rucksack))))
+ (dotimes (i delete)
+ (when (zerop (mod (1+ i) 1000))
+ (format t "~D " (1+ i)))
+ (btree-delete btree (aref array i)))
+ (check-order btree)
+ (check-contents btree)))
+ (with-transaction* ()
+ (let ((btree (first (rucksack-roots rucksack))))
+ (check-order btree)
+ (check-size btree (- n delete))
+ (when check-contents
+ (check-contents btree))
+ (format t "~&Reinserting~%")
+ (shuffle array)
+ (dotimes (i delete)
+ (when (zerop (mod (1+ i) 1000))
+ (format t "~D " (1+ i)))
+ (let ((key (aref array i)))
+ (btree-insert btree key (format nil "~R" key))))))
+ (with-transaction ()
+ (let ((btree (first (rucksack-roots rucksack))))
+ (check-order btree)
+ (check-size btree n)
+ (when check-contents
+ (check-contents btree)))))))
+ :ok)
+
(defun test-btree-map (&key (display t))
@@ -187,3 +260,34 @@
(lambda (key value)
(when display
(format t "~&~D -> ~A~%" key value))))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Garbage collector
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun check-gc (n)
+ (with-rucksack (rucksack *test-suite* :if-exists :supersede)
+ (with-transaction ()
+ ;; after this, INNER can be reached directly from the root
+ (let* ((inner (p-cons "Waldorf" "Statler"))
+ (root (p-cons 42 inner)))
+ (add-rucksack-root root rucksack)))
+ (with-transaction ()
+ (let* ((root (first (rucksack-roots rucksack)))
+ (inner (p-cdr root))
+ (array (p-make-array n)))
+ ;; after this, INNER can't be reached from the root anymore
+ (setf (p-cdr root) 43)
+ ;; now let the GC do some work
+ (dotimes (i n)
+ (let ((string (format nil "~R" i)))
+ (setf (p-aref array i) (p-cons string string))))
+ ;; hook INNER back to the root again before we finish the
+ ;; transaction
+ (setf (p-car root) array
+ (p-cdr root) (p-cons 'bar (p-cons 'foo inner)))))
+ (with-transaction ()
+ (let* ((root (first (rucksack-roots rucksack)))
+ (inner (p-cdr (p-cdr (p-cdr root)))))
+ ;; we expect the list ("Waldorf" "Statler") here
+ (list (p-car inner) (p-cdr inner))))))
More information about the rucksack-cvs
mailing list