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

    futur-client.el: New file
    
    * futur-client.el: New file.
    * futur-server.el (futur--elisp-impossible-string): New var.
    (futur-elisp-server): Check that the requests are actual requests.
    * futur-tests.el (futur-elisp-server): New test.
---
 futur-client.el | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 futur-server.el |  59 ++++++++++-------
 futur-tests.el  |   7 ++
 3 files changed, 241 insertions(+), 23 deletions(-)

diff --git a/futur-client.el b/futur-client.el
new file mode 100644
index 0000000000..09249d0c2f
--- /dev/null
+++ b/futur-client.el
@@ -0,0 +1,198 @@
+;;; futur-client.el --- A client to Futur's ELisp server  -*- lexical-binding: 
t -*-
+
+;; Copyright (C) 2026  Free Software Foundation, Inc.
+
+;; 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 'trace)
+;; (trace-function 'futur--elisp-process-filter)
+;; (trace-function 'futur--elisp-process-answer)
+
+(require 'futur)
+
+(defconst futur--elisp-impossible-string "\n# \"# "
+  "String that will necessarily cause `read' to signal an error.
+This has to be the same used by `futur-server'.")
+
+(defvar futur--elisp-servers nil)
+
+(defun futur--elisp-process-filter (proc string)
+  (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)
+      (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)))))))
+        (:sexp
+         (when pending
+           (cl-assert (< (length pending)
+                         (length futur--elisp-impossible-string)))
+           (setq string (concat pending string))
+           (process-put proc 'futur--pending nil))
+         (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)
+           (cond
+            ((string-prefix-p futur--elisp-impossible-string string)
+             (let* ((pendings (process-get proc 'futur--pendings))
+                    (sexp-string (mapconcat #'identity (nreverse pendings) 
"")))
+               (process-put proc 'futur--pendings nil)
+               (process-put proc 'futur--state :next)
+               (futur--funcall #'futur--elisp-process-answer proc sexp-string)
+               (when (< (length futur--elisp-impossible-string) (length 
string))
+                 (loop (substring string
+                                  (length futur--elisp-impossible-string))))))
+            ((< (length string) (length futur--elisp-impossible-string))
+             (process-put proc 'futur--pending string))
+            ((string-match "\n" string 1)
+             (push (substring string 0 (match-beginning 0))
+                   (process-get proc 'futur--pendings))
+             (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))
+           (cond
+            ((string-match sid string)
+             (let ((before (string-trim (substring string 0 (match-beginning 
0))))
+                   (after (substring string (match-end 0))))
+               (unless (equal "" before)
+                 (message "Skipping output from futur-server: %S" before))
+               (process-put proc 'futur--state :sexp)
+               (loop after)))
+            (t
+             (string-match "[:0-9a-fs]*\\'" string ;; This regexp Can't fail.
+                           (max 0 (- (length string) (length sid))))
+             (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--pending
+                            (substring string (match-beginning 0))))))))))))
+
+(defun futur--elisp-process-filter-stderr (proc string)
+  (let ((pending (process-get proc 'futur--pending)))
+    (process-put proc 'futur--pending
+                 (if (not (string-match "\n" string))
+                     (if pending (concat pending string) string)
+                   (let ((head (substring string 0 (match-beginning 0)))
+                         (tail (substring string (match-end 0))))
+                     (message "futur-server: %S"
+                              (if pending (concat pending head) head))
+                     tail)))))
+
+(defun futur--elisp-process-sentinel (proc status)
+  (if (futur--process-completed-p proc)
+      (setq futur--elisp-servers (delq proc futur--elisp-servers))
+    (message "futur--elisp-process-sentinel before end: %S" status)))
+
+(defun futur--elisp-launch ()
+  (let* ((buffer (get-buffer-create " *futur-server*"))
+         (stderr (make-pipe-process
+                  :name "futur-server-stderr"
+                  :noquery t
+                  :coding 'emacs-internal
+                  :buffer buffer
+                  :filter #'futur--elisp-process-filter-stderr
+                  :sentinel #'ignore))
+         (proc (make-process
+                :name "futur-server"
+                :noquery t
+                :buffer buffer
+                :connection-type 'pipe
+                :coding 'emacs-internal
+                :stderr stderr
+                :filter #'futur--elisp-process-filter
+                :sentinel #'futur--elisp-process-sentinel
+                :command
+                `(,(expand-file-name invocation-name invocation-directory)
+                  "-Q" "--batch"
+                  "-l" ,(locate-library "futur-server")
+                  "-f" "futur-elisp-server"))))
+    (process-put proc 'futur--state :booting)
+    (push proc futur--elisp-servers)
+    proc))
+
+(defun futur--elisp-process-answer (proc sexp-string)
+  (pcase-let* ((`(,sexp . ,end)
+                (condition-case err
+                         (read-from-string sexp-string)
+                       (error `((:unreadable-answer . ,err)
+                                . ,(length sexp-string)))))
+               (sexp (if (string-match "[^ \n\t]" sexp-string end)
+                         `(:trailing-garbage ,sexp ,(substring sexp-string 
end))
+                       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)
+      (process-put proc 'futur--destination nil)
+      (futur-deliver-value futur sexp))))
+
+(defun futur--elisp-get-process ()
+  (or (seq-find (lambda (proc) (process-get proc 'futur--ready))
+                futur--elisp-servers)
+      (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))))
+        (if (eq answer :ready)
+            (progn
+              (process-put proc 'futur--ready t)
+              proc)
+          (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
+;;   ;; requests.
+;;   nil)
+;; (cl-defmethod futur-blocker-wait ((blocker (head futur-server)))
+;;   (while ?? (accept-process-output proc ...)))
+
+
+(provide 'futur-client)
+;;; futur-client.el ends here
diff --git a/futur-server.el b/futur-server.el
index 4bd13211fe..3b24d35e58 100644
--- a/futur-server.el
+++ b/futur-server.el
@@ -31,6 +31,13 @@
 
 ;;;; Base protocol
 
+;; (require 'trace)
+;; (trace-function 'futur--read-stdin)
+;; (trace-function 'futur--print-stdout)
+
+(defconst futur--elisp-impossible-string "\n# \"# "
+  "String that will necessarily cause `read' to signal an error.")
+
 (defun futur--read-stdin ()
   "Read a sexp from a single line on stdin."
   (unless noninteractive (error "futur--read-stdin works only in batch mode"))
@@ -50,39 +57,45 @@
         (print-symbols-bare t))
     (princ sid t)
     (prin1 sexp t)
+    (princ futur--elisp-impossible-string t)
     (terpri t)))
 
 (defun futur-elisp-server ()
   ;; We don't need a cryptographically secure ID, but just something that's
   ;; *very* unlikely to occur by accident elsewhere and which `read' wouldn't
   ;; process without signaling an error.
-  (let ((sid (format "\n # \" # fes:%s "
-                     (secure-hash 'sha1
-                                  (format "%S:%S:%S"
-                                          (random t) (current-time)
-                                          (emacs-pid))))))
+  (let* ((sid (format " fes:%s "
+                      (secure-hash 'sha1
+                                   (format "%S:%S:%S"
+                                           (random t) (current-time)
+                                           (emacs-pid)))))
+         (sid-sym (intern (string-trim sid))))
     (futur--print-stdout :ready sid)
     (while t
       (let ((input (condition-case err (cons :read-success (futur--read-stdin))
                      (t err))))
-        (if (not (eq :read-success (car-safe input)))
-            ;; FIXME: We can get an `end-of-file' error if the input line
-            ;; is not a complete sexp but also if stdin was closed.
-            ;; To distinguish the two it seems we have to look at
-            ;; the actual error string :-(.
-            (if (equal input '(end-of-file "Error reading from stdin"))
-                (kill-emacs)
-              (futur--print-stdout `(:read-error . ,input) sid))
-          ;; Confirm we read successfully so the client can
-          ;; distinguish where problems come from.
-          (let ((rid (cadr input)))
-            (futur--print-stdout `(:read-success ,rid) sid)
-            (let ((result
-                   (condition-case err
-                       `(:funcall-success ,rid
-                         . ,(apply #'funcall (cddr input)))
-                     (t `(:funcall-error ,rid . ,err)))))
-              (futur--print-stdout result sid))))))))
+        (pcase input
+          ;; Check `sid-sym' for every request, since we may have just read
+          ;; "successfully" the garbage that follows a failed read.
+          (`(:read-success ,(pred (eq sid-sym)) ,rid ,func . ,args)
+           ;; Confirm we read successfully so the client can
+           ;; distinguish where problems come from.
+           (futur--print-stdout `(:read-success ,rid) sid)
+           (let ((result
+                  (condition-case err
+                      `(:funcall-success ,rid . ,(apply func args))
+                    (t `(:funcall-error ,rid . ,err)))))
+             (futur--print-stdout result sid)))
+          (`(:read-success . ,rest)
+           (futur--print-stdout `(:unrecognized-request . ,rest) sid))
+          (_
+           ;; FIXME: We can get an `end-of-file' error if the input line
+           ;; is not a complete sexp but also if stdin was closed.
+           ;; To distinguish the two it seems we have to look at
+           ;; the actual error string :-(.
+           (if (equal input '(end-of-file "Error reading from stdin"))
+               (kill-emacs)
+             (futur--print-stdout `(:read-error . ,input) sid))))))))
                                  
 
 (provide 'futur-server)
diff --git a/futur-tests.el b/futur-tests.el
index 4c1084ac43..ee2a82ddba 100644
--- a/futur-tests.el
+++ b/futur-tests.el
@@ -25,6 +25,7 @@
 ;;; Code:
 
 (require 'futur)
+(require 'futur-client)
 (require 'ert)
 
 (ert-deftest futur--resignal ()
@@ -178,5 +179,11 @@
     (futur-blocking-wait-to-get-result (apply #'futur-list futures))
     (should (<= 0.4 (- (float-time) start) 0.5))))
 
+(ert-deftest futur-elisp-server ()
+  (let* ((futur (futur--elisp-get-process))
+         (proc (futur-blocking-wait-to-get-result futur)))
+    (should (process-get proc 'futur--ready))
+    (should (null (process-get proc 'futur--destination)))))
+
 (provide 'futur-tests)
 ;;; futur-tests.el ends here

Reply via email to