branch: externals/futur
commit 04357fe4116b6e51c3e2fb997c2f82748a3fbc1c
Author: Stefan Monnier <[email protected]>
Commit: Stefan Monnier <[email protected]>

    (futur--elisp-funcall): New function
    
    * futur-client.el (futur--elisp-process-filter): Fix mis-handling of
    `pending` and beware that `string-trim` clobbers the match data.
    (futur--elisp-process-answer): Store future answers.
    (futur--elisp-set-destination, futur--elisp-answer-futur): New functions.
    (futur--elisp-get-process): Use it and make sure we always return a future.
    (futur--elisp-funcall): New function.
    
    * futur-tests.el (futur-process-bounded): Tweak timing constraints.
    (futur-elisp-funcall): New test.
    
    * futur.el (futur-new): Don't burp if BUILDER delivers.
    (futur-bind, futur-blocking-wait-to-get-result):
    Allow FUTUR to be a plain already-computed value rather than a `futur`.
---
 futur-client.el | 138 +++++++++++++++++++++++++++++++++++++++-----------------
 futur-server.el |   4 +-
 futur-tests.el  |  22 +++++++--
 futur.el        |  49 +++++++++++---------
 4 files changed, 145 insertions(+), 68 deletions(-)

diff --git a/futur-client.el b/futur-client.el
index 09249d0c2f..98a8909c74 100644
--- a/futur-client.el
+++ b/futur-client.el
@@ -22,6 +22,11 @@
 ;; (require 'trace)
 ;; (trace-function 'futur--elisp-process-filter)
 ;; (trace-function 'futur--elisp-process-answer)
+;; (trace-function 'process-send-region)
+;; (trace-function 'process-send-string)
+;; (trace-function 'futur--funcall)
+;; (trace-function 'futur--elisp-get-process)
+;; (trace-function 'futur--elisp-funcall)
 
 (require 'futur)
 
@@ -35,37 +40,34 @@ This has to be the same used by `futur-server'.")
   (cl-assert (memq proc futur--elisp-servers))
   (let ((pending (process-get proc 'futur--pending))
         (case-fold-search nil))
-    (named-let loop ((string string))
-      ;; (trace-values 'looping string)
+    (process-put proc 'futur--pending nil)
+    (named-let loop ((string (if pending (concat pending string) string)))
+      ;; (trace-values :looping (process-get proc 'futur--state) string)
       (pcase-exhaustive (process-get proc 'futur--state)
         (:booting
-         (let ((string (if pending (concat pending string) string)))
-           (if (not (string-match " \\(fes:[0-9a-f]+\\) " string))
-               (process-put proc 'futur--pending string)
-             (let ((before (string-trim
-                            (substring string 0 (match-beginning 0)))))
-               (unless (equal "" before)
-                 (message "Skipping output from futur-server: %S" before)))
-             (process-put proc 'futur--sid (match-string 0 string))
-             (process-put proc 'futur--sid-sym (intern (match-string 1 
string)))
-             (process-put proc 'futur--state :sexp)
-             (process-put proc 'futur--pending nil)
-             (process-put proc 'futur--pendings nil)
-             (when (< (match-end 0) (length string))
-               (loop (substring string (match-end 0)))))))
+         (if (not (string-match " \\(fes:[0-9a-f]+\\) " string))
+             (process-put proc 'futur--pending string)
+           (let ((before (string-trim
+                          (substring string 0 (match-beginning 0)))))
+             (unless (equal "" before)
+               (message "Skipping output from futur-server: %S" before)))
+           (process-put proc 'futur--sid (match-string 0 string))
+           (process-put proc 'futur--sid-sym (intern (match-string 1 string)))
+           (process-put proc 'futur--state :sexp)
+           (process-put proc 'futur--pendings nil)
+           (when (< (match-end 0) (length string))
+             (loop (substring string (match-end 0))))))
         (:sexp
          (when pending
            (cl-assert (< (length pending)
-                         (length futur--elisp-impossible-string)))
-           (setq string (concat pending string))
-           (process-put proc 'futur--pending nil))
+                         (length futur--elisp-impossible-string))))
          (if (not (string-match "\n" string))
              (push string (process-get proc 'futur--pendings))
            (unless (eq 0 (match-beginning 0))
              (push (substring string 0 (match-beginning 0))
                    (process-get proc 'futur--pendings))
              (setq string (substring string (match-beginning 0))))
-           ;; (trace-values ':sexp string)
+           ;; (trace-values :sexp string)
            (cond
             ((string-prefix-p futur--elisp-impossible-string string)
              (let* ((pendings (process-get proc 'futur--pendings))
@@ -74,6 +76,7 @@ This has to be the same used by `futur-server'.")
                (process-put proc 'futur--state :next)
                (futur--funcall #'futur--elisp-process-answer proc sexp-string)
                (when (< (length futur--elisp-impossible-string) (length 
string))
+                 ;; (trace-values :loop1)
                  (loop (substring string
                                   (length futur--elisp-impossible-string))))))
             ((< (length string) (length futur--elisp-impossible-string))
@@ -81,31 +84,32 @@ This has to be the same used by `futur-server'.")
             ((string-match "\n" string 1)
              (push (substring string 0 (match-beginning 0))
                    (process-get proc 'futur--pendings))
+             ;; (trace-values :loop2)
              (loop (substring string (match-beginning 0))))
             (t (push string (process-get proc 'futur--pendings))))))
         (:next
          (let ((sid (process-get proc 'futur--sid)))
            (when pending
-             (cl-assert (< (length pending) (length sid)))
-             (setq string (concat pending string))
-             (process-put proc 'futur--pending nil))
+             (cl-assert (< (length pending) (length sid))))
            (cond
             ((string-match sid string)
-             (let ((before (string-trim (substring string 0 (match-beginning 
0))))
-                   (after (substring string (match-end 0))))
+             (let ((after (substring string (match-end 0)))
+                   (before (string-trim
+                            (substring string 0 (match-beginning 0)))))
                (unless (equal "" before)
                  (message "Skipping output from futur-server: %S" before))
                (process-put proc 'futur--state :sexp)
+               ;; (trace-values :loop3 before sid after)
                (loop after)))
             (t
              (string-match "[:0-9a-fs]*\\'" string ;; This regexp Can't fail.
                            (max 0 (- (length string) (length sid))))
-             (let ((before (string-trim
+             (let ((after (substring string (match-beginning 0)))
+                   (before (string-trim
                             (substring string 0 (match-beginning 0)))))
                (unless (equal "" before)
                  (message "Skipping output from futur-server: %S" before))
-               (process-put proc 'futur--pending
-                            (substring string (match-beginning 0))))))))))))
+               (process-put proc 'futur--pending after))))))))))
 
 (defun futur--elisp-process-filter-stderr (proc string)
   (let ((pending (process-get proc 'futur--pending)))
@@ -147,6 +151,7 @@ This has to be the same used by `futur-server'.")
                   "-l" ,(locate-library "futur-server")
                   "-f" "futur-elisp-server"))))
     (process-put proc 'futur--state :booting)
+    (process-put proc 'futur--rid 0)
     (push proc futur--elisp-servers)
     proc))
 
@@ -161,30 +166,41 @@ This has to be the same used by `futur-server'.")
                        sexp))
                (futur (process-get proc 'futur--destination)))
     (if (null futur)
-        ;; FIXME: Maybe it's just that we haven't finished processing
-        ;; the previous answer and thus haven't yet installed the next
-        ;; `futur--destination'.
-        (message "Unsolicited futur-server answer: %S" sexp)
+        ;; Hopefully, some destination will show up later to consume it.
+        (process-put proc 'futur--answers
+                     (nconc (process-get proc 'futur--answers) (list sexp)))
       (process-put proc 'futur--destination nil)
       (futur-deliver-value futur sexp))))
 
+(defun futur--elisp-set-destination (proc futur)
+  (cl-assert (null (process-get proc 'futur--destination)))
+  (let ((answers (process-get proc 'futur--answers)))
+    (if answers
+        (let ((answer (car answers)))
+          (process-put proc 'futur--answers (cdr answers))
+          (futur-deliver-value futur answer))
+      (process-put proc 'futur--destination futur))))
+
+(defun futur--elisp-answer-futur (proc)
+  (futur-new (lambda (futur)
+               (futur--elisp-set-destination proc futur)
+               ;; FIXME: Wait more efficiently and abort
+               ;; more cleanly.
+               ;; `(futur-server . ,proc)
+               nil)))
+
 (defun futur--elisp-get-process ()
-  (or (seq-find (lambda (proc) (process-get proc 'futur--ready))
-                futur--elisp-servers)
+  (let ((ready (seq-find (lambda (proc) (process-get proc 'futur--ready))
+                         futur--elisp-servers)))
+    (if ready (futur-done ready)
       (futur-let*
           ((proc (futur--elisp-launch))
-           (answer
-            <- (futur-new (lambda (futur)
-                            (process-put proc 'futur--destination futur)
-                            ;; FIXME: Wait more efficiently and abort
-                            ;; more cleanly.
-                            ;; `(futur-server . ,proc)
-                            nil))))
+           (answer <- (futur--elisp-answer-futur proc)))
         (if (eq answer :ready)
             (progn
               (process-put proc 'futur--ready t)
               proc)
-          (error "unexpected boot message from futur-server: %S" answer)))))
+          (error "unexpected boot message from futur-server: %S" answer))))))
 
 ;; (cl-defmethod futur-blocker-abort ((_ (head futur-server)) _)
 ;;   ;; Don't kill the server, since we may want to reuse it for other
@@ -193,6 +209,44 @@ This has to be the same used by `futur-server'.")
 ;; (cl-defmethod futur-blocker-wait ((blocker (head futur-server)))
 ;;   (while ?? (accept-process-output proc ...)))
 
+(defun futur--elisp-funcall (func &rest args)
+  (futur-let*
+      ((proc <- (futur--elisp-get-process))
+       (rid (cl-incf (process-get proc 'futur--rid)))
+       (_ (with-temp-buffer
+            ;; (trace-values :funcall rid func args)
+            (process-put proc 'futur--ready nil)
+            (let ((print-length nil)
+                  (print-level nil)
+                  (coding-system-for-write 'emacs-internal)
+                  (print-circle t)
+                  (print-gensym t)
+                  ;; The server reads with `read-from-minibuffer' which
+                  ;; works only on single-lines, so it's super-important
+                  ;; we don't include any LF by accident.
+                  (print-escape-newlines t)
+                  ;; SWP aren't currently printed in a `read'able way, so we 
may
+                  ;; as well print them bare.
+                  (print-symbols-bare t))
+              (prin1 `(,(process-get proc 'futur--sid-sym) ,rid
+                       ,func ,@args)
+                     (current-buffer))
+              (insert "\n")
+              (process-send-string proc (buffer-string))
+              ;; (process-send-region proc (point-min) (point-max))
+              )))
+       (read-answer <- (futur--elisp-answer-futur proc)))
+    ;; (trace-values :read-answer read-answer)
+    (pcase-exhaustive read-answer
+      (`(:read-success ,(pred (equal rid)))
+       (futur-let* ((call-answer  <- (futur--elisp-answer-futur proc)))
+         (pcase-exhaustive call-answer
+           (`(:funcall-success ,(pred (equal rid)) . ,val)
+            (process-put proc 'futur--ready t)
+            val)
+           (`(:funcall-error ,(pred (equal rid)) . ,err)
+            (process-put proc 'futur--ready t)
+            (futur--resignal err))))))))
 
 (provide 'futur-client)
 ;;; futur-client.el ends here
diff --git a/futur-server.el b/futur-server.el
index 3b24d35e58..d709e5830a 100644
--- a/futur-server.el
+++ b/futur-server.el
@@ -46,9 +46,9 @@
 (defun futur--print-stdout (sexp sid)
   "Print SEXP on stdout using ID as the leading marker."
   (unless noninteractive (error "futur--print-stdout works only in batch 
mode"))
-  (let ((print-length nil)
+  (let ((coding-system-for-write 'emacs-internal)
+        (print-length nil)
         (print-level nil)
-        (coding-system-for-write 'emacs-internal)
         (print-circle t)
         (print-gensym t)
         (print-escape-newlines nil)
diff --git a/futur-tests.el b/futur-tests.el
index ee2a82ddba..3132c75292 100644
--- a/futur-tests.el
+++ b/futur-tests.el
@@ -170,14 +170,14 @@
     (dotimes (_ 10)
       (push (futur-concurrency-bound #'futur-timeout 0.1) futures))
     (futur-blocking-wait-to-get-result (apply #'futur-list futures))
-    (should (<= 0.5 (- (float-time) start) 0.6)))
+    (should (<= 0.5 (- (float-time) start) 0.7)))
   (let* ((futures ())
          (start (float-time))
-         (futur-concurrency-bound 3))
+         (futur-concurrency-bound 4))
     (dotimes (_ 10)
       (push (futur-concurrency-bound #'futur-timeout 0.1) futures))
     (futur-blocking-wait-to-get-result (apply #'futur-list futures))
-    (should (<= 0.4 (- (float-time) start) 0.5))))
+    (should (<= 0.3 (- (float-time) start) 0.5))))
 
 (ert-deftest futur-elisp-server ()
   (let* ((futur (futur--elisp-get-process))
@@ -185,5 +185,21 @@
     (should (process-get proc 'futur--ready))
     (should (null (process-get proc 'futur--destination)))))
 
+(ert-deftest futur-elisp-funcall ()
+  (let ((fut (futur--elisp-funcall #'+ 5 7)))
+    (should (equal 12 (futur-blocking-wait-to-get-result fut))))
+
+  (let ((fut (futur--elisp-funcall #'car 7)))
+    (should (equal (condition-case err1
+                       (futur-blocking-wait-to-get-result fut)
+                     (error err1))
+                   (condition-case err2
+                       (car 7)
+                     (error err2)))))
+
+  (let ((fut (futur--elisp-funcall #'documentation 'car)))
+    (should (equal (futur-blocking-wait-to-get-result fut)
+                   (documentation 'car)))))
+
 (provide 'futur-tests)
 ;;; futur-tests.el ends here
diff --git a/futur.el b/futur.el
index f6e8169280..7d770c50a6 100644
--- a/futur.el
+++ b/futur.el
@@ -350,8 +350,9 @@ The blocker can be any object for which there are 
`futur-blocker-wait'
 and `futur-blocker-abort' methods.  `nil' is a valid blocker."
   (let* ((f (futur--waiting))
          (x (funcall builder f)))
-    (cl-assert (null (futur--blocker f)))
-    (setf (futur--blocker f) x)
+    (when x
+      (cl-assert (null (futur--blocker f)))
+      (setf (futur--blocker f) x))
     f))
 
 (defun futur-abort (futur)
@@ -391,14 +392,18 @@ 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.
-ERROR-FUN and FUN can also return non-future values,"
+ERROR-FUN and FUN can also return non-future values,
+If FUTUR can also be a non-`futur' object, in which case it's passed
+as-is to FUN."
   (let ((new (futur--waiting futur)))
-    (futur--register-callback
-     futur (lambda (err val)
-             (cond
-              ((null err) (futur--run-continuation new fun (list val)))
-              (error-fun (futur--run-continuation new error-fun (list err)))
-              (t (futur-deliver-failure new err)))))
+    (if (not (futur--p futur))
+        (futur--run-continuation new fun (list futur))
+      (futur--register-callback
+       futur (lambda (err val)
+               (cond
+                ((null err) (futur--run-continuation new fun (list val)))
+                (error-fun (futur--run-continuation new error-fun (list err)))
+                (t (futur-deliver-failure new err))))))
     new))
 
 (defun futur--run-continuation (futur fun args)
@@ -441,18 +446,20 @@ its result, or (re)signals the error if ERROR-FUN is nil."
   ;; 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--failed err) (funcall (or error-fun #'futur--resignal) err))
-    ((futur--done val) val)))
+  (if (not (futur--p futur))
+      futur
+    (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--failed err) (funcall (or error-fun #'futur--resignal) err))
+      ((futur--done val) val))))
 
 (defmacro futur-let* (bindings &rest body)
   "Sequence asynchronous operations via futures.

Reply via email to