diff --git a/code/analyzer.lisp b/code/analyzer.lisp index e5603b3..20bd2df 100644 --- a/code/analyzer.lisp +++ b/code/analyzer.lisp @@ -1,29 +1,88 @@ (cl:in-package #:incrementalist) +;;; Initial reader state + +(defun make-initial-reader-state + (&key (readtable eclector.reader:*readtable*) + (package "COMMON-LISP-USER") + (read-base 10) + (read-default-float-format 'single-float) + (read-eval t) + (features *features*)) + ;; Should be ordered roughly by access frequency. + `(;; Reader state aspects that do not correspond to standard + ;; variables. + (eclector.reader::*consing-dot-allowed-p* . nil) + (eclector.reader::*quasiquotation-state* . (nil . nil)) + (eclector.reader::*quasiquotation-depth* . 0) + ;; Reader state aspect that correspond to standard variables. + (*readtable* . ,readtable) + (*read-suppress* . nil) + (*package* . ,package) + (*read-base* . ,read-base) + (*read-default-float-format* . ,read-default-float-format) + (*read-eval* . ,read-eval) + (*features* . ,features) + ;; Labeled objects are not a reader state aspect from Eclector's + ;; point of view. + (labeled-objects . ()))) + +(defun merge-initial-reader-states (delta-state base-state) + (if (eq delta-state base-state) + base-state + (loop :for (aspect . value) :in base-state + :for new-cell = (assoc aspect delta-state) + :collect (cons aspect (if (not (null new-cell)) + (cdr new-cell) + value))))) + (defclass analyzer (buffer-stream) - ((%buffer :initarg :buffer - :reader buffer) - (%cache :initarg :cache - :reader cache)) + ((%buffer :initarg :buffer + :reader buffer) + (%cache :initarg :cache + :reader cache) + ;; Specifies the reader state that should be used for reading the + ;; first toplevel expression of the buffer. The value is a list of + ;; `cell's. + (%initial-reader-state :type #+incrementalist-debug dep:list-of-cells-for-distinct-aspects + #-incrementalist-debug list + :reader initial-reader-state)) (:default-initargs - :buffer (alexandria:required-argument :buffer) - :lines nil)) + :buffer (alexandria:required-argument :buffer) + :lines nil + :initial-reader-state (make-initial-reader-state))) -(defmethod shared-initialize :around ((instance analyzer) - (slot-names t) - &rest args - &key (buffer nil buffer-supplied-p) - (cache nil cache-supplied-p)) - (cond ((not buffer-supplied-p) - (call-next-method)) - (cache-supplied-p - (let ((lines (lines cache))) - (apply #'call-next-method instance slot-names :lines lines args))) - (t - (let* ((cache (make-instance 'cache :buffer buffer)) - (lines (lines cache))) - (apply #'call-next-method instance slot-names - :cache cache :lines lines args))))) +(defmethod shared-initialize :around + ((instance analyzer) + (slot-names t) + &rest args + &key (buffer nil buffer-supplied-p) + (cache nil cache-supplied-p) + (initial-reader-state nil initial-reader-state-supplied-p)) + (prog1 + (cond ((not buffer-supplied-p) + (call-next-method)) + (cache-supplied-p + (let ((lines (lines cache))) + (apply #'call-next-method instance slot-names :lines lines + args))) + (t + (let* ((cache (make-instance 'cache :buffer buffer)) + (lines (lines cache))) + (apply #'call-next-method instance slot-names + :cache cache :lines lines args)))) + ;; If supplied, use aspects from INITIAL-READER-STATE to add + ;; aspects to or change aspects in the default initial reader + ;; state. + (when initial-reader-state-supplied-p + (loop :for (aspect . value) :in (merge-initial-reader-states + initial-reader-state + (make-initial-reader-state)) + :collect (dep:make-cell :global aspect value) :into cells + :finally (setf (slot-value instance '%initial-reader-state) + cells))))) + +;;; (defmethod position< ((left basic-wad) (right analyzer)) (%position< (start-line left) (start-column left) @@ -48,24 +107,46 @@ ;;; stream position, then remove them. ;;; ;;; If we return a wad at all, it has the following properties: -;;; 1. it is absolute -;; 2. all its descendants are relative -;;; 3. the absolute line start line number are up-to-date for the -;;; returned wad and all its descendants +;;; 1. it is valid (`dep:validp' returns true) +;; 2. all its descendants are valid +;;; 3. it is absolute +;;; 4. all its descendants are relative +;;; 5. its absolute start line number is up-to-date +;;; 6. the absolute start line numbers are up-to-date for all its +;;; descendants (defun cached-wad (analyzer) + ;; When a wad is removed here (due to being invalid or not matching + ;; the stream position of ANALYZER), the wad and its descendants are + ;; removed/detached from multiple data structures: + ;; 1. The wad is popped from the residue or suffix (The descendants + ;; of the wad may still be on the residue or worklist) + ;; 2. The wad and its descendants are removed from user lists of any + ;; cells + ;; 3. Any cells that are defined by the wad or its descendants + ;; become invalid (let ((cache (cache analyzer))) - (macrolet ((skip (reader popper) - `(progn - (drain-result-list analyzer cache ,reader ,popper) - (,reader cache)))) - (let ((residue (skip residue pop-from-residue))) - (cond ((null residue) - (let ((suffix (skip suffix pop-from-suffix))) - (if (and (not (null suffix)) - (position= (first suffix) analyzer)) - (compute-absolute-line-numbers (pop-from-suffix cache)) - nil))) - ((position= (first residue) analyzer) - (compute-absolute-line-numbers (first residue))) - (t - nil)))))) + (macrolet + ((maybe-result (reader popper) + `(progn + (drain-result-list analyzer cache ,reader ,popper) + (let ((rest (,reader cache)) + wad validp) + (cond ((null rest) + nil) + ((not (position= (setf wad (first rest)) analyzer)) + nil) + ((and (not (setf validp (dep:validp wad))) + (not (dep:valid-for-state-p wad))) + (detach-wad wad) + (,popper cache) + nil) + (t + (let ((wad (,popper cache))) + (dbg:log :wad "Preparing wad for reuse ~A~%" wad) + (when (not validp) ; not valid but valid for state + (update-invalid-cells wad)) + (compute-absolute-line-numbers wad)))))))) + (let ((residue-wad (maybe-result residue pop-from-residue))) + (if (not (null residue-wad)) + residue-wad + (maybe-result suffix pop-from-suffix)))))) diff --git a/code/cache.lisp b/code/cache.lisp index 79fc836..82ba83a 100644 --- a/code/cache.lisp +++ b/code/cache.lisp @@ -12,49 +12,52 @@ (defclass cache () (;; This slot contains the Cluffer buffer that is being analyzed by ;; this cache instance. - (%buffer :initarg :buffer - :reader buffer) + (%buffer :initarg :buffer + :reader buffer) ;; The time stamp passed to and returned by the Cluffer update ;; protocol. - (%time-stamp :accessor time-stamp - :type (or null alexandria:non-negative-integer) - :initform nil) + (%time-stamp :accessor time-stamp + :type (or null alexandria:non-negative-integer) + :initform nil) ;; This slot contains a list that parallels the prefix and it ;; contains the width of the prefix starting with the first element ;; of the prefix. - (%lines :reader lines - :initform (make-instance 'flx:standard-flexichain)) - (%cluffer-lines :reader cluffer-lines - :initform (make-instance 'flx:standard-flexichain)) + (%lines :reader lines + :initform (make-instance 'flx:standard-flexichain)) + (%cluffer-lines :reader cluffer-lines + :initform (make-instance 'flx:standard-flexichain)) ;; The prefix contains top-level wads in reverse order, so that the ;; last wad in the prefix is the first wad in the buffer. Every ;; top-level wad in the prefix has an absolute line number. - (%prefix :accessor prefix - :type list - :initform '()) - (%prefix-width :accessor prefix-width - :initform '()) + (%prefix :accessor prefix + :type list + :initform '()) + (%prefix-width :accessor prefix-width + :initform '()) ;; The suffix contains top-level wads in the right order. The ;; first top-level wad on the suffix has an absolute line number. ;; All the others have relative line numbers. - (%suffix :accessor suffix - :type list - :initform '()) + (%suffix :accessor suffix + :type list + :initform '()) + (%suffix-invalid-count :accessor suffix-invalid-count + :type alexandria:non-negative-integer + :initform 0) ;; This slot contains a list that parallels the suffix and it ;; contains the width of the suffix starting with the first element ;; of the suffix. - (%suffix-width :accessor suffix-width - :initform '()) + (%suffix-width :accessor suffix-width + :initform '()) ;; The residue is normally empty. The `scavenge' phase puts orphan ;; wads that are still valid on the residue, and these are used by ;; the `read-forms' phase to avoid reading characters when the ;; result is known. - (%residue :accessor residue - :type list - :initform '()) - (%worklist :accessor worklist - :type list - :initform '())) + (%residue :accessor residue + :type list + :initform '()) + (%worklist :accessor worklist + :type list + :initform '())) (:default-initargs :buffer (alexandria:required-argument :buffer))) @@ -109,6 +112,25 @@ (loop for line-number from first-line-number to last-line-number maximize (line-length cache line-number))) +(defmethod (setf dep:invalid) :around ((new-value t) (wad wad)) + ;; If WAD is top-level (that is, it does not have a parent) and the + ;; validity of WAD changes and WAD is on the suffix of the + ;; associated cache, adjust the `suffix-invalid-count' of the cache. + (if (null (parent wad)) + (let ((old-value (dep:invalid wad))) + (flet ((maybe-adjust (amount) + (let ((cache (cache wad)) + suffix) + (when (or (relative-p wad) + (eq wad (first (setf suffix (suffix cache))))) + (incf (suffix-invalid-count cache) amount))))) + (cond ((and (null old-value) (not (null new-value))) + (maybe-adjust 1)) + ((and (not (null old-value)) (null new-value)) + (maybe-adjust -1)))) + (call-next-method)) + (call-next-method))) + (defgeneric pop-from-suffix (cache) (:method ((cache cache)) (let* ((old-suffix (suffix cache)) @@ -117,6 +139,8 @@ (new-suffix-top (first new-suffix))) (assert (not (null old-suffix))) (pop (suffix-width cache)) + (unless (null (dep:invalid old-suffix-top)) + (decf (suffix-invalid-count cache))) (setf (suffix cache) new-suffix) (unless (null new-suffix-top) (relative-to-absolute new-suffix-top (start-line old-suffix-top))) @@ -142,6 +166,8 @@ (first old-suffix-width) (max-line-length cache (1+ (end-line wad)) (1- (start-line old-suffix-top)))))) + (unless (null (dep:invalid wad)) + (incf (suffix-invalid-count cache))) (setf (suffix-width cache) (list* new-width old-suffix-width) (suffix cache) new-suffix) (link-siblings (first (prefix cache)) wad) @@ -182,12 +208,24 @@ (defgeneric suffix-to-prefix (cache) (:method ((cache cache)) - (let ((wad (compute-absolute-line-numbers (pop-from-suffix cache)))) + (let ((wad (pop-from-suffix cache))) + ;; WAD has been taken from the suffix so + ;; 1) the absolute line numbers in WAD itself and its + ;; descendants are potentially invalid. + ;; 2) the list of inherited cells in WAD is not populated + (compute-absolute-line-numbers wad) + (setf (dep:inherited wad) (dep:inheritable (first (prefix cache)))) (push-to-prefix cache wad)))) (defgeneric prefix-to-suffix (cache) (:method ((cache cache)) - (push-to-suffix cache (pop-from-prefix cache)))) + (let ((wad (pop-from-prefix cache))) + ;; During the time WAD will spend on the suffix, its list of + ;; inherited cells would not be kept up-to-date. So we clear + ;; the slot here and recompute a new value when WAD is moved to + ;; the prefix. + (setf (dep:inherited wad) :invalid) + (push-to-suffix cache wad)))) (defun pop-from-worklist (cache) (pop (worklist cache))) @@ -208,7 +246,8 @@ ;; Detach before popping because detaching may traverse the ;; ancestors of the wad and popping may sever the parent ;; link. - :do (,popper ,cache))) + :do (detach-wad (first remaining)) + (,popper ,cache))) (defun finish-scavenge (cache) ;; Move entire worklist to residue @@ -281,6 +320,7 @@ (let ((wad (pop-from-worklist cache))) (if (line-is-inside-wad-p wad line-number) (let ((children (children wad))) + (detach-wad wad :recursive nil) (make-absolute children (start-line wad)) (setf (worklist cache) (append children (worklist cache)))) (push-to-residue cache wad)))) diff --git a/code/client.lisp b/code/client.lisp index 890fc33..e4d6906 100644 --- a/code/client.lisp +++ b/code/client.lisp @@ -25,7 +25,12 @@ ;;; Read-time evaluation (defmethod reader:evaluate-expression ((client client) (expression t)) - 1) + ;; HACK: we support setting `*read-base*' via + ;; #.(setf *read-base* ...) for testing + (multiple-value-bind (value valuep) (maybe-set-read-base client expression) + (if valuep + value + 1))) ;;; Token interpretation @@ -263,7 +268,8 @@ ;; that is still on the residue or suffix without performing the ;; corresponding recursive `read-maybe-nothing' call. As a ;; workaround, consume any such wads here. - (cached-wad stream) + (alexandria:when-let ((cached (cached-wad stream))) + (detach-wad cached)) ;; Separate CHILDREN into children of type `cst:cst' and "extra" ;; children. Extra children arise mainly due to comments and other ;; skipped input which are represented as `wad's which are not of diff --git a/code/dependencies.lisp b/code/dependencies.lisp new file mode 100644 index 0000000..96f7efd --- /dev/null +++ b/code/dependencies.lisp @@ -0,0 +1,155 @@ +(cl:in-package #:incrementalist) + +;;; Invalidation + +;;; Invalidate wads on the suffix (thus "following" the current parse +;;; location) that use CELL. +(defun invalidate-following-users (cell) + (dbg:log :invalidation " Invalidating following users of ~A~%" cell) + (let (suffix residue worklist (following nil)) + (labels ((initialize (cache) + (setf suffix (suffix cache) + residue (residue cache) + worklist (worklist cache)) + (when (and (null suffix) (null residue) (null worklist)) + (return-from invalidate-following-users)) + (setf following (make-hash-table :test #'eq))) + (compute-followingp (wad) + (alexandria:if-let ((parent (parent wad))) + (followingp parent) + (when (or (relative-p wad) ; in rest of suffix + (eq wad (first suffix)) + (find wad residue :test #'eq) + (find wad worklist :test #'eq)) + t))) + (followingp (wad) + (alexandria:ensure-gethash + wad following (compute-followingp wad))) + (visit-user (user) + ;; Initialize lazily and possibly exit early if there + ;; are no following users. + (when (null following) + (initialize (cache user))) + (when (followingp user) + (dbg:log :invalidation " Invalidating ~A~%" user) + (invalidate-wad user cell)))) + (declare (dynamic-extent #'initialize #'compute-followingp #'followingp + #'visit-user)) + (dep:map-users #'visit-user cell)))) + +;;; Cell updates + +;;; Update the invalid WAD to make it valid again so that it can be +;;; reused. To this end, replace cells on the `invalid' cell list of +;;; WAD with the current cell for the respective aspect. Perform the +;;; update in descendants and parents of WAD as well. +(defun update-invalid-cells (wad) + (loop :for old-cell :in (dep:invalid wad) + :for aspect = (dep:aspect old-cell) + :for new-cell = (dep:find-cell aspect) + :do (dbg:log :wad " Updating cell ~A~%" old-cell) + ;; Collect all descendants of WAD in which CELL is also + ;; invalid then update OLD-CELL to NEW-CELL in all wads. + (let ((descendants (make-array 1 :adjustable t :fill-pointer 0))) + (labels ((visit (wad) + (when (find old-cell (dep:invalid wad) :test #'eq) + (vector-push-extend wad descendants) + (map-children #'visit wad)))) + (visit wad)) + (update-invalid-cell old-cell new-cell descendants)))) + +(defun update-invalid-cell (old-cell new-cell wads) + (assert (dep:validp new-cell)) ; important when (eq old-cell new-cell) + (labels ((update-use (wad) + (let ((invalid (dep:invalid wad))) + (when (find old-cell invalid :test #'eq) + (setf (dep:invalid wad) (delete old-cell invalid + :test #'eq :count 1)) + ;; Update use relation in WAD from OLD-CELL to + ;; NEW-CELL if necessary. + (unless (eq old-cell new-cell) + (let ((used (dep:used wad))) + (when (find old-cell used :test #'eq) + (dep:remove-user wad old-cell) + (alexandria:deletef used old-cell :test #'eq :count 1) + (unless (find new-cell used :test #'eq) + (dep:add-user wad new-cell) + (setf used (list* new-cell used))) + (setf (dep:used wad) used)))) + ;; The parent of WAD may have been invalidated without + ;; directly using OLD-CELL. We still have to remove + ;; OLD-CELL from the invalidation list of the parent. + (alexandria:when-let ((parent (parent wad))) + (update-use parent)))))) + (dbg:log :wad " Replacing cell ~A -> ~A~%" old-cell new-cell) + (map nil #'update-use wads))) + +;;; Labeled objects state + +(defmethod eclector.reader:find-labeled-object ((client client) (label t)) + (let* ((cell (dep:find-cell 'labeled-objects)) + (value (dep:value cell)) + (object (alexandria:assoc-value value label))) + (assert (not (null cell))) + (dep:register-use cell) + (if (null object) + (call-next-method) + object))) + +(defmethod eclector.reader:note-labeled-object ((client client) + (input-stream t) + (label t) + (parent t)) + (let* ((values (multiple-value-list (call-next-method))) + (object (first values)) + (parent (dep:find-cell 'labeled-objects)) + (old-value (dep:value parent)) + (new-value (list* (cons label object) old-value))) + (dep:make-and-register-cell + 'labeled-objects :top-level-expression new-value + #'invalidate-following-users :parent parent) + (values-list values))) + +;;; Other state value + +(defmethod eclector.reader:state-value ((client client) (aspect t)) + (let ((cell (dep:find-cell aspect))) + (assert (not (null cell))) + ;; When this is called from `call-as-top-level-read', + ;; `dep:*used-cells*' is not bound yet since that happens in the + ;; `read-maybe-nothing' method. + (when (boundp 'dep:*used-cells*) + (dep:register-use cell)) + (dep:value cell))) + +(flet ((check-state-value-type (client aspect value) + (unless (eclector.reader:valid-state-value-p client aspect value) + (error 'eclector.reader:state-value-type-error + :stream (stream* client) + :datum value + :expected-type (case aspect ; TODO: get type from Eclector? + (*read-base* '(integer 2 36)) + (t nil)) + :aspect aspect)))) + + (defmethod (setf eclector.reader:state-value) ((new-value t) + (client client) + (aspect t)) + (check-state-value-type client aspect new-value) + (let* ((parent (dep:find-cell aspect)) + (scope (dep:scope parent))) + (dep:make-and-register-cell + aspect scope new-value #'invalidate-following-users)) + new-value) + + (defmethod eclector.reader:call-with-state-value ((client client) + (thunk t) + (aspect t) + (value t)) + (check-state-value-type client aspect value) + (let ((parent (dep:find-cell aspect))) + (dep:make-and-register-cell + aspect :lexical value #'invalidate-following-users :parent parent) + (unwind-protect + (funcall thunk) + (dep:pop-cell-until aspect parent))))) diff --git a/code/dependencies/debug-types.lisp b/code/dependencies/debug-types.lisp new file mode 100644 index 0000000..cd051d0 --- /dev/null +++ b/code/dependencies/debug-types.lisp @@ -0,0 +1,25 @@ +(cl:in-package #:incrementalist.dependencies) + +(defun %every-cell (object) + (and (listp object) + (every (alexandria:of-type 'cell) object))) + +(defun %distinct-aspects (object) + (and (listp object) + (let ((seen-aspects '())) + (every (lambda (cell) + (let ((aspect (aspect cell))) + (if (find aspect seen-aspects :test #'eq) + nil + (progn + (push aspect seen-aspects) + t)))) + object)))) + +(deftype list-of-cells () + `(and list (satisfies %every-cell))) + +(deftype list-of-cells-for-distinct-aspects () + `(and list + (satisfies %every-cell) + (satisfies %distinct-aspects))) diff --git a/code/dependencies/dependencies.lisp b/code/dependencies/dependencies.lisp new file mode 100644 index 0000000..accad5c --- /dev/null +++ b/code/dependencies/dependencies.lisp @@ -0,0 +1,446 @@ +(cl:in-package #:incrementalist.dependencies) + +;;; Scopes and aspects + +(defun scope-string (scope) + (ecase scope + (:lexical "v") + (:top-level-expression ">") + (:global "+"))) + +(defun aspect-string (aspect) + (check-type aspect symbol) + (let ((name (symbol-name aspect))) + (if (< (length name) 12) + name + (with-output-to-string (stream) + (reduce (lambda (state char) + (case char + ((#\* #\-) + (write-char char stream) + :start) + (t + (when (eq state :start) + (write-char char stream)) + :middle))) + name :initial-value :start))))) + +(#+sbcl sb-ext:defglobal #-sbcl defvar **value-pprint-dispatch** + (let ((table (copy-pprint-dispatch))) + (set-pprint-dispatch '(or standard-object structure-object) + (lambda (stream object) + (declare (ignore object)) + (write-string "#<..>" stream)) + 1 table) + table)) + +(defun value-string (value) + (with-output-to-string (stream) + (let ((*print-length* 3) + (*print-pprint-dispatch* **value-pprint-dispatch**)) + (prin1 value stream)))) + +;;; Class `cell' and cell protocol implementation + +(defclass cell () + ((%aspect :initarg :aspect + :reader aspect) + (%value :initarg :value + :reader value) + ;; Objects (such as wads) that use the cell. Since there are often + ;; no or few users, start with a compact representation and upgrade + ;; as necessary. + (%users :accessor %users + :type (or null hash-table) + :initform nil) + ;; Validity + (%validp :accessor validp + :type boolean + :initform t)) + (:default-initargs + :aspect (a:required-argument :aspect) + :value (a:required-argument :value))) + +(defmethod print-object ((object cell) stream) + (print-unreadable-object (object stream :identity t) + (let* ((validp (validp object)) + (aspect (aspect object)) + (scope-string (scope-string (scope object))) + (aspect-string (aspect-string aspect)) + (value (value object)) + (user-count (length (users object)))) + (format stream "~A ~:[INVALID ~;~]~A ~A → " + (symbol-name 'cell) validp scope-string aspect-string) + (let ((*print-length* 3) + (*print-pprint-dispatch* **value-pprint-dispatch**)) + (prin1 value stream)) + (format stream " (~D user~:P)" user-count)))) + +(defmethod map-users ((function t) (cell cell)) + (a:when-let ((users (%users cell))) + (let ((function (alexandria:ensure-function function))) + (a:maphash-values function users)))) + +(defmethod users ((cell cell)) + (a:if-let ((users (%users cell))) + (a:hash-table-values users) + nil)) + +(defmethod add-user ((user t) (cell cell)) + (let ((users (%users cell))) + (when (null users) + (setf users (make-hash-table :test #'eq) + (%users cell) users)) + (setf (gethash user users) user))) + +(defmethod remove-user ((user t) (cell cell)) + (remhash user (%users cell))) + +(defmethod remove-user ((user vector) (cell cell)) + (loop :with users = (%users cell) + :for user* :across user + :do (remhash user* users))) + +(macrolet ((define (class-name scope) + `(progn + (defclass ,class-name (cell) ()) + + (defmethod scope ((cell ,class-name)) ',scope)))) + (define globally-scoped-cell :global) + (define top-level-expression-scoped-cell :top-level-expression) + (define lexically-scoped-cell :lexical)) + +(declaim (inline make-cell)) +(defun make-cell (scope aspect value) + (ecase scope + (:global + (make-instance 'globally-scoped-cell :aspect aspect :value value)) + (:top-level-expression + (make-instance 'top-level-expression-scoped-cell + :aspect aspect :value value)) + (:lexical + (make-instance 'lexically-scoped-cell :aspect aspect :value value)))) + +;;; Class `cell-user-mixin' + +(deftype object-or-list (list) + `(or null standard-object ,list)) + +(defclass cell-user-mixin () + (;; Either `nil' if the wad is valid, `t' if the wad has been + ;; explicitly invalidated or a list of invalid cells that is a + ;; subset of the used cells. In the latter case, the wad may or may + ;; not be restored from the cache and reused depending on whether + ;; the invalid cells have been replaced by new cells with + ;; compatible values. + (%invalid :accessor invalid + :type (or (eql t) #+incrementalist-debug list-of-cells + #-incrementalist-debug list) + :initform '()) + ;; Cells used in this wad: invalidation of any of the listed cells + ;; may cause the wad to become invalid. + (%used :accessor used + :type #+incrementalist-debug list-of-cells-for-distinct-aspects + #-incrementalist-debug list + :initform '()) + ;; Cells directly defined in this wad. + (%defined :accessor %defined + :type (object-or-list #+incrementalist-debug list-of-cells + #-incrementalist-debug list) + :initform nil) + ;; Cells that are defined in descendants of the wad and can affect + ;; wads which follow this wad. + (%escaped :accessor %escaped + :type (object-or-list + #+incrementalist-debug list-of-cells-for-distinct-aspects + #-incrementalist-debug list) + :initform nil) + ;; This slot is used only when the wad is at the top-level. Cells + ;; inherited from the previous top-level wad or the initial + ;; environment. The set of cells can intersect the sets in the + ;; USED and ESCAPED slots (in terms of both, cell objects and + ;; "covered" aspects). + ;; + ;; Note: The value is potentially shared between users and thus + ;; must not be destructively modified. + (%inherited :accessor inherited + :type (or (eql :invalid) + #+incrementalist-debug list-of-cells-for-distinct-aspects + #-incrementalist-debug list) + :initform '()))) + +(defmethod validp ((object cell-user-mixin)) + (null (invalid object))) + +(macrolet ((%map (function defined) + (alexandria:once-only (defined) + `(typecase ,defined + (null) + (cons (mapc ,function ,defined)) + (t (funcall ,function ,defined))))) + (do-external ((cell-var external-scopes defined escaped) &body body) + `(labels ((do-it (,cell-var) + ,@body) + (consider (cell) + (let ((scope (scope cell))) + (when ,(if (keywordp external-scopes) + `(eq scope ,external-scopes) + `(find scope ,external-scopes + :test #'eq)) + (do-it cell))))) + (declare (dynamic-extent #'do-it #'consider)) + (%map #'consider ,defined) + (%map #'do-it ,escaped)))) + + (defmethod map-defined ((function t) (object cell-user-mixin)) + (%map function (%defined object))) + + (defmethod defined ((object cell-user-mixin)) + (let ((defined (%defined object))) + (typecase defined + (list defined) + (t (list defined))))) + + (defmethod add-defined ((defined cell) (object cell-user-mixin)) + (let ((old-defined (%defined object))) + (setf (%defined object) (typecase old-defined + (null defined) + (cons (list* defined old-defined)) + (t (list defined old-defined)))))) + + (defmethod add-defined ((defined list) (object cell-user-mixin)) + (let* ((old-defined (%defined object)) + (new-defined (typecase old-defined + (null defined) + (cons (nconc old-defined defined)) + (t (list* old-defined defined))))) + (setf (%defined object) (typecase new-defined + (null '()) + ((cons t null) (first new-defined)) + (t new-defined))))) + + (defmethod map-escaped ((function t) (object cell-user-mixin)) + (%map function (%escaped object))) + + (defmethod escaped ((object cell-user-mixin)) + (let ((escaped (%escaped object))) + (typecase escaped + (list escaped) + (t (list escaped))))) + + (defmethod (setf escaped) ((new-value list) (object cell-user-mixin)) + (setf (%escaped object) (typecase new-value + ((cons t null) (first new-value)) + (t new-value))) + new-value) + + (defmethod map-external ((function t) (object cell-user-mixin) + &key (external-scopes '(:global + :top-level-expression))) + (let ((function (alexandria:ensure-function function))) + (do-external (cell external-scopes (%defined object) (%escaped object)) + (funcall function cell)))) + + ;; Only for top-level objects + (defmethod inheritable ((object cell-user-mixin)) + (let ((defined (%defined object)) + (escaped (%escaped object))) + (if (and (null defined) (null escaped)) + (inherited object) + (let ((result (copy-list (inherited object)))) + (do-external (cell :global defined escaped) + (setf result (nsubstitute cell (aspect cell) result + :test #'eq :key #'aspect))) + result))))) + +(defun add-dependencies (object defined-cells used-cells escaping-cells) + (unless (null defined-cells) + (add-defined defined-cells object)) + (unless (null used-cells) + (alexandria:nconcf (used object) used-cells) + (loop :for cell :in used-cells + :do (add-user object cell))) + (unless (null escaping-cells) + (setf (escaped object) escaping-cells))) + +(defun update-escaping-for-toplevel-user (user invalidate-users) + (let ((escaping '())) + (map-escaped (lambda (cell) + (if (eq (scope cell) :global) + (push cell escaping) + (progn + (dbg:log :invalidate " ~A reached toplevel, invalidating following users~%" cell) + (funcall invalidate-users cell)))) + user) + (dbg:log :register " For ~A, reducing escaping~% from ~:A~% to ~:A~% " + user (escaped user) escaping) + (setf (escaped user) (nreverse escaping)))) + +;;; Lexical state protocol + +;;; Alist is faster than hash-table for maybe seven or eight distinct +;;; aspects when the frequently accessed aspects are at the front of +;;; the list. +(declaim (type list *cells*)) +(defvar *cells*) + +(defmacro with-cells (() &body body) + `(let ((*cells* '())) ,@body)) + +(declaim (inline find-cell push-cell pop-cell)) + +(defun find-cell (aspect) + (first (cdr (assoc aspect *cells* :test #'eq)))) + +(defun push-cell (cell aspect) + (let* ((cells *cells*) + (entry (or (assoc aspect cells :test #'eq) + (let ((entry (cons aspect '()))) + (setf *cells* (nconc cells (list entry))) + entry)))) + (push cell (cdr entry)))) + +(defun pop-cell (aspect) + (pop (cdr (assoc aspect *cells* :test #'eq)))) + +(defun pop-cell-until (aspect new-top-cell) + (loop :with entry = (assoc aspect *cells* :test #'eq) + :for stack = (cdr entry) :then rest + :for (first . rest) = stack + :when (eq first new-top-cell) + :do (setf (cdr entry) stack) + (loop-finish))) + +(defun pop-local-cells () + (loop :for entry :in *cells* + :for (nil . stack) = entry + :for cell = (first stack) + :for scope = (scope cell) + :when (eq scope :top-level-expression) + ;; Remove all but global value of the cell. + :do (setf (cdr entry) (last stack)))) + +;;; Restoring current reader state + +(defun install-state (cells) + (dbg:log :state "Initial state~%") + (loop :for cell :in cells + :do (dbg:log :state " ~10A ~A -> ~A~%" + :initial (aspect cell) (value-string (value cell))) + (push-cell cell (aspect cell)))) + +(defun install-state-from-user (user toplevelp) + (dbg:log :state "State from ~A~%" user) + (let ((aspects (make-array 16 :adjustable nil :fill-pointer 0))) + (labels ((install-cell (cell where-from) + (assert (validp cell)) + (if (case (scope cell) + (:lexical nil) + (:top-level-expression (not toplevelp)) + (:global t)) + (let ((aspect (aspect cell))) + (dbg:log :state " ~10A ~A -> ~A~%" + where-from aspect (value-string (value cell))) + (assert (not (find aspect aspects :test #'eq))) + (vector-push aspect aspects) + (push-cell cell aspect)) + (dbg:log :state " ~10A Not using ~A, valid ~A, toplevel ~A~%" + where-from cell (validp cell) toplevelp))) + (maybe-install-cell (cell where-from) + (unless (find (aspect cell) aspects :test #'eq) + (install-cell cell where-from)))) + ;; Escaped cells must take precedence over defined cells in situations + ;; such as + ;; + ;; #1=(#2=foo #3=bar) + ;; + ;; in which the value of the defined cell will be ((1 . #<..>)) but the + ;; value of the escaped cell will be + ;; ((3 . #<..>) (2 . #<..>) (1 . #<..>)). + (map-escaped (lambda (cell) (install-cell cell :escaped)) user) + (map-defined (lambda (cell) (maybe-install-cell cell :defined)) user) + (mapc (lambda (cell) (maybe-install-cell cell :inherited)) + (inherited user))))) + +(defmethod valid-for-state-p ((object cell-user-mixin)) + (let ((invalid (invalid object))) + (and (listp invalid) ; When INVALID is `t', OBJECT is always invalid + (loop :for cell :in invalid + :for aspect = (aspect cell) + :for state-cell = (find-cell aspect) + :always (and state-cell + (equal (value cell) (value state-cell))))))) + +;;; Cell definition and use protocol + +;;; List of cells that are being newly defined in the scope of the current +;;; `read-maybe-nothing' call. +(declaim (type list *defined-cells*)) +(defvar *defined-cells*) + +;;; List of cells that have been used in the scope of the current +;;; `read-maybe-nothing' call. +(declaim (type list *used-cells*)) +(defvar *used-cells*) + +;;; List of cells that have been defined in the scope of the current +;;; `read-maybe-nothing' call or descendant `read-maybe-nothing' calls and may +;;; affect the results of subsequent `read-maybe-nothing' calls. +(declaim (type #+incrementalist-debug list-of-cells-for-distinct-aspects + #-incrementalist-debug list + *escaping-cells*)) +(defvar *escaping-cells*) + +(defun register-definition (new-cell parent invalidate-users) + ;; When the new definition NEW-CELL is inserted between the + ;; definition PARENT and uses of PARENT, those uses may become + ;; invalid since they may have to use NEW-CELL instead of + ;; PARENT. `invalidate-users' must take care of this invalidation. + (dbg:log :invalidation " Registering definitions~%") + (when (not (null parent)) + (dbg:log :invalidation " Defined ~A, invalidating users~%" new-cell) + (funcall invalidate-users parent)) + (pushnew new-cell *defined-cells* :test #'eq)) + +;;; Declaimed INLINE so that for constant SCOPE, when MAKE-CELL is inlined, the +;;; MAKE-INSTANCE call for the cell sees a constant class. +(declaim (inline make-and-register-cell)) +(defun make-and-register-cell (aspect scope value invalidate-users + &key (parent (find-cell aspect))) + (let ((cell (make-cell scope aspect value))) + (register-definition cell parent invalidate-users) + (push-cell cell aspect) + cell)) + +(declaim (inline register-use)) +(defun register-use (cell) + (pushnew cell *used-cells* :test #'eq)) + +(defun register-escaping (definer) + (dbg:log :register " Registering escaping cells~%") + ;; The variable is bound if the `read' call which calls this + ;; function is surrounded by another read call (the cells which + ;; escape from the inner call escape to the outer call). + (when (boundp '*escaping-cells*) + (let ((escaping *escaping-cells*)) + (flet ((visit-cell (cell) + (dbg:log :register " ~A~%" cell) + ;; ESCAPING may already contain a cell for the aspect + ;; of CELL for two reasons: 1) a previous call to + ;; `register-escaping' 2) DEFINER may contain multiple + ;; definitions for the same aspect. + (let ((other (delete (aspect cell) escaping + :test #'eq :key #'aspect :count 1))) + (setf escaping (list* cell other))))) + (declare (dynamic-extent #'visit-cell)) + (map-external #'visit-cell definer)) + (setf *escaping-cells* escaping)))) + +(defmacro with-cell-dependencies ((add-dependencies-name) &body body) + `(let ((*defined-cells* '()) + (*used-cells* '()) + (*escaping-cells* '())) + (flet ((,add-dependencies-name (object) + (add-dependencies + object *defined-cells* *used-cells* *escaping-cells*))) + ,@body))) diff --git a/code/dependencies/extra-assertions.lisp b/code/dependencies/extra-assertions.lisp new file mode 100644 index 0000000..13bdd20 --- /dev/null +++ b/code/dependencies/extra-assertions.lisp @@ -0,0 +1,28 @@ +(cl:in-package #:incrementalist.dependencies) + +;;; Class `cell' + +(defmethod remove-user :before ((user t) (cell cell)) + (assert (not (null (%users cell))))) + +;;; Class `cell-user-mixin' + +(dbg:define-invariant add-dependencies + :before (((object 0) (defined-cells 1) #|(used-cells 2)|# (escaping-cells 3)) + (assert (= (length defined-cells) + (length (remove-duplicates defined-cells :test #'eq)))) + (loop :for cell :in escaping-cells + :do (assert (member (scope cell) + '(:top-level-expression :global))) + (assert (not (find cell (defined object))))))) + +(defmethod inheritable :before ((object cell-user-mixin)) + ;; Since this is only called for top-level objects, all escaping + ;; cells must have global scope. + (assert (every (lambda (cell) (eq (scope cell) :global)) (escaped object))) + ;; Every external cell overwrites an inherited cell. + (let ((inherited (inherited object))) + (map-external + (lambda (cell) + (assert (find (aspect cell) inherited :test #'eq :key #'aspect))) + object :external-scopes '(:global)))) diff --git a/code/dependencies/package.lisp b/code/dependencies/package.lisp new file mode 100644 index 0000000..2002829 --- /dev/null +++ b/code/dependencies/package.lisp @@ -0,0 +1,80 @@ +(cl:defpackage #:incrementalist.dependencies + (:use + #:cl) + + (:local-nicknames + (#:a #:alexandria) + + (#:dbg #:incrementalist.debug)) + + ;; Types + (:export + #+incrementalist-debug #:list-of-cells + #+incrementalist-debug #:list-of-cells-for-distinct-aspects) + + ;; Validity protocol + (:export + #:validp) + + ;; Cell protocol and classes + (:export + #:aspect + #:scope + #:value + #:map-users + #:users + #:add-user + #:remove-user + + #:cell + #:globally-scoped-cell + #:top-level-expression-scoped-cell + #:lexically-scoped-cell + + #:make-cell) + + ;; Cell user protocol and mixin + (:export + #:valid-for-state-p + #:create-time + #:invalid ; also `setf' + #:used ; also `setf' + #:map-defined + #:defined + #:add-defined + #:map-escaped + #:escaped ; also `setf' + #:map-external + #:inheritable + #:inherited ; also `setf' + + #:add-dependencies + + #:update-escaping-for-toplevel-user + + #:cell-user-mixin) + + ;; Lexical state protocol + (:export + #:with-cells + #:find-cell + #:push-cell + #:pop-cell + #:pop-cell-until + #:pop-local-cells + + #:install-state + #:install-state-from-user) + + ;; Cell definition and use protocol + (:export + #:*defined-cells* + #:*used-cells* + #:*escaping-cells* + + #:register-definition + #:make-and-register-cell + #:register-use + #:register-escaping + + #:with-cell-dependencies)) diff --git a/code/dependencies/protocol.lisp b/code/dependencies/protocol.lisp new file mode 100644 index 0000000..e59b286 --- /dev/null +++ b/code/dependencies/protocol.lisp @@ -0,0 +1,57 @@ +(cl:in-package #:incrementalist.dependencies) + +;;; Validity protocol + +(defgeneric validp (object)) + +;;; Cell protocol +;;; extends validity protocol + +(defgeneric aspect (cell)) + +(defgeneric scope (cell)) + +(defgeneric value (cell)) + +(defgeneric map-users (function cell)) + +(defgeneric users (cell)) + +(defgeneric (setf validp) (new-value cell)) + +(defgeneric add-user (user cell)) + +(defgeneric remove-user (user cell)) + +;;; Cell user protocol +;;; extends validity protocol + +(defgeneric valid-for-state-p (cell-user)) + +(defgeneric create-time (cell-user)) + +(defgeneric invalid (cell-user)) + +(defgeneric (setf invalid) (new-value cell-user)) + +(defgeneric used (cell-user)) + +(defgeneric (setf used) (new-value cell-user)) + +(defgeneric map-defined (function cell-user)) + +(defgeneric defined (cell-user)) + +(defgeneric add-defined (new-value cell-user)) + +(defgeneric map-escaped (function cell-user)) + +(defgeneric escaped (cell-user)) + +(defgeneric (setf escaped) (new-value cell-user)) + +(defgeneric map-external (function cell-user &key external-scopes)) + +(defgeneric inherited (cell-user)) + +(defgeneric (setf inherited) (new-value cell-user)) diff --git a/code/extra-assertions.lisp b/code/extra-assertions.lisp index e694771..bebd0ea 100644 --- a/code/extra-assertions.lisp +++ b/code/extra-assertions.lisp @@ -154,6 +154,10 @@ (defmethod (setf left-sibling) :before ((new-value t) (object t)) (assert (not (eq new-value object)))) +(dbg:define-invariant invalidate-cell + :before (((cell 0)) + (assert (dep:validp cell)))) + ;;; Reader (dbg:define-invariant make-children-relative-and-set-family-relations @@ -269,6 +273,11 @@ ;;; Cache +(defmethod suffix-invalid-count :around ((cache cache)) + (let ((value (call-next-method))) + (assert (= value (count-if-not #'dep:validp (suffix cache)))) + value)) + (dbg:define-invariant pop-from-suffix :before (((cache 0)) (let* ((old-suffix (suffix cache)) @@ -318,9 +327,34 @@ (check-absolute-line-numbers result) (assert (null (parent result)))))) -(let (cache) +(defun no-dangling-nodes (analyzer) + (let* ((cache (cache analyzer)) + (results (append (prefix cache) + (suffix cache) + (worklist cache) + (residue cache))) + (nodes (make-hash-table :test #'eq))) + (labels ((rec (node) + (setf (gethash node nodes) t) + (map-children #'rec node))) + (mapc #'rec results)) + (loop :for cell :in (initial-reader-state analyzer) + :do (loop :for user :in (dep:users cell) + :do (assert (nth-value 1 (gethash user nodes))))))) + +(let (analyzer cache) (dbg:define-invariant update - :before (((analyzer 0)) - (setf cache (cache analyzer))) + :before (((analyzer* 0)) + (setf analyzer analyzer* + cache (cache analyzer*))) + :after (() + (check-wad-graph cache) + (no-dangling-nodes analyzer)))) + +(let (analyzer) + (dbg:define-invariant read-forms + :before (((analyzer* 0)) + (setf analyzer analyzer*) + (no-dangling-nodes analyzer)) :after (() - (check-wad-graph cache)))) + (no-dangling-nodes analyzer)))) diff --git a/code/package.lisp b/code/package.lisp index b0bbd62..ad8ec9a 100644 --- a/code/package.lisp +++ b/code/package.lisp @@ -8,6 +8,8 @@ (#:stream #:trivial-gray-streams) (#:flx #:flexichain) + (#:dep #:incrementalist.dependencies) + (#:dbg #:incrementalist.debug)) (:shadow @@ -45,6 +47,8 @@ #:start-column #:end-column #:items + #:state-aspect-cell + #:state-value #:errors #:indentation ; wad protocol @@ -109,6 +113,7 @@ ;; Analyzer protocol ;; Extends associated buffer protocol (:export + #:make-initial-reader-state #:analyzer ; class #:cache ; readers diff --git a/code/process.lisp b/code/process.lisp new file mode 100644 index 0000000..7887567 --- /dev/null +++ b/code/process.lisp @@ -0,0 +1,53 @@ +(cl:in-package #:incrementalist) + +(defun maybe-set-read-base (client expression) + (if (and (typep expression '(cons existing-symbol-token + (cons existing-symbol-token + (cons t null)))) + (and (equal (package-name (first expression)) + **common-lisp-package-name**) + (equal (name (first expression)) "SETF")) + (and (equal (package-name (second expression)) + **common-lisp-package-name**) + (equal (name (second expression)) "*READ-BASE*"))) + (let* ((form (subst `(eclector.reader:state-value ,client '*read-base*) + '*read-base* + `(setf *read-base* ,(third expression)))) + (result (eval form))) + (dbg:log :eval "Expression ~S~%~2@T-> ~S~%~2@T-> ~S~%" + expression form result) + (values result t)))) + +(defun maybe-change-package (client wad) + (let (second) + (when (and (typep wad 'cst:cst) + (cst:consp wad) + (let ((first (cst:first wad))) + (and (cst:atom first) + (typep (cst:raw first) 'existing-symbol-token) + (string= (package-name (cst:raw first)) "COMMON-LISP") + (let ((raw (cst:raw first))) + (string= (typecase raw + (string raw) + (t (name raw))) + "IN-PACKAGE")))) + (let ((rest (cst:rest wad))) + (and (cst:consp rest) + (cst:null (cst:rest rest)) + (typep (setf second (cst:first rest)) 'cst:cst) + (cst:atom second) + (let ((raw (cst:raw second))) + (typep raw '(or string symbol-token)))))) + (setf (reader:state-value client '*package*) + (let ((raw (cst:raw (cst:first (cst:rest wad))))) + (typecase raw + (string raw) + (t (name raw))))) + t))) + +(defun apply-semantics (client wad) + ;; HACK: we support setting `*package*' as a special case for + ;; testing + (dep:with-cell-dependencies (add-dependencies) + (when (maybe-change-package client wad) + (add-dependencies wad)))) diff --git a/code/read.lisp b/code/read.lisp index 8b8dbf0..ec9daaf 100644 --- a/code/read.lisp +++ b/code/read.lisp @@ -44,17 +44,25 @@ ;; not make any progress. (and (zerop (height cached)) (= (start-column cached) (end-column cached)))) - ;; Nothing has been cached, so call - ;; `read-maybe-nothing'. Collect errors in *ERRORS* and - ;; integrate them into RESULT. - (let ((*errors* '())) - (multiple-value-bind (object kind result) - (call-next-method) - (when (and (not (null result)) ; RESULT can be `null' for `:skip' - (member kind '(:object :skip))) + ;; Nothing has been cached, so call `read-maybe-nothing'. + (let (object kind result + (*errors* '())) + ;; Collect errors into *ERRORS* as well as defined, used and + ;; escaping cells into the respective variable. Later, + ;; integrate the collected things into the RESULT. + (dep:with-cell-dependencies (add-dependencies) + (setf (values object kind result) (call-next-method)) + (when (not (null result)) + (add-dependencies result))) + (when (not (null result)) ; RESULT can be `null' for KIND `:skip' + (dbg:log :state "Got fresh wad ~A~%" result) + (when (member kind '(:object :skip)) (add-errors result *errors*)) - (values object kind result))) - ;; There is a cached wad for the current input position. Turn + ;; Put defined and escaping cells into escaping cells for + ;; surround `read' call (if any). + (dep:register-escaping result)) + (values object kind result)) + ;; There is a cached wad for the current input position. Turn ;; the wad into appropriate return values, inject it into ;; Eclector's result stack and advance STREAM. (multiple-value-prog1 @@ -62,8 +70,36 @@ (read-suppress-wad (values nil :suppress cached t)) (non-cst-wad (values nil :skip cached t)) (cst-wad (values (cst:raw cached) :object cached t))) + ;; STREAM has to be advanced before the stream line and item + ;; number are read below. + (advance-stream-to-beyond-wad stream cached) + (dbg:log :state "Using restored wad ~A~%" cached) + ;; Put defined and escaping cells into escaping cells for + ;; parent. + (dep:register-escaping cached) + ;; Cells that escape from the restored wad CACHED may now + ;; appear between some definition and a user, so invalidate + ;; users of such definitions. + (dep:map-external + (lambda (cell) + (dbg:log :invalidation " Invalidating parent users for escaping cell ~A~%" cell) + ;; We could process the same aspect twice but according + ;; to an instrumented run of the test suite, that issue + ;; is rare enough to not matter. + (let ((aspect (dep:aspect cell))) + (alexandria:when-let ((parent (dep:find-cell aspect))) + (invalidate-following-users parent)))) + cached) + ;; Clear list of inherited cells in case CACHED used to be a + ;; top-level wad but will now become a child of some other + ;; wad. + (setf (dep:inherited cached) '()) + ;; Install state from cells that are defined, inherited, + ;; etc. by CACHED. + (dep:install-state-from-user cached nil) + ;; (push cached (first eclector.parse-result::*stack*)) ; HACK - (advance-stream-to-beyond-wad stream cached))))) + )))) (defun read-and-cache-top-level-expression (analyzer client) ;; Use `eclector.reader:read-maybe-nothing' to read either a single @@ -79,57 +115,103 @@ (with-error-recording () (eclector.reader:read-maybe-nothing client analyzer nil nil)) (declare (ignore object)) + ;; After reading a top-level expression, pop state cells that have + ;; top-level-expression scope but not global scope. + (dep:pop-local-cells) (case kind (:eof) ; nothing to do for end of input (:whitespace) ; nothing to do for whitespace (t ; got a top-level, absolute wad (if (null wad) ; or possibly nothing, but only if KIND is `:skip' (assert (eq kind :skip)) - (push-to-prefix (cache analyzer) wad)))) + (let ((cache (cache analyzer))) + ;; For the "escaping" cells of WAD, disregard those that + ;; have top-level-expression scope since WAD represents a + ;; toplevel expression and there is no further escaping + ;; beyond WAD for those cells. For the disregarded + ;; cells, invalidate users that follow in the buffer text + ;; since there may have been a toplevel expression which + ;; contained WAD and also some of the those users. In + ;; that case, cells which previously "escaped" from WAD + ;; and could be used in those users are now no longer + ;; available and thus the users must be invalidated. + (dep:update-escaping-for-toplevel-user + wad #'invalidate-following-users) + ;; Set inherited cells of WAD either to the inheritable + ;; cells of the preceding wad or the initial reader + ;; state. The inherited cells are used when parsing + ;; starts after WAD and a reader state for that location + ;; has to be computed. + (setf (dep:inherited wad) + (alexandria:if-let ((prefix (prefix cache))) + (dep:inheritable (first prefix)) + (initial-reader-state analyzer))) + ;; Add the fully processed WAD to the prefix of CACHE. + (push-to-prefix cache wad))))) (values kind wad))) (defun read-forms (analyzer) - (let* ((cache (cache analyzer)) + (let* ((client (make-instance 'client :stream* analyzer)) + (cache (cache analyzer)) (prefix (prefix cache))) + (update-lines-cache analyzer (lines analyzer)) ;; Position ANALYZER (which is a stream) after the last prefix wad ;; (remember the cache prefix is stored in reverse order) if any. - (update-lines-cache analyzer (lines analyzer)) (setf (values (line-number analyzer) (item-number analyzer)) (if (null prefix) (values 0 0) (let ((first (first prefix))) (values (end-line first) (end-column first))))) - ;; Keep reading top-level expressions until the stream position is - ;; at the start of the cache suffix (wads on the cache suffix are - ;; unmodified and relative except the first one, so neither - ;; re-`read'ing nor adjusting them is necessary) or the EOF is - ;; reached. Remove wads from the cache residue and cache suffix - ;; as the stream positions passes them by. Push newly created - ;; wads to the cache prefix (this happens in - ;; `read-and-cache-top-level-expression'). - (loop :with client = (make-instance 'client :stream* analyzer) - :with *cache* = cache - :for kind = (eclector.reader:call-as-top-level-read - client (lambda () - (read-and-cache-top-level-expression - analyzer client)) - analyzer t nil nil) - ;; If we reach EOF while reading whitespace, the cache - ;; suffix must be empty, and the cache residue is either - ;; empty or it contains wads that should be removed. If we - ;; do not reach EOF, then we stop only if the current - ;; position is that of the first parse result on the cache - ;; suffix. - :when (or (eq kind :eof) - (let ((suffix (suffix cache))) - (and (not (null suffix)) - (position= (first suffix) analyzer)))) - :do (setf (residue cache) '()) - (return-from read-forms nil) - ;; In case we skipped some whitespace, discard any wads on - ;; the cache residue and cache suffix that are now before - ;; the current stream position. - :unless (eq kind :whitespace) - :do (drain-result-list analyzer cache residue pop-from-residue) - (when (null (residue cache)) - (drain-result-list analyzer cache suffix pop-from-suffix))))) + ;; Set up the reader state for the following `read' calls. If no + ;; wad in CACHE precedes the buffer location of ANALYZER, install + ;; the initial reader state which is stored in ANALYZER (and was + ;; provided by the client). + (dep:with-cells () + (dep:install-state (initial-reader-state analyzer)) + (when (not (null prefix)) + (dep:install-state-from-user (first prefix) t)) + ;; Keep reading top-level expressions until the stream position + ;; is at the start of the cache suffix (wads on the cache suffix + ;; are unmodified and relative except the first one, so neither + ;; re-`read'ing nor adjusting them is necessary) or the EOF is + ;; reached. Remove wads from the cache residue and cache suffix + ;; as the stream positions passes them by. Push newly created + ;; wads to the cache prefix (this happens in + ;; `read-and-cache-top-level-expression'). + (loop :with *cache* = cache + :for kind = (progn + (dbg:log :read "at ~D:~D~%" + (line-number analyzer) (item-number analyzer)) + (multiple-value-bind (kind wad orphan-result) + (eclector.reader:call-as-top-level-read + client (lambda () + (read-and-cache-top-level-expression + analyzer client)) + analyzer t nil nil) + (dbg:log :read "~A~% wad ~A~% orphan ~:A~%" + kind wad orphan-result) + (when (eq kind :object) + (apply-semantics client wad)) + kind)) + ;; If we reach EOF while reading whitespace, the cache + ;; suffix must be empty, and the cache residue is either + ;; empty or it contains wads that should be removed. If + ;; we do not reach EOF, then we stop only if the current + ;; position is that of the first parse result on the cache + ;; suffix. + :when (or (eq kind :eof) + (let ((suffix (suffix cache))) + (and (not (null suffix)) + (zerop (suffix-invalid-count cache)) + (position= (first suffix) analyzer)))) + :do (alexandria:when-let ((residue (residue cache))) + (mapc #'detach-wad residue) + (setf (residue cache) '())) + (return-from read-forms nil) + ;; In case we skipped some whitespace, discard any wads on + ;; the cache residue and cache suffix that are now before + ;; the current stream position. + :unless (eq kind :whitespace) + :do (drain-result-list analyzer cache residue pop-from-residue) + (when (null (residue cache)) + (drain-result-list analyzer cache suffix pop-from-suffix)))))) diff --git a/code/wad.lisp b/code/wad.lisp index 4a0b401..5a64738 100644 --- a/code/wad.lisp +++ b/code/wad.lisp @@ -129,7 +129,7 @@ (assert (not (relative-p wad))) (+ (start-line wad) (height wad))) -(defclass wad (family-relations-mixin basic-wad) +(defclass wad (dep:cell-user-mixin family-relations-mixin basic-wad) (;; This slot contains the maximum line width of any line that is ;; part of the wad. (%max-line-width :initarg :max-line-width @@ -151,6 +151,12 @@ :accessor indentation :initform nil))) +(defmethod print-object ((object wad) stream) + (print-unreadable-object (object stream :type t) + (unless (dep:validp object) + (write-string "INVALID " stream)) + (print-wad-position object stream))) + (defmethod errors ((wad wad)) (let ((errors (%errors wad))) (typecase errors @@ -348,6 +354,62 @@ (and (wad-starts-before-wad-p wad1 wad2) (wad-ends-after-wad-p wad1 wad2))) +;;; Dependencies and invalidation + +(defvar *drop-depth* 0) +(defmethod detach-wad ((wad basic-wad) &key (recursive t)) + (if recursive + (labels ((rec (wad) + (%detach-wad wad) + (let ((*drop-depth* (+ *drop-depth* 2))) + (map-children #'rec wad)))) + (rec wad)) + (%detach-wad wad))) + +(defun %detach-wad (wad) + (dbg:log :invalidation "~V<~>Dropping wad ~A~%" + (* 2 *drop-depth*) wad) + (let ((*drop-depth* (1+ *drop-depth*))) + (dep:map-defined #'invalidate-cell wad) + (loop :for cell :in (dep:used wad) + :when (dep:validp cell) + :do (dbg:log :invalidation "~V<~>USE ~A~%" (* 2 *drop-depth*) cell) + (dep:remove-user wad cell)))) + +(defun invalidate-cell (cell) + (dbg:log :invalidation "~V<~>DEF ~A~%" (* 2 *drop-depth*) cell) + (setf (dep:validp cell) nil) + (let ((*drop-depth* (1+ *drop-depth*))) + (dep:map-users (lambda (wad) + (invalidate-wad wad cell)) + cell))) + +(defun invalidate-wad (wad cell) + (labels ((rec (wad) + (dbg:log :invalidation "~V<~>Invalidate ~A~%" (* 2 *drop-depth*) wad) + (let ((old-invalid (dep:invalid wad))) + (unless (find cell old-invalid :test #'eq) + (setf (dep:invalid wad) (list* cell old-invalid)) + (alexandria:when-let ((parent (parent wad))) + (let ((*drop-depth* (1+ *drop-depth*))) + (rec parent))))))) + (rec wad))) + +(defun state-aspect-cell (wad aspect) + (labels ((rec (wad) + (or (find aspect (dep:inherited wad) :test #'eq :key #'dep:aspect) + (alexandria:when-let ((parent (parent wad))) + (rec parent))))) + ;; TODO: Maybe we should have a parameter to indicate whether we + ;; want incoming or outgoing values + (or (find aspect (dep:used wad) :test #'eq :key #'dep:aspect) + (rec wad)))) + +(defun state-value (wad aspect) + (alexandria:if-let ((cell (state-aspect-cell wad aspect))) + (values (dep:value cell) t) + (values nil nil))) + ;;; CST wads ;;; ;;; CST wads contain a "raw" expression. Note that wads based on @@ -387,6 +449,14 @@ (eq (name raw) **nil-symbol-name**) (eq (package-name raw) **common-lisp-package-name**)))) +(defmethod detach-wad ((wad atom-wad) &key recursive) + (declare (ignore recursive)) + (call-next-method) + (when (stringp (cst:raw wad)) + ;; The children are `text-wad's so they do not define any cells + ;; and do not have descendants of their own. + (map-children (lambda (child) (setf (dep:invalid child) t)) wad))) + (defclass atom-wad-with-extra-children (extra-children-mixin atom-wad) ()) @@ -474,6 +544,13 @@ (defclass comment-wad (maybe-extra-children-mixin skipped-wad) ()) +(defmethod detach-wad ((wad comment-wad) &key recursive) + (declare (ignore recursive)) + (%detach-wad wad) + ;; The children are `text-wad's so they do not define any cells and + ;; do not have descendants of their own. + (map-children (lambda (child) (setf (dep:invalid child) t)) wad)) + ;;; This class is used for a block comment introduced by #|. (defclass block-comment-wad (comment-wad) ()) diff --git a/incrementalist.asd b/incrementalist.asd index 7159a86..651397d 100644 --- a/incrementalist.asd +++ b/incrementalist.asd @@ -21,9 +21,22 @@ (:file "invariant" :if-feature :incrementalist-debug))) - (:module "code" - :depends-on ("debug") - :serial t + (:module "dependencies" + :pathname "code/dependencies" + :depends-on ("debug") + :serial t + :components ((:file "package") + (:file "protocol") + (:file "debug-types" + :if-feature :incrementalist-debug) + (:file "dependencies") + ;; Debugging + (:file "extra-assertions" + :if-feature :incrementalist-debug))) + + (:module "code" + :depends-on ("debug" "dependencies") + :serial t :components ((:file "package") (:file "utilities") (:file "protocol") @@ -36,7 +49,9 @@ (:file "analyzer") (:file "text") (:file "client") + (:file "dependencies") (:file "read") + (:file "process") (:file "update-cache") ;; Queries (:file "find-wad-beginning-line") @@ -68,11 +83,14 @@ (:file "code-reading-utilities") ;; Model (:file "wad") + ;; Cache + (:file "dependencies") ;; Queries (:file "find-wad-containing-position") ;; Other tests (:file "test") (:file "random") + (:file "random-dependencies") (:file "read-code") (:file "regressions") (:file "performance")))) diff --git a/test/code-reading-utilities.lisp b/test/code-reading-utilities.lisp index 365f739..6e66922 100644 --- a/test/code-reading-utilities.lisp +++ b/test/code-reading-utilities.lisp @@ -67,7 +67,12 @@ (check-parse-result result)) :finally (setf delete-time (funcall reporter t))) ;; Buffer is empty. - (is-true (a:emptyp (buffer-string buffer)))) + (is-true (a:emptyp (buffer-string buffer))) + (let ((result (update-cache analyzer cache))) + ;; Parse result list is empty. + (is (null result)) + ;; Check initial reader state for dangling user references. + (no-dangling-nodes '() analyzer))) (values count insert-time delete-time))) ;;; Reading code from files diff --git a/test/dependencies.lisp b/test/dependencies.lisp new file mode 100644 index 0000000..148e98c --- /dev/null +++ b/test/dependencies.lisp @@ -0,0 +1,535 @@ +(cl:in-package #:incrementalist.test) + +(def-suite* :incrementalist.dependencies + :in :incrementalist) + +(test dependencies.within-expression.1 + (edits-cases () + ("(foo + + bar)" + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 4)) "FOO") + ,(expected-symbol-wad '((2 1) (2 4)) "BAR"))) + '(:insert (1 0) " (cl:in-package #:BAZ)") + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 4)) "FOO") + (inc:cons-wad ((1 1) (1 22)) () + ,(expected-symbol-wad + '((1 2) (1 15)) "IN-PACKAGE" + :token-class 'incrementalist:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad + '((1 16) (1 21)) "BAZ" + :token-class 'incrementalist:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + ,(expected-symbol-wad '((2 1) (2 4)) "BAR"))) + '(:erase (1 1) 21) + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 4)) "FOO") + ,(expected-symbol-wad '((2 1) (2 4)) "BAR")))))) + +#-ccl ; CCL doesn't allow the way we mix labeled objects and quasiquote +(test dependencies.within-expression.2 + ;; As sub-expressions of a single toplevel expression, a form that + ;; sets `*read-base*' at read-time between other expressions. + (edits-cases () + ("(10 + + 10)" + '((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 10)) + (inc:atom-wad ((2 1) (2 3)) (:raw 10)))) + ;; At read-time, set `*read-base*' to 16 + '(:insert (1 0) " #.(cl:setf cl:*read-base* 16)") + `((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 10)) + (inc:atom-wad ((1 1) (1 30)) (:raw 16) + (inc:cons-wad ((1 3) (1 30)) () + ,#1=(expected-symbol-wad '((1 4) (1 11)) "SETF" + :token-class 'incrementalist:existing-symbol-token + :package-name "COMMON-LISP") + ,#2=(expected-symbol-wad '((1 12) (1 26)) "*READ-BASE*" + :token-class 'incrementalist:existing-symbol-token + :package-name "COMMON-LISP") + (inc:atom-wad ((1 27) (1 29)) (:raw 16)))) + (inc:atom-wad ((2 1) (2 3)) (:raw 16)))) + ;; Set `*read-base*' to 17 instead of 16. + '(:replace (1 27) "17") + `((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 10)) + #3=(inc:atom-wad ((1 1) (1 30)) (:raw 17) + (inc:cons-wad ((1 3) (1 30)) () + ,#1# + ,#2# + (inc:atom-wad ((1 27) (1 29)) (:raw 17)))) + (inc:atom-wad ((2 1) (2 3)) (:raw 17)))) + ;; Change the first child from 10 to 11 + '(:replace (0 1) "11") + `((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 11)) + #3# + (inc:atom-wad ((2 1) (2 3)) (:raw 17)))) + ;; Change the third child from 10 to 11 + '(:replace (2 1) "11") + `((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 11)) + #3# + (inc:atom-wad ((2 1) (2 3)) (:raw 18)))) + ;; Remove form which sets `*read-base*' + '(:erase (1 1) 29) + '((inc:cons-wad ((0 0) (2 4)) () + (inc:atom-wad ((0 1) (0 3)) (:raw 11)) + (inc:atom-wad ((2 1) (2 3)) (:raw 11))))))) + +(test dependencies.within-expression.3 + ;; Insert and delete a labeled object definition and a reference + ;; within the same expression. + (edits-cases () + ("(progn + + #1#)" + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:atom-wad ((2 1) (2 4)) + (:errors ((((2 2) (2 3)) + eclector.reader:sharpsign-sharpsign-undefined-label)))))) + '(:insert (1 0) " #1=foo") + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:labeled-object-definition-wad ((1 1) (1 7)) () + ,(expected-symbol-wad '((1 4) (1 7)) "FOO")) + (inc:labeled-object-reference-wad ((2 1) (2 4)) ()))) + '(:insert (2 2) "0") + `((inc:cons-wad ((0 0) (2 6)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:labeled-object-definition-wad ((1 1) (1 7)) () + ,(expected-symbol-wad '((1 4) (1 7)) "FOO")) + (inc:labeled-object-reference-wad ((2 1) (2 5)) ()))) + '(:erase (1 0) 7) + `((inc:cons-wad ((0 0) (2 6)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:atom-wad ((2 1) (2 5)) + (:errors ((((2 3) (2 4)) + eclector.reader:sharpsign-sharpsign-undefined-label)))))) + '(:insert (1 0) " (progn #1=foo)") + `((inc:cons-wad ((0 0) (2 6)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:cons-wad ((1 1) (1 15)) () + ,(expected-symbol-wad '((1 2) (1 7)) "PROGN") + (inc:labeled-object-definition-wad ((1 8) (1 14)) () + ,(expected-symbol-wad '((1 11) (1 14)) "FOO"))) + (inc:labeled-object-reference-wad ((2 1) (2 5)) ()))) + '(:erase (2 2) 1) + `((inc:cons-wad ((0 0) (2 5)) () + ,(expected-symbol-wad '((0 1) (0 6)) "PROGN") + (inc:cons-wad ((1 1) (1 15)) () + ,(expected-symbol-wad '((1 2) (1 7)) "PROGN") + (inc:labeled-object-definition-wad ((1 8) (1 14)) () + ,(expected-symbol-wad '((1 11) (1 14)) "FOO"))) + (inc:labeled-object-reference-wad ((2 1) (2 4)) ())))))) + +(test dependencies.within-expressions.4 + "Splitting and joining expressions must propagate labeled objects aspect +correctly." + (edits-cases () + ("(#1=1 #2=2 + + #1=1 #2=2)" + #1=`((inc:cons-wad ((0 0) (2 11)) () + #2=(inc:labeled-object-definition-wad ((0 1) (0 5)) () + (inc:atom-wad ((0 4) (0 5)) (:raw 1))) + #3=(inc:labeled-object-definition-wad ((0 6) (0 10)) () + (inc:atom-wad ((0 9) (0 10)) (:raw 2))) + (inc:atom-wad ((2 1) (2 5)) + (:raw 1 + :errors ((((2 2) (2 3)) + eclector.reader:sharpsign-equals-label-defined-more-than-once))) + (inc:atom-wad ((2 4) (2 5)) (:raw 1))) + (inc:atom-wad ((2 6) (2 10)) + (:raw 2 + :errors ((((2 7) (2 8)) + eclector.reader:sharpsign-equals-label-defined-more-than-once))) + (inc:atom-wad ((2 9) (2 10)) (:raw 2))))) + '(:insert (1 0) ")(") + `((inc:cons-wad ((0 0) (1 1)) () #2# #3#) + (inc:cons-wad ((1 1) (2 11)) () + (inc:labeled-object-definition-wad ((2 1) (2 5)) () + (inc:atom-wad ((2 4) (2 5)) (:raw 1))) + (inc:labeled-object-definition-wad ((2 6) (2 10)) () + (inc:atom-wad ((2 9) (2 10)) (:raw 2))))) + '(:erase (1 0) 2) + #1#))) + +(test dependencies.between-expressions.1 + (edits-cases () + ("foo + +bar" + `(,(expected-symbol-wad '((0 0) (0 3)) "FOO") + ,(expected-symbol-wad '((2 0) (2 3)) "BAR")) + '(:insert (1 0) "(cl:in-package #:BAZ)") + `(,(expected-symbol-wad '((0 0) (0 3)) "FOO") + (inc:cons-wad ((1 0) (1 21)) () + ,(expected-symbol-wad '((1 1) (1 14)) "IN-PACKAGE" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((1 15) (1 20)) "BAZ" + :token-class 'inc:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + ,(expected-symbol-wad + '((2 0) (2 3)) "BAR" + :token-class 'inc:non-existing-package-symbol-token + :package-name "BAZ")) + '(:erase (1 0) 21) + `(,(expected-symbol-wad '((0 0) (0 3)) "FOO") + ,(expected-symbol-wad '((2 0) (2 3)) "BAR"))))) + +#-ccl ; CCL doesn't allow the way we mix labeled objects and quasiquote +(test dependencies.between-expressions.2 + ;; Two toplevel expressions, a form that sets `*read-base*' at + ;; read-time between other expressions. + (edits-cases () + ("10 + +10" + '((inc:atom-wad ((0 0) (0 2)) (:raw 10)) + (inc:atom-wad ((2 0) (2 2)) (:raw 10))) + ;; At read-time, set `*read-base*' to 16 + '(:insert (1 0) "#.(cl:setf cl:*read-base* 16)") + `((inc:atom-wad ((0 0) (0 2)) (:raw 10)) + (inc:atom-wad ((1 0) (1 29)) (:raw 16) + (inc:cons-wad ((1 2) (1 29)) () + ,#1=(expected-symbol-wad + '((1 3) (1 10)) "SETF" + :token-class 'incrementalist:existing-symbol-token + :package-name "COMMON-LISP") + ,#2=(expected-symbol-wad + '((1 11) (1 25)) "*READ-BASE*" + :token-class 'incrementalist:existing-symbol-token + :package-name "COMMON-LISP") + (inc:atom-wad ((1 26) (1 28)) (:raw 16)))) + (inc:atom-wad ((2 0) (2 2)) (:raw 16))) + ;; Set `*read-base*' to 17 instead of 16. + '(:replace (1 26) "17") + `((inc:atom-wad ((0 0) (0 2)) (:raw 10)) + #3=(inc:atom-wad ((1 0) (1 29)) () + (inc:cons-wad ((1 2) (1 29)) () + ,#1# + ,#2# + (inc:atom-wad ((1 26) (1 28)) (:raw 17)))) + (inc:atom-wad ((2 0) (2 2)) (:raw 17))) + ;; Change the first child from 10 to 11 + '(:replace (0 0) "11") + `((inc:atom-wad ((0 0) (0 2)) (:raw 11)) + #3# + (inc:atom-wad ((2 0) (2 2)) (:raw 17))) + ;; Change the third child from 10 to 11 + '(:replace (2 0) "11") + `((inc:atom-wad ((0 0) (0 2)) (:raw 11)) + #3# + (inc:atom-wad ((2 0) (2 2)) (:raw 18))) + ;; Remove form which sets `*read-base*' + '(:erase (1 0) 29) + '((inc:atom-wad ((0 0) (0 2)) (:raw 11)) + (inc:atom-wad ((2 0) (2 2)) (:raw 11)))))) + +#-ccl ; CCL doesn't allow the way we mix labeled objects and quasiquote +(test dependencies.between-expressions.3 + (edits-cases () + ;; + (" +#.(cl:setf cl:*read-base* 10) +foo" + `(#1=(inc:atom-wad ((1 0) (1 29)) (:raw 10) + (inc:cons-wad ((1 2) (1 29)) () + ,(expected-symbol-wad '((1 3) (1 10)) "SETF" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((1 11) (1 25)) "*READ-BASE*" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:atom-wad ((1 26) (1 28)) (:raw 10)))) + ,(expected-symbol-wad '((2 0) (2 3)) "FOO")) + ;; Change the current package. + '(:insert (0 0) "(cl:in-package #:foo)") + `((inc:cons-wad ((0 0) (0 21)) () + ,(expected-symbol-wad '((0 1) (0 14)) "IN-PACKAGE" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((0 15) (0 20)) "FOO" + :token-class 'inc:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + #1# + ,(expected-symbol-wad + '((2 0) (2 3)) "FOO" + :token-class 'inc:non-existing-package-symbol-token + :package-name "FOO"))))) + +(test dependencies.between-expressions.4 + (edits-cases () + ;; A string followed by a symbol. + ("\" +foo +\" +bar +" + `((inc:atom-wad ((0 0) (2 1)) (:raw #.(format nil "~%foo~%")) + (inc:word-wad ((1 0) (1 3)))) + ,(expected-symbol-wad '((3 0) (3 3)) "BAR")) + ;; Turn line 1 into a symbol and lines 2 to 4 into a string. + '(progn + (:erase (0 0) 1) + (:insert (4 0) "\"")) + `(,(expected-symbol-wad '((1 0) (1 3)) "FOO") + (inc:atom-wad ((2 0) (4 1)) (:raw #.(format nil "~%bar~%")) + (inc:word-wad ((3 0) (3 3)))))))) + +(test dependencies.between-expressions.5 + (edits-cases () + (" +#+foo +#-bar baz +111" + #1=`((inc:skipped-positive-conditional-wad ((1 0) (2 9)) () + ,(expected-symbol-wad '((1 2) (1 5)) "FOO" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + (inc:read-suppress-wad ((2 0) (2 9)) () + ,(expected-symbol-wad '((2 2) (2 5)) "BAR" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + (inc:read-suppress-wad ((2 6) (2 9)) ()))) + (inc:atom-wad ((3 0) (3 3)) (:raw 111))) + ;; Add reader conditional before everything + '(:insert (0 0) "#+()") + `((inc:skipped-positive-conditional-wad ((0 0) (3 3)) () + ,(expected-symbol-wad '((0 2) (0 4)) "NIL" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:skipped-positive-conditional-wad ((1 0) (2 9)) () + ,(expected-symbol-wad '((1 2) (1 5)) "FOO" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + (inc:read-suppress-wad ((2 0) (2 9)) () + ,(expected-symbol-wad '((2 2) (2 5)) "BAR" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + (inc:read-suppress-wad ((2 6) (2 9)) ()))) + (inc:read-suppress-wad ((3 0) (3 3))))) + ;; Delete the initial reader conditional again + '(:erase (0 0) 4) + #1#))) + +(test dependencies.between-expressions.6 + (edits-cases () + ("#+() +#+() +1" + `((inc:skipped-positive-conditional-wad ((0 0) (2 1)) + (:errors ((((2 1) (2 1)) eclector.reader:end-of-file))) + ,(expected-symbol-wad '((0 2) (0 4)) "NIL" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:skipped-positive-conditional-wad ((1 0) (2 1)) () + ,(expected-symbol-wad '((1 2) (1 4)) "NIL" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:read-suppress-wad ((2 0) (2 1)))))) + '(:erase (0 0) 4) + `((inc:skipped-positive-conditional-wad ((1 0) (2 1)) () + ,(expected-symbol-wad '((1 2) (1 4)) "NIL" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:read-suppress-wad ((2 0) (2 1)))))))) + +(test dependencies.between-expressions.7 + (edits-cases () + ("(cl:in-package #:common-lisp) +; (cl:in-package #:incrementalist.test.test-package) +;;; +defun" + `((inc:cons-wad ((0 0) (0 29)) () + ,(expected-symbol-wad '((0 1) (0 14)) "IN-PACKAGE" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((0 15) (0 28)) "COMMON-LISP" + :token-class 'inc:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + (inc:semicolon-comment-wad ((1 0) (2 0)) () :ignore-children) + (inc:semicolon-comment-wad ((2 0) (3 0)) () :ignore-children) + ,(expected-symbol-wad '((3 0) (3 5)) "DEFUN" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP")) + ;; Activate the second `in-package' form. + '(:erase (1 0) 2) + `((inc:cons-wad ((0 0) (0 29)) () + ,(expected-symbol-wad '((0 1) (0 14)) "IN-PACKAGE" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((0 15) (0 28)) "COMMON-LISP" + :token-class 'inc:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + (inc:cons-wad ((1 0) (1 50)) () + ,(expected-symbol-wad '((1 1) (1 14)) "IN-PACKAGE" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((1 15) (1 49)) "INCREMENTALIST.TEST.TEST-PACKAGE" + :token-class 'inc:uninterned-symbol-token + :package-name nil + :words '(:ignore-children))) + (inc:semicolon-comment-wad ((2 0) (3 0)) () :ignore-children) + ,(expected-symbol-wad '((3 0) (3 5)) "DEFUN" + :package-name "INCREMENTALIST.TEST.TEST-PACKAGE" + :words '(:ignore-children)))))) + +(test dependencies.between-expressions.8 + "Labeled objects must not propagate between toplevel expressions." + (edits-cases () + ("(#1=1 #2=2) +(#1=1 #2=2)" + #1=`((inc:cons-wad ((0 0) (0 11)) () + (inc:labeled-object-definition-wad ((0 1) (0 5)) () + (inc:atom-wad ((0 4) (0 5)) (:raw 1))) + (inc:labeled-object-definition-wad ((0 6) (0 10)) () + (inc:atom-wad ((0 9) (0 10)) (:raw 2)))) + (inc:cons-wad ((1 0) (1 11)) () + (inc:labeled-object-definition-wad ((1 1) (1 5)) () + (inc:atom-wad ((1 4) (1 5)) (:raw 1))) + (inc:labeled-object-definition-wad ((1 6) (1 10)) () + (inc:atom-wad ((1 9) (1 10)) (:raw 2))))) + ;; Force re-reading of the first expression. The changes to the + ;; labeled objects must not affect the second expression. + '(:poke (0 0)) + #1# + ;; Force re-reading of the second expression. + '(:poke (1 0)) + #1#))) + +(test dependencies.between-expressions.9 + "Changing whether a symbol is part of a feature expression has to +update the symbol package correctly." + (edits-cases () + ("#+ + +foo +bar +" + #1=`((inc:skipped-positive-conditional-wad ((0 0) (3 3)) () + ,(expected-symbol-wad '((2 0) (2 3)) "FOO" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + (inc:read-suppress-wad ((3 0) (3 3)) ()))) + ;; Make both symbols part of the feature expression + '(:insert (1 0) "(") + `((inc:skipped-positive-conditional-wad ((0 0) (4 0)) + (:errors ((((4 0) (4 0)) + eclector.reader:end-of-input-after-feature-expression))) + (inc:cons-wad ((1 0) (4 0)) + (:errors ((((4 0) (4 0)) eclector.reader:unterminated-list))) + ,(expected-symbol-wad '((2 0) (2 3)) "FOO" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD") + ,(expected-symbol-wad '((3 0) (3 3)) "BAR" + :token-class 'inc:existing-symbol-token + :package-name "KEYWORD")))) + ;; Restore previous state + '(:replace (1 0) " ") + #1#))) + +(test dependencies.between-expressions.10 + "Ensure that invalidation works for inherited cells without users." + (edits-cases () + ("(cl:in-package #:common-lisp-user) +10 +11" + #1='((inc:cons-wad ((0 0) (0 34)) () :ignore-children) + (inc:atom-wad ((1 0) (1 2)) (:raw 10)) + (inc:atom-wad ((2 0) (2 2)) (:raw 11))) + ;; Force an update of the `in-package' expression. This + ;; invalidates the `*package*' cell that is inherited by the + ;; following expressions which are on the suffix at that + ;; time. Since no following expression explicitly uses the cell, + ;; the required invalidation must take into account inherited + ;; cells in toplevel wads in addition to explicit cell usage. + '(:poke (0 0)) + #1# + ;; In case the invalidation didn't work properly (which was the + ;; case at one point), the state that is installed from the + ;; middle expression will contain the invalid inherited + ;; `*package*' cell. + '(:poke (2 0)) + #1#))) + +(test dependencies.between-expressions.11 + "Multiple cells escaping from a single expression." + ;; The quoted expression on the first line has two escaping cells: + ;; one for the labeled object definition and one for the read-time + ;; evaluation. + (edits-cases () + ("'(#1=20 #.(cl:setf cl:*read-base* 11)) +10" + `((inc:cons-wad ((0 0) (0 38)) () + (inc:cons-wad ((0 1) (0 38)) () + (inc:labeled-object-definition-wad ((0 2) (0 7)) () + (inc:atom-wad ((0 5) (0 7)) (:raw 20))) + (inc:atom-wad ((0 8) (0 37)) (:raw 11) + (inc:cons-wad ((0 10) (0 37)) () + ,(expected-symbol-wad '((0 11) (0 18)) "SETF" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((0 19) (0 33)) "*READ-BASE*" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:atom-wad ((0 34) (0 36)) (:raw 11)))))) + (inc:atom-wad ((1 0) (1 2)) (:raw 11)))) + ("'(#.(cl:setf cl:*read-base* 11) #1=20) +10" + `((inc:cons-wad ((0 0) (0 38)) () + (inc:cons-wad ((0 1) (0 38)) () + (inc:atom-wad ((0 2) (0 31)) (:raw 11) + (inc:cons-wad ((0 4) (0 31)) () + ,(expected-symbol-wad '((0 5) (0 12)) "SETF" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + ,(expected-symbol-wad '((0 13) (0 27)) "*READ-BASE*" + :token-class 'inc:existing-symbol-token + :package-name "COMMON-LISP") + (inc:atom-wad ((0 28) (0 30)) (:raw 11)))) + (inc:labeled-object-definition-wad ((0 32) (0 37)) () + (inc:atom-wad ((0 35) (0 37)) (:raw 22))))) + (inc:atom-wad ((1 0) (1 2)) (:raw 11)))))) + +(test dependencies.dangling.1 + "Ensure absence of dangling user references in global cells." + (insert-then-delete + "( +(a . +`b))" + :stream nil)) + + +(test dependencies.dangling.2 + "Ensure absence of dangling user references in global cells." + (insert-then-delete + "( +(a +))" + :stream nil)) + +(test dependencies.dangling.3 + "Ensure absence of dangling user references in global cells." + (insert-then-delete + "(\"a\" +'\"(b)\" +)" + :stream nil)) + +(test dependencies.dangling.4 + "Ensure absence of dangling user references in global cells." + (insert-then-delete "#+a b" :stream nil)) diff --git a/test/package.lisp b/test/package.lisp index 1eab935..b2b5c55 100644 --- a/test/package.lisp +++ b/test/package.lisp @@ -5,7 +5,8 @@ (:local-nicknames (#:a #:alexandria) - (#:inc #:incrementalist)) + (#:inc #:incrementalist) + (#:dep #:incrementalist.dependencies)) (:import-from #:fiveam #:def-suite diff --git a/test/random-dependencies.lisp b/test/random-dependencies.lisp new file mode 100644 index 0000000..39f0672 --- /dev/null +++ b/test/random-dependencies.lisp @@ -0,0 +1,92 @@ +(cl:in-package #:incrementalist.test) + +(in-suite :incrementalist.random) + +;;; This string is intended to be put into a buffer and then edited by +;;; randomly replacing the first three characters of certain lines +;;; with either ";; " or " " in order to the test the invalidation +;;; of expressions that follow the changed location in the buffer. +(defvar *random-dependencies-input* + " (cl:in-package #:cl-user) + #.(setf *read-base* 17) +;;; This is a distraction + \" + some words + \" + #+(and) +( + #.(setf *read-base* 10) + 10 + #01=foo + bar + #01# +) + ` + ` + ( + 11 + foo + ,(bar 12) + ,,(baz 13) + ) + #+ + foo + bar + #- + baz + fez +") + +(defvar *random-dependencies-edits* + (flet ((comment-or-uncomment (line column) + (list `(:replace (,line ,column) ";; ") + `(:replace (,line ,column) " "))) + (change-label (line column) + (list `(:replace (,line ,column) "01") + `(:replace (,line ,column) "02")))) + (append ;; Package + (comment-or-uncomment 0 0) + ;; Read base + (comment-or-uncomment 1 0) + '((:replace (1 23) "17") + (:replace (1 23) "10")) + ;; String + (comment-or-uncomment 3 0) + (comment-or-uncomment 5 0) + ;; Reader conditional + (comment-or-uncomment 6 0) + ;; Labeled objects + (comment-or-uncomment 10 0) + (change-label 10 4) + (comment-or-uncomment 12 0) + (change-label 12 4) + ;; Backquote + (comment-or-uncomment 14 0) + (comment-or-uncomment 15 0) + ;; Reader conditionals + (loop :for line :from 22 :to 27 + :append (comment-or-uncomment line 0))))) + +(defun random-edit () + (a:random-elt *random-dependencies-edits*)) + +(defun apply-random-edit (cursor) + (let ((edit (random-edit))) + (apply-edit cursor edit) + edit)) + +(test dependencies.random + "Ensure that incremental and from-scratch parsing after random edits +yields identical results." + (let ((fiveam:*test-dribble* nil)) + (loop :with (analyzer cache buffer cursor) + = (multiple-value-list + (prepared-analyzer *random-dependencies-input*)) + :repeat 200 + :for () = (apply-random-edit cursor) + :for content = (buffer-string buffer) + :for results-with-cache = (update-cache analyzer cache) + :for results-no-cache = (parse-result content) + :do (no-dangling-nodes results-with-cache analyzer) + (are-results= results-no-cache results-with-cache + :input content)))) diff --git a/test/random.lisp b/test/random.lisp index bbd4491..7e5d827 100644 --- a/test/random.lisp +++ b/test/random.lisp @@ -103,13 +103,15 @@ reader macro." (5am:for-all ((writer-and-info (gen-sub-expression))) (destructuring-bind (writer sub-expressions infos) writer-and-info (declare (ignore infos)) - (let* ((input (with-output-to-string (stream) - (write-string "#{ " stream) - (funcall writer stream) - (write-string " }" stream))) - (readtable (augmented-readtable sub-expressions)) - (result (let ((eclector.reader:*readtable* readtable)) - (first (parse-result input))))) + (let* ((input (with-output-to-string (stream) + (write-string "#{ " stream) + (funcall writer stream) + (write-string " }" stream))) + (readtable (augmented-readtable sub-expressions)) + (initial-state `((*readtable* . ,readtable))) + (result (first (parse-result + input + :initial-reader-state initial-state)))) (is-true (typep result 'inc:cons-wad)) ;; Ensure that the structure, raw value and source information ;; of the CST corresponds to that of the input and reader diff --git a/test/regressions.lisp b/test/regressions.lisp index 5f026db..99b671f 100644 --- a/test/regressions.lisp +++ b/test/regressions.lisp @@ -107,6 +107,15 @@ correctly." (test-case "(; (0 . 0) . ())") (test-case "(0 . 0) . ())"))) +(test regressions.nested-labeled-objects + "Ensure that nested labeled objects work." + ;; Nested labeled object definitions lead to a situation in which an + ;; escaping cell and a defined cell exist for the same aspect. The + ;; escaping cell must be used when computing the "external" state + ;; for the WAD. + (insert-then-delete "( + #1=(#2=foo #3=bar))" :stream nil)) + (test regressions.kind-of-restored-read-suppressed-wad "Ensure that `read-maybe-nothing' returns the correct kind for a `read-suppress-wad' that has been restored from the cache." diff --git a/test/utilities.lisp b/test/utilities.lisp index 47adfeb..4ab65b7 100644 --- a/test/utilities.lisp +++ b/test/utilities.lisp @@ -28,19 +28,25 @@ (insert-string cursor content) (values buffer cursor))) -(defun prepared-analyzer (buffer-content) +(defun prepared-analyzer (buffer-content + &key (initial-reader-state + (inc:make-initial-reader-state + :package "INCREMENTALIST.TEST.TEST-PACKAGE"))) (multiple-value-bind (buffer cursor) (prepared-buffer buffer-content) - (let* ((analyzer (make-instance 'inc:analyzer :buffer buffer)) + (let* ((analyzer (make-instance 'inc:analyzer + :buffer buffer + :initial-reader-state initial-reader-state)) (cache (inc:cache analyzer))) (values analyzer cache buffer cursor)))) (defun update-cache (analyzer cache) - (let ((*package* (find-package '#:incrementalist.test.test-package))) - (inc:update analyzer)) + (inc:update analyzer) (append (reverse (inc::prefix cache)) (inc::suffix cache))) -(defun parse-result (buffer-content) - (multiple-value-bind (analyzer cache) (prepared-analyzer buffer-content) +(defun parse-result (buffer-content &rest args &key initial-reader-state) + (declare (ignore initial-reader-state)) + (multiple-value-bind (analyzer cache) + (apply #'prepared-analyzer buffer-content args) (update-cache analyzer cache))) ;;; Reporting utilities @@ -62,7 +68,24 @@ (unless (typep wad 'inc:error-wad) (append (inc:errors wad) (inc:children wad)))))))) -;;; Predicates +(defun format-results (stream results &optional colon? at?) + (declare (ignore colon? at?)) + (loop :for result :in results + :do (fresh-line stream) + (format-node stream (cons result nil)))) + +;;; The following functions (and helpers) check results (trees or +;;; forests of wads) in a style that matches fiveam assertions with +;;; `fiveam:is'. The functions form two groups which perform similar +;;; checks but on different kinds of arguments: +;;; +;;; Arguments | "spec" vs result | result vs result | +;;; ------------------------+------------------+------------------+ +;;; Wad location and type | `is-wad-data' | `is-wad-data=' | +;;; Error location and type | `is-error' | `is-error=' | +;;; Raw value | `is-raw' | `is-raw=' | +;;; Single result | `is-result' | `is-result=' | +;;; List of results | `are-results' | `are-results=' | (defun wad-location (wad) (let* ((start-line (inc:absolute-start-line wad)) @@ -105,6 +128,12 @@ (is-with-node (equal expected-location (wad-location wad)) "location of the wad" result-info input)) +(defun is-wad-data= (expected-wad actual-wad result-info &key input) + (is-with-node (a:type= (type-of expected-wad) (type-of actual-wad)) + "type of" result-info input) + (is-with-node (equal (wad-location expected-wad) (wad-location actual-wad)) + "location of the wad" result-info input)) + (defun is-error (expected-error wad result-info &key input) (let ((result-info (cons (car result-info) wad))) (destructuring-bind (expected-location expected-condition-type) expected-error @@ -122,6 +151,15 @@ expected-condition-type (class-name (class-of condition))))))) +(defun is-error= (expected-error-wad actual-error-wad result-info &key input) + (let ((result-info (cons (car result-info) actual-error-wad))) + (is-wad-data= expected-error-wad actual-error-wad result-info :input input) + (let ((expected-condition (inc:condition expected-error-wad)) + (actual-condition (inc:condition actual-error-wad))) + (is-with-node (a:type= (class-of expected-condition) + (class-of actual-condition)) + "condition type" result-info input)))) + (defun is-raw (expected-raw raw result-info &key input) (if (consp expected-raw) (destructuring-bind (expected-type &key ((:symbol expected-symbol) @@ -144,9 +182,34 @@ (is-with-node (equal expected-raw raw) "raw value of the node" result-info input))) +(defun is-raw= (expected actual result-info &key input) + (labels ((rec (expected actual) + (is-with-node (a:type= (type-of expected) (type-of actual)) + "type of the raw value of the node" + result-info input) + (when (a:type= (type-of expected) (type-of actual)) + (typecase expected + (cons + (rec (car expected) (car actual)) + (rec (cdr expected) (cdr actual))) + (inc:symbol-token + (is-with-node (string= (inc:package-name expected) + (inc:package-name actual)) + "package name of the symbol token of the node" + result-info input) + (is-with-node (string= (inc:name expected) (inc:name actual)) + "name of the symbol token of the node" + result-info input)) + (t + (is-with-node (equal expected actual) + "raw value of the node" result-info input)))))) + (rec expected actual))) + (defun is-result (expected root-result &key input) (labels ((rec (expected result) + (is-true (dep:validp result)) + (is (null (dep:invalid result))) (destructuring-bind (expected-type expected-location &optional ((&key ((:errors expected-errors) '()) @@ -195,6 +258,28 @@ :input input)))))) (rec expected root-result))) +(defun is-result= (expected-result actual-result &key input) + (is (a:type= (type-of expected-result) (type-of actual-result))) + (labels ((rec (expected result) + (let ((result-info (cons actual-result result))) + (is-true (dep:validp result)) + (is (null (dep:invalid result))) + ;; Compare type and location + (is-wad-data= expected result result-info :input input) + ;; Compare raw values + (when (typep expected 'cst:cst) + (is-raw= (cst:raw expected) (cst:raw result) + result-info :input input)) + ;; Compare errors + (is-sequence (a:rcurry #'is-error= result-info :input input) + (inc:errors expected) (inc:errors result) + result-info "error~:P" :input input) + ;; Compare children + (is-sequence #'rec (inc:children expected) (inc:children result) + result-info "~:*child~[ren~;~:;ren~]" + :input input)))) + (rec expected-result actual-result))) + (defun is-result-count= (expected-results actual-results &key input) (is (= (length expected-results) (length actual-results)) "~@<~@[For input~@:_~@:_~ @@ -207,6 +292,26 @@ (is-result-count= expected-results actual-results :input input) (mapc (a:rcurry #'is-result :input input) expected-results actual-results)) +(defun are-results= (expected-results actual-results &key input) + (is-result-count= expected-results actual-results :input input) + (mapc (a:rcurry #'is-result= :input input) expected-results actual-results)) + +;;; Ensure that the cells in the initial reader state of ANALYZER do +;;; not have any users that are not contained in the wad forest +;;; RESULTS. +(defun no-dangling-nodes (results analyzer) + (let ((nodes (make-hash-table :test #'eq))) + (labels ((rec (node) + (setf (gethash node nodes) t) + (inc:map-children #'rec node))) + (mapc #'rec results)) + (loop :for cell :in (inc::initial-reader-state analyzer) + :do (dep:map-users (lambda (user) + (when (not (nth-value 1 (gethash user nodes))) + (fiveam:fail "dangling node ~A in cell ~A" + user cell))) + cell)))) + ;;; Utilities for analysis test cases (defun expected-semicolon-comment-wad diff --git a/test/wad.lisp b/test/wad.lisp index 96b90cf..df4ff85 100644 --- a/test/wad.lisp +++ b/test/wad.lisp @@ -14,3 +14,73 @@ (is (string= #.(format nil "(a~%)") (inc:items first)))) (let ((second (second result))) (is (string= #.(format nil "(~%b)") (inc:items second)))))) + +(defun state-value-equal (expected actual) + (typecase expected + (symbol (string= expected actual)) + (t (equal expected actual)))) + +(test state-value.smoke + "Smoke test for the `state-value' function." + (labels ((get-wad (path result) + (labels ((lookup (path node) + (if (null path) + node + (destructuring-bind (index &rest remaining) path + (typecase node + (cons + (lookup remaining (nth index node))) + (t + (let ((children (inc:children node))) + (lookup remaining (elt children index))))))))) + (lookup path result))) + (check-wad (result path expected-state-values) + (let ((wad (get-wad path result))) + (loop :for (aspect . expected-value) :in expected-state-values + :for (actual-value actual-value-p) + = (multiple-value-list (inc:state-value wad aspect)) + :do (case expected-value + (:missing + (is-false + actual-value-p + "~@" + aspect wad)) + (t + (is-true + actual-value-p + "~@" + aspect wad) + (is (state-value-equal expected-value actual-value) + "~@" + aspect wad expected-value actual-value)))))) + (test-case (input specs) + (loop :with result = (parse-result input) + :for (path expected-state-values) :in specs + :do (check-wad result path expected-state-values)))) + (test-case + "1" + '(((0) ((*package* . "INCREMENTALIST.TEST.TEST-PACKAGE") + (*read-suppress* . nil) + (*read-base* . 10) + (foo . :missing))))) + (test-case + "#-common-lisp 1" + '(((0 0) ((*package* . #:KEYWORD) + (*read-suppress* . nil) + (*read-base* . 10))) + ((0 1) ((*package* . "INCREMENTALIST.TEST.TEST-PACKAGE") + (*read-suppress* . t) + (*read-base* . 10))))) + (test-case + "#.(cl:setf cl:*read-base* 11) 1" + '(((1) ((*package* . "INCREMENTALIST.TEST.TEST-PACKAGE") + (*read-suppress* . nil) + (*read-base* . 11))))) + (test-case + "(cl:in-package \"FOO\") 1" + '(((1) ((*package* . "FOO") + (*read-suppress* . nil) + (*read-base* . 10)))))))