branch: externals/minimail
commit 5cc5f411a3957c742c01956b0d1ba892f8c753c6
Author: Augusto Stoffel <[email protected]>
Commit: Augusto Stoffel <[email protected]>
In athunk-let*, add syntax to mix simple and asynchronous bindings
The <- syntax is stolen from Stefan Monnier's futur.el, see
https://lists.gnu.org/archive/html/emacs-devel/2023-03/msg00630.html
---
minimail-tests.el | 26 ++---
minimail.el | 277 +++++++++++++++++++++++++++++-------------------------
2 files changed, 163 insertions(+), 140 deletions(-)
diff --git a/minimail-tests.el b/minimail-tests.el
index 156b61edb2..b78bb6c27d 100644
--- a/minimail-tests.el
+++ b/minimail-tests.el
@@ -44,31 +44,33 @@
(ert-deftest minimail-tests-let* ()
(-with-polling
- (athunk-let* ((x (athunk-wrap 2))
- (y (athunk-wrap (1+ x))))
+ (athunk-let* ((x <- (athunk-wrap 2))
+ (y <- (athunk-wrap (1+ x))))
(should (eq y 3)))))
(ert-deftest minimail-tests-sleep ()
(-should-take-seconds 1
(-with-polling
- (athunk-let* ((x (athunk-sleep 1 'xxx)))
+ (athunk-let* ((x <- (athunk-sleep 1 'xxx)))
(should (eq x 'xxx))))))
(ert-deftest minimail-tests-gather ()
(-should-take-seconds 0.3
(-with-polling
- (athunk-let* ((vec (athunk-gather (list (athunk-sleep 0.1 1)
- (athunk-sleep 0.3 2)
- (athunk-sleep 0.2 3)))))
+ (athunk-let* ((vec <- (athunk-gather (list (athunk-sleep 0.1 1)
+ (athunk-sleep 0.3 2)
+ (athunk-sleep 0.2 3)))))
(should (equal vec [1 2 3]))))))
(ert-deftest minimail-tests-let ()
(-should-take-seconds 0.2
(-with-polling
- (athunk-let ((x (athunk-sleep 0.2 1))
- (y (athunk-sleep 0.1 2)))
+ (athunk-let ((x <- (athunk-sleep 0.2 1))
+ (y <- (athunk-sleep 0.1 2))
+ (z 3))
(should (eq x 1))
- (should (eq y 2))))))
+ (should (eq y 2))
+ (should (eq z 3))))))
(ert-deftest minimail-tests-memoization ()
(-with-polling
@@ -76,7 +78,7 @@
(place nil)
(getter (lambda (v)
(athunk-memoize (alist-get 'key place)
- (athunk-let ((_ (athunk-sleep 0)))
+ (athunk-let* ((_ <- (athunk-sleep 0)))
(cl-incf count)
v)))))
(athunk-let ((x (funcall getter 10))
@@ -92,8 +94,8 @@
(athunk-memoize (alist-get 'key place)
(athunk-wrap v)))))
(athunk-let*
- ((x (funcall getter 1))
- (y (progn
+ ((x <- (funcall getter 1))
+ (y <- (progn
(athunk-unmemoize (alist-get 'key place))
(funcall getter 2))))
(should (eq x 1))
diff --git a/minimail.el b/minimail.el
index 0b64d53066..469ec64903 100644
--- a/minimail.el
+++ b/minimail.el
@@ -59,27 +59,48 @@
;;
;; References:
;; - https://jyp.github.io/posts/elisp-cps.html
-;; - https://emacsconf.org/2022/talks/async/
+;; - https://nullprogram.com/blog/2019/03/10/
+;; - https://lists.gnu.org/archive/html/emacs-devel/2023-03/msg00630.html
+
+(defmacro athunk--let*-1 (cont bindings form)
+ "Helper macro for `athunk-let*'."
+ (declare (indent 1))
+ (cl-flet ((protect (form)
+ (let ((esym (gensym)))
+ `(condition-case ,esym ,form
+ (t (funcall ,cont (car ,esym) (cdr ,esym)))))))
+ (pcase-exhaustive bindings
+ ('()
+ `(funcall ,cont nil ,(protect form)))
+ (`((,var ,exp) . ,rest)
+ `(let ((,var ,(protect exp)))
+ (athunk--let*-1 ,cont ,rest ,form)))
+ (`((,var <- ,athunk) . ,rest)
+ (let ((esym (gensym)) ;the error, possibly nil
+ (vsym (gensym))) ;the computed value
+ `(funcall ,(protect athunk)
+ (lambda (,esym ,vsym)
+ (if ,esym
+ (funcall ,cont ,esym ,vsym)
+ (let ((,var ,vsym))
+ (athunk--let*-1 ,cont ,rest ,form))))))))))
(defmacro athunk-let* (bindings &rest body)
"Sequentially resolve athunks then evaluate BODY.
-BINDINGS is a list of elements of the form (SYMBOL FORM), where FORM
-evaluates to an athunk. Return an athunk which resolves to the value of
-the last form in BODY."
- (declare (indent 1))
- (let* ((csym (gensym)) ;the continuation
- (esym (gensym)) ;the error, possibly nil
- (vsym (gensym)) ;the computed value
- (form `(condition-case ,vsym ,(macroexp-progn body)
- (:success (funcall ,csym nil ,vsym))
- (t (funcall ,csym (car ,vsym) (cdr ,vsym))))))
- (pcase-dolist (`(,var ,athunk) (reverse bindings))
- (setq form `(funcall ,athunk
- (lambda (,esym ,vsym)
- (if ,esym
- (funcall ,csym ,esym ,vsym)
- (let ((,var ,vsym)) ,form))))))
- `(lambda (,csym) ,form)))
+BINDINGS are elements of the form (SYMBOL FORM) or (SYMBOL <- FORM).
+The former simply binds FORM's value to SYMBOL. In the latter, FORM
+should evaluate to an athunk, and SYMBOL is bound to it resolved value.
+
+Return an athunk which resolves to the value of the last form in BODY."
+ (declare (indent 1) (debug ((&rest (sexp . [&or ("<-" form) (form)])) body)))
+ (let ((csym (gensym)))
+ `(lambda (,csym)
+ (athunk--let*-1 ,csym ,bindings ,(macroexp-progn body)))))
+
+(defmacro athunk-wrap (&rest body)
+ "Wrap BODY in an athunk for delayed execution."
+ (declare (indent 0))
+ `(athunk-let* nil ,@body))
(defun athunk-gather (athunks)
"Resolve all ATHUNKS and return a vector of results."
@@ -93,29 +114,30 @@ the last form in BODY."
(funcall cont err val)
(setf (aref result i) val)
(when (zerop (cl-decf n))
- (funcall cont nil result)))))))))
+ (run-with-timer 0 nil cont nil result)))))))))
(defmacro athunk-let (bindings &rest body)
"Concurrently resolve athunks then evaluate BODY.
-BINDINGS is a list of elements of the form (SYMBOL FORM), where FORM
-evaluates to an athunk. Return an athunk which resolves to the value of
-the last form in BODY."
+BINDINGS are elements of the form (SYMBOL FORM) or (SYMBOL <- FORM).
+The former simply binds FORM's value to SYMBOL. In the latter, FORM
+should evaluate to an athunk, and SYMBOL is bound to it resolved value.
+
+Return an athunk which resolves to the value of the last form in BODY."
(declare (indent 1))
- (if (length< bindings 2) ;optimization
+ (if (length< bindings 2)
`(athunk-let* ,bindings ,@body)
(let ((vec (gensym))
- (athunks (mapcar #'cadr bindings))
+ (athunks (mapcar (lambda (binding)
+ (pcase-exhaustive binding
+ (`(,_ <- ,athunk) athunk)
+ (`(,_ ,val) `(athunk-wrap ,val))))
+ bindings))
(vars (mapcar #'car bindings)))
- `(athunk-let* ((,vec (athunk-gather (list ,@athunks))))
+ `(athunk-let* ((,vec <- (athunk-gather (list ,@athunks))))
(let ,(seq-map-indexed (lambda (v i) `(,v (aref ,vec ,i))) vars)
,@body)))))
-(defun athunk-sleep (secs &optional value)
- "Return an athunk that waits SECS seconds and then returns VALUE."
- (lambda (cont)
- (run-with-timer secs nil cont nil value)))
-
-(defun athunk-do (athunk)
+(defun athunk-run (athunk)
"Execute ATHUNK for side-effects.
Any uncatched errors are signaled, but notice this will happen at a
later point in time."
@@ -129,10 +151,10 @@ later point in time."
(message "%s:%s:%S" (or prefix "athunk-debug") err val)
(when err (signal err val))))))
-(defmacro athunk-wrap (&rest body)
- "Wrap BODY in an athunk for delayed execution."
- (declare (indent 0))
- `(athunk-let* nil ,@body))
+(defun athunk-sleep (secs &optional value)
+ "Return an athunk that waits SECS seconds and then returns VALUE."
+ (lambda (cont)
+ (run-with-timer secs nil cont nil value)))
(defmacro athunk-condition-case (var form &rest handlers)
"Like `condition-case', but for asynchronous code.
@@ -710,7 +732,8 @@ being used."
(defun -aget-capability (account)
(athunk-memoize (-get-in -account-state account 'capability)
- (athunk-let* ((buffer (-amake-request account nil "CAPABILITY")))
+ (athunk-let*
+ ((buffer <- (-amake-request account nil "CAPABILITY")))
(with-current-buffer buffer
(-parse-capability)))))
@@ -718,35 +741,34 @@ being used."
(when refresh
(athunk-unmemoize (-get-in -account-state account 'mailboxes)))
(athunk-memoize (-get-in -account-state account 'mailboxes)
- (let* ((props (alist-get account minimail-accounts))
- (url (url-generic-parse-url (plist-get props :incoming-url)))
- (path (string-remove-prefix "/" (car (url-path-and-query url)))))
- (athunk-let*
- ((caps (-aget-capability account))
- (buffer (-amake-request
- account nil
- (format "LIST %s *%s"
- (-imap-quote path)
- (if (memq 'list-status caps)
- " RETURN (SPECIAL-USE STATUS (MESSAGES UIDNEXT
UNSEEN))" ;FIXME check special-use cap
- "")))))
+ (athunk-let*
+ ((props (alist-get account minimail-accounts))
+ (url (url-generic-parse-url (plist-get props :incoming-url)))
+ (path (string-remove-prefix "/" (car (url-path-and-query url))))
+ (caps <- (-aget-capability account))
+ (cmd (format "LIST %s *%s"
+ (-imap-quote path)
+ (if (memq 'list-status caps)
+ " RETURN (SPECIAL-USE STATUS (MESSAGES UIDNEXT
UNSEEN))" ;FIXME check special-use cap
+ "")))
+ (buffer <- (-amake-request account nil cmd)))
(with-current-buffer buffer
- (-parse-list))))))
+ (-parse-list)))))
(defun -aget-mailbox-status (account mailbox)
(athunk-let*
- ((buffer (-amake-request account nil
- (format "EXAMINE %s" (-imap-quote mailbox)))))
+ ((cmd (format "EXAMINE %s" (-imap-quote mailbox)))
+ (buffer <- (-amake-request account nil cmd)))
(with-current-buffer buffer
(-parse-select))))
(defun -afetch-id (account mailbox uid)
"Fetch a message ID given its UID, MAILBOX and ACCOUNT."
(athunk-let*
- ((buffer (-amake-request account mailbox
- (format "%sFETCH %s (UID)"
- (if uid "UID " "")
- (or uid "*")))))
+ ((buffer <- (-amake-request account mailbox
+ (format "%sFETCH %s (UID)"
+ (if uid "UID " "")
+ (or uid "*")))))
;;FIXME: uid=nil was supposed to retrieve the highest id, but
;;servers seem to implement some kind of caching that make it not
;;work.
@@ -755,20 +777,20 @@ being used."
(defun -afetch-mailbox (account mailbox num &optional end)
(athunk-let*
- ((status (-aget-mailbox-status account mailbox))
- (buffer (-amake-request account mailbox
- (let* ((endid (alist-get 'exists status))
- (last (if end (1- endid) endid)) ;FIXME?
- (first (max 1 (- last num -1))))
- (format "FETCH %s:%s (UID FLAGS RFC822.SIZE
ENVELOPE)"
- first last)))))
+ ((status <- (-aget-mailbox-status account mailbox))
+ (endid (alist-get 'exists status))
+ (last (if end (1- endid) endid)) ;FIXME?
+ (first (max 1 (- last num -1)))
+ (cmd (format "FETCH %s:%s (UID FLAGS RFC822.SIZE ENVELOPE)"
+ first last))
+ (buffer <- (-amake-request account mailbox cmd)))
(with-current-buffer buffer
(-parse-fetch))))
(defun -afetch-message (account mailbox uid)
(athunk-let*
- ((buffer (-amake-request account mailbox
- (format "UID FETCH %s (BODY[])" uid))))
+ ((cmd (format "UID FETCH %s (BODY[])" uid))
+ (buffer <- (-amake-request account mailbox cmd)))
(with-current-buffer buffer
(pcase-let* ((data (car (-parse-fetch)))
(`(,start . ,end) (alist-get 'content data)))
@@ -807,12 +829,12 @@ being used."
(defun -afetch-search (account mailbox query)
(athunk-let*
- ((sbuf (-amake-request account mailbox
- (concat "UID SEARCH CHARSET UTF-8 "
(-format-search query))))
- (fbuf (let ((uids (with-current-buffer sbuf (-parse-search))))
- (-amake-request account mailbox
- (format "UID FETCH %s (UID FLAGS RFC822.SIZE
ENVELOPE)"
- (mapconcat #'number-to-string uids
","))))))
+ ((sbuf <- (-amake-request account mailbox
+ (concat "UID SEARCH CHARSET UTF-8 "
(-format-search query))))
+ (uids (with-current-buffer sbuf (-parse-search)))
+ (fbuf <- (-amake-request account mailbox
+ (format "UID FETCH %s (UID FLAGS RFC822.SIZE
ENVELOPE)"
+ (mapconcat #'number-to-string uids
",")))))
(with-current-buffer fbuf
(-parse-fetch))))
@@ -824,13 +846,13 @@ being used."
(defun -amove-messages (account mailbox destination uids)
(athunk-let*
- ((caps (-aget-capability account))
- (_ (if (not (memq 'move caps))
- (error "Account %s doesn't support moving messages" account)
- (-amake-request account mailbox
- (format "UID MOVE %s %s"
- (-format-sequence-set uids)
- (-imap-quote destination))))))
+ ((caps <- (-aget-capability account))
+ (cmd (if (memq 'move caps)
+ (format "UID MOVE %s %s"
+ (-format-sequence-set uids)
+ (-imap-quote destination))
+ (error "Account %s doesn't support moving messages" account)))
+ (_ <- (-amake-request account mailbox cmd)))
t))
;;; Commands
@@ -885,25 +907,24 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(- (minibuffer-prompt-end) 1)))
(overlay-put ov 'display " (loading):")
(dolist (acct accounts)
- (let* ((buffer (current-buffer))
- (mkcand (pcase-lambda (`(,mbx . ,props))
- (unless (memq '\\Noselect (alist-get 'attributes
props))
- (propertize (-mailbox-display-name acct mbx)
- '-data `(,props ,acct . ,mbx))))))
- (athunk-do
- (athunk-let*
- ((mailboxes (athunk-condition-case err
- (-aget-mailbox-listing acct)
- (t (overlay-put ov 'display " (error):")
- (message "Error loading mailboxes for
account %s: %S"
- acct err)
- nil))))
- (when ov ;non-nil means we're still reading from minibuffer
- (setq cands (nconc (delq nil (mapcar mkcand mailboxes))
cands))
- (with-current-buffer buffer
- (run-hooks '-minibuffer-update-hook))
- (cl-remf accounts acct)
- (unless accounts (delete-overlay ov))))))))
+ (athunk-run
+ (athunk-let*
+ ((mkcand (pcase-lambda (`(,mbx . ,props))
+ (unless (memq '\\Noselect (alist-get 'attributes
props))
+ (propertize (-mailbox-display-name acct mbx)
+ '-data `(,props ,acct . ,mbx)))))
+ (mailboxes <- (athunk-condition-case err
+ (-aget-mailbox-listing acct)
+ (t (overlay-put ov 'display " (error):")
+ (message "Error loading mailboxes for
account %s: %S"
+ acct err)
+ nil))))
+ (when ov ;non-nil means we're still reading from minibuffer
+ (setq cands (nconc (delq nil (mapcar mkcand mailboxes))
cands))
+ (with-current-buffer (overlay-buffer ov)
+ (run-hooks '-minibuffer-update-hook))
+ (cl-remf accounts acct)
+ (unless accounts (delete-overlay ov)))))))
(let ((cand (unwind-protect
(completing-read prompt coll nil t nil
'minimail-mailbox-history)
(setq ov nil))))
@@ -965,13 +986,13 @@ Return a cons cell consisting of the account symbol and
mailbox name."
buffer)))
(defun -amove-messages-and-redisplay (account mailbox destination uids)
- (let ((prog (make-progress-reporter
- (format-message "Moving messages to `%s'..."
- (-mailbox-display-name account destination)))))
- (athunk-let*
- ((_ (-amove-messages account mailbox destination uids)))
- (progress-reporter-done prog)
- (when-let*
+ (athunk-let*
+ ((prog (make-progress-reporter
+ (format-message "Moving messages to `%s'..."
+ (-mailbox-display-name account destination))))
+ (_ <- (-amove-messages account mailbox destination uids)))
+ (progress-reporter-done prog)
+ (when-let*
((mbxbuf (seq-some (lambda (b)
(with-current-buffer b
(and (derived-mode-p 'minimail-mailbox-mode)
@@ -984,7 +1005,7 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(objs (vtable-objects table)))
(dolist (obj objs)
(when (memq (alist-get 'uid obj) uids)
- (vtable-remove-object table obj)))))))))
+ (vtable-remove-object table obj))))))))
(defun minimail-move-to-mailbox (&optional destination)
(interactive nil minimail-mailbox-mode minimail-message-mode)
@@ -994,7 +1015,7 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(format "Move %s messages to: " (length uids))))
(dest (or destination
(cdr (-read-mailbox prompt (list acct))))))
- (athunk-do (-amove-messages-and-redisplay acct mbx dest uids))))
+ (athunk-run (-amove-messages-and-redisplay acct mbx dest uids))))
(defun -find-mailbox-by-attribute (attr mailboxes)
(seq-some (pcase-lambda (`(,mbx . ,items))
@@ -1004,10 +1025,10 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(defun minimail-move-to-archive ()
(interactive nil minimail-mailbox-mode minimail-message-mode)
(pcase-let* ((`(,acct ,mbx ,uids) (-selected-messages)))
- (athunk-do
+ (athunk-run
(athunk-let*
- ((mailboxes (-aget-mailbox-listing acct))
- (_ (let ((dest (or (plist-get (alist-get acct minimail-accounts)
+ ((mailboxes <- (-aget-mailbox-listing acct))
+ (_ <- (let ((dest (or (plist-get (alist-get acct minimail-accounts)
:archive-mailbox)
(-find-mailbox-by-attribute '\\Archive mailboxes)
(-find-mailbox-by-attribute '\\All mailboxes)
@@ -1017,10 +1038,10 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(defun minimail-move-to-trash ()
(interactive nil minimail-mailbox-mode minimail-message-mode)
(pcase-let* ((`(,acct ,mbx ,uids) (-selected-messages)))
- (athunk-do
+ (athunk-run
(athunk-let*
- ((mailboxes (-aget-mailbox-listing acct))
- (_ (let ((dest (or (plist-get (alist-get acct minimail-accounts)
+ ((mailboxes <- (-aget-mailbox-listing acct))
+ (_ <- (let ((dest (or (plist-get (alist-get acct minimail-accounts)
:trash-mailbox)
(-find-mailbox-by-attribute '\\Trash mailboxes)
(user-error "Trash mailbox not found"))))
@@ -1029,10 +1050,10 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(defun minimail-move-to-junk ()
(interactive nil minimail-mailbox-mode minimail-message-mode)
(pcase-let* ((`(,acct ,mbx ,uids) (-selected-messages)))
- (athunk-do
+ (athunk-run
(athunk-let*
- ((mailboxes (-aget-mailbox-listing acct))
- (_ (let ((dest (or (plist-get (alist-get acct minimail-accounts)
+ ((mailboxes <- (-aget-mailbox-listing acct))
+ (_ <- (let ((dest (or (plist-get (alist-get acct minimail-accounts)
:junk-mailbox)
(-find-mailbox-by-attribute '\\Junk mailboxes)
(user-error "Junk mailbox not found"))))
@@ -1166,16 +1187,16 @@ Return a cons cell consisting of the account symbol and
mailbox name."
(user-error "This should be called only from a mailbox buffer."))
(let ((buffer (current-buffer)))
(setq -mode-line-suffix ":Loading")
- (athunk-do
+ (athunk-run
(let-alist -local-state
(athunk-let*
- ((messages (athunk-condition-case err
- (if .search
- (-afetch-search .account .mailbox .search)
- (-afetch-mailbox .account .mailbox 100))
- (t (with-current-buffer buffer
- (setq -mode-line-suffix ":Error"))
- (signal (car err) (cdr err))))))
+ ((messages <- (athunk-condition-case err
+ (if .search
+ (-afetch-search .account .mailbox .search)
+ (-afetch-mailbox .account .mailbox 100))
+ (t (with-current-buffer buffer
+ (setq -mode-line-suffix ":Error"))
+ (signal (car err) (cdr err))))))
(with-current-buffer buffer
(setq -mode-line-suffix nil)
(let ((inhibit-read-only t))
@@ -1341,13 +1362,13 @@ window shorter than 6 lines."
(setq -mode-line-suffix ":Loading")
(setf (alist-get 'next-message -local-state)
(list account mailbox uid))
- (athunk-do
+ (athunk-run
(athunk-let*
- ((msgbuf (athunk-condition-case err
- (-afetch-message account mailbox uid)
- (t (with-current-buffer buffer
- (setq -mode-line-suffix ":Error"))
- (signal (car err) (cdr err))))))
+ ((msgbuf <- (athunk-condition-case err
+ (-afetch-message account mailbox uid)
+ (t (with-current-buffer buffer
+ (setq -mode-line-suffix ":Error"))
+ (signal (car err) (cdr err))))))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when (equal (alist-get 'next-message -local-state)