branch: elpa/loopy
commit b7b2399f403532de07f7a50bf18a8462c9780a0e
Author: okamsn <[email protected]>
Commit: GitHub <[email protected]>
Fix detecting destructured `with`-bound variables. (#260)
Closes issues #259 and #252. See also for `loopy-dash` related PR
https://github.com/okamsn/loopy-dash/pull/4.
- In `loopy--pcase-destructure-for-iteration`:
- Return list of single variable when given. Previously, we were
mistakenly
returning the symbol instead of a list containing the symbol.
- Prepend the `rest` variable in the `lambda` function with an underscore
to silence a compiler warning.
- In `loopy--pcase-destructure-for-with-vars`:
- Change the return value from a list containing the symbol `pcase-let*`
and
the list of variable bindings to a list containing a list of found
variables
as symbols and a function that receives an expression and produces
wrapped
code correctly binding the variables.
- Add the argument `error`, like `loopy--pcase-destructure-for-with-vars`,
so that we signal `loopy-bad-run-time-destructuring` when desired.
- In `loopy--process-special-arg-with` and
`loopy-iter--process-special-arg-with`, set `loopy--with-vars`
to the new output of `loopy--pcase-destructure-for-with-vars`
instead of just the pairs given to the special macro argument.
- Update the documentation of `loopy--with-vars` to match the new output
of `loopy--pcase-destructure-for-with-vars`.
- Update `loopy-seq--destructure-for-with-vars` and
`loopy-pcase--destructure-for-with-vars` to match the new output
of `loopy--pcase-destructure-for-with-vars`.
- Update `loopy--with-bound-p` to use the new output
of `loopy--pcase-destructure-for-with-vars`.
- Update `loopy--expand-to-loop` to use the new output
of `loopy--pcase-destructure-for-with-vars`.
- Add tests `with-var-destructured-still-detected`,
`seq-with-var-destructured-still-detected`, and
`pcase-with-var-destructured-still-detected`
to make sure accumulation commands properly detect destructured
`with` variables.
---
lisp/loopy-destructure.el | 55 +++++++++++++++++++++++++-----
lisp/loopy-iter.el | 3 +-
lisp/loopy-seq.el | 15 +++++++--
lisp/loopy-vars.el | 26 ++++++++-------
lisp/loopy.el | 85 +++++++++++++----------------------------------
tests/pcase-tests.el | 12 +++++++
tests/seq-tests.el | 12 +++++++
tests/tests.el | 24 +++++++++++--
8 files changed, 144 insertions(+), 88 deletions(-)
diff --git a/lisp/loopy-destructure.el b/lisp/loopy-destructure.el
index 3624277dea9..6d211fecb06 100644
--- a/lisp/loopy-destructure.el
+++ b/lisp/loopy-destructure.el
@@ -900,12 +900,12 @@ Returns a list. The elements are:
If ERROR is non-nil, then signal an error in the produced code if
the pattern doesn't match."
(if (symbolp var)
- `((setq ,var ,val)
- ,var)
+ (list `(setq ,var ,val)
+ (list var))
(let* ((var-list nil)
(always-used-cases
(cons var (lambda (varvals &rest _)
- (cons 'setq (mapcan (pcase-lambda (`(,var ,val .
,rest))
+ (cons 'setq (mapcan (pcase-lambda (`(,var ,val .
,_rest))
(push var var-list)
(list var val))
varvals))))))
@@ -917,13 +917,52 @@ the pattern doesn't match."
(list always-used-cases)))
(seq-uniq var-list #'eq)))))
-(defun loopy--pcase-destructure-for-with-vars (bindings)
- "Return a way to destructure BINDINGS by `pcase-let*'.
+(cl-defun loopy--pcase-destructure-for-with-vars (bindings &key error)
+ "Get function to wrap code and destructure values in BINDINGS.
+
+Each binding in BINDINGS is a (VARIABLE VALUE) pair, where VARIABLE is a
+symbol or a `pcase' pattern. If VARIABLE is a symbol, then it is used
+directly. If ERROR is non-nil, then `loopy-bad-run-time-destructuring'
+is signaled if a binding does not match.
Returns a list of two elements:
-1. The symbol `pcase-let*'.
-2. A new list of bindings."
- (list 'pcase-let* bindings))
+1. A list of symbols being all the variables to be bound in BINDINGS.
+2. A function to be called with the code to be wrapped, which
+ should produce wrapped code appropriate for BINDINGS,
+ such as a `let*' form."
+ (let ((new-bindings nil)
+ (all-vars nil))
+ (pcase-dolist (`(,var ,val) bindings)
+ (if (symbolp var)
+ (progn
+ (cl-callf2 cl-adjoin var all-vars :test #'eq)
+ (push `(nil (,var ,val))
+ new-bindings))
+ ;; `loopy--pcase-destructure-for-iteration' does not return any capture
+ ;; variables that `pcase' might use, so we need to `let' bind our own
+ ;; capture variable before we `let' bind the found variables, to avoid
+ ;; hiding any needed variable values when binding the found variables
to
+ ;; `nil'.
+ (let ((capture-var (gensym "loopy--with-capture")))
+ (pcase-let ((`(,setter ,found-vars)
+ (loopy--pcase-destructure-for-iteration
+ var capture-var
+ :error error)))
+ (cl-callf cl-union all-vars found-vars :test #'eq)
+ (push `(,setter
+ (,capture-var ,val)
+ ,@(cl-loop for v in found-vars
+ collect `(,v nil)))
+ new-bindings)))))
+ (list all-vars
+ (lambda (body)
+ (let ((result (macroexp-progn body)))
+ (dolist (b new-bindings)
+ (setq result
+ `(let ,(cdr b)
+ ,(car b)
+ ,result)))
+ result)))))
(cl-defun loopy--pcase-parse-for-destructuring-accumulation-command
((name var val &rest args) &key error)
diff --git a/lisp/loopy-iter.el b/lisp/loopy-iter.el
index 237bd4484b0..f62ce79b413 100644
--- a/lisp/loopy-iter.el
+++ b/lisp/loopy-iter.el
@@ -389,7 +389,8 @@ Returns BODY without the `%s' argument."
((= 1 (length binding)) (list (cl-first binding) nil))
(t binding)))
(finally-do
- (setq loopy--with-vars loopy-result))))
+ (setq loopy--with-vars (loopy--destructure-for-with-vars
+ loopy-result)))))
(loopy-iter--def-special-processor finally-return
diff --git a/lisp/loopy-seq.el b/lisp/loopy-seq.el
index 28bc99cf56a..5e664f4f40e 100644
--- a/lisp/loopy-seq.el
+++ b/lisp/loopy-seq.el
@@ -85,9 +85,18 @@
"Return a way to destructure BINDINGS as if by a `seq-let*'.
Returns a list of two elements:
-1. The symbol `loopy-seq--seq-let*'.
-2. A new list of bindings."
- (list 'loopy-seq--seq-let* bindings))
+1. A list of symbols being all the variables to be bound in BINDINGS.
+2. A function to be called with the code to be wrapped, which
+ should produce wrapped code appropriate for BINDINGS,
+ such as a `let*' form."
+ (loopy--pcase-destructure-for-with-vars
+ (cl-loop for b in bindings
+ for (var val) = b
+ collect (if (seqp var)
+ `(,(loopy-seq--make-pcase-pattern var)
+ ,val)
+ b))
+ :error nil))
(defmacro loopy-seq--seq-let* (bindings &rest body)
"Bind variables in BINDINGS according via `seq-let' and `let'.
diff --git a/lisp/loopy-vars.el b/lisp/loopy-vars.el
index ed0f262532c..d68d645cb3e 100644
--- a/lisp/loopy-vars.el
+++ b/lisp/loopy-vars.el
@@ -425,14 +425,20 @@ This is used to check for errors with the `at' command.")
(defvar loopy--with-vars nil
"With Forms are variables explicitly created using the `with' keyword.
-This is a list of ((VAR1 VAL1) (VAR2 VAL2) ...). If VAR is a
-sequence, then it will be destructured. How VAR and VAL are
-used, as well as how the bindings are expanded into the loop's
-surrounding code, is determined by the destructuring system being
-used.
+This is a list of the form (VARIABLES BINDING-FUNCTION). VARIABLES
+is a list of symbols naming which variables are found in the bindings,
+including destructured bindings. BINDING-FUNCTION is a function
+that will receive code to be wrapped in a `let'-like form
+and should return an expression binding the VARIABLES and setting
+their values.
They are created by passing (with (VAR1 VAL1) (VAR2 VAL2) ...) to
-`loopy'.")
+`loopy'.
+
+Because it can affect expansion of the loop commands,
+`loopy--with-vars' is by `loopy--process-special-arg-with',
+which uses `loopy--destructure-for-with-vars' and the destructuring
+flags found by `loopy--process-special-arg-flag'.")
(defvar loopy--without-vars nil
"A list of variables that `loopy' won't try to initialize.
@@ -739,12 +745,8 @@ This list is mainly fed to the macro
`loopy--wrap-variables-around-body'."))
Some iteration commands (e.g., `reduce') will change their behavior
depending on whether the accumulation variable is given an initial
value."
- (or (cl-loop for (var val) in loopy--with-vars
- when (eq var var-name)
- return (cons 'with val))
- (cl-loop for x in loopy--without-vars
- when (eq x var-name)
- return (cons 'without nil))))
+ (or (memq var-name (car-safe loopy--with-vars))
+ (memq var-name loopy--without-vars)))
(defun loopy--command-bound-p (var-name)
"Whether VAR-NAME was bound by a command (and not a special macro argument).
diff --git a/lisp/loopy.el b/lisp/loopy.el
index 4d1172a990d..f5c2eea7845 100644
--- a/lisp/loopy.el
+++ b/lisp/loopy.el
@@ -156,69 +156,34 @@ this means that an explicit \"nil\" is always required."
(error "Invalid binding in `loopy' expansion: %s" binding)))
(defun loopy--destructure-for-with-vars (bindings)
- "Destructure BINDINGS into bindings suitable for something like `let*'.
+ "Get function to wrap code and destructure values in BINDINGS.
This function named by this variables receives the bindings given
to the `with' macro argument and should usually return a list of
two elements:
-1. A function/macro that works like `let*' and can be used to wrap
- the expanded macro code.
-2. The bindings that will be given to this macro.
-
-For example, an acceptable return value might be something like
-
- (list \\='pcase-let* BINDINGS)
-
-which will be used to wrap the loop and other code."
+1. A list of symbols being all the variables to be bound in BINDINGS.
+2. A function to be called with the code to be wrapped, which
+ should produce wrapped code appropriate for BINDINGS,
+ such as a `let*' form."
(funcall (or loopy--destructuring-for-with-vars-function
#'loopy--destructure-for-with-vars-default)
bindings))
(defun loopy--destructure-for-with-vars-default (bindings)
- "Destructure BINDINGS into bindings suitable for something like `let*'.
+ "Get function to wrap code and destructure values in BINDINGS.
Returns a list of two elements:
-1. The symbol `pcase-let*'.
-2. A new list of bindings."
- ;; We do this instead of passing to `pcase-let*' so that:
- ;; 1) We sure that variables are bound even when unmatched.
- ;; 2) We can signal an error if the pattern doesn't match a value.
- ;; This keeps the behavior of the old implementation.
- ;;
- ;; Note: Binding the found variables to `nil' would overwrite any values that
- ;; we might try to access while binding, so we can't do that like we do
- ;; for iteration commands in which we already know the scope.
- ;; (let ((new-binds)
- ;; (all-set-exprs))
- ;; (dolist (bind bindings)
- ;; (cl-destructuring-bind (var val)
- ;; bind
- ;; (if (symbolp var)
- ;; (push `(,var ,val) new-binds)
- ;; (let ((sym (gensym)))
- ;; (push `(,sym ,val) new-binds)
- ;; (cl-destructuring-bind (set-expr found-vars)
- ;; (loopy--pcase-destructure-for-iteration `(loopy ,var) sym
:error t)
- ;; (dolist (v found-vars)
- ;; (push `(,v nil) new-binds))
- ;; (push set-expr all-set-exprs))))))
- ;; (list 'let* (nreverse new-binds) (macroexp-progn (nreverse
- ;; all-set-exprs))))
- (let ((new-binds))
- (dolist (bind bindings)
- (cl-destructuring-bind (var val)
- bind
- (if (symbolp var)
- (push `(,var ,val) new-binds)
- (let ((sym (gensym)))
- (push `(,sym ,val) new-binds)
- (cl-destructuring-bind (set-expr found-vars)
- (loopy--pcase-destructure-for-iteration `(loopy ,var) sym
:error t)
- (dolist (v found-vars)
- (push `(,v nil) new-binds))
- (push `(_ ,set-expr) new-binds))))))
- (list 'let* (nreverse new-binds))))
+1. A list of symbols being all the variables to be bound in BINDINGS.
+2. A function to be called with the code to be wrapped, which
+ should produce wrapped code appropriate for BINDINGS,
+ such as a `let*' form."
+ (loopy--pcase-destructure-for-with-vars (cl-loop for b in bindings
+ for (var val) = b
+ collect (if (symbolp var)
+ b
+ `((loopy ,var)
,val)))
+ :error t))
;;;; The Macro Itself
(defun loopy--expand-to-loop ()
@@ -426,8 +391,7 @@ The function creates quoted code that should be used by a
macro."
;; Declare the With variables.
(when loopy--with-vars
- (setq result `(,@(loopy--destructure-for-with-vars loopy--with-vars)
- ,@(get-result))
+ (setq result (funcall (cl-second loopy--with-vars) (get-result))
result-is-one-expression t))
;; Declare the symbol macros.
@@ -527,14 +491,13 @@ Returns BODY without the `%s' argument."
(loopy--def-special-processor with
(setq loopy--with-vars
- ;; Note: These values don't have to be used literally, due to
- ;; destructuring.
- (mapcar (lambda (binding)
- (cond ((symbolp binding) (list binding nil))
- ((= 1 (length binding)) (list (cl-first binding)
- nil))
- (t binding)))
- arg-value))
+ (loopy--destructure-for-with-vars
+ (mapcar (lambda (binding)
+ (cond ((symbolp binding) (list binding nil))
+ ((= 1 (length binding)) (list (cl-first binding)
+ nil))
+ (t binding)))
+ arg-value)))
(seq-remove (lambda (x) (eq (car x) arg-name)) body))
(loopy--def-special-processor without
diff --git a/tests/pcase-tests.el b/tests/pcase-tests.el
index 60f2ee0e5a7..b6257f77324 100644
--- a/tests/pcase-tests.el
+++ b/tests/pcase-tests.el
@@ -112,3 +112,15 @@
(should-not loopy--destructuring-for-with-vars-function)
(should-not loopy--destructuring-for-iteration-function)
(should-not loopy--destructuring-accumulation-parser))
+
+(ert-deftest pcase-with-var-destructured-still-detected ()
+ "Make sure destructured `with' variables are still detected by other
commands.
+For example, make sure we don't see an error for incompatible accumulations
+since we are binding `acc' in `with'."
+ (should (= 45 (eval '(loopy (flag pcase)
+ (with (`(,acc ,b) '(3 4)))
+ (list i '(1 2 3))
+ (sum acc i)
+ (multiply acc i)
+ (finally-return acc))
+ t))))
diff --git a/tests/seq-tests.el b/tests/seq-tests.el
index abf0e54273c..b47aee395fc 100644
--- a/tests/seq-tests.el
+++ b/tests/seq-tests.el
@@ -114,3 +114,15 @@
(should-not loopy--destructuring-for-with-vars-function)
(should-not loopy--destructuring-for-iteration-function)
(should-not loopy--destructuring-accumulation-parser))
+
+(ert-deftest seq-with-var-destructured-still-detected ()
+ "Make sure destructured `with' variables are still detected by other
commands.
+For example, make sure we don't see an error for incompatible accumulations
+since we are binding `acc' in `with'."
+ (should (= 45 (eval '(loopy (flag seq)
+ (with ([acc b] '(3 4)))
+ (list i '(1 2 3))
+ (sum acc i)
+ (multiply acc i)
+ (finally-return acc))
+ t))))
diff --git a/tests/tests.el b/tests/tests.el
index fa67acf2369..5686a7529c4 100644
--- a/tests/tests.el
+++ b/tests/tests.el
@@ -41,7 +41,7 @@
args doc repeat body multi-body
repeat-loopy repeat-iter-bare repeat-iter-keyword
wrap
- macroexpand
+ (macroexpand nil macroexpand-provided)
(loopy nil loopy-provided)
(iter-bare nil iter-bare-provided)
(iter-keyword nil iter-keyword-provided)
@@ -98,7 +98,7 @@ prefix the items in LOOPY or ITER-BARE."
(declare (indent 1))
(unless (or result-provided error-provided should-provided)
- (error "Must include `result' or `error'"))
+ (error "Must include `result', `error', or `should' (even for
`macroexpand')"))
(unless (or loopy iter-bare iter-keyword)
(error "Must include `loopy' or `iter-bare'"))
(unless body
@@ -138,7 +138,9 @@ prefix the items in LOOPY or ITER-BARE."
(output-wrap (x) (cond (should-provided `(should ,x))
(result-provided `(should (equal ,result ,x)))
(error-provided `(should-error ,x :type
- (quote
,error)))))
+ (quote ,error)))
+ (t
+ (error "Didn't specify how to wrap output
(`result', `should', etc.)"))))
;; Replace given placeholder command names with actual names,
;; maybe including the `for' keyword for `loopy-iter'.
(translate (group-alist this-body &optional keyword)
@@ -396,6 +398,22 @@ writing a `seq-do' method for the custom seq."
:loopy t
:iter-bare ((return . returning)))
+(loopy-deftest with-var-destructured-still-detected
+ :doc "Make sure destructured `with' variables are still detected by other
commands.
+For example, make sure we don't see an error for incompatible accumulations
+since we are binding `acc' in `with'."
+ :result 45
+ :body ((with ((acc b) '(3 4)))
+ (list i '(1 2 3))
+ (sum acc i)
+ (multiply acc i)
+ (finally-return acc))
+ :loopy t
+ :iter-keyword (list sum multiply)
+ :iter-bare ((list . listing)
+ (sum . summing)
+ (multiply . multiplying)))
+
;;;; Without
(loopy-deftest without
:result '(4 5)