branch: externals/futur
commit 4ac3b6cc7e0de25f40b24185909d7b7b7a6d29f8
Author: Stefan Monnier <[email protected]>
Commit: Stefan Monnier <[email protected]>
futur.el, futur-tests.el: New files
---
futur-tests.el | 131 ++++++++++++
futur.el | 662 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 793 insertions(+)
diff --git a/futur-tests.el b/futur-tests.el
new file mode 100644
index 0000000000..735e23fdea
--- /dev/null
+++ b/futur-tests.el
@@ -0,0 +1,131 @@
+;;; futur-tests.el --- Tests for the Futur library -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2024 Stefan Monnier
+
+;; Author: Stefan Monnier <[email protected]>
+;; Keywords:
+
+;; 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 'futur)
+
+(ert-deftest futur--resignal ()
+ (let ((err1 (list 'error "hello")))
+ (should (eq err1 (condition-case err2
+ (progn (futur--resignal err1) nil)
+ (error err2))))))
+
+(ert-deftest futur-simple ()
+ (should (equal (futur-blocking-wait-to-get-result (futur-done 5)) 5))
+
+ (let ((p (futur-error '(scan-error "Oops"))))
+ (should-error (futur-blocking-wait-to-get-result p) :type 'scan-error))
+
+ (should (equal
+ (futur-blocking-wait-to-get-result
+ (futur-let* ((x1 5)
+ (x2 <- (futur-done 7)))
+ (+ x1 x2)))
+ 12))
+
+ (let ((p (futur-let* ((x1 5)
+ (x2 <- (futur-error '(scan-error "Oops"))))
+ (list (+ x1 x2) (error "Wow!")))))
+ (should-error (futur-blocking-wait-to-get-result p) :type 'scan-error)))
+
+(ert-deftest futur-timeout ()
+ (let* ((x '())
+ (_timer1 (run-with-timer 0.1 nil (lambda () (push 'timer1 x))))
+ (_timer2 (run-with-timer 0.3 nil (lambda () (push 'timer2 x))))
+ (futur (futur-let* ((_ <- (futur-timeout 0.2))
+ (x1 x)
+ (_ <- (futur-timeout 0.2))
+ (x2 x))
+ (list x1 x2)))
+ (res (futur-blocking-wait-to-get-result futur)))
+ (should (equal res '((timer1) (timer2 timer1))))))
+
+(ert-deftest futur-list ()
+ (let* ((x '())
+ (_timer1 (run-with-timer 0.1 nil (lambda () (push 'timer1 x))))
+ (_timer2 (run-with-timer 0.6 nil (lambda () (push 'timer2 x))))
+ (futur (futur-list
+ (futur-let* ((_ <- (futur-timeout 0.4))) (cons 'a x))
+ (futur-let* ((_ <- (futur-timeout 0.3))) (cons 'b x))))
+ (res (futur-blocking-wait-to-get-result futur)))
+ (should (equal res '((a timer1) (b timer1))))))
+
+(ert-deftest futur-abort ()
+ (let* ((x '())
+ (start (float-time))
+ (timescale 0.2)
+ (_fut1 (futur-let* ((_ <- (futur-timeout (* timescale 1))))
+ (push 'timer1 x)))
+ (fut6 (futur-let* ((_ <- (futur-timeout (* timescale 6))))
+ (push 'timer6 x)))
+ (fut2 (futur-timeout (* timescale 2)))
+ (fut4 (futur-timeout (* timescale 4)))
+ (fut22 nil)
+ (futA (futur-list
+ (futur-let*
+ ((_ <- (futur-list fut2 (futur-timeout (* timescale 2))))
+ (_ (setq fut22 (futur-timeout (* timescale 2))))
+ (_ <- (futur-list fut4 fut22)))
+ (cons 'a x))
+ (futur-let* ((_ <- (futur-timeout (* timescale 3))))
+ (signal 'error (cons "" x)))))
+ (futB (futur-list fut4)))
+ (should (equal x ()))
+ (should (equal (condition-case err
+ (progn (futur-blocking-wait-to-get-result futA) t)
+ (error err))
+ '(error "" timer1)))
+ (should (equal x '(timer1)))
+ (should (< (- (float-time) start) (* timescale 4)))
+ (should (pcase fut6 ((futur-waiting _) t)))
+ (should (pcase fut2 ((futur-done 'nil) t)))
+ (should (pcase fut22 ((futur-error '(futur-aborted)) t)))
+ (should (pcase fut4 ((futur-waiting _) t) ((futur-done 'nil) t)))
+ (should (pcase futB ((futur-waiting _) t)))
+ (should (equal '(nil) (futur-blocking-wait-to-get-result futB)))
+ (should (equal x '(timer1)))))
+
+(ert-deftest futur-process ()
+ (with-temp-buffer
+ (let* ((tmpfile (make-temp-file "futur"))
+ (buf (current-buffer))
+ (_ (write-region "Emacs" nil tmpfile nil 'silent))
+ (futur
+ (futur-let* ((exitcode
+ <- (futur-process-make
+ :name "futur-hexl"
+ :command (list (expand-file-name
+ "hexl" exec-directory)
+ tmpfile)
+ :buffer buf)))
+ (list exitcode
+ (with-current-buffer buf
+ (buffer-string)))))
+ (res (futur-blocking-wait-to-get-result futur)))
+ (should (equal res
+ '(0 "00000000: 456d 6163 73
Emacs\n"))))))
+
+(provide 'futur-tests)
+;;; futur-tests.el ends here
diff --git a/futur.el b/futur.el
new file mode 100644
index 0000000000..98ec8b9f0d
--- /dev/null
+++ b/futur.el
@@ -0,0 +1,662 @@
+;;; futur.el --- Future/promise-based async library -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2022 Stefan Monnier
+
+;; Author: Stefan Monnier <[email protected]>
+;; Keywords:
+
+;; 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:
+
+;; A library to try and make async programming a bit easier.
+;; This is inspired from Javscript's async/await, Haskell's monads,
+;; and ConcurrentML's events.
+
+;; You can create trivial futures with `futur-done'.
+;; You can create a "process future" with `futur-process-make'.
+;; And the main way to use futures is to compose them with `futur-let*',
+;; which can be used as follows:
+
+;; (futur-let*
+;; ((cmd (build-arg-list))
+;; (exitcode <- (futur-process-make :command cmd :buffer t))
+;; (out (buffer-string)) ;; Get the process's output.
+;; (cmd2 (build-second-arg-list exitcode out))
+;; (otherexit <- (futur-process-make :command cmd :buffer t)))
+;; (buffer-string))
+
+;; This example builds a future which runs two commands in sequence.
+;; For those rare cases where you really do want to block everything
+;; else and wait for a future to complete, you can
+;; use`futur-blocking-wait-to-get-result'.
+
+;; New kinds of futures can be constructed from:
+;; - `futur-waiting' to create the actual future.
+;; - `futur-deliver-value' to deliver the value to the future created earlier
+;; with `futur-waiting'.
+
+;;; Code:
+
+;; TODO:
+;; - Handle exceptions.
+
+(require 'cl-lib)
+
+(defvar futur--pending () "List of pending operations.")
+(defvar futur--pending-r ()
+ "List of additional pending operations in reverse-order.")
+
+(defconst futur--pending-mutex (make-mutex "futur-pending"))
+(defconst futur--pending-condition
+ (make-condition-variable futur--pending-mutex))
+
+(defvar futur--in-background nil)
+
+(defun futur--background ()
+ (let ((futur--in-background t))
+ (while t
+ (let ((pending
+ (with-mutex futur--pending-mutex
+ (while (and (null futur--pending)
+ (or (null futur--pending-r)
+ (progn
+ (setq futur--pending
+ (nreverse futur--pending-r))
+ (setq futur--pending-r nil)
+ nil)))
+ (condition-wait futur--pending-condition))
+ (pop futur--pending))))
+ (with-demoted-errors "future--background: %S"
+ (apply pending))))))
+
+(defconst futur--background
+ (make-thread #'futur--background "futur--background" 'silently))
+
+(defun futur--funcall (&rest args)
+ "Call ARGS like `funcall' but outside of the current dynamic scope.
+The code is conceptually run in another thread and while we try to run as
+soon as possible, and fairly, we do not guarantee the specific
+time or order of execution."
+ (with-mutex futur--pending-mutex
+ (push args futur--pending-r)
+ ;; FIXME: Maybe we should have combination
+ ;; `mutex-unlock+condition-notify', i.e. a variant of
+ ;; `condition-notify' which doesn't regrab the lock?
+ (condition-notify futur--pending-condition)))
+
+(defvar futur--idle-loop-bug80286
+ ;; "Idle loop" thread to try and make sure we run timers, filters, etc...
+ ;; Seems to give me assertion errors:
+ ;;
+ ;; process.c:5174: Emacs fatal error: assertion failed: XTHREAD
(ps->thread) == current_thread
+ ;; [Switching to Thread 0x7fffe186d6c0 (LWP 3046715)]
+ ;;
+ ;; Thread 8 "futur-idle-loop" hit Breakpoint 1, terminate_due_to_signal (
+ ;; sig=sig@entry=6, backtrace_limit=backtrace_limit@entry=2147483647)
+ ;; at emacs.c:445
+ ;;
+ ;;(when (fboundp 'make-thread)
+ ;; (make-thread
+ ;; (lambda ()
+ ;; (while t (accept-process-output nil (* 60 60 24))))
+ ;; "futur-idle-loop"))
+ nil)
+
+;;;; The `futur' data structure
+
+(cl-defstruct (futur
+ (:conc-name futur--)
+ (:noinline t)
+ (:predicate futur--p)
+ (:constructor nil)
+ (:constructor futur--done (value &aux (clients 't))
+ "Return a new `futur' that just returns VALUE.")
+ (:constructor futur--error (value &aux (clients 'error))
+ "Return a new `futur' that signals error VALUE")
+ (:constructor futur--waiting (&optional blocker clients
+ &aux (value blocker))
+ "Return a new `futur' that's waiting for BLOCKER."))
+ "A promise/future.
+A futur has 3 possible states:
+- (futur-done VAL): in that state, `clients' is `t', and `value' holds VAL.
+- (futur-error ERR): in that state, `clients' is `error', and `value' holds
ERR.
+- (futur-waiting BLOCKER CLIENTS): in that state, `clients' is a list
+ of \"callbacks\" waiting for the value or the error, and `value' holds
+ the BLOCKER that will deliver the value (can be another future,
+ a process, a thread, a list (of futures), or possibly other objects
+ with a `futur-blocker-wait' method)."
+ (clients nil)
+ (value nil))
+
+(pcase-defmacro futur-done (result)
+ `(and (pred futur--p)
+ (app futur--clients 't)
+ (app futur--value ,result)))
+
+(pcase-defmacro futur-error (error-object)
+ `(and (pred futur--p)
+ (app futur--clients 'error)
+ (app futur--value ,error-object)))
+
+(pcase-defmacro futur-waiting (&optional blocker clients)
+ `(and (pred futur--p)
+ (app futur--clients (and (pred listp) ,(or clients '_)))
+ (app futur--value ,(or blocker '_))))
+
+(defun futur--waiting-p (futur)
+ (pcase futur ((or (futur-waiting)
+ ;; Tell Pcase to presume FUTUR *is* a futur.
+ (and (pred (not futur--p)) pcase--dontcare))
+ t)))
+
+(define-inline futur--blocker (futur)
+ "Pseudo-slot for a waiting FUTUR."
+ (inline-letevals (futur)
+ (inline-quote (progn (cl-assert (futur--waiting-p ,futur))
+ (futur--value ,futur)))))
+
+(defun futur--deliver (futur err val)
+ (pcase-exhaustive futur
+ ((futur-waiting _ clients)
+ (setf (futur--clients futur) (if err 'error t))
+ (setf (futur--value futur) (or err val))
+ ;; CLIENTS is usually in reverse order since we always `push' to them.
+ (dolist (client (nreverse clients))
+ ;; Don't run the clients directly from here, so we don't nest,
+ ;; and also because we may be in an "interrupt" context where
+ ;; operations like blocking could be dangerous.
+ (futur--funcall client err val)))
+ ((pred futur--p)
+ (error "Delivering a second time: %S %S %S" futur err val))))
+
+(defun futur-deliver-value (futur val)
+ "Announce completion of FUTUR with result VAL."
+ (futur--deliver futur nil val))
+
+(defun futur-deliver-failure (futur error)
+ "Announce that computation of FUTUR encountered an ERROR."
+ (futur--deliver futur error nil))
+
+(defun futur-done (val)
+ "Build a trivial `futur' which returns VAL."
+ (futur--done val))
+
+(defun futur-error (error-object)
+ "Build a trivial `futur' which just signals ERROR-OBJECT."
+ (futur--error error-object))
+
+(defun futur-new (builder)
+ "Build a future.
+BUILDER is a function that will be called with one argument
+\(the new `futur' object, not yet fully initialized) and it should
+return the object on which the future is waiting.
+The code creating this future needs to call `futur-deliver-value'
+when the object has done the needed work.
+The object can be any object for which there is a `futur-blocker-wait' method."
+ (let* ((f (futur--waiting))
+ (x (funcall builder f)))
+ (cl-assert (null (futur--blocker f)))
+ (setf (futur--blocker f) x)
+ f))
+
+(defun futur-abort (futur)
+ "Interrupt execution of FUTUR, marking it as having failed.
+The error is `futur-aborted'. Does nothing if FUTUR was already complete."
+ (pcase futur
+ ((futur-waiting blocker)
+ (let ((error (list 'futur-aborted)))
+ (futur-blocker-abort blocker error)
+ (futur-deliver-failure futur error)))
+ (_ nil))) ;; No point in throwing away the result we already got.
+
+;;;; Composing futures
+
+(defun futur-register-callback (futur fun)
+ "Call FUN when FUTUR completes.
+Calls it with two arguments (ERR VAL), where only one of the two is non-nil,
+and throws away the return value. If FUTUR fails ERR is the error object,
+otherwise ERR is nil and VAL is the result value.
+When FUN is called, FUTUR is already marked as completed.
+If FUTUR already completed, FUN is called immediately."
+ (pcase futur
+ ((futur-waiting _ clients)
+ (setf (futur--clients futur) (cons fun clients)))
+ ((futur-error err) (funcall fun err nil))
+ ((futur-done val) (funcall fun nil val)))
+ nil)
+
+(defun futur-ize (val)
+ "Make sure VAL is a `futur'. If not, make it a trivial one that returns
VAL."
+ (if (futur--p val) val (futur--done val)))
+
+(defun futur-bind (futur fun &optional error-fun)
+ "Build a new future by composition.
+That future calls FUN with the return value of FUTUR and returns
+the same value as the future returned by FUN.
+If ERROR-FUN is non-nil, it should be a function that will be called instead of
+FUN when FUTUR fails. It is called with a single argument (the error object).
+By default any error in FUTUR is propagated to the returned future."
+ ;; This should behave like:
+ ;;
+ ;; (let ((new (futur--waiting futur)))
+ ;; (futur-register-callback futur
+ ;; (lambda (err val)
+ ;; (if err (futur-deliver-failure new err)
+ ;; (futur--run-continuation new fun (list val)))))
+ ;; new)
+ ;;
+ ;; But we try to skip the `new' futur if `futur' is already completed.
+ (pcase-exhaustive futur
+ ((futur-waiting _ clients)
+ (let ((new (futur--waiting futur)))
+ (setf (futur--clients futur)
+ (cons
+ (lambda (err val)
+ (if err (futur-deliver-failure new err)
+ (futur--run-continuation new fun (list val))))
+ clients))
+ new))
+ ((and (futur-error _) (guard (null error-fun))) futur)
+ ((or (futur-done value) (futur-error err1))
+ (condition-case-unless-debug err2
+ (let ((res (if err1 (funcall error-fun err1) (funcall fun value))))
+ (futur-ize res))
+ (t (futur-error err2))))))
+
+(defun futur--run-continuation (futur fun args)
+ ;; The thing FUTUR was waiting for is completed, maybe we'll soon be waiting
+ ;; for another future, but for now, we're waiting for some piece of ELisp
+ ;; (namely FUN) to terminate.
+ (setf (futur--blocker futur) 'elisp)
+ (condition-case-unless-debug err
+ (let ((res (apply fun args)))
+ (if (not (futur--p res))
+ (futur-deliver-value futur res)
+ (setf (futur--blocker futur) res)
+ (futur-register-callback res
+ (lambda (err val)
+ (if err (futur-deliver-failure futur err)
+ (futur-deliver-value futur val))))))
+ (t (futur-deliver-failure futur err))))
+
+(defun futur--resignal (error-object)
+ ;; Undocumented feature of `signal', this re-signals an error using the exact
+ ;; same error object:
+ ;; (should (eq e1 (condition-case e2 (signal e1 nil) (error e2))))
+ (signal error-object nil))
+
+(defun futur-blocking-wait-to-get-result (futur &optional error-fun)
+ "Wait for FUTUR to deliver and then return its value.
+Ideally, this should never be used, hence the long name to discourage
+abuse. Instead, you should use `futur-bind' or `futur-let*' to execute
+what you need when FUTUR completes.
+If FUTUR fails, calls ERROR-FUN with the error object and returns
+its result, or (re)signals the error if ERROR-FUN is nil."
+ ;; Waiting for a task to finish has always been a PITA in ELisp,
+ ;; because `sit-for/accept-process-output/sleep-for' have proved brittle
+ ;; with lots of weird corner cases. `futur-blocker-wait' does its best,
+ ;; thanks to years of bug fixing, but it's still messy and brittle.
+ ;; See the VCS history of `url-retrieve-synchronously' for another example.
+ ;; The use of `condition-notify' should side-step this problem, except
+ ;; that bug#80286 means that `condition-wait' can lock up your
+ ;; Emacs session hard.
+ ;; FIXME: Even `futur--idle-loop-bug80286' doesn't seem sufficient.
+ (when futur--in-background
+ (error "Blocking/waiting within an asynchronous context is not supported"))
+ (if t ;; (null futur--idle-loop-bug80286)
+ (futur-blocker-wait futur)
+ (let* ((mutex (make-mutex "futur-wait"))
+ (condition (make-condition-variable mutex)))
+ (with-mutex mutex
+ (futur-register-callback futur (lambda (_err _val)
+ (with-mutex mutex
+ (condition-notify condition))))
+ (condition-wait condition))))
+ (pcase-exhaustive futur
+ ((futur-error err) (funcall (or error-fun #'futur--resignal) err))
+ ((futur-done val) val)))
+
+(defmacro futur-let* (bindings &rest body)
+ "Sequence asynchronous operations via futures.
+BINDINGS can contain the usual (VAR EXP) bindings of `let*' but also
+\(VAR <- EXP) bindings where EXP should return a future, in which case
+the rest of the code is executed only once the future terminates,
+binding the result in VAR. BODY is executed at the very end and should
+return a future.
+BODY can start with `:error-fun ERROR-FUN' in which case errors in
+the futures in BINDINGS will cause execution of ERROR-FUN instead of BODY.
+ERROR-FUN is called with a single argument, the error object."
+ (declare (indent 1) (debug ((&rest (sexp . [&or ("<-" form) (form)])) body)))
+ (cl-assert lexical-binding)
+ (let ((error-fun (when (eq :error-fun (car body))
+ (prog1 (cadr body)
+ (setq body (cddr body))))))
+ (if (not (symbolp error-fun))
+ (macroexp-let2 nil error-fun error-fun
+ `(futur-let* ,bindings :error-fun ,error-fun ,@body))
+ (named-let loop ((bindings bindings))
+ (pcase-exhaustive bindings
+ ('() (macroexp-progn body))
+ (`((,var ,exp) . ,bindings)
+ ;; FIXME: Catch errors in EXP to run `error-fun'?
+ `(pcase-let ((,var ,exp)) ,(loop bindings)))
+ (`((,var <- ,exp) . ,bindings)
+ `(futur-bind ,exp
+ (lambda (,var) ,(loop bindings))
+ ,error-fun)))))))
+
+(oclosure-define futur--aux
+ "An auxiliary function used internally. Does not need the future's
completion.")
+
+(defun futur--multi-clients-p (clients)
+ (let ((count 0))
+ (while (and clients (< count 2))
+ (let ((client (pop clients)))
+ (if (cl-typep client 'futur--aux) nil
+ (cl-incf count))))
+ (>= count 2)))
+
+(defun futur--unwind-protect (futur fun)
+ "Make sure FUN is called, with no arguments, once FUTUR completes.
+Calls it both when FUTUR succeeds and when it fails.
+Unlike what happens with `unwind-protect', there is no guarantee of
+exactly when FUN is called, other than not before FUTUR completes."
+ ;; FIXME: Not sure if this implementation is making enough efforts to make
+ ;; sure not to forget to run FUN. Maybe we should register FUTUR+FUN
+ ;; on some global list somewhere that we can occasionally scan, in case
+ ;; something happened that prevented running FUN?
+ (let ((futur (futur-ize futur)))
+ ;; Use `futur--aux' to let `futur--multi-clients-p' know not to count
+ ;; this function as a "real" client.
+ (futur-register-callback futur (oclosure-lambda (futur--aux) (_ _)
+ (funcall fun)))
+ futur))
+
+(defmacro futur-unwind-protect (form &rest forms)
+ "Run FORM, and when that completes, run FORMS.
+FORM is supposed to return a `futur'.
+When that future completes, run FORMS.
+Returns a future that returns the same value as FORM.
+Execution of FORMS is guarantee to occur after completion of FORM,
+but it is not guaranteed to occur before completion of the returned future."
+ (declare (indent 1) (debug t))
+ `(futur--unwind-protect ,form (lambda () ,@forms)))
+
+;;;; Futur blockers
+;; Futur blockers are the objects over which a futur can be waiting, like
+;; a process, a timer, another futur, ...
+;; These need to implement `futur-blocker-wait' and `futur-blocker-abort'.
+
+(cl-defgeneric futur-blocker-wait (_object)
+ "Wait for OBJECT to complete.
+OBJECT is an object some FUTUR is waiting.
+Return non-nil if we successfully waited until the completion of BLOCKER."
+ nil)
+
+(cl-defmethod futur-blocker-wait ((futur futur))
+ (if (not (futur--waiting-p futur))
+ nil ;; FUTUR already completed.
+ (let ((i 0))
+ (while
+ (pcase futur
+ ((futur-waiting blocker)
+ (if (futur-blocker-wait blocker)
+ (setq i 0)
+ (let ((delay (* 0.01 (expt 1.1 i))))
+ (if (> delay 1.0)
+ (sit-for 0) ;; Just redisplay every 1s, just in case.
+ (setq i (1+ i)))
+ (accept-process-output nil delay)))
+ ;; Always retry since even if `futur-blocker-wait' succeeded,
+ ;; the futur might not have completed yet (and it may have
+ ;; a new blocker).
+ t)))
+ t)))
+
+(define-error 'futur-aborted "Future aborted")
+
+(cl-defgeneric futur-blocker-abort (futur error)
+ "Abort processing of FUTUR and all of its clients.
+If it had not been computed yet, then make it fail with ERROR.")
+
+(cl-defmethod futur-blocker-abort ((futur futur) error)
+ (let ((blocker (futur--blocker futur)))
+ (if (pcase blocker ((futur-waiting _ (pred futur--multi-clients-p)) t))
+ ;; If there are more than 1 clients, presumably someone else is
+ ;; still interested in FUTURs result, so we shouldn't abort it.
+ ;; FIXME: We should "unbind" ourselves from it, tho, otherwise
+ ;; when it completes it will deliver its result to us.
+ nil
+ ;; If CLIENTS has only one element, it's presumably ourselves,
+ ;; so we should definitely abort that futur.
+ (futur-blocker-abort blocker error))
+ ;; Regardless if we aborted the blocker, abort this future,
+ ;; but don't "deliver" since our caller should take care of it.
+ (setf (futur--clients futur) 'error)
+ (setf (futur--value futur) error)))
+
+(cl-defmethod futur-blocker-abort ((_ (eql 'elisp)) _error)
+ ;; FIXME: No idea how to do that!
+ nil)
+
+;;;; Postpone
+
+(defun futur-timeout (time &optional idle)
+ "Return nil in TIME seconds.
+If IDLE is non-nil, then wait for that amount of idle time."
+ (futur-new
+ (lambda (futur)
+ (cons 'timer ;; FIXME: Make timers proper structs instead!
+ (funcall (if idle #'run-with-idle-timer #'run-with-timer)
+ time nil
+ (lambda (futur) (futur-deliver-value futur nil))
+ futur)))))
+
+(cl-defmethod futur-blocker-wait ((timer (head timer)))
+ (setq timer (cdr timer))
+ ;; No support for repeated timers (yet?).
+ (cl-assert (not (timer--repeat-delay timer)))
+ (if (timer--triggered timer)
+ nil ;; Already completed.
+ (while (not (timer--triggered timer))
+ (let* ((idle (timer--idle-delay timer))
+ (time (timer--time timer))
+ (delay (time-to-seconds
+ (if idle time (time-subtract time (current-time))))))
+ (accept-process-output nil (min 1.0 (max 0.01 delay)))
+ (if (> delay 1) (sit-for 0)))) ;; Redisplay every 1s, just in case.
+ t))
+
+(cl-defmethod futur-blocker-abort ((timer (head timer)) _error)
+ (cancel-timer (cdr timer)))
+
+;;;; Processes
+
+(defun futur--process-completed-p (proc)
+ (memq (process-status proc) '(exit signal closed failed)))
+
+(defun futur--process-sentinel (proc futur)
+ (when (futur--process-completed-p proc)
+ (futur-deliver-value futur (process-exit-status proc))))
+
+(defun futur-process-make (&rest args)
+ "Create a process and return a future that delivers its exit code.
+The ARGS are like those of `make-process' except that they can't include
+`:sentinel' because that is used internally."
+ (futur-new
+ (lambda (f) (apply #'make-process
+ :sentinel
+ (lambda (proc _state)
+ (futur--process-sentinel proc f))
+ args))))
+
+(defun futur-process-call--filter (proc string)
+ (let* ((file (process-get proc 'futur-destination)))
+ (write-region string nil file 'append 'silent)))
+
+(defun futur-process-call (program &optional infile destination _display
+ &rest args)
+ "Like `call-process' but runs concurrently as a `futur'.
+The DISPLAY argument is ignored: redisplay always happens."
+ (when (eq t (car-safe (cdr-safe destination)))
+ (setq destination (car destination)))
+ (pcase destination
+ (0 (error "A value of 0 is not supported for DESTINATION"))
+ ('t (setq destination (current-buffer)))
+ ((pred stringp) (setq destination (get-buffer-create destination)))
+ (`(:file ,(and file (pred stringp)))
+ (setq destination (expand-file-name file)))
+ (`(,_ . ,_) (error "Separate handling of stderr is not supported yet")))
+ (let* ((futur (futur-process-make
+ :name program
+ :command (cons program args)
+ :coding (if (stringp destination) '(binary . nil))
+ :connection-type 'pipe
+ :buffer (if (bufferp destination) destination)
+ :filter (if (bufferp destination) nil
+ #'futur-process-call--filter)))
+ (proc (pcase-exhaustive futur ((futur-waiting blocker) blocker))))
+ (when (stringp destination)
+ (write-region "" nil destination nil 'silent))
+ (pcase-exhaustive infile
+ ('nil (process-send-eof proc))
+ ((pred stringp) (futur-send-file proc infile)))
+ (process-put proc 'futur-destination destination)
+ futur))
+
+(defun futur-process-exit-status (proc)
+ "Create a future that returns the exit code of the process PROC."
+ (if (memq (process-status proc) '(exit signal closed failed))
+ (futur-done (process-exit-status proc))
+ (futur-new
+ (lambda (f)
+ ;; FIXME: If the process's sentinel signals an error, it won't run us
:-(
+ (add-function :after (process-sentinel proc)
+ (lambda (proc _state)
+ (futur--process-sentinel proc f)))
+ proc))))
+
+(cl-defmethod futur-blocker-wait ((proc process))
+ (if (futur--process-completed-p proc)
+ nil
+ (while (and
+ (not (futur--process-completed-p proc))
+ (accept-process-output proc 1.0))
+ (sit-for 0)) ;; Redisplay every 1s, just in case.
+ t))
+
+(cl-defmethod futur-blocker-abort ((proc process) _error)
+ (delete-process proc))
+
+(defun futur-process-send (proc string)
+ ;; FIXME: This is quite inefficient. Our C code should instead provide
+ ;; a non-blocking `(process-send-string PROC STRING CALLBACK)'.
+ (futur-new
+ (lambda (f) (make-thread
+ (lambda () (futur-deliver-value f (process-send-string proc
string)))
+ "futur-process-send" 'silently))))
+
+(cl-defmethod futur-blocker-wait ((th thread))
+ (if (not (thread-live-p th))
+ nil
+ (thread-join th)
+ t))
+
+(cl-defmethod futur-blocker-abort ((th thread) error)
+ ;; FIXME: This doesn't guarantee that the thread is aborted.
+ ;; FIXME: Let's hope that the undocumented feature of `signal' applies
+ ;; also to `thread-signal'.
+ (thread-signal th error nil))
+
+;;;; Multi futures: Futures that are waiting for several other futures.
+
+(defun futur-list (&rest futurs)
+ "Build a futur that returns the list of values of FUTURS.
+If one of FUTURS fails, fails the whole future and aborts those FUTURS
+that have not yet completed."
+ (if (null futurs)
+ (futur-done nil)
+ (let* ((new (futur--waiting futurs))
+ (count (length futurs))
+ (failed nil)
+ (args (make-list count :futur--waiting-for-result))
+ (i 0))
+ (dolist (futur futurs)
+ (futur-register-callback futur
+ (let ((cell (nthcdr i args)))
+ (lambda (err val)
+ (cl-assert (eq :futur--waiting-for-result (car cell)))
+ (cond
+ (failed nil)
+ (err
+ (setq failed t)
+ (futur-deliver-failure new err)
+ ;; Abort the remaining ones.
+ (let ((abort-error (list 'futur-aborted)))
+ (futur-blocker-abort futurs abort-error)))
+ (t
+ (setf (car cell) val)
+ (setq count (1- count))
+ (when (zerop count)
+ (pcase new
+ ;; We don't unbind ourselves from some FUTURs
+ ;; when aborting, so ignore their delivery here.
+ ((futur-error '(futur-aborted)) nil)
+ (_ (futur-deliver-value new args)))))))))
+ (setq i (1+ i)))
+ new)))
+
+(cl-defmethod futur-blocker-wait ((_blockers cons))
+ ;; FIXME: The loop below can misbehave when there's an early-exit
+ ;; because of an error: we may remain waiting for the first blocker
+ ;; while the second blocker has already signaled an error causing the
+ ;; whole future to be aborted. So we just "punt" and fallback on the
+ ;; "generic" (and thus less efficient) wait loop
+ ;;(let ((waited nil))
+ ;; (dolist (blocker blockers)
+ ;; (when (futur-blocker-wait blocker) (setq waited t)))
+ ;; waited)
+ nil)
+
+(cl-defmethod futur-blocker-abort ((futurs cons) error)
+ ;; Propagate the abort to the futurs we're still waiting for.
+ (dolist (futur futurs)
+ (pcase futur
+ ((futur-waiting _ clients)
+ (if (futur--multi-clients-p clients)
+ ;; If there are more than 1 clients, presumably someone else is
+ ;; still interested in FUTURs result, so we shouldn't abort it.
+ ;; FIXME: We should "unbind" ourselves from it, tho, otherwise
+ ;; when it completes it will deliver its result to us.
+ nil
+ ;; If CLIENTS has only one element, it's presumably ourselves,
+ ;; so we should definitely abort that futur.
+ (futur-blocker-abort futur error))))))
+
+;;;; Other helpers
+
+(defmacro futur-with-temp-buffer (&rest body)
+ (declare (indent 0) (debug t))
+ `(futur--with-temp-buffer (lambda () ,@body)))
+
+(defun futur--with-temp-buffer (body-fun)
+ (let ((buf (generate-new-buffer " *temp*" t)))
+ (futur--unwind-protect
+ (with-current-buffer buf (funcall body-fun))
+ (lambda () (and (buffer-live-p buf)
+ (kill-buffer buf))))))
+
+(provide 'futur)
+;;; futur.el ends here