aboutsummaryrefslogtreecommitdiffhomepage
path: root/coq/coq-par-compile.el
diff options
context:
space:
mode:
Diffstat (limited to 'coq/coq-par-compile.el')
-rw-r--r--coq/coq-par-compile.el706
1 files changed, 463 insertions, 243 deletions
diff --git a/coq/coq-par-compile.el b/coq/coq-par-compile.el
index 20440758..8901a008 100644
--- a/coq/coq-par-compile.el
+++ b/coq/coq-par-compile.el
@@ -85,12 +85,15 @@
;; back into proof-action-list only if all top-level jobs of those
;; modules that are required before it are finished.
;;
-;; A problem occurs with "Require b a.", where b depends on a. To
-;; avoid cycles in the dependency graph, here the top-level
-;; compilation job for "a" will be a so-called clone of the real
-;; compilation job. The Require item is stored with the clone. The
-;; real job has dependency links to all its clones. Every clone waits
-;; until its real job has finished.
+;; A problem occurs with "Require a. Require a.", where two different
+;; action list pieces must be stored with the job for a. The solution
+;; here is to clone the original job when it is needed more than one
+;; time. This cloning is done in general and not only for top-level
+;; jobs. So also when a.v and b.v both depend on c.v, the second
+;; dependency link is managed by a clone of the job for c.v. Every
+;; real job has dependency links to all its clones. All clones wait
+;; until the original job has finished. (In retrospect it seems a
+;; design without clone jobs might have been cleaner.)
;;
;; For 2- make sure ancestors are properly locked:
;;
@@ -101,27 +104,44 @@
;; when only "Require a." is retracted.
;;
;; The problem is solved with the 'coq-locked-ancestors property of
-;; spans that contain Require commands and with the
-;; coq--par-ancestor-files hash. Ancestors in the 'coq-locked-ancestors
-;; property are unlocked when this span is retracted. As locking is
-;; done eagerly (as soon as coqdep runs first on the file), I only
-;; have to make sure the right files appear in 'coq-locked-ancestors.
+;; spans that contain Require commands. Ancestors in the
+;; 'coq-locked-ancestors property are unlocked when this span is
+;; retracted. As locking is done eagerly (as soon as coqdep runs first
+;; on the file), I only have to make sure the right files appear in
+;; 'coq-locked-ancestors.
;;
-;; Ancestor files accumulate in compilation jobs when the compilation
-;; walks upwards the dependency tree. In the end, every top-level job
-;; contains a list of all its direct and indirect ancestors. Because
-;; of eager locking, all its ancestors are already locked, when a
-;; top-level job is about to be retired. The coq--par-ancestor-files
-;; hash therefore records whether some ancestor does already appear in
-;; the 'coq-locked-ancestors property of some span before the current
-;; one. If it doesn't, I store it in the current span.
+;; Ancestors accumulate in compilation jobs when the compilation walks
+;; upwards the dependency tree. In the end, every top-level job
+;; contains a list of all its direct and indirect ancestors in its
+;; 'ancestors property. Because of eager locking, all its ancestors
+;; are already locked, when a top-level job is about to be retired.
+;; Every job records in his 'locked propery whether the file
+;; corresponding to this job has been registered in some
+;; 'coq-locked-ancestors property already.
;;
;; For 3- error reporting:
;;
-;; For now, all compilation jobs are killed on the first error. All
-;; items that are not yet asserted are retract. This is done with
-;; signalling an error and calling `coq-par-emergency-cleanup' in the
-;; sentinel, if there was an error.
+;; Depending on `coq-compile-keep-going' compilation can continue
+;; after an error or stop immediately. For stopping immediately,
+;; processing is aborted with a signal that eventually leads to
+;; `coq-par-emergency-cleanup', which kills all compilation jobs,
+;; retracts the queue region and resets all internal data.
+;;
+;; For `coq-compile-keep-going', the failing job and all dependants
+;; are marked as 'failed. Queue dependants are marked with
+;; 'queue-failed. These marked jobs continue with their normal state
+;; transition, but omit certain steps (eg., running coqc). The first
+;; tricky part is how to unlock ancestors. When marking jobs as
+;; failed, their ancestors (and thereby also the files for the jobs
+;; themselves) are unlocked, unless they are still participating in an
+;; ongoing compilation. If a coqc compilation finishes and all
+;; dependants are marked as failed, ancestors are also unlocked in the
+;; same way. If a top-level job is marked as 'queue-failed, its
+;; ancestors are unlocked when this job finishes coqc compilation.
+;;
+;; The second tricky part is how to delete the queue region. For that
+;; the last top-level job is delayed until proof-action-list is empty.
+;; Then the whole queue is deleted.
;;
;; For 4- using -quick and the handling of .vo/.vio prerequisites
;;
@@ -185,13 +205,22 @@
;; 'src-file - the .v file name
;; 'load-path - value of coq-load-path, propagated to all
;; dependencies
-;; 'ancestor-files - list of ancestors, including the source file
-;; of this job
-;; 'require-span - present for top-level jobs only, there it
+;; 'ancestors - list of ancestor jobs, for real compilation jobs
+;; this list includes the job itself; may contain
+;; duplicates
+;; 'lock-state - nil for clone jobs, 'unlocked if the file
+;; corresponding to job is not locked, 'locked if that
+;; file has been locked, 'asserted if it has been
+;; registered in some span in the 'coq-locked-ancestors
+;; property already
+;; 'require-span - present precisely for top-level jobs only, there it
;; contains the span that must finally store the
;; ancestors
;; 'vio2vo-needed - t if a subsequent vio2vo process is required to
;; build the .vo file. Otherwiese nil.
+;; 'failed - t if coqdep or coqc for the job or one dependee failed.
+;; 'queue-failed - t if some direct or indirect queue dependee is
+;; marked 'failed
;; 'visited - used in the dependency cycle detection to mark
;; visited jobs
;;
@@ -244,16 +273,6 @@
;; the process is killed
;;
;;
-;; Symbols in the coq--par-ancestor-files hash
-;;
-;; This hash maps file names to symbols. A file is present in the
-;; hash, if it has been locked.
-;;
-;; 'locked - the file is not yet stored in the
-;; 'coq-locked-ancestors property of some span
-;; 'asserted - the file has been stored in some span
-;;
-;;
;; To print the states of the compilation jobs for debugging, eval
;;
;; (let ((clones))
@@ -283,22 +302,6 @@
;;; Variables
-(defvar coq--par-ancestor-files nil
- "Hash remembering the state of locked ancestor files.
-This hash maps true file names (in the sense of `file-truename')
-to either 'locked or 'asserted.
-
-'locked means that this ancestor file has been locked
-already (because it appeared in the dependency tree somewhere and
-coqdep has been started on it) but has not been assigned to the
-'coq-locked-ancestors property of some span. That is, 'locked
-ancestors are not an ancestor of any required module in the
-asserted region.
-
-'asserted means that this ancestor is the ancestor of some
-asserted required module (and is in some 'coq-locked-ancestors
-property).")
-
(defvar coq--current-background-jobs 0
"Number of currently running background jobs.")
@@ -321,10 +324,25 @@ Used to link top-level jobs with queue dependencies.")
(defvar coq--compile-vio2vo-delay-timer nil
"Holds the timer for the vio2vo delay.")
+(defvar coq--compile-vio2vo-start-id 0
+ "Integer counter to detect races for `coq-par-require-processed'.
+Assume compilation for the last top-level ``Require'' command
+finishes but executing the ``Require'' takes so long that the
+user can assert a next ``Require'' and that the second
+compilation finishes before the first ``Require'' has been
+processed. In this case there are two `coq-par-require-processed'
+callbacks active, of which the first one must be ignored. For
+each new callback this counter is incremented and when there is a
+difference the call to `coq-par-require-processed' is ignored.")
+
(defvar coq--par-next-id 1
"Increased for every job and process, to get unique job names.
The names are only used for debugging.")
+(defvar coq--par-delayed-last-job nil
+ "Inform the cycle detection that there is a delayed top-level job.
+If t, there is a delayed top-level job (for which the compilation failed).")
+
;;; utility functions
@@ -366,14 +384,6 @@ latter greater then everything else."
"(Re-)Initialize `coq--compilation-object-hash'."
(setq coq--compilation-object-hash (make-hash-table :test 'equal)))
-(defun coq-par-init-ancestor-hash ()
- "(Re-)Initialize `coq--par-ancestor-files'"
- (setq coq--par-ancestor-files (make-hash-table :test 'equal))
- (mapc
- (lambda (locked-anc)
- (puthash locked-anc 'asserted coq--par-ancestor-files))
- proof-included-files-list))
-
;;; generic queues
;; Standard implementation with two lists.
@@ -570,8 +580,8 @@ this buffer visible and returns a string.
This function does always return .vo dependencies, regardless of the
value of `coq-compile-quick'. If necessary, the conversion into .vio
files must be done elsewhere."
- (when coq--debug-auto-compilation
- (message "analyse coqdep output \"%s\"" output))
+ ;; (when coq--debug-auto-compilation
+ ;; (message "analyse coqdep output \"%s\"" output))
(if (or
(not (eq status 0))
(string-match coq-coqdep-error-regexp output))
@@ -707,7 +717,8 @@ function returns () if MODULE-ID comes from the standard library."
;;; manage background jobs
(defun coq-par-kill-all-processes ()
- "Kill all background coqc, coqdep or vio2vo compilation processes."
+ "Kill all background coqc, coqdep or vio2vo compilation processes.
+Return t if some process was killed."
;; need to first mark processes as killed, because delete process
;; starts running sentinels in the order processes terminated, so
;; after the first delete-process we see sentinentels of non-killed
@@ -731,40 +742,48 @@ function returns () if MODULE-ID comes from the standard library."
(get (process-get process 'coq-compilation-job) 'name)
(process-name process)))))
(process-list))
- (setq coq--current-background-jobs 0)))
+ (setq coq--current-background-jobs 0)
+ kill-needed))
-(defun coq-par-unlock-ancestors-on-error ()
+(defun coq-par-unlock-all-ancestors-on-error ()
"Unlock ancestors which are not in an asserted span.
Used for unlocking ancestors on compilation errors."
- (when coq--par-ancestor-files
- ;; nil e.g. when enabling on-the-fly compilation after processing imports.
+ (when coq--compilation-object-hash
(maphash
- (lambda (ancestor state)
- (when (eq state 'locked)
- (coq-unlock-ancestor ancestor)
- (puthash ancestor nil coq--par-ancestor-files)))
- coq--par-ancestor-files)))
+ (lambda (key job)
+ (when (eq (get job 'lock-state) 'locked)
+ (coq-unlock-ancestor (get job 'src-file))
+ (put job 'lock-state 'unlocked)))
+ coq--compilation-object-hash)))
(defun coq-par-emergency-cleanup ()
"Emergency cleanup for parallel background compilation.
Kills all processes, unlocks ancestors, clears the queue region
and resets the internal state."
- (when coq--debug-auto-compilation
- (message "emergency cleanup"))
- (coq-par-kill-all-processes)
- (setq coq-par-compilation-queue (coq-par-new-queue))
- (setq coq--last-compilation-job nil)
- (setq coq-par-vio2vo-queue (coq-par-new-queue))
- (setq coq--compile-vio2vo-in-progress nil)
- (when coq--compile-vio2vo-delay-timer
- (cancel-timer coq--compile-vio2vo-delay-timer))
- (when proof-action-list
- (setq proof-shell-interrupt-pending t))
- (coq-par-unlock-ancestors-on-error)
- (proof-release-lock)
- (proof-detach-queue)
- (setq proof-second-action-list-active nil)
- (coq-par-init-compilation-hash))
+ (interactive) ; needed for menu
+ (let (proc-killed was-busy)
+ (when coq--debug-auto-compilation
+ (message "emergency cleanup"))
+ (setq proc-killed (coq-par-kill-all-processes))
+ (when (and (boundp 'prover-was-busy)
+ (or proc-killed coq--last-compilation-job
+ coq--compile-vio2vo-in-progress
+ coq--compile-vio2vo-delay-timer))
+ (setq prover-was-busy t))
+ (setq coq-par-compilation-queue (coq-par-new-queue))
+ (setq coq--last-compilation-job nil)
+ (setq coq-par-vio2vo-queue (coq-par-new-queue))
+ (setq coq--compile-vio2vo-in-progress nil)
+ (when coq--compile-vio2vo-delay-timer
+ (cancel-timer coq--compile-vio2vo-delay-timer)
+ (setq coq--compile-vio2vo-delay-timer nil))
+ (coq-par-unlock-all-ancestors-on-error)
+ (when proof-action-list
+ (setq proof-shell-interrupt-pending t))
+ (proof-release-lock)
+ (proof-detach-queue)
+ (setq proof-second-action-list-active nil)
+ (coq-par-init-compilation-hash)))
(defun coq-par-process-filter (process output)
"Store output from coq background compilation."
@@ -855,6 +874,7 @@ errors are reported with an error message."
(setq coq--compile-vio2vo-in-progress nil)
(message "vio2vo compilation finished"))
(when (and
+ (not coq--par-delayed-last-job)
(eq coq--current-background-jobs 0)
coq--last-compilation-job)
(let ((cycle (coq-par-find-dependency-circle)))
@@ -891,15 +911,30 @@ errors are reported with an error message."
(length (cdr coq-par-vio2vo-queue)))))
(coq-par-start-jobs-until-full))
-(defun coq-par-require-processed (span)
- "Callback for `proof-action-list' to start vio2vo compilation.
-This callback is inserted with a dummy item after the last
-require command to start vio2vo compilation after
-`coq-compile-vio2vo-delay' seconds."
- (assert (not coq--last-compilation-job)
- nil "normal compilation and vio2vo in parallel 1")
- (setq coq--compile-vio2vo-delay-timer
- (run-at-time coq-compile-vio2vo-delay nil 'coq-par-run-vio2vo-queue)))
+(defun coq-par-require-processed (race-counter)
+ "Callback for `proof-action-list' to signal completion of the last require.
+This function ensures that vio2vo compilation starts after
+`coq-compile-vio2vo-delay' seconds after the last module has been
+loaded into Coq. When background compilation is successful, this
+callback is inserted with a dummy item into proof-action-list
+somewhere after the last require command."
+ ;; When the user asserts new stuff while the (previously) last
+ ;; require command is being processed, `coq--last-compilation-job'
+ ;; might get non-nil. In this case there is a new last compilation
+ ;; job that will eventually trigger vio2vo compilation.
+ (unless (or coq--last-compilation-job
+ (not (eq race-counter coq--compile-vio2vo-start-id)))
+ (setq coq--compile-vio2vo-delay-timer
+ (run-at-time coq-compile-vio2vo-delay nil
+ 'coq-par-run-vio2vo-queue))))
+
+(defun coq-par-callback-queue-item (callback)
+ ;; A proof-action-list item has the form of
+ ;; (SPAN COMMANDS ACTION [DISPLAYFLAGS])
+ ;; If COMMANDS is nil, the item is processed as comment and not sent
+ ;; to the proof assistant, only the callback is called, see
+ ;; proof-shell.el.
+ (list nil nil callback))
;;; background job tasks
@@ -1105,21 +1140,75 @@ therefore delete a file if it might be in the way. Sets the
(signal 'coq-compile-error-rm err))))
result))
+(defun coq-par-retire-top-level-job (job)
+ "Register ancestors and start queue items.
+This function performs the essential tasks for top-level jobs
+when they transition from 'waiting-queue to 'ready:
+- Registering ancestors in the span and recording this fact in
+ the 'lock-state property.
+- Moving queue items back to `proof-action-list' and start their
+ execution.
+- Insert `coq-par-require-processed' as callback if this is the
+ last top-level job, such that vio2vo compilation will start
+ eventually.
+
+This function can safely be called for non-top-level jobs. This
+function must not be called for failed jobs."
+ (assert (not (get job 'failed))
+ nil "coq-par-retire-top-level-job precondition failed")
+ (let ((span (get job 'require-span))
+ (items (get job 'queueitems)))
+ (when (and span coq-lock-ancestors)
+ (dolist (anc-job (get job 'ancestors))
+ (assert (not (eq (get anc-job 'lock-state) 'unlocked))
+ nil "bad ancestor lock state")
+ (when (eq (get anc-job 'lock-state) 'locked)
+ (put anc-job 'lock-state 'asserted)
+ (push (get anc-job 'src-file)
+ (span-property span 'coq-locked-ancestors)))))
+ (when items
+ (when (and (eq coq-compile-quick 'quick-and-vio2vo)
+ (eq coq--last-compilation-job job))
+ (let ((vio2vo-counter
+ (setq coq--compile-vio2vo-start-id
+ (1+ coq--compile-vio2vo-start-id))))
+ ;; Insert a notification callback for when the last require
+ ;; queue item has been processed.
+ (setq items
+ (cons
+ (car items) ; this is the require
+ (cons
+ (coq-par-callback-queue-item
+ `(lambda (span) (coq-par-require-processed ,vio2vo-counter)))
+ (cdr items))))))
+ (proof-add-to-queue items 'advancing)
+ (when coq--debug-auto-compilation
+ (message "%s: add %s items to action list"
+ (get job 'name) (length items)))
+ (put job 'queueitems nil))))
+
(defun coq-par-kickoff-queue-maybe (job)
"Try transition 'waiting-queue -> 'ready for job JOB.
This transition is only possible if JOB is in state
'waiting-queue and if it has no queue dependee. If this is the
case, the following actions are taken:
-- for top-level jobs (non-nil 'require-span property), ancestors
- are registered in `coq--par-ancestor-files' and in the span in
- 'queue-span
-- processing of items in 'queueitems is started
+- for successful top-level jobs (non-nil 'require-span property), ancestors
+ are registered in the 'queue-span and marked as 'asserted in their
+ 'lock-state property
+- processing of items in 'queueitems is started (if JOB is successful)
- a possible queue dependant gets it's dependency cleared, and,
if possible the 'waiting-queue -> 'ready transition
is (recursively) done for the dependant
- if this job is the last top-level compilation
job (`coq--last-compilation-job') then the last compilation job
- and `proof-second-action-list-active' are cleared."
+ and `proof-second-action-list-active' are cleared and vio2vo
+ processing is triggered.
+- If compilation failed, the (failing) last top-level job is
+ delayed until `proof-action-list' is empty, possibly by
+ registering this call as a callback in an empty
+ proof-action-list item. When proof-action-list is empty, the
+ queue span is deleted, remaining spans are cleared and the
+ `proof-shell-busy' lock is freed."
(if (or (not (eq (get job 'state) 'waiting-queue))
(get job 'queue-dependant-waiting))
(when coq--debug-auto-compilation
@@ -1131,62 +1220,61 @@ case, the following actions are taken:
(get job 'name))))
(when coq--debug-auto-compilation
(message "%s: has itself no queue dependency" (get job 'name)))
- (when (and (get job 'require-span) coq-lock-ancestors)
- (let ((span (get job 'require-span)))
- (dolist (f (get job 'ancestor-files))
- (unless (eq (gethash f coq--par-ancestor-files) 'asserted)
- (puthash f 'asserted coq--par-ancestor-files)
- (span-set-property
- span 'coq-locked-ancestors
- (cons f (span-property span 'coq-locked-ancestors)))))))
- (when (get job 'queueitems)
- (let ((items (get job 'queueitems)))
- (when (and (eq coq--last-compilation-job job)
- (eq coq-compile-quick 'quick-and-vio2vo))
- ;; Insert the vio2vo start notification callback after the
- ;; require item.
- (setq items
- (cons
- (car items)
- (cons
- ;; A proof-action-list item is
- ;; (SPAN COMMANDS ACTION [DISPLAYFLAGS])
- ;; If COMMANDS is nil, the item is processed as
- ;; comment and not sent to the proof assistant, see
- ;; proof-shell.el.
- (list nil nil 'coq-par-require-processed)
- (cdr items)))))
- (proof-add-to-queue items 'advancing)
- (when coq--debug-auto-compilation
- (message "%s: add %s items to action list"
- (get job 'name) (length items)))
- (put job 'queueitems nil)))
- (put job 'state 'ready)
- (when coq--debug-auto-compilation
- (message "%s: ready" (get job 'name)))
- (let ((dependant (get job 'queue-dependant)))
- (if dependant
- (progn
- (assert (not (eq coq--last-compilation-job job))
- nil "coq--last-compilation-job invariant error")
- (put dependant 'queue-dependant-waiting nil)
- (when coq--debug-auto-compilation
- (message
- "%s -> %s: clear queue dependency, kickoff queue at %s"
- (get job 'name) (get dependant 'name) (get dependant 'name)))
- (coq-par-kickoff-queue-maybe dependant)
+ (unless (get job 'failed)
+ (coq-par-retire-top-level-job job))
+ (when (and (get job 'failed) (get job 'require-span))
+ (setq coq--par-delayed-last-job nil))
+ (if (and (get job 'failed)
+ (eq coq--last-compilation-job job)
+ proof-action-list)
+ (progn
+ (when coq--debug-auto-compilation
+ (message "%s: delay queue kickoff until action list is empty"
+ (get job 'name)))
+ (setq coq--par-delayed-last-job t)
+ (proof-add-to-queue
+ (list (coq-par-callback-queue-item
+ `(lambda (span) (coq-par-kickoff-queue-maybe ',job))))
+ 'advancing))
+ (put job 'state 'ready)
+ (when coq--debug-auto-compilation
+ (message "%s: ready" (get job 'name)))
+ (let ((dependant (get job 'queue-dependant)))
+ (if dependant
+ (progn
+ (assert (not (eq coq--last-compilation-job job))
+ nil "coq--last-compilation-job invariant error")
+ (put dependant 'queue-dependant-waiting nil)
+ (when coq--debug-auto-compilation
+ (message
+ "%s -> %s: clear queue dependency, kickoff queue at %s"
+ (get job 'name) (get dependant 'name) (get dependant 'name)))
+ (coq-par-kickoff-queue-maybe dependant)
+ (when coq--debug-auto-compilation
+ (message "%s: queue kickoff finished"
+ (get job 'name))))
+ (when (eq coq--last-compilation-job job)
+ (when (get job 'failed)
+ ;; proof-action-list is empty, see above
+ ;; variables that hold the queue span are buffer local
+ (with-current-buffer (or proof-script-buffer (current-buffer))
+ (proof-script-clear-queue-spans-on-error nil))
+ (proof-release-lock)
+ (when (eq coq-compile-quick 'quick-and-vio2vo)
+ (assert (not coq--compile-vio2vo-delay-timer)
+ nil "vio2vo timer set before last compilation job")
+ (setq coq--compile-vio2vo-delay-timer
+ (run-at-time coq-compile-vio2vo-delay nil
+ 'coq-par-run-vio2vo-queue))))
+ (setq coq--last-compilation-job nil)
+ (setq proof-second-action-list-active nil)
(when coq--debug-auto-compilation
- (message "%s: queue kickoff finished"
- (get job 'name))))
- (when (eq coq--last-compilation-job job)
- (setq coq--last-compilation-job nil)
- (setq proof-second-action-list-active nil)
+ (message "clear last compilation job"))
+ (message "Library compilation %s"
+ (if (get job 'failed) "failed" "finished successfully")))
(when coq--debug-auto-compilation
- (message "clear last compilation job"))
- (message "Library compilation finished"))
- (when coq--debug-auto-compilation
- (message "%s: no queue dependant, queue kickoff finished"
- (get job 'name)))))))
+ (message "%s: no queue dependant, queue kickoff finished"
+ (get job 'name))))))))
(defun coq-par-compile-job-maybe (job)
"Choose next action for JOB after dependencies are ready.
@@ -1199,19 +1287,21 @@ JOB stays in 'enqueued-coqc for the time being. Otherwise, the
transition 'enqueued-coqc -> 'waiting-queue is done and, if
possible, also 'waiting-queue -> 'ready."
(put job 'state 'enqueued-coqc)
- ;; note that coq-par-job-needs-compilation sets 'required-obj-file
+ ;; Note that coq-par-job-needs-compilation sets 'required-obj-file
;; as a side effect and deletes .vo or .vio files that are in the way.
- ;; It also puts job into the vio2vo queue, if necessary.
- (if (coq-par-job-needs-compilation job)
+ ;; It also sets the 'vio2vo-needed property if needed.
+ (if (and (not (get job 'failed)) (coq-par-job-needs-compilation job))
(coq-par-start-or-enqueue job)
(when coq--debug-auto-compilation
- (message "%s: up-to-date, no compilation" (get job 'name)))
+ (message "%s: %s, no compilation"
+ (get job 'name)
+ (if (get job 'failed) "failed" "up-to-date")))
(when (get job 'vio2vo-needed)
(coq-par-vio2vo-enqueue job))
(coq-par-kickoff-coqc-dependants job (get job 'youngest-coqc-dependency))))
(defun coq-par-decrease-coqc-dependency (dependant dependee-time
- dependee-ancestor-files)
+ dependee-ancestors)
"Clear Coq dependency and update dependee information in DEPENDANT.
This function handles a Coq dependency from child dependee to
parent dependant when the dependee has finished compilation (ie.
@@ -1229,8 +1319,8 @@ For 'file jobs this is 'waiting-dep -> 'enqueued-coqc and for
(when (coq-par-time-less (get dependant 'youngest-coqc-dependency)
dependee-time)
(put dependant 'youngest-coqc-dependency dependee-time))
- (put dependant 'ancestor-files
- (append dependee-ancestor-files (get dependant 'ancestor-files)))
+ (put dependant 'ancestors
+ (append dependee-ancestors (get dependant 'ancestors)))
(put dependant 'coqc-dependency-count
(1- (get dependant 'coqc-dependency-count)))
(assert (<= 0 (get dependant 'coqc-dependency-count))
@@ -1258,7 +1348,8 @@ waiting-queue for JOB.
DEP-TIME is either 'just-compiled, when JOB has just finished
compilation, or the most recent modification time of all
-dependencies of JOB.
+dependencies of JOB. (If compilation for JOB failed, DEP-TIME is
+meaningless but should nevertheless be a non-nil valid argument.)
This function makes the following actions.
- Clear the dependency from JOB to all its dependants, thereby
@@ -1267,16 +1358,22 @@ This function makes the following actions.
- save the maximum of DEP-TIME and .vo modification time in
'youngest-coqc-dependency, in case we later create a clone of this job
- put JOB into state 'waiting-queue
-- try to trigger the transition 'waiting-queue -> ready for JOB"
- (let ((ancestor-files (get job 'ancestor-files)))
+- try to trigger the transition 'waiting-queue -> ready for JOB
+- If JOB is successful but all dependants have failed, unlock all
+ ancestors in case they are not participating in a still ongoing
+ compilation."
+ (let ((ancestors (get job 'ancestors))
+ (dependant-alive nil))
+ (put job 'state 'waiting-queue)
;; take max of dep-time and obj-mod-time
;;
;; dep-time is either 'just-compiled or 'youngest-coqc-dependency of
;; the dependee, in the latter case obj-mod-time is greater than
;; dep-time, because otherwise we would have compiled the file. For
;; a clone job the max has already been taken when processing the
- ;; original file.
- (unless (or (eq dep-time 'just-compiled) (eq (get job 'type) 'clone))
+ ;; original file. If coqdep failed, 'obj-mod-time is not set.
+ (unless (or (eq dep-time 'just-compiled) (eq (get job 'type) 'clone)
+ (get job 'failed))
(setq dep-time (get job 'obj-mod-time)))
(put job 'youngest-coqc-dependency dep-time)
(when coq--debug-auto-compilation
@@ -1285,33 +1382,45 @@ This function makes the following actions.
(if (eq dep-time 'just-compiled)
'just-compiled
(current-time-string dep-time))))
- (put job 'state 'waiting-queue)
- (mapc
- (lambda (dependant)
- (coq-par-decrease-coqc-dependency dependant dep-time ancestor-files))
- (get job 'coqc-dependants))
+ (dolist (dependant (get job 'coqc-dependants))
+ (coq-par-decrease-coqc-dependency dependant dep-time ancestors)
+ (unless (get dependant 'failed)
+ (setq dependant-alive t)))
(when coq--debug-auto-compilation
- (message "%s: coqc kickoff finished, maybe kickoff queue"
- (get job 'name)))
+ (message (concat "%s: coqc kickoff finished, %s dependant alive, "
+ "maybe kickoff queue")
+ (get job 'name)
+ (if dependant-alive "some" "no")))
+ (assert (or (not (get job 'failed)) (not dependant-alive))
+ nil "failed job with non-failing dependant")
+ (when (or (and (not dependant-alive)
+ (not (get job 'require-span))
+ (not (get job 'failed)))
+ (and (get job 'queue-failed) (not (get job 'failed))))
+ ;; job has not failed, but all dependants have 'failed set, or
+ ;; top-level job marked with 'queue-failed changes to 'failed
+ (when (get job 'queue-failed)
+ (when coq--debug-auto-compilation
+ (message "%s: queue-failed -> failed, unlock ancestors"
+ (get job 'name)))
+ (put job 'failed t))
+ (coq-par-unlock-job-ancestors-on-error job))
(coq-par-kickoff-queue-maybe job)))
(defun coq-par-start-coqdep (job)
"Start coqdep for JOB.
-Besides starting the background process, the source file is
-locked, registered in the 'ancestor-files property of JOB and in
-`coq--par-ancestor-files'"
- (let ((true-src (file-truename (get job 'src-file))))
- (when coq-lock-ancestors
- (proof-register-possibly-new-processed-file true-src)
- (put job 'ancestor-files (list true-src))
- (unless (gethash true-src coq--par-ancestor-files)
- (puthash true-src 'locked coq--par-ancestor-files)))
- (coq-par-start-process
- coq-dependency-analyzer
- (coq-par-coqdep-arguments (get job 'src-file) (get job 'load-path))
- 'coq-par-process-coqdep-result
- job
- nil)))
+Lock the source file and start the coqdep background process"
+ (when (and coq-lock-ancestors
+ (eq (get job 'lock-state) 'unlocked))
+ (proof-register-possibly-new-processed-file (get job 'src-file))
+ (push job (get job 'ancestors))
+ (put job 'lock-state 'locked))
+ (coq-par-start-process
+ coq-dependency-analyzer
+ (coq-par-coqdep-arguments (get job 'src-file) (get job 'load-path))
+ 'coq-par-process-coqdep-result
+ job
+ nil))
(defun coq-par-start-vio2vo (job)
"Start vio2vo background job."
@@ -1431,7 +1540,7 @@ This function returns the newly created job."
(put new-job 'state 'ready))
(put new-job 'youngest-coqc-dependency
(get orig-job 'youngest-coqc-dependency))
- (put new-job 'ancestor-files (get orig-job 'ancestor-files)))
+ (put new-job 'ancestors (get orig-job 'ancestors)))
(coq-par-add-coqc-dependency orig-job new-job)
(put new-job 'state 'waiting-dep)
(put new-job 'youngest-coqc-dependency '(0 0))))
@@ -1443,32 +1552,131 @@ This function returns the newly created job."
(buffer-file-name proof-script-buffer))
(signal 'coq-compile-error-circular-dep
(concat dependant " -> scripting buffer")))
- (message "Check %s" (get new-job 'src-file))
(put new-job 'load-path coq-load-path)
(put new-job 'youngest-coqc-dependency '(0 0))
(puthash module-vo-file new-job coq--compilation-object-hash)
(when coq--debug-auto-compilation
(message "%s: create %s compilation for %s"
(get new-job 'name) (get new-job 'type) module-vo-file))
+ (if (member (file-truename (get new-job 'src-file))
+ proof-included-files-list)
+ (put new-job 'lock-state 'asserted)
+ (put new-job 'lock-state 'unlocked))
(when queue-dep
(coq-par-add-queue-dependency queue-dep new-job))
+ (message "Check %s" (get new-job 'src-file))
(coq-par-start-or-enqueue new-job))
new-job))
+(defun coq-par-ongoing-compilation (job)
+ "Determine if the source file for JOB needs to stay looked.
+Return t if job has a direct or indirect dependant that has not
+failed yet and that is in a state before 'waiting-queue. Also,
+return t if JOB has a dependant that is a top-level job which has
+not yet failed."
+ (assert (not (eq (get job 'lock-state) 'asserted))
+ nil "coq-par-ongoing-compilation precondition failed")
+ (cond
+ ((get job 'failed)
+ nil)
+ ((or (eq (get job 'state) 'waiting-dep)
+ (eq (get job 'state) 'enqueued-coqc)
+ ;; top-level job that has compilation finished but has not
+ ;; been asserted yet
+ (and (eq (get job 'state) 'waiting-queue) (get job 'require-span))
+ ;; Note that job cannot be a top-level in state 'ready,
+ ;; because we started from job with 'lock-state property equal
+ ;; to 'locked. Top-level job in state 'ready have all
+ ;; dependees with 'lock-state equal to 'asserted.
+ )
+ t)
+ ;; Note that non-top-level jobs switch to 'waiting-queue as soon as
+ ;; all dependencies are ready, before they start to deal with the
+ ;; ancestors. We might therefore see here non-top-level jobs in
+ ;; state 'waiting-queue: they have successfully finished their
+ ;; compilation and are about to go to state 'ready.
+ ((or (eq (get job 'state) 'ready)
+ (eq (get job 'state) 'waiting-queue))
+ ;; internal ready job
+ (let ((dependants (get job 'coqc-dependants))
+ (res nil)
+ dep)
+ (while (and (not res) (setq dep (pop dependants)))
+ (setq res (coq-par-ongoing-compilation dep)))
+ res))
+ (t
+ (assert nil nil
+ "impossible ancestor state %s on job %s"
+ (get job 'state) (get job 'name)))))
+
+(defun coq-par-unlock-job-ancestors-on-error (job)
+ "Unlock those ancestors of JOB that need to be unlocked.
+For a failing job JOB, an ancestor need to stay looked if there
+is still some compilation going on for which this ancestor is a
+dependee or if a top level job with JOB as ancestor has finished
+it's compilation successfully. In all other cases the ancestor
+must be unlocked."
+ (dolist (anc-job (get job 'ancestors))
+ (when (and (eq (get anc-job 'lock-state) 'locked)
+ (not (coq-par-ongoing-compilation anc-job)))
+ (when coq--debug-auto-compilation
+ (message "%s: %s unlock because no ongoing compilation"
+ (get anc-job 'name) (get anc-job 'src-file)))
+ (coq-unlock-ancestor (get anc-job 'src-file))
+ (put anc-job 'lock-state 'unlocked))))
+
+(defun coq-par-mark-queue-failing (job)
+ "Mark JOB with 'queue-failed.
+Mark JOB with 'queue-failed, and, if JOB is in state
+'waiting-queue, transition to 'failed and unlock ancestors as
+appropriate."
+ (unless (or (get job 'failed) (get job 'queue-failed))
+ (put job 'queue-failed t)
+ (assert (not (eq (get job 'state) 'ready))
+ nil "coq-par-mark-queue-failing impossible state")
+ (when coq--debug-auto-compilation
+ (message "%s: mark as queue-failed, %s"
+ (get job 'name)
+ (if (eq (get job 'state) 'waiting-queue)
+ "failed, and unlock ancestors"
+ "wait")))
+ (when (eq (get job 'state) 'waiting-queue)
+ (put job 'failed t)
+ (coq-par-unlock-job-ancestors-on-error job))
+ (when (get job 'queue-dependant)
+ (coq-par-mark-queue-failing (get job 'queue-dependant)))))
+
+(defun coq-par-mark-job-failing (job)
+ "Mark all dependants of JOB as failing and unlock ancestors as appropriate.
+Set the 'failed property on all direct and indirect dependants of
+JOB. Along the way, unlock ancestors as determined by
+`coq-par-ongoing-compilation'. Mark queue dependants with
+'queue-failed."
+ (unless (get job 'failed)
+ (put job 'failed t)
+ (when coq--debug-auto-compilation
+ (message "%s: mark as failed and unlock free ancestors" (get job 'name)))
+ (coq-par-unlock-job-ancestors-on-error job)
+ (dolist (dependant (get job 'coqc-dependants))
+ (coq-par-mark-job-failing dependant))
+ (when (get job 'queue-dependant)
+ (coq-par-mark-queue-failing (get job 'queue-dependant)))))
+
(defun coq-par-process-coqdep-result (process exit-status)
"Coqdep continuation function: Process coqdep output.
-This function analyses the coqdep output of PROCESS and signals
-an error if necessary. If there was no coqdep error, the
-following actions are taken.
+This function analyses the coqdep output of PROCESS. In case of
+error, the job is marked as failed or compilation is aborted via
+a signal (depending on `coq-compile-keep-going'). If there was no
+coqdep error, the following actions are taken.
- the job that started PROCESS is put into sate 'waiting-dep
- a new job is created for every dependency. If this new job is
not immediately ready, a Coq dependency is registered from the
new job to the current job. For dependencies that are 'ready
already, the most recent ancestor modification time is
propagated.
-- if there are no dependencies or all dependencies are ready
- already, the next transition to 'enqueued-coqc is triggered for
- the current job
+- if there are no dependencies (especially if coqdep failed) or
+ all dependencies are ready already, the next transition to
+ 'enqueued-coqc is triggered for the current job
- otherwise the current job is left alone until somebody
decreases its dependency count to 0
@@ -1482,55 +1690,65 @@ is directly passed to `coq-par-analyse-coq-dep-exit'."
(process-get process 'coq-process-command)))
job-max-time)
(if (stringp dependencies-or-error)
- (signal 'coq-compile-error-coqdep (get job 'src-file))
+ (if coq-compile-keep-going
+ (coq-par-mark-job-failing job)
+ (signal 'coq-compile-error-coqdep (get job 'src-file)))
;; no coqdep error -- work on dependencies
(when coq--debug-auto-compilation
(message "%s: dependencies of %s are %s"
(get job 'name) (get job 'src-file) dependencies-or-error))
- (put job 'state 'waiting-dep)
(setq job-max-time (get job 'youngest-coqc-dependency))
- (mapc
- (lambda (dep-vo-file)
- (unless (coq-compile-ignore-file dep-vo-file)
- (let* ((dep-job (coq-par-create-library-job dep-vo-file
- (get job 'load-path)
- nil nil
- (get job 'src-file)))
- (dep-time (get dep-job 'youngest-coqc-dependency)))
- (when (coq-par-time-less job-max-time dep-time)
- (setq job-max-time dep-time))
- (unless (coq-par-job-coqc-finished dep-job)
- (coq-par-add-coqc-dependency dep-job job)))))
- dependencies-or-error)
- (put job 'youngest-coqc-dependency job-max-time)
- (if (coq-par-dependencies-ready job)
- (progn
- (when coq--debug-auto-compilation
- (message "%s: coqc dependencies finished" (get job 'name)))
- (coq-par-compile-job-maybe job))
- (when coq--debug-auto-compilation
- (message "%s: wait for %d dependencies"
- (get job 'name) (get job 'coqc-dependency-count)))))))
+ (dolist (dep-vo-file dependencies-or-error)
+ (unless (coq-compile-ignore-file dep-vo-file)
+ (let* ((dep-job (coq-par-create-library-job dep-vo-file
+ (get job 'load-path)
+ nil nil
+ (get job 'src-file)))
+ (dep-time (get dep-job 'youngest-coqc-dependency)))
+ (when (coq-par-time-less job-max-time dep-time)
+ (setq job-max-time dep-time))
+ (unless (coq-par-job-coqc-finished dep-job)
+ (coq-par-add-coqc-dependency dep-job job)))))
+ (put job 'youngest-coqc-dependency job-max-time))
+ ;; common part for job where coqdep was successful and where
+ ;; coqdep failed (when coq-compile-keep-going)
+ (put job 'state 'waiting-dep)
+ (if (coq-par-dependencies-ready job)
+ (progn
+ (when coq--debug-auto-compilation
+ (message "%s: coqc dependencies finished" (get job 'name)))
+ (coq-par-compile-job-maybe job))
+ (when coq--debug-auto-compilation
+ (message "%s: wait for %d dependencies"
+ (get job 'name) (get job 'coqc-dependency-count))))))
(defun coq-par-coqc-continuation (process exit-status)
"Coqc continuation function.
-Signal an error, if coqc failed. Otherwise, trigger the
-transition 'enqueued-coqc -> 'waiting-queue for the job behind
-PROCESS."
- (if (eq exit-status 0)
- (let ((job (process-get process 'coq-compilation-job)))
- ;; coqc success
- (when (get job 'vio2vo-needed)
- (coq-par-vio2vo-enqueue job))
- (coq-par-kickoff-coqc-dependants job 'just-compiled))
- ;; coqc error
- (coq-compile-display-error
- (mapconcat 'identity (process-get process 'coq-process-command) " ")
- (process-get process 'coq-process-output)
- t)
- (signal 'coq-compile-error-coqc
- (get (process-get process 'coq-compilation-job) 'src-file))))
+If coqc failed, signal an error or mark the job as 'failed, and
+unlock ancestors as appropriate. If coqc was successful, trigger
+the transition 'enqueued-coqc -> 'waiting-queue for the job
+behind PROCESS."
+ (let ((job (process-get process 'coq-compilation-job)))
+ (if (eq exit-status 0)
+ (progn
+ ;; coqc success
+ (when (get job 'vio2vo-needed)
+ (coq-par-vio2vo-enqueue job))
+ (coq-par-kickoff-coqc-dependants job 'just-compiled))
+ ;; coqc error
+ (coq-compile-display-error
+ (mapconcat 'identity (process-get process 'coq-process-command) " ")
+ (process-get process 'coq-process-output)
+ t)
+ (if coq-compile-keep-going
+ (progn
+ (coq-par-mark-job-failing job)
+ (coq-par-kickoff-coqc-dependants
+ job
+ (get job 'youngest-coqc-dependency)))
+ (signal 'coq-compile-error-coqc
+ (get (process-get process 'coq-compilation-job) 'src-file))))))
(defun coq-par-vio2vo-continuation (process exit-status)
"vio2vo continuation function."
@@ -1688,7 +1906,6 @@ the maximal number of background compilation jobs is started."
(length queueitems)))
(unless coq--last-compilation-job
(coq-par-init-compilation-hash)
- (coq-par-init-ancestor-hash)
(coq-init-compile-response-buffer))
(let ((splitted-items
(split-list-at-predicate queueitems
@@ -1711,7 +1928,8 @@ the maximal number of background compilation jobs is started."
;; notification
(when (cdr splitted-items)
(when coq--compile-vio2vo-delay-timer
- (cancel-timer coq--compile-vio2vo-delay-timer))
+ (cancel-timer coq--compile-vio2vo-delay-timer)
+ (setq coq--compile-vio2vo-delay-timer nil))
(when coq--compile-vio2vo-in-progress
(assert (not coq--last-compilation-job)
nil "normal compilation and vio2vo in parallel 2")
@@ -1720,9 +1938,8 @@ the maximal number of background compilation jobs is started."
(setq coq--compile-vio2vo-in-progress nil))
;; save buffers before invoking the first coqdep
(coq-compile-save-some-buffers)
- (mapc (lambda (require-items)
- (coq-par-handle-require-list require-items))
- (cdr splitted-items)))
+ (dolist (require-items (cdr splitted-items))
+ (coq-par-handle-require-list require-items)))
(when coq--last-compilation-job
(setq proof-second-action-list-active t))
(coq-par-start-jobs-until-full)
@@ -1752,6 +1969,9 @@ does the error checking/reporting for
"Proof General; please customize coq-pinned-version"))
(message "%s \"%s\"; consider customizing coq-pinned-version"
(get (car err) 'error-message) (cdr err))))
+ (file-error
+ (coq-par-emergency-cleanup)
+ (message "Error: %s" (mapconcat 'identity (cdr err) ": ")))
(error
(message "unexpected error during parallel compilation: %s"
err)