From 7c630fbb16d61df67d0f6e6774075786ee4ae737 Mon Sep 17 00:00:00 2001 From: okamsn Date: Mon, 2 Oct 2023 22:27:51 -0400 Subject: [PATCH] Fix use of `macroexpand-all` in `loopy-iter` and `loopy`. We should always be passing an environment to the function, otherwise the environment is reset to nil, which breaks macros like `cl-flet`. - Stop doing top-level and sub-level expansion with different functions. - Instead, add `loopy-iter--level`. - Remove `loopy-iter--sub-level-expanders`, `loopy-iter--top-level-expanders`, `loopy-iter--macroexpand-sub`, `loopy-iter--macroexpand-top`, `loopy-iter--keyword-expander-sub`, and `loopy-iter--keyword-expander-top`. - Pass the macro temporary environment in all places where we didn't before: - `loopy-iter` - `loopy` - `loopy-iter--parse-at-command` - `loopy-iter--opt-accum-expand-val` - Add a `loopy--with-protected-stack` and `loopy--bind-main-body`. - Add tests. --- CHANGELOG.md | 17 +++ README.org | 2 + loopy-commands.el | 8 ++ loopy-iter.el | 330 +++++++++++++++++++++----------------------- loopy-misc.el | 1 + loopy.el | 9 +- tests/iter-tests.el | 2 - tests/tests.el | 90 ++++++++++++ 8 files changed, 287 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39de0b30..83685f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,22 @@ This document describes the user-facing changes to Loopy. - Better signal an error with conflicting arguments in `numbers`. See [#172]. +- Fix macro expansion in some cases by not resetting the macro environment + ([#173]). For example, we failed to pass the current/modified environment to + `macroexpand-all` in some cases. + + ```emacs-lisp + ;; Previously failed to use `cl-flet''s internally defined function + ;; for `10+': + ;; => (11 12 13 14 15) + (loopy (named outer) + (list i '((1 2) (3 4) (5))) + (loopy-iter (listing j i) + (cl-flet ((10+ (y) (+ 10 y))) + (at outer + (collecting (10+ j)))))) + ``` + ### Breaking Changes - Make it an error to re-use iteration variables with multiple iteration @@ -172,6 +188,7 @@ This document describes the user-facing changes to Loopy. [#164]: https://github.com/okamsn/loopy/pull/164 [#165]: https://github.com/okamsn/loopy/pull/165 [#171]: https://github.com/okamsn/loopy/pull/172 +[#173]: https://github.com/okamsn/loopy/pull/173 ## 0.11.2 diff --git a/README.org b/README.org index 1c230271..950bd981 100644 --- a/README.org +++ b/README.org @@ -56,6 +56,8 @@ please let me know. - The non-keyword arguments of =numbers= are deprecated. Use the keyword arguments instead. A =:test= keyword argument was added, which is more flexible and explicit than the non-keyword arguments. + - Fixed a problem with macro expansion in some cases for sub-macros + that created a new macro environment (e.g., ~cl-flet~). - Versions 0.11.1 and 0.11.2: None. Bug fixes. - Version 0.11.0: - More incorrect destructured bindings now correctly signal an error. diff --git a/loopy-commands.el b/loopy-commands.el index abd35e62..686cc75c 100644 --- a/loopy-commands.el +++ b/loopy-commands.el @@ -114,6 +114,14 @@ The lists will be in the order parsed (correct for insertion)." ;; Return the sub-lists. (list (nreverse wrapped-main-body) (nreverse other-instructions)))) +;; We find ourselves doing this pattern a lot. +(cl-defmacro loopy--bind-main-body ((main-expr other-instrs) value &rest body) + "Bind MAIN-EXPR and OTHER-INSTRS for those items in VALUE for BODY." + (declare (indent 2)) + `(cl-destructuring-bind (,main-expr ,other-instrs) + (loopy--extract-main-body ,value) + ,@body)) + (defun loopy--convert-iteration-vars-to-other-vars (instructions) "Convert instructions for `loopy--iteration-vars' to `loopy--other-vars'. diff --git a/loopy-iter.el b/loopy-iter.el index ce23d4b2..ce6a698e 100644 --- a/loopy-iter.el +++ b/loopy-iter.el @@ -47,6 +47,11 @@ ;; Previously, `loopy-iter' used its tree-walking functions, as done by CL's ;; Iterate, but now it just defers to what Emacs already does when expanding ;; macros, such as in `macroexpand-all'. +;; +;; NOTE: When using `macroexpand-all', always pass an environment like +;; `macroexpand-all-environment'. Otherwise, the environment is +;; set to nil, which breaks any macros that build their own environment +;; while expanding commands. For example, `cl-flet'. ;;;; Custom User Options (defgroup loopy-iter nil @@ -169,7 +174,9 @@ This variable is bound while `loopy-iter' is running, combining `loopy-iter-overwritten-command-parsers'.") (defun loopy-iter--parse-command (command) - "An Iter version of `loopy--parse-loop-command'." + "Parse COMMAND using parsers in`loopy-iter--command-parsers'. + +See also `loopy--parse-loop-command'." (let* ((cmd-name (cl-first command)) (parser (loopy--get-command-parser cmd-name @@ -182,61 +189,47 @@ This variable is bound while `loopy-iter' is running, combining (defvar loopy-iter--non-main-body-instructions nil "Used to capture other instructions while expanding. -Expanding functions `push' lists of instructions into this variable.") +Expanding functions `push' lists of instructions into this +variable. The contents of main-body instructions are inserted +into the expanded body in the command's place during macro +expansion.") ;;;;; Expanders -(defvar loopy-iter--sub-level-expanders nil - "Macro expanders for sub-level expressions.") - -(defvar loopy-iter--top-level-expanders nil - "Macro expanders for top-level expressions.") - -(defun loopy-iter--macroexpand-top (expr) - "Expand a top-level expression using `loopy-iter--top-level-expanders'" - (macroexpand-1 expr loopy-iter--top-level-expanders)) - -(defun loopy-iter--macroexpand-sub (expr) - "Expand a top-level expression using `loopy-iter--sub-level-expanders'" - (macroexpand-all expr loopy-iter--sub-level-expanders)) - -(defun loopy-iter--keyword-expander-top (&rest args) - "Expand top-level commands preceded by keywords in `loopy-iter-keywords'." - (cl-destructuring-bind (main other) - (loopy--extract-main-body - (loopy-iter--parse-command args)) - (push other loopy-iter--non-main-body-instructions) - (macroexp-progn main))) - -(defun loopy-iter--keyword-expander-sub (&rest args) - "Expand sub-level commands preceded by keywords in `loopy-iter-keywords'." - (cl-destructuring-bind (main other) - (loopy--extract-main-body - (let ((loopy--in-sub-level t)) - (loopy-iter--parse-command args))) - (push other loopy-iter--non-main-body-instructions) - (macroexp-progn main))) +(defvar loopy-iter--level nil + "The level of the expression `loopy-iter' is currently processing. + +For example, iteration commands should only be processed during +level 1, which is the top level. The next level of nesting is +level 2, and so on. If `loopy-iter--level' is greater than 1, +then `loopy--in-sub-level' is set to `t'. + +The macro initially `let'-binds this variable to 0, and it is +incremented upon parsing a new function.") (defun loopy-iter--opt-accum-expand-val (arg) - "Macro expand only the value of an optimized accumulation. + "Macro expand only the value of the optimized accumulation expression ARG. Optimized accumulations are expanded into a special form, after which this function will recursively expand the expression of the accumulated value. To avoid an infinite loop, this function replaces the `loopy--optimized-accum' -in the expression with `loopy--optimized-accum-2'." +in the expression with `loopy--optimized-accum-2', which is then processed +during a second pass on the expanded code." (loopy (with (plist (cadr arg))) (cons (k v) plist :by #'cddr) (collect k) ;; By this point, command expansion are already defined, so we don't ;; need to try to handle instructions. (collect (if (eq k :val) - (macroexpand-all v loopy-iter--sub-level-expanders) + (let ((loopy-iter--level (1+ loopy-iter--level)) + (loopy--in-sub-level t)) + (macroexpand-all v macroexpand-all-environment)) v)) (finally-return `(loopy--optimized-accum-2 (quote ,loopy-result))))) -;;;;; Overwritten definitions +;;;;; Overwritten parser definitions (defcustom loopy-iter-overwritten-command-parsers '((at . loopy-iter--parse-at-command)) @@ -258,15 +251,17 @@ These commands affect other loops higher up in the call list." (loopy--check-target-loop-name target-loop) ;; We need to capture all non-main-body instructions into a new `at' ;; instruction, so we just temporarily `let'-bind - ;; `loopy-iter--non-main-body-instructions' while expanders push to it, - ;; we which then wrap back in a new instruction and pass up to the calling - ;; function, which consumes instructions. + ;; `loopy-iter--non-main-body-instructions' while the expanding functions push + ;; to it, which we then wrap back in a new instruction and pass up to the + ;; calling function, which consumes instructions. (loopy (with (loopy-iter--non-main-body-instructions nil) (loopy--loop-name target-loop) - (loopy--in-sub-level t)) + (loopy--in-sub-level t) + (loopy-iter--level (1+ loopy-iter--level))) (list cmd commands) - (collect (list 'loopy--main-body - (loopy-iter--macroexpand-sub cmd))) + (collect (list 'loopy--main-body (macroexpand-all + cmd + macroexpand-all-environment))) (finally-return ;; Return list of instructions to comply with expectations of calling ;; function, which thinks that this is a normal loop-command parser. @@ -444,7 +439,6 @@ for other reasons. The macros `cl-block', `cl-return-from', and `cl-return' are known to fall into the first group.") ;;;; The macro itself - (defmacro loopy-iter (&rest body) "Allows embedding loop commands in arbitrary code within this macro's body. @@ -462,137 +456,135 @@ on how to use `loopy-iter'. See the Info node `(loopy)' for how to use `loopy' in general. \(fn CODE-or-COMMAND...)" + ;; We expand the code in BODY in two passes. The macro works like this: + ;; + ;; 1) Like normal, process the special macro arguments. + ;; + ;; 2) Set up the environments used for macro expansion. These are alists of + ;; macro names and functions that process the macro arguments. The + ;; functions only receive the arguments, not the name. + ;; + ;; 3) Parse commands and process the initial resulting instructions. The + ;; result is a new body with the commands expanded, but the optimized + ;; accumulations incomplete and `at' instructions are not finished. + ;; + ;; 4) Parse the optimized accumulations and process the `at' instructions the + ;; resulted from processing the commands and instructions in Step 3. + ;; + ;; 5) Set `loopy--main-body' to the now expanded expressions (as a list, no + ;; `macroexpand-progn'). + ;; + ;; 6) Then we manipulate the variables and build the loop like normal, as we + ;; do in `loopy'. + (loopy--wrap-variables-around-body (mapc #'loopy--apply-flag loopy-default-flags) - (setq body (loopy-iter--process-special-arg-loop-name body)) - (setq body (loopy-iter--process-special-arg-flag body)) - (setq body (loopy-iter--process-special-arg-with body)) - (setq body (loopy-iter--process-special-arg-without body)) - (setq body (loopy-iter--process-special-arg-accum-opt body)) - (setq body (loopy-iter--process-special-arg-wrap body)) - (setq body (loopy-iter--process-special-arg-before-do body)) - (setq body (loopy-iter--process-special-arg-after-do body)) - (setq body (loopy-iter--process-special-arg-finally-do body)) - (setq body (loopy-iter--process-special-arg-finally-return body)) - (setq body (loopy-iter--process-special-arg-finally-protect body)) - - ;; Process the main body. - (unwind-protect - (progn - (let ((suppressed-alist (loopy (list i loopy-iter-suppressed-macros) + (setq body (thread-first body + loopy-iter--process-special-arg-loop-name + loopy-iter--process-special-arg-flag + loopy-iter--process-special-arg-with + loopy-iter--process-special-arg-without + loopy-iter--process-special-arg-accum-opt + loopy-iter--process-special-arg-wrap + loopy-iter--process-special-arg-before-do + loopy-iter--process-special-arg-after-do + loopy-iter--process-special-arg-finally-do + loopy-iter--process-special-arg-finally-return + loopy-iter--process-special-arg-finally-protect)) + + (loopy--with-protected-stack + (let* ((suppressed-expanders (loopy (list i loopy-iter-suppressed-macros) (collect (cons i nil)))) - (loopy-iter--command-parsers - (or loopy-iter--command-parsers - (append loopy-iter-overwritten-command-parsers - loopy-command-parsers)))) - - ;; During the initial top-level expansion and the subsequent - ;; all-level expansion, we make an effort to keep instructions in the - ;; same order that they are received. This might help to avoid - ;; unexpected behavior regarding variable declarations. For example, - ;; if the top level of a following expression refers back to a - ;; variable initialized in a preceeding sub-expression. - (let ((loopy-iter--non-main-body-instructions) - (cmd-alist-1) - (cmd-alist-sub) - (keyword-alist-1) - (keyword-alist-sub)) - - ;; Entries for command names. - (dolist (cmd loopy-iter-bare-commands) - (let ((cmd cmd)) - (push (cons cmd (lambda (&rest args) - (cl-destructuring-bind (main other) - (loopy--extract-main-body - (loopy-iter--parse-command - (cons cmd args))) - (push other - loopy-iter--non-main-body-instructions) - (macroexp-progn main)))) - cmd-alist-1) - (push (cons cmd (lambda (&rest args) - (cl-destructuring-bind (main other) - (loopy--extract-main-body - (let ((loopy--in-sub-level t)) - (loopy-iter--parse-command - (cons cmd args)))) - (push other - loopy-iter--non-main-body-instructions) - (macroexp-progn main)))) - cmd-alist-sub))) - - ;; Entries for keyword commands - (dolist (keyword loopy-iter-keywords) - (push (cons keyword #'loopy-iter--keyword-expander-top) - keyword-alist-1) - (push (cons keyword #'loopy-iter--keyword-expander-sub) - keyword-alist-sub)) - - (let* ((loopy-iter--top-level-expanders - `(,@suppressed-alist - (loopy--optimized-accum . loopy-iter--opt-accum-expand-val) - (loopy--optimized-accum-2 . nil) - ,@cmd-alist-1 - ,@keyword-alist-1)) - (loopy-iter--sub-level-expanders - `(,@suppressed-alist - (loopy--optimized-accum . loopy-iter--opt-accum-expand-val) - (loopy--optimized-accum-2 . nil) - ,@cmd-alist-sub - ,@keyword-alist-sub - ,@macroexpand-all-environment))) - - ;; Now process the main body. - (loopy (accum-opt new-body) - (list expr body) - (collect new-body - (thread-last expr - loopy-iter--macroexpand-top - loopy-iter--macroexpand-sub)) - (finally-do - (setq loopy--main-body new-body) - (loopy--process-instructions - (thread-last loopy-iter--non-main-body-instructions - nreverse - (apply #'append))))))) - - ;; Expand any uses of `loopy--optimized-accum' as if it were a macro, - ;; using the function `loopy--expand-optimized-accum'. - (loopy - ;; TODO: - ;; - Is there a way to only expand `loopy--optimized-accum'? - (with (macro-funcs `(,@suppressed-alist - ;; Identify second version of optimized accumulation. - (loopy--optimized-accum-2 . loopy--expand-optimized-accum) - ,@macroexpand-all-environment))) - (list i loopy--main-body) - (collect (macroexpand-all i macro-funcs)) - (finally-do (setq loopy--main-body loopy-result)))) - - - (loopy--process-instructions (map-elt loopy--at-instructions - loopy--loop-name) - :erroring-instructions - '(loopy--main-body))) - (loopy--clean-up-stack-vars)) - - ;; Make sure the order-dependent lists are in the correct order. - (loopy--correct-var-structure :exclude-main-body t) - ;; (setq loopy--iteration-vars (nreverse loopy--iteration-vars) - ;; loopy--accumulation-vars (nreverse loopy--accumulation-vars) - ;; loopy--implicit-return (when (consp loopy--implicit-return) - ;; (if (= 1 (length loopy--implicit-return)) - ;; ;; If implicit return is just a single thing, - ;; ;; don't use a list. - ;; (car loopy--implicit-return) - ;; ;; If multiple items, be sure to use a list - ;; ;; in the correct order. - ;; `(list ,@(nreverse loopy--implicit-return))))) - - ;; Produce the expanded code, based on the `let'-bound variables. - (loopy--expand-to-loop))) + (loopy-iter--command-parsers (or loopy-iter--command-parsers + (append loopy-iter-overwritten-command-parsers + loopy-command-parsers))) + (loopy-iter--non-main-body-instructions) + (loopy-iter--level 0) + (command-env + (append (loopy (list keyword loopy-iter-keywords) + (collect + (cons keyword + (lambda (&rest args) + (loopy--bind-main-body (main other) + ;; Bind here in case a command required to be + ;; in the top level is found in an expression + ;; while parsing an actual top-level command. + (let* ((loopy-iter--level (1+ loopy-iter--level)) + (loopy--in-sub-level (> loopy-iter--level 1))) + (loopy-iter--parse-command args)) + (push other loopy-iter--non-main-body-instructions) + (macroexp-progn main)))))) + (loopy (list command loopy-iter-bare-commands) + (collect + (cons command + ;; Expanding functions do not receive the head + ;; of the expression, only the arguments, so + ;; we use a lexical lambda to include that + ;; information. + (let ((cmd command)) + (lambda (&rest args) + (loopy--bind-main-body (main other) + ;; Bind here in case a command required to + ;; be in the top level is found in an + ;; expression while parsing an actual + ;; top-level command. + (let* ((loopy-iter--level (1+ loopy-iter--level)) + (loopy--in-sub-level (> loopy-iter--level 1))) + (loopy-iter--parse-command (cons cmd args))) + (push other loopy-iter--non-main-body-instructions) + (macroexp-progn main))))))))) + (common-env `(,@suppressed-expanders + ,@command-env + ,@macroexpand-all-environment)) + (first-pass-env `((loopy--optimized-accum . loopy-iter--opt-accum-expand-val) + (loopy--optimized-accum-2 . nil) + ,@common-env)) + (second-pass-env `(;; Identify second version of optimized accumulation. + (loopy--optimized-accum-2 . loopy--expand-optimized-accum) + ,@common-env))) + + (cl-labels (;; A wrapper to set `loopy--in-sub-level' correctly: + ;; If this is a known command, expand as normal. The command + ;; parser will handle sub-level-ness. Otherwise, while EXPR + ;; isn't a command itself, bind `loopy--in-sub-level' in case + ;; of any commands further down. + (iter-macroexpand-all (expr) + (if (map-elt command-env (car expr)) + (macroexpand-all expr first-pass-env) + (let ((loopy-iter--level (1+ loopy-iter--level)) + (loopy--in-sub-level t)) + (macroexpand-all expr first-pass-env)))) + ;; Process body, insert data for optimized accumulations, + ;; then process the other instructions: + (first-pass (body) + (prog1 + (mapcar #'iter-macroexpand-all body) + (loopy--process-instructions + (thread-last loopy-iter--non-main-body-instructions + nreverse + (apply #'append))))) + ;; Expand the optimized accumulation variables, + ;; then process the `at' instructions for this loop: + (second-pass (body) + (prog1 + (mapcar (lambda (expr) (macroexpand-all expr second-pass-env)) + body) + (loopy--process-instructions (map-elt loopy--at-instructions + loopy--loop-name) + :erroring-instructions + '(loopy--main-body))))) + (setq loopy--main-body + (thread-first body + first-pass + second-pass))) + + ;; Make sure the order-dependent lists are in the correct order. + (loopy--correct-var-structure :exclude-main-body t) + + ;; Produce the expanded code, based on the `let'-bound variables. + (loopy--expand-to-loop))))) ;;;; Add `loopy-iter' to `loopy' (cl-defun loopy-iter--parse-loopy-iter-command ((_ &rest body)) diff --git a/loopy-misc.el b/loopy-misc.el index 255d7662..45cdbf8e 100644 --- a/loopy-misc.el +++ b/loopy-misc.el @@ -309,6 +309,7 @@ splitting (1 2 3) or (1 2 . 3) returns ((1 2) 3)." ;;;; Destructuring + ;; This better allows for things to change in the future. (defun loopy--var-ignored-p (var) "Return whether VAR should be ignored." diff --git a/loopy.el b/loopy.el index 09d7a06e..1b172232 100644 --- a/loopy.el +++ b/loopy.el @@ -654,6 +654,12 @@ code and must instead be cleaned up manually." (cl-callf2 seq-drop-while (lambda (x) (eq loopy--loop-name (caar x))) loopy--accumulation-variable-info)) +(defmacro loopy--with-protected-stack (&rest body) + "Protect the stack variables from BODY during unwind and cleanup." + `(unwind-protect + ,(macroexp-progn body) + (loopy--clean-up-stack-vars))) + ;;;;; Process Instructions (cl-defun loopy--process-instruction (instruction &key erroring-instructions) "Process INSTRUCTION, assigning values to the variables in `loopy--variables'. @@ -1003,7 +1009,8 @@ see the Info node `(loopy)' distributed with this package." with macro-funcs = `(,@(cl-loop for i in loopy--suppressed-macros collect (cons i nil)) (loopy--optimized-accum - . loopy--expand-optimized-accum)) + . loopy--expand-optimized-accum) + ,@macroexpand-all-environment) for i in loopy--main-body collect (macroexpand-all i macro-funcs))) diff --git a/tests/iter-tests.el b/tests/iter-tests.el index 5f0cd2d1..d3c93bfd 100644 --- a/tests/iter-tests.el +++ b/tests/iter-tests.el @@ -762,8 +762,6 @@ E.g., \"(let ((for list)) ...)\" should not try to operate on the (push j target))) target)))) - - (ert-deftest loopy-iter-clean-stack-variables () (let ((loopy--known-loop-names) (loopy--accumulation-places) diff --git a/tests/tests.el b/tests/tests.el index 9bf18086..f1376971 100644 --- a/tests/tests.el +++ b/tests/tests.el @@ -5549,6 +5549,96 @@ This assumes that you're on guix." :iter-bare ((list . listing) (cycle . cycling))) +(loopy-deftest accum-flet-outside + :doc "Make sure that macro expansion doesn't mess with `cl-flet' environment. +We don't want to rebind the environment to nil by failing to pass +the existing environment (`macroexpand-all-environment') to +`macroexpand-all'." + :result '(11 12 13 14 15) + :wrap ((x . `(cl-flet ((10+ (y) (+ 10 y))) ,x))) + :body ((list i '(1 2 3 4 5)) + (collect (10+ i))) + :loopy t + :iter-keyword (list collect) + :iter-bare ((list . listing) + (collect . collecting))) + +(loopy-deftest accum-flet-outside-wrap-sma + :doc "Make sure that macro expansion doesn't mess with `cl-flet' environment. +We don't want to rebind the environment to nil by failing to pass +the existing environment (`macroexpand-all-environment') to +`macroexpand-all'." + :result '(11 12 13 14 15) + :body ((wrap (cl-flet ((10+ (y) (+ 10 y))))) + (list i '(1 2 3 4 5)) + (collect (10+ i))) + :loopy t + :iter-keyword (list collect) + :iter-bare ((list . listing) + (collect . collecting))) + +(loopy-deftest flet-iter-subloop + :doc "Make sure that macro expansion doesn't mess with `cl-flet' environment. +We don't want to rebind the environment to nil by failing to pass +the existing environment (`macroexpand-all-environment') to +`macroexpand-all'." + :result '(11 12 13 14 15) + :multi-body t + :body [((named outer) + (list i '((1 2) (3 4) (5))) + (loopy-test-escape + (loopy-iter (listing j i) + (at outer + (cl-flet ((10+ (y) (+ 10 y))) + (collecting (10+ j))))))) + + ((named outer) + (list i '((1 2) (3 4) (5))) + (loopy-test-escape + (loopy-iter (listing j i) + (at outer + (cl-flet ((10+ (y) (+ 10 y))) + (collecting (funcall #'10+ j))))))) + + ((named outer) + (list i '((1 2) (3 4) (5))) + (loopy-test-escape + (loopy-iter (listing j i) + (cl-flet ((10+ (y) (+ 10 y))) + (at outer + (collecting (10+ j))))))) + + ((named outer) + (list i '((1 2) (3 4) (5))) + (loopy-test-escape + (loopy-iter (listing j i) + (cl-flet ((10+ (y) (+ 10 y))) + (at outer + (collecting (10+ j))))))) + + ((named outer) + (list i '((1 2) (3 4) (5))) + (loopy-test-escape + (loopy-iter (listing j i) + (at outer + (cl-flet ((10+ (y) (+ 10 y))) + (collecting (funcall #'10+ j)))))))] + :loopy t + :iter-keyword (list collect) + :iter-bare ((list . listing) + (collect . collecting))) + +(loopy-deftest iter-list-in-top-level-expr + :doc "Macros that are required to be at the top level should not consider +a sub-expression as the top level." + :error loopy-iteration-in-sub-level + :macroexpand t + :body ((let ((var 1)) + (listing i '(1 2 3 4 5))) + (collecting i)) + :iter-keyword (listing collecting) + :iter-bare t) + ;; Local Variables: ;; End: ;; LocalWords: destructurings backquote