branch: externals/phpinspect commit c0823a265b2e8c8aa4b5c01f84059306e5549d79 Author: Hugo Thunnissen <de...@hugot.nl> Commit: Hugo Thunnissen <de...@hugot.nl>
WIP create common base library for threading operations --- phpinspect-autoload.el | 6 +- phpinspect-buffer.el | 224 ++++++++++++-------------- phpinspect-pipeline.el | 415 ++++++++++++++++++++----------------------------- phpinspect-project.el | 2 +- phpinspect-thread.el | 183 ++++++++++++++++++++++ test/test-autoload.el | 6 +- test/test-buffer.el | 21 +-- test/test-eldoc.el | 5 +- test/test-pipeline.el | 31 +++- 9 files changed, 497 insertions(+), 396 deletions(-) diff --git a/phpinspect-autoload.el b/phpinspect-autoload.el index 53453d25ec..87a7643937 100644 --- a/phpinspect-autoload.el +++ b/phpinspect-autoload.el @@ -383,7 +383,7 @@ FILE-NAME does not contain any wildcards, instead of nil." (t (phpinspect--log "Error indexing file %s: %s" file err)))))) (phpinspect--log "indexing files list: %s" list) (phpinspect-pipeline list - :into (funcall :with-context wrapped-indexer)))) + :into `(funcall :with-context ,wrapped-indexer)))) (cl-defmethod phpinspect-autoloader-put-type-bag ((al phpinspect-autoloader) (type-fqn (head phpinspect-name))) (let* ((base-name (phpinspect-name-base type-fqn)) @@ -538,8 +538,8 @@ FILE-NAME does not contain any wildcards, instead of nil." (hash-table-count (phpinspect-autoloader-types autoloader)) (string-to-number (format-time-string "%s%3N" (time-since time-start))))))) result error)) - :into (phpinspect-iterate-composer-jsons :with-context autoloader) - :into phpinspect-al-strategy-execute)))))) + :into `(phpinspect-iterate-composer-jsons :with-context ,autoloader) + :into #'phpinspect-al-strategy-execute)))))) (provide 'phpinspect-autoload) ;;; phpinspect-autoload.el ends here diff --git a/phpinspect-buffer.el b/phpinspect-buffer.el index 665c326ef9..f22e51e7a5 100644 --- a/phpinspect-buffer.el +++ b/phpinspect-buffer.el @@ -23,6 +23,7 @@ ;;; Code: +(require 'phpinspect-thread) (require 'phpinspect-parser) (require 'phpinspect-bmap) (require 'phpinspect-edtrack) @@ -101,6 +102,7 @@ edits does not count as fresh (because incremental parsing has its flaws)." (defun phpinspect-buffer-reparse-if-not-fresh (buffer) "If BUFFER's tree is fresh, return it. Otherwise reparse the buffer and return the result." + (phpi-shadow-await-synced (phpinspect-buffer-shadow buffer)) (if (phpinspect-buffer-fresh-p buffer) (phpinspect-buffer-tree buffer) (phpinspect-buffer-reparse buffer))) @@ -256,7 +258,6 @@ tokens that have been deleted from a buffer." (t (error "Cannot delete index for token %s" token)))) (defun phpinspect-buffer--delete-function-index-reference (buffer token) - (message "Deleting func %s" token) (when-let ((func (gethash token (phpinspect-buffer-token-index buffer)))) (let ((arg-list (phpinspect-function-argument-list token))) (if-let ((arg-list-meta (phpinspect-buffer-token-meta buffer arg-list)) @@ -273,20 +274,15 @@ tokens that have been deleted from a buffer." (phpinspect-function-p)))) (progn ;; Declaration is equal, update index reference - (message "updating index ref, %s was equal to %s" - (phpinspect-meta-token new-declaration) - (phpinspect-function-declaration token)) (phpinspect-buffer-update-index-reference-for-token buffer token (phpinspect-meta-token (phpinspect-meta-parent new-declaration)))) (progn - (message "Deleting index") ;; Declaration is not equal, delete index (remhash token (phpinspect-buffer-token-index buffer)) (cond ((phpinspect-project-p (car func)) (phpinspect-project-delete-function (phpinspect-buffer-project buffer) (phpinspect--function-name (cdr func)))) ((phpinspect--type-p (car func)) - (message "Getting typedef for %s" (car func)) (when-let ((class (phpinspect-project-get-typedef (phpinspect-buffer-project buffer) (car func)))) @@ -301,16 +297,16 @@ tokens that have been deleted from a buffer." (setf (phpinspect-buffer-tree buffer) nil (phpinspect-buffer--tokens buffer) nil - (phpinspect-buffer-map buffer) (phpinspect-make-bmap) + (phpinspect-buffer-map buffer) nil ;; TODO: figure out what the desired behaviour is here ;; (phpinspect-buffer--additions buffer) nil ;; (phpinspect-buffer--deletions buffer) nil (phpinspect-buffer-token-index buffer) - (make-hash-table :test 'eq :size 100 :rehash-size 1.5)) + (make-hash-table :test 'eq :size 100 :rehash-size 1.5))) + - (phpi-shadow-enqueue-task (phpinspect-buffer-shadow buffer) 'parse-fresh)) (defun phpinspect-buffer-state (buffer) (interactive (list (or phpinspect-current-buffer @@ -330,13 +326,13 @@ tokens that have been deleted from a buffer." (interactive (list (or phpinspect-current-buffer (error "Not a phpinspect buffer")))) (phpinspect-buffer-reset buffer) (phpi-shadow-enqueue-task (phpinspect-buffer-shadow buffer) 'parse-fresh) - (phpinspect-buffer-parse buffer 'no-interrupt)) + (phpi-shadow-await-synced (phpi-buffer-shadow buffer)) + (phpinspect-buffer-tree buffer)) (defun phpinspect-buffer-reindex (buffer) "Delete all existing index entities for tokens in BUFFER and re-index." (interactive (list (or phpinspect-current-buffer (error "Not a phpinspect buffer")))) (dolist (token (hash-table-keys (phpinspect-buffer-token-index buffer))) - (phpinspect-buffer-delete-index-for-token buffer token)) (phpinspect-buffer-reparse buffer) @@ -696,6 +692,7 @@ continuing execution." (defun phpinspect-buffer-tokens-enclosing-point (buffer point) "Return token metadata objects for tokens enclosing POINT in BUFFER." (cl-assert (phpinspect-buffer-p buffer)) + (phpi-shadow-await-synced (phpinspect-buffer-shadow buffer)) (phpinspect-buffer--query-with-cache buffer `(tokens-enclosing ,point) (phpinspect-bmap-tokens-overlapping (phpinspect-buffer-map buffer) point))) @@ -784,6 +781,7 @@ If provided, PROJECT must be an instance of `phpinspect-project'." ;;;;;;;;;; SHADOWING ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar phpinspect--shadow-counter 0) +(defvar phpinspect--shadow-run-sync nil) (defvar phpinspect-shadow-pause-time 0.05) @@ -792,13 +790,14 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (cl-defstruct (phpinspect-shadow (:constructor phpinspect-make-shadow-generated) (:conc-name phpi-shadow-)) - (synced-p t :type boolean) - (index-synced-p t :type boolean) (origin nil :type phpinspect-buffer) (buffer nil :type buffer) (queue nil :type phpinspect--queue) (thread nil :type thread) (id nil :type integer) + (-last-change nil) + (-synced-change nil) + (-indexed-change nil) (-deletions nil :type list) (-additions (make-hash-table :test #'eq) :type hash-table)) @@ -812,14 +811,16 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (setf (phpinspect-buffer-map buffer) bmap) (if buffer-tokens - (let ((local-additions (phpi-shadow--additions shadow))) - ;; Determine which tokens are new and which were already present in the - ;; buffer - (maphash - (lambda (token meta) (unless (gethash token buffer-tokens) - (puthash token meta buffer-tokens) - (puthash token meta local-additions))) - (phpinspect-bmap-meta bmap))) + (let ((local-additions (with-memoization (phpi-shadow--additions shadow) + (make-hash-table :test #'eq)))) + ;; Determine which tokens are new and which were already present in the + ;; buffer + (maphash + (lambda (token meta) (unless (gethash token buffer-tokens) + (puthash token meta buffer-tokens) + (puthash token meta local-additions))) + (phpinspect-bmap-meta bmap))) + ;; There were no tokens registered, so we can adopt the map's token table (setf (phpinspect-buffer--tokens buffer) (phpinspect-bmap-meta bmap) ;; All tokens are new additions @@ -837,7 +838,7 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (remhash (phpinspect-meta-token deletion) buffer-tokens))) ;; There is no previous bmap, so there should also not be any previous additions - (setf (phpi-shadow--additions shadow) (make-hash-table :test #'eq))) + (setf (phpi-shadow--additions shadow) (phpinspect-bmap-meta bmap))) ;; A new bmap was provided, so the structure of the token tree was ;; changed. All previous query results should be regarded as invalid. @@ -846,22 +847,12 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (defun phpi-shadow-wakeup-thread (shadow) (thread-signal (phpi-shadow-thread shadow) 'phpinspect-wakeup-shadow nil)) -(defun phpi-shadow-thread-check-pause () - (if (or (phpinspect--input-pending-p) - quit-flag) - (let* ((mx (make-mutex)) - (continue (make-condition-variable mx))) - (phpinspect-thread-pause phpinspect-shadow-pause-time mx continue)) - (ignore-errors (thread-yield)))) - (defun phpi-shadow-make-queue-subscription (shadow) (lambda () - (setf (phpi-shadow-synced-p shadow) nil) - (setf (phpi-shadow-index-synced-p shadow) nil) (phpi-shadow-wakeup-thread shadow))) (defun phpi-shadow--thread-make-parser-interrupt-predicate () - (lambda () (phpi-shadow-thread-check-pause) nil)) + (lambda () (phpi-thread-yield) nil)) (defun phpi-shadow-process-change (shadow change) (with-current-buffer (phpi-shadow-buffer shadow) @@ -884,7 +875,9 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (setf (phpinspect-buffer-tree buffer) result)) (phpi-shadow--set-buffer-map - shadow (phpinspect-pctx-bmap pctx) (phpinspect-pctx-previous-bmap pctx))))) + shadow (phpinspect-pctx-bmap pctx) (phpinspect-pctx-previous-bmap pctx)) + + (setf (phpi-shadow--synced-change shadow) change)))) (defun phpi-shadow-parse-fresh (shadow) (with-current-buffer (phpi-shadow-buffer shadow) @@ -903,57 +896,27 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (setf (phpinspect-buffer-tree buffer) result) (phpi-shadow--set-buffer-map - shadow (phpinspect-pctx-bmap pctx) nil))))) + shadow (phpinspect-pctx-bmap pctx) nil) + + (setf (phpi-shadow--synced-change shadow) 'parse-fresh))))) (defun phpinspect-visit-shadow-buffer (buffer) (interactive (list (or phpinspect-current-buffer (error "Not a phpinspect buffer")))) (pop-to-buffer (phpi-shadow-buffer (phpinspect-buffer-shadow buffer)))) -(defun phpi-shadow-make-thread-function (shadow) - (lambda () - (let ((inhibit-quit t)) - (while t - (condition-case err - (ignore-error phpinspect-wakeup-shadow - (if-let ((task (phpinspect-queue-dequeue (phpi-shadow-queue shadow)))) - (progn - (pcase task - ((pred phpinspect-change-p) - (phpi-shadow-process-change shadow task)) - ('parse-fresh - (phpi-shadow-parse-fresh shadow)) - (_ - (phpinspect-message - "Shadow thread received unsupported task type: %s" - (type-of task)))) - - ;; Rest after task completion - (phpi-shadow-thread-check-pause)) - - ;; No parsing work to do, we're in sync with the origin buffer - (setf (phpi-shadow-synced-p shadow) t) - - (if (phpi-shadow-index-synced-p shadow) - ;; Index has been synced, we can join the main thread and - ;; wait for more tasks. - (ignore-errors - (thread-join main-thread)) - ;; Update project index - (phpi-shadow-update-project-index shadow) - - ;; Only flag index as synced when no additional tasks have been - ;; enqueued while indexing. - (unless (phpinspect-queue-first (phpi-shadow-queue shadow)) - (setf (phpi-shadow-index-synced-p shadow) t))))) - (error - (phpinspect-message "%s encountered unexpected error: %s" - (thread-name (current-thread)) - err))))))) - ;; (error - ;; (phpinspect-message "%s exited: %s" - ;; (thread-name (current-thread)) - ;; err)))))) +(defun phpi-shadow-perform-task (shadow task) + (pcase task + ((pred phpinspect-change-p) + (phpi-shadow-process-change shadow task)) + ('parse-fresh + (phpi-shadow-parse-fresh shadow)) + ('update-project-index + (phpi-shadow-update-project-index shadow)) + (_ + (phpinspect-message + "Shadow thread received unsupported task type: %s" + (type-of task))))) (defun phpi-shadow-thread-live-p (shadow) (thread-live-p (phpi-shadow-thread shadow))) @@ -977,9 +940,11 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (phpi-shadow-assert-live-p shadow) - (phpi-shadow-wakeup-thread shadow) (thread-yield)))) +(defun phpi-shadow-synced-p (shadow) + (eq (phpi-shadow--synced-change shadow) (phpi-shadow--last-change shadow))) + (defun phpi-shadow-await-synced (shadow &optional allow-interrupt) (phpi-shadow-await-predicate shadow #'phpi-shadow-synced-p allow-interrupt)) @@ -990,16 +955,17 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (let ((buffer (phpi-shadow-origin shadow))) ;; Process deleted tokens (dolist (deletion (phpi-shadow--deletions shadow)) - (pcase (phpinspect-meta-token deletion) - ((pred phpinspect--can-delete-buffer-index-for-token) - (phpinspect-buffer-delete-index-for-token buffer (phpinspect-meta-token deletion))) - ((pred phpinspect-use-trait-p) - (when-let ((class (seq-find (phpinspect-meta-token-predicate #'phpinspect-class-p) - (phpinspect-buffer-tokens-enclosing-point - buffer (phpinspect-meta-start deletion)))) - (declaration (phpinspect-meta-find-first-child-matching-token - class #'phpinspect-class-declaration-p))) - (phpinspect-buffer--index-class-declaration buffer declaration 'force))))) + (phpi-progn + (pcase (phpinspect-meta-token deletion) + ((pred phpinspect--can-delete-buffer-index-for-token) + (phpinspect-buffer-delete-index-for-token buffer (phpinspect-meta-token deletion))) + ((pred phpinspect-use-trait-p) + (when-let ((class (seq-find (phpinspect-meta-token-predicate #'phpinspect-class-p) + (phpinspect-buffer-tokens-enclosing-point + buffer (phpinspect-meta-start deletion)))) + (declaration (phpinspect-meta-find-first-child-matching-token + class #'phpinspect-class-declaration-p))) + (phpinspect-buffer--index-class-declaration buffer declaration 'force)))))) (setf (phpi-shadow--deletions shadow) nil))) @@ -1009,36 +975,48 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (buffer (phpi-shadow-origin shadow))) (maphash (lambda (token addition) - (pcase (phpinspect-meta-token addition) - ((pred phpinspect-class-declaration-p) - (phpinspect-buffer--index-class-declaration buffer addition)) - ((pred phpinspect-function-p) - (phpinspect-buffer--index-function buffer addition)) - ((pred phpinspect-use-trait-p) - (phpinspect-buffer--index-trait-use buffer addition)) - ((pred phpinspect-this-p) - (phpinspect-buffer--index-this buffer addition)) - ((or (pred phpinspect-class-variable-p) - (pred phpinspect-const-p)) - (phpinspect-buffer--index-class-variable buffer addition))) - - ;; Pause in between potentially expensive indexing operations. - (phpi-shadow-thread-check-pause)) + (phpi-progn + (pcase (phpinspect-meta-token addition) + ((pred phpinspect-class-declaration-p) + (phpinspect-buffer--index-class-declaration buffer addition)) + ((pred phpinspect-function-p) + (phpinspect-buffer--index-function buffer addition)) + ((pred phpinspect-use-trait-p) + (phpinspect-buffer--index-trait-use buffer addition)) + ((pred phpinspect-this-p) + (phpinspect-buffer--index-this buffer addition)) + ((or (pred phpinspect-class-variable-p) + (pred phpinspect-const-p)) + (phpinspect-buffer--index-class-variable buffer addition))))) additions)) (setf (phpi-shadow--additions shadow) nil)) (defun phpi-shadow-update-project-index (shadow) - (when (phpinspect-buffer-project (phpi-shadow-origin shadow)) - (phpinspect--log "Updating project index") - (phpi-shadow-process-deletions shadow) - (phpi-shadow-thread-check-pause) - (phpi-shadow-process-additions shadow))) + (let ((change (phpi-shadow--last-change shadow))) + (when (phpinspect-buffer-project (phpi-shadow-origin shadow)) + (phpi-shadow-process-deletions shadow) + (phpi-shadow-process-additions shadow) + + (setf (phpi-shadow--indexed-change shadow) change)))) + +(defun phpi-shadow--handle-job (shadow job) + (phpi-progn + (phpi-shadow-perform-task shadow job)) + + (when (and (phpi-shadow-synced-p shadow) + (not (phpi-shadow-index-synced-p shadow))) + (phpi-shadow-enqueue-task shadow 'update-project-index))) -(defun phpi-shadow-make-thread (shadow) - (make-thread - (phpi-shadow-make-thread-function shadow) - (format " **phpinspect-shadow-thread**<%d>" (phpi-shadow-id shadow)))) +(defun phpi-shadow-index-synced-p (shadow) + (and (phpi-shadow-synced-p shadow) + (eq (phpi-shadow--synced-change shadow) + (phpi-shadow--indexed-change shadow)))) + +(defun phpi-shadow-make-job-queue (shadow) + (phpi-start-job-queue (format " **phpinspect-shadow-thread**<%d>" (phpi-shadow-id shadow)) + (lambda (job) + (phpi-shadow--handle-job shadow job)))) (defun phpinspect-make-shadow (origin) (cl-assert (phpinspect-buffer-p origin)) @@ -1054,19 +1032,21 @@ If provided, PROJECT must be an instance of `phpinspect-project'." (with-current-buffer (phpi-shadow-buffer shadow) (insert (phpinspect-with-current-buffer origin (buffer-string)))) + (let ((job-queue (phpi-shadow-make-job-queue shadow))) + (setf (phpi-shadow-queue shadow) job-queue + (phpi-shadow-thread shadow) (phpi-job-queue-thread job-queue))) - (setf (phpi-shadow-queue shadow) - (phpinspect-make-queue (phpi-shadow-make-queue-subscription shadow)) - - (phpi-shadow-thread shadow) - (phpi-shadow-make-thread shadow)) - - ;(phpi-shadow-enqueue-task shadow 'parse-fresh) + (phpi-shadow-enqueue-task shadow 'parse-fresh) shadow)) (defun phpi-shadow-enqueue-task (shadow task) - (phpinspect-queue-enqueue (phpi-shadow-queue shadow) task)) + (when (or (phpinspect-change-p task) (eq 'parse-fresh task)) + (setf (phpi-shadow--last-change shadow) task)) + + (if phpinspect--shadow-run-sync + (phpi-shadow--handle-job shadow task) + (phpinspect-queue-enqueue (phpi-shadow-queue shadow) task))) (provide 'phpinspect-buffer) diff --git a/phpinspect-pipeline.el b/phpinspect-pipeline.el index 4ffd54fec6..4893d853b5 100644 --- a/phpinspect-pipeline.el +++ b/phpinspect-pipeline.el @@ -24,10 +24,14 @@ ;;; Code: (require 'phpinspect-queue) (require 'phpinspect-util) +(require 'phpinspect-thread) (define-error 'phpinspect-pipeline-incoming "Signal for incoming pipeline data") (define-error 'phpinspect-pipeline-error "Signal for pipeline errors") +(defun phpinspect-pipeline-error-p (obj) + (eq 'phpinspect-pipeline-error (car-safe obj))) + (defcustom phpinspect-pipeline-pause-time 0.1 "Number of seconds to pause a pipeline thread when emacs receives user input. This is similar to `phpinspect-worker-pause-time', @@ -74,7 +78,8 @@ directories." (cl-defmethod phpinspect-pipeline-ctx-register-end ((ctx phpinspect-pipeline-ctx) (end phpinspect-pipeline-end)) (let ((thread (phpinspect-pipeline-ctx-get-thread ctx (phpinspect-pipeline-end-thread end)))) - (setf (phpinspect-pipeline-thread-end thread) end))) + (unless (phpinspect-pipeline-thread-end thread) + (setf (phpinspect-pipeline-thread-end thread) end)))) (cl-defmethod phpinspect-pipeline-ctx-close ((ctx phpinspect-pipeline-ctx)) (let (errors err end thread-live) @@ -86,8 +91,10 @@ directories." (when thread-live (if end - (setq errors (nconc errors (list (format "Thread %s ended pipeline, but is still running" - (thread-name (car thread)))))) + ;; Give thread cpu time to wrap up + (thread-join (car thread)) + ;; Even if thread is still live, it should have signaled its end at + ;; this point. (setq errors (nconc errors (list (format "Thread %s is still running when pipeline is closing" (thread-name (car thread)))))))) @@ -97,10 +104,7 @@ directories." err))))) (unless end (setq errors (nconc errors (list (format "Thread %s never ended" - (thread-name (car thread))))))) - - (when (thread-live-p (car thread)) - (thread-signal (car thread) 'quit nil))) + (thread-name (car thread)))))))) (when errors (signal 'phpinspect-pipeline-error errors)))) @@ -128,13 +132,7 @@ directories." (define-inline phpinspect-pipeline-pause () "Pause the current pipeline thread" - (inline-quote - ;; If quit flag is set, behave as it input is pending. - (if (or quit-flag (phpinspect--input-pending-p)) - (let ((mx (make-mutex))) - (phpinspect-thread-pause - phpinspect-pipeline-pause-time mx (make-condition-variable mx "phpinspect-pipeline-pause"))) - (ignore-errors (thread-yield))))) + (inline-quote (phpi-thread-yield))) (define-inline phpinspect--read-pipeline-emission (&rest body) (push 'progn body) @@ -143,240 +141,7 @@ directories." ,body nil))) -(defmacro phpinspect--run-as-pipeline-step (func-name queue consumer-queue pipeline-ctx &optional local-ctx) - (unless (symbolp func-name) - (error "Function name must be a symbol, got: %s" func-name)) - - - (let* ((thread-name (concat "phpinspect-pipeline-" (symbol-name func-name))) - (statement (list func-name)) - (statement-rear statement) - (incoming (gensym "incoming")) - (outgoing (gensym "outgoing")) - (inc-queue (gensym "queue")) - (out-queue (gensym "queue")) - (context-sym (gensym "context")) - (continue-running (gensym "continue-running")) - (pctx-sym (gensym "pipeline-ctx")) - (incoming-end (gensym "incoming-end")) - (end (gensym "end"))) - - (when local-ctx - (setq statement-rear (setcdr statement-rear (cons context-sym nil)))) - - (setq statement-rear (setcdr statement-rear (cons incoming nil))) - - `(let ((,inc-queue ,queue) - (,out-queue ,consumer-queue) - (,context-sym ,local-ctx) - (,pctx-sym ,pipeline-ctx)) - (make-thread - (lambda () - (let ((,continue-running t) - ;; Inhibit quitting - (inhibit-quit t) - ,incoming ,outgoing ,end ,incoming-end) - - (phpinspect-pipeline--register-wakeup-function ,inc-queue) - (while ,continue-running - (condition-case err - (progn - (phpinspect-pipeline-pause) - (setq ,incoming (phpinspect-pipeline-receive ,inc-queue)) - - (if (phpinspect-pipeline-end-p ,incoming) - (progn - (setq ,incoming-end ,incoming) - (when (phpinspect-pipeline-end-value ,incoming) - (progn - (setq ,incoming (phpinspect-pipeline-end-value ,incoming) - ,outgoing (phpinspect--read-pipeline-emission ,statement)) - (phpinspect-pipeline--enqueue ,out-queue ,outgoing 'no-notify))) - - (setq ,end (phpinspect-make-pipeline-end :thread (current-thread))) - (phpinspect-pipeline-ctx-register-end ,pctx-sym ,end) - (setq ,continue-running nil) - (phpinspect-pipeline--enqueue ,out-queue ,end)) - - ;; Else - (setq ,outgoing (phpinspect--read-pipeline-emission ,statement)) - (when (phpinspect-pipeline-end-p ,outgoing) - (setq ,end (phpinspect-make-pipeline-end :thread (current-thread))) - (phpinspect-pipeline-ctx-register-end ,pctx-sym ,end) - (setq ,continue-running nil)) - (phpinspect-pipeline--enqueue ,out-queue ,outgoing))) - (phpinspect-pipeline-incoming) - (t (phpinspect-message "Pipeline thread errored: %s" err) - (setq ,end (phpinspect-make-pipeline-end :thread (current-thread) :error err)) - (setq ,continue-running nil) - (phpinspect-pipeline-ctx-register-end ,pctx-sym ,end) - (phpinspect-pipeline--enqueue ,out-queue ,end)))))) - ,thread-name)))) - - -(defun phpinspect--chain-pipeline-steps (steps start-queue end-queue ctx) - (let ((result (gensym "result")) - (incoming (gensym "incoming")) - (outgoing (gensym "outgoing")) - (ctx-sym (gensym "ctx")) - body name step statement) - (while (setq step (pop steps)) - (setq name (phpinspect--pipeline-step-name step)) - - (setq statement - (if (phpinspect--pipeline-step--context-var-name step) - `(phpinspect--run-as-pipeline-step - ,name ,incoming ,outgoing ,ctx-sym ,(phpinspect--pipeline-step--context-var-name step)) - `(phpinspect--run-as-pipeline-step ,name ,incoming ,outgoing ,ctx-sym))) - (setq body (nconc body `(,(if steps - `(setq ,outgoing (phpinspect-make-queue)) - `(setq ,outgoing ,end-queue)) - (phpinspect-pipeline-ctx-register-thread ,ctx-sym ,statement ,incoming) - (setq ,incoming ,outgoing))))) - - `(let ((,incoming ,start-queue) (,ctx-sym ,ctx) ,result ,outgoing) - ,@body))) - -(cl-defstruct (phpinspect--pipeline-step (:constructor phpinspect--make-pipeline-step)) - (context nil - :type any - :documentation - "An object that is passed as first argument to all step executions") - (-context-var-name nil - :type symbol - :documentation - "Variable name used to store context in") - (name nil - :type symbol - :documentation - "The name of this step")) - -(defmacro phpinspect--pipeline (seed-form &rest parameters) - (let (key value steps let-vars) - - (while parameters - (setq key (pop parameters) - value (pop parameters)) - - (pcase key - (:into - (let* ((construct-params (cons nil nil)) - (cons-params-rear construct-params) - parameters name) - - (if (listp value) - (progn - (setq name (car value) - parameters (cdr value))) - (setq name value)) - - (unless (symbolp name) - (error "Step name should be a symbol")) - - (let (key value) - (while parameters - (setq key (pop parameters) - value (pop parameters)) - (setq key (intern (string-replace ":with-" ":" (symbol-name key)))) - (setq cons-params-rear - (setcdr cons-params-rear (cons key (cons value nil)))))) - (push (apply #'phpinspect--make-pipeline-step `(,@(cdr construct-params) :name ,name)) - steps))) - (_ (error "unexpected key %s" key)))) - - (setq steps (nreverse steps)) - - (dolist (step steps) - (when (phpinspect--pipeline-step-context step) - (setf (phpinspect--pipeline-step--context-var-name step) (gensym "ctx")) - (push `(,(phpinspect--pipeline-step--context-var-name step) - ,(phpinspect--pipeline-step-context step)) - let-vars))) - - (let ((queue-sym (gensym "queue")) - (end-queue-sym (gensym "end-queue")) - (ctx-sym (gensym "ctx")) - (recv-sym (gensym)) - (result-sym (gensym)) - (seed-sym (gensym)) - (collecting-sym (gensym))) - `(progn - (when (eq main-thread (current-thread)) - (error "Pipelines should not run in the main thread")) - - (let* (,@let-vars - (,ctx-sym (phpinspect-make-pipeline-ctx)) - (,queue-sym (phpinspect-make-queue)) - (,end-queue-sym (phpinspect-make-queue)) - (,collecting-sym t) - (inhibit-quit t) - ,recv-sym ,result-sym ,seed-sym) - - ,(phpinspect--chain-pipeline-steps steps queue-sym end-queue-sym ctx-sym) - - (setq ,seed-sym ,seed-form) - (when ,seed-sym - (phpinspect-pipeline--enqueue - ,queue-sym - (phpinspect-make-pipeline-emission :collection ,seed-sym) 'no-notify)) - - (phpinspect-pipeline--enqueue - ,queue-sym (phpinspect-make-pipeline-end :thread (current-thread))) - - (while ,collecting-sym - (ignore-error phpinspect-pipeline-incoming - (progn - (phpinspect-pipeline--register-wakeup-function ,end-queue-sym) - (while (not (phpinspect-pipeline-end-p - (setq ,recv-sym (phpinspect-pipeline-receive ,end-queue-sym)))) - (setq ,result-sym (nconc ,result-sym (list ,recv-sym)))) - (setq ,collecting-sym nil)))) - - (phpinspect-pipeline-ctx-close ,ctx-sym) - ,result-sym))))) - -(defmacro phpinspect-pipeline (seed-form &rest parameters) - (declare (indent defun)) - (let ((result (gensym)) - (async-sym (gensym)) - key value async macro-params) - (while parameters - (setq key (pop parameters) - value (pop parameters)) - - (pcase key - (:async (setq async value)) - (_ (setq macro-params (nconc macro-params (list key value)))))) - - `(if-let ((,async-sym ,async)) - (make-thread - (lambda () - (let ((inhibit-quit t)) - (condition-case err - (let ((,result (phpinspect--pipeline ,seed-form ,@macro-params))) - (funcall ,async-sym (or ,result 'phpinspect-pipeline-nil-result) nil)) - (error (funcall ,async-sym nil err))))) - "phpinspect-pipeline-async") - (phpinspect--pipeline ,seed-form ,@macro-params)))) - -(define-inline phpinspect-pipeline-receive (queue) - (inline-letevals (queue) - (inline-quote - (let ((val)) - (while (not (setq val (phpinspect-queue-dequeue ,queue))) - (thread-yield)) - val)))) - -(defun phpinspect-pipeline-step-name (name &optional suffix) - (intern (concat (symbol-name name) (if suffix (concat "-" suffix) "")))) - -(define-inline phpinspect-pipeline--register-wakeup-function (queue) - (inline-quote - (let ((thread (current-thread))) - (setf (phpinspect-queue-subscription ,queue) - (lambda () (thread-signal thread 'phpinspect-pipeline-incoming nil)))))) - -(define-inline phpinspect-pipeline--enqueue (queue emission &optional no-notify) +(define-inline phpinspect--pipeline-enqueue (queue emission &optional no-notify) (inline-letevals (queue emission no-notify) (inline-quote (when ,emission @@ -388,7 +153,159 @@ directories." ,no-notify)) (phpinspect-queue-enqueue ,queue (pop (phpinspect-pipeline-emission-collection ,emission)) ,no-notify)) - (phpinspect-queue-enqueue ,queue ,emission ,no-notify)))))) + (if (and (phpinspect-pipeline-end-p ,emission) + (phpinspect-pipeline-end-value ,emission)) + (progn + (phpinspect-queue-enqueue ,queue (phpinspect-pipeline-end-value ,emission) ,no-notify) + (phpinspect-queue-enqueue ,queue ,emission ,no-notify)) + (phpinspect-queue-enqueue ,queue ,emission ,no-notify))))))) + +(defun phpinspect--pipeline-parse-step (step-arguments) + (pcase-let ((`(,name ,step) + (if (listp step-arguments) + (list (car step-arguments) + (apply #'phpinspect--make-pipeline-step + (append (cdr step-arguments) + (list :name (car step-arguments))))) + (list step-arguments + (phpinspect--make-pipeline-step :name step-arguments))))) + + (unless (and (symbolp name) (fboundp name)) + (error "Pipeline step name must be a symbol bound to a function")) + + step)) + +(defun phpinspect--pipeline-parse-steps (arguments-plist &optional steps async) + (if arguments-plist + (let ((key (pop arguments-plist)) + (value (pop arguments-plist))) + (pcase key + (:into + (phpinspect--pipeline-parse-steps + arguments-plist + (cons (phpinspect--pipeline-parse-step value) steps) + async)) + (:async + (phpinspect--pipeline-parse-steps + arguments-plist steps value)) + (_ (error "Unexpected pipeline argument key: %s" key)))) + + (list async steps))) + +(defun phpinspect-pipeline-step-format-name (step) + (format "PHPInspect pipeline thread [%s]" + (symbol-name (phpinspect--pipeline-step-name step)))) + +(defun phpinspect--pipeline-fn-wrap-with-read-pipeline (fn step) + (if (phpinspect--pipeline-step-with-auto-emit step) + fn + (lambda (task) (phpinspect--read-pipeline-emission (funcall fn task))))) + +(defun phpinspect--pipeline-fn-wrap-with-end-handler (fn pipeline-ctx) + (lambda (task) + (let ((result (funcall fn task))) + (when (phpinspect-pipeline-end-p result) + (phpinspect-pipeline-ctx-register-end pipeline-ctx result)) + + result))) + +(defun phpinspect--pipeline-fn-wrap-with-error-handler (fn step pipeline-ctx) + (lambda (task) + (condition-case err + (funcall fn task) + (t + (phpinspect-make-pipeline-end + :error err :thread (current-thread)))))) + +(defun phpinspect--pipeline-fn-wrap-with-ctx (fn ctx) + (if ctx + (lambda (task) + (funcall fn ctx task)) + (lambda (task) (funcall fn task)))) + +(defun phpinspect--pipeline-fn-wrap-with-emitter (fn out-queue) + (lambda (task) + (let ((emission (if (phpinspect-pipeline-end-p task) + (phpinspect-make-pipeline-end :thread (current-thread)) + (funcall fn task)))) + (phpinspect--pipeline-enqueue out-queue emission) + emission))) + +(defun phpinspect--pipeline-make-run-step-function (pipeline-ctx step out-queue) + (let ((step-fn (thread-first + (phpinspect--pipeline-step-name step) + (phpinspect--pipeline-fn-wrap-with-ctx (phpinspect--pipeline-step-with-context step)) + (phpinspect--pipeline-fn-wrap-with-read-pipeline step) + (phpinspect--pipeline-fn-wrap-with-error-handler step pipeline-ctx) + (phpinspect--pipeline-fn-wrap-with-emitter out-queue) + (phpinspect--pipeline-fn-wrap-with-end-handler pipeline-ctx)))) + (lambda (task) + (let ((result (funcall step-fn task))) + (when (phpinspect-pipeline-end-p task) + (phpi-job-queue-end)))))) + +(defun phpinspect--pipeline-chain (ctx steps &optional out-queue) + (if-let ((step (pop steps))) + (let* ((job-queue (phpi-start-job-queue (phpinspect-pipeline-step-format-name step) + (phpinspect--pipeline-make-run-step-function ctx step out-queue)))) + (phpinspect-pipeline-ctx-register-thread ctx (phpi-job-queue-thread job-queue) job-queue) + (phpinspect--pipeline-chain ctx steps job-queue)) + out-queue)) + +(defun phpinspect-pipeline (seed-form &rest arguments-plist) + (declare (indent defun)) + (pcase-let ((`(,async ,steps) (phpinspect--pipeline-parse-steps arguments-plist)) + (ctx (phpinspect-make-pipeline-ctx))) + (when seed-form + (let* (results + (in-queue (phpi-start-job-queue "Pipeline result accumulator" + (lambda (result) + (if (phpinspect-pipeline-end-p result) + (phpi-progn + (setq results (nreverse results)) + (phpi-job-queue-end)) + (push result results)))))) + + (let ((out-queue (phpinspect--pipeline-chain ctx steps in-queue))) + (phpinspect--pipeline-enqueue + out-queue + (phpinspect-make-pipeline-emission :collection seed-form) 'no-notify) + + (phpinspect--pipeline-enqueue + out-queue (phpinspect-make-pipeline-end :thread (current-thread)))) + + (if async + (phpi-run-threaded "Pipeline result awaiter" + (thread-join (phpi-job-queue-thread in-queue)) + + (condition-case err + (progn + (phpinspect-pipeline-ctx-close ctx) + ;; async consumers may use the result being non-nil as a + ;; means to determine whether the pipeline has finished + ;; executing or not. So we return a symbol when the result + ;; is nil to prevent consumer threads from waiting + ;; endlessly. + (funcall async (or results 'phpinspect-pipeline-nil-result) nil)) + (t + (funcall async results err)))) + (progn + (thread-join (phpi-job-queue-thread in-queue)) + (phpinspect-pipeline-ctx-close ctx) + results)))))) + + +(cl-defstruct (phpinspect--pipeline-step (:constructor phpinspect--make-pipeline-step)) + (with-context nil + :type any + :documentation + "An object that is passed as first argument to all step executions") + (with-auto-emit nil + :type boolean) + (name nil + :type symbol + :documentation + "The name of this step")) (provide 'phpinspect-pipeline) ;;; phpinspect-pipeline.el ends here diff --git a/phpinspect-project.el b/phpinspect-project.el index e6f99391ff..a895cd322b 100644 --- a/phpinspect-project.el +++ b/phpinspect-project.el @@ -446,7 +446,7 @@ before the search is executed." (dir (phpinspect-index-dir-task-dir task))) (phpinspect--log "Indexing directory %s" dir) (phpinspect-pipeline (phpinspect-fs-directory-files-recursively fs dir "\\.php$") - :into (phpinspect-project-add-file-index :with-context project)))) + :into `(phpinspect-project-add-file-index :with-context ,project)))) (provide 'phpinspect-project) ;;; phpinspect-project.el ends here diff --git a/phpinspect-thread.el b/phpinspect-thread.el new file mode 100644 index 0000000000..d5449ed809 --- /dev/null +++ b/phpinspect-thread.el @@ -0,0 +1,183 @@ +;;; phpinspect-thread.el --- Threading functions for phpinspect -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Hugo Thunnissen + +;; Author: Hugo Thunnissen <de...@hugot.nl> + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; + +;;; Code: + +(require 'phpinspect-queue) +(require 'phpinspect-util) + +(cl-defstruct (phpi-condition (:constructor phpi--make-condition)) + (-value nil) + (-condvar nil :type condition-variable) + (-mx nil :type mutex)) + +(gv-define-setter phpi-condition-value (val condition) + `(phpi-condition--set-value ,condition ,val)) + +(define-error 'phpinspect-kill-thread + "Thread killed") + +(define-error 'phpinspect-job-queue-end + "Job queue ended") + +(defun phpi-condition-value (condition) + (phpi-condition--value condition)) + +(defun phpi-condition-notify (condition) + (with-mutex (phpi-condition--mx condition) + (condition-notify (phpi-condition--condvar condition) t))) + +(defun phpi-condition--set-value (condition value) + (setf (phpi-condition--value condition) value) + (phpi-condition-notify condition)) + +(defun phpi-condition-wait (condition) + (let ((mx (phpi-condition--mx condition)) + (condvar (phpi-condition--condvar condition)) + result) + + (while (not (setq result (phpi-condition--value condition))) + (with-mutex mx + (condition-wait condvar))) + + (setf (phpi-condition--value condition) nil) + result)) + +(defun phpi-make-condition (&optional value) + (let* ((mx (make-mutex)) + (condvar (make-condition-variable mx))) + (phpi--make-condition :-mx mx :-condvar condvar :-value value))) + +(defvar phpinspect--main-thread-starving (phpi-make-condition 'no)) + +(defun phpi-thread-kill (thread) + (thread-signal thread 'phpinspect-kill-thread nil)) + +(defmacro phpi-run-threaded (thread-name &rest body) + (declare (indent 1)) + (let ((err-sym (gensym))) + `(make-thread + (lambda () + (condition-case ,err-sym + (progn ,@body) + (phpinspect-kill-thread) + (error + (phpinspect-message + "Thread [%s (exited)] encountered an error: %s" + (thread-name (current-thread)) + ,err-sym)))) + + ,thread-name))) + +(defun phpi--main-thread-starving-p () + (if (or quit-flag (phpinspect--input-pending-p)) + 'yes + 'no)) + +(defun phpi-main-thread-starving-p () + (let ((starving (phpi--main-thread-starving-p))) + (when (eq 'yes starving) + (setf (phpi-condition-value phpinspect--main-thread-starving) starving) + t))) + +(defun phpi-await-main-thread-nourished () + (when (phpi-main-thread-startving-p) + (while + (eq 'yes (phpi-condition-wait + phpinspect--main-thread-starving))))) + +(defun phpi-job-queue-end () + (signal 'phpinspect-job-queue-end nil)) + +(defun phpi--notify-main-thread-nourished () + (setf (phpi-condition-value phpinspect--main-thread-starving) 'no)) + +(defvar phpinspect-main-thread-nourishment 0.1 + "Amount of seconds to pause all threads when input is pending.") + +(defvar phpinspect-main-thread-nourishment-timer + (run-with-idle-timer phpinspect-main-thread-nourishment t #'phpi--notify-main-thread-nourished)) + +(define-inline phpi-thread-yield () + "Like `thread-yield', but takes extra care not to starve the main thread. + +If current thread is the main thread, this function does nothing." + (inline-quote + (unless (eq main-thread (current-thread)) + (if (phpi-main-thread-starving-p) + (phpi-await-main-thread-nourished) + (thread-yield))))) + +(defmacro phpi-progn (&rest body) + `(prog1 + (progn ,@body) + (phpi-thread-yield))) + +(cl-defstruct (phpinspect-job-queue (:constructor phpi--make-job-queue) + (:conc-name phpi-job-queue-) + (:include phpinspect-queue)) + (thread nil :type thread)) + +(defun phpi-start-job-queue (name job-handler) + (declare (indent 1)) + (let* ((condition (phpi-make-condition)) + queue) + (setq queue (phpi--make-job-queue + :subscription + (lambda () + (setf (phpi-condition-value condition) + (phpinspect-queue-first queue))))) + + (setf (phpi-job-queue-thread queue) + (phpi-run-threaded (format "(job queue) %s" name) + (let ((ended nil)) + (catch 'phpi--break + (while t + (if-let ((job (phpinspect-queue-dequeue queue))) + (phpi-progn + (condition-case err + (funcall job-handler job) + (phpinspect-job-queue-end + ;; If job queue end is signaled, exit after queue has + ;; been fully depleted. + (setq ended t) + (unless (phpinspect-queue-first queue) + (throw 'phpi--break nil))))) + + (if ended + ;; End was signaled previously and the queue is empty. Exit. + (throw 'phpi--break nil) + (phpi-condition-wait condition)))))))) + queue)) + +(defun phpi-job-queue-live-p (queue) + (and (threadp (phpi-job-queue-thread queue)) + (thread-live-p (phpi-job-queue-thread queue)))) + +(defun phpi-job-queue-kill (queue) + (when (phpi-job-queue-live-p queue) + (phpi-kill-thread (phpi-job-queue-thread queue)))) + + +(provide 'phpinspect-thread) +;;; phpinspect-thread.el ends here diff --git a/test/test-autoload.el b/test/test-autoload.el index e210c041dc..64583d0850 100644 --- a/test/test-autoload.el +++ b/test/test-autoload.el @@ -88,7 +88,7 @@ :async (lambda (res err) (setq result res error err)) - :into (phpinspect-iterate-composer-jsons :with-context autoloader)) + :into `(phpinspect-iterate-composer-jsons :with-context ,autoloader)) (while (not (or result error)) (thread-yield)) @@ -159,8 +159,8 @@ :async (lambda (res err) (setq result res error err)) - :into (phpinspect-iterate-composer-jsons :with-context autoloader) - :into phpinspect-al-strategy-execute) + :into `(phpinspect-iterate-composer-jsons :with-context ,autoloader) + :into #'phpinspect-al-strategy-execute) (while (not (or result error)) (thread-yield)) diff --git a/test/test-buffer.el b/test/test-buffer.el index 26724f87d3..d426ac6297 100644 --- a/test/test-buffer.el +++ b/test/test-buffer.el @@ -772,15 +772,16 @@ class TestClass ;; (should (seq-every-p #'phpinspect-meta-p (phpinspect-buffer--deletions buffer)))))) (ert-deftest phpinspect-buffer-parse-class-insertion () - (with-temp-buffer - (let ((buffer (phpinspect-claim-buffer (current-buffer)))) - (insert "<?php ") + (dlet ((phpi-shadow--run-sync t)) + (with-temp-buffer + (let ((buffer (phpinspect-claim-buffer (current-buffer)))) + (insert "<?php ") - (insert "class") - (phpinspect-buffer-parse buffer) - (insert " Name") - (phpinspect-buffer-parse buffer) - (insert " {} ") + (insert "class") + (phpinspect-buffer-parse buffer) + (insert " Name") + (phpinspect-buffer-parse buffer) + (insert " {} ") - (should (equal '(:root (:class (:class-declaration (:word "Name")) (:block))) - (phpinspect-buffer-parse buffer)))))) + (should (equal '(:root (:class (:class-declaration (:word "Name")) (:block))) + (phpinspect-buffer-parse buffer))))))) diff --git a/test/test-eldoc.el b/test/test-eldoc.el index a34767de2f..5752266e35 100644 --- a/test/test-eldoc.el +++ b/test/test-eldoc.el @@ -28,7 +28,6 @@ class Thing (project (phpinspect--make-project :autoload (phpinspect-make-autoloader) :worker 'nil-worker)) (buffer (phpinspect-claim-buffer (current-buffer) project)) second-arg-pos inside-nested-list-pos first-arg-pos) - (setq-local phpinspect-current-buffer buffer) (insert php-code) (backward-char) @@ -38,8 +37,6 @@ class Thing (backward-char 8) (setq first-arg-pos (point)) - (phpinspect-buffer-reindex buffer) - ;; Strategy should not trigger inside constructor/function arguments (let ((query (phpinspect-make-eldoc-query :point inside-nested-list-pos :buffer buffer)) (strat (phpinspect-make-eld-function-args)) @@ -52,6 +49,8 @@ class Thing (strat (phpinspect-make-eld-function-args)) (rctx (phpinspect-get-resolvecontext project (phpinspect-buffer-parse-map buffer) (car expected)))) + (phpinspect-buffer-update-project-index buffer) + ;; Subject is correct (should (phpinspect-word-p (car (phpinspect--resolvecontext-subject rctx)))) diff --git a/test/test-pipeline.el b/test/test-pipeline.el index 27e3334723..131d86d0c4 100644 --- a/test/test-pipeline.el +++ b/test/test-pipeline.el @@ -33,7 +33,7 @@ (let (result error) (phpinspect-pipeline (list "Linux" "Emacs") - :into phpinspect--correct-the-record + :into #'phpinspect--correct-the-record :async (lambda (res err) (setq result res error err))) @@ -52,7 +52,7 @@ (let (result error) (phpinspect-pipeline (list "Holy smokey") - :into phpinspect--aah-it-broke + :into #'phpinspect--aah-it-broke :async (lambda (res err) (setq result res error err))) @@ -61,6 +61,27 @@ (thread-yield)) (should error) - (should (equal '(phpinspect-pipeline-error - "Thread phpinspect-pipeline-phpinspect--aah-it-broke signaled error: (it-brokey . Holy smokey)") - error)))) + (should (phpinspect-pipeline-error-p error)) + (should (string-suffix-p "(it-brokey . Holy smokey)" (cadr error))))) + +(ert-deftest phpinspect-pipeline-auto-emit () + (let (result error) + + (phpinspect-pipeline (list "Linux" "Emacs") + :into `(format :with-context "It's not %s" + :with-auto-emit t) + :into `(format :with-context "%s, but GNU/... are you listening?" + :with-auto-emit t) + :async (lambda (res err) + (setq result res + error err))) + + (while (not (or result error)) + (thread-yield)) + + (should-not error) + + (should (equal '("It's not Linux, but GNU/... are you listening?" + "It's not Emacs, but GNU/... are you listening?") + result)) + (should-not error)))