branch: elpa/loopy
commit d34569439eec8f4bd6becd4e7a46107644c1f82c
Author: okamsn <[email protected]>
Commit: GitHub <[email protected]>
Add the `no-loop` flag. (#265)
This flag prevents the creation of a `while` loop and triggers an error
when features are used that only make sense inside the while loop,
such as:
- iteration commands
- `skip`-like commands
`leave`-like commands are allowed, because the `find` command depends on
such behavior and it is not illogical to use `leave` inside something
like `dowhile`.
- Update Org documentation and Texi file
- Move destructuring flags to their own subsection under Using Flags.
- Fix typos in this section.
- Create subsection for `no-loop` flag under Using Flags.
- In `lisp/loopy-misc.el`:
- Add the following new error symbols:
- `loopy-no-loop-skip`
- `loopy-no-loop-iteration`
- In `lisp/loopy-vars.el` :
- Define dynamic special internal variable `loopy--no-loop`.
- Add `loopy--no-loop` to `loopy--variables`.
- In `lisp/loopy.el`:
- Add setting `loopy--no-loop` to nil in `loopy--enable-flag-default`.
- Create functions `loopy--enable-flag-no-loop` and
`loopy--disable-flag-no-loop`, both setting the variable.
Add these functions to `loopy--flag-settings`.
- In the function `loopy--expand-to-loop`:
- If `loopy--no-loop` is non-nil and `loopy--skip-used` is non-nil,
signal `loopy-no-loop-skip`.
- If `loopy--no-loop` is non-nil and `loopy--latter-body` is non-nil,
signal `loopy-no-loop-iteration`.
- If `loopy--no-loop` is non-nil and `loopy--post-conditions` is
non-nil,
signal `loopy-no-loop-iteration`.
- If `loopy--no-loop` is non-nil and `loopy--pre-conditions` is non-nil,
signal `loopy-no-loop-iteration`.
- If `loopy--no-loop` is non-nil and `loopy--iteration-vars` is non-nil,
signal `loopy-no-loop-iteration`.
- If `loopy--no-loop` is not nil, then do not wrap the body in a
`while`-loop.
- In `tests/tests.el`, add the following tests:
- `flag-no-loop`
- `flag-no-loop-skip-error`
- `flag-no-loop-leave-error`
- `flag-no-loop-iterate-error`
- `flag-no-loop-accumulate`
- `flag-no-loop-find`
- `flag-no-loop-leave`
- `flag-no-loop-wrapping-example`
This flag, when used in a wrapping macro, can be used to implement the
`loopy-block` feature requested in issue #109.
---
CHANGELOG.md | 10 ++++
doc/loopy-doc.org | 118 +++++++++++++++++++++++++++++++++++++----
doc/loopy.texi | 152 +++++++++++++++++++++++++++++++++++++++++++++--------
lisp/loopy-misc.el | 13 ++++-
lisp/loopy-vars.el | 9 ++++
lisp/loopy.el | 79 ++++++++++++++++++----------
tests/tests.el | 107 +++++++++++++++++++++++++++++++++++++
7 files changed, 426 insertions(+), 62 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 377721fc9cf..300cf3f1223 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ For Loopy Dash, see <https://github.com/okamsn/loopy-dash>.
## Unreleased
+### New Features
+
+- Add the `no-loop` flag ([#265]). This stops the looping macros from creating
+ the `while`-loop and causes an error to be signalled when used with features
+ that only make sense with the `while`-loop, such as iteration commands. The
+ intended use is wrapping macros don't need the default `while`-loop but would
+ still like access to other features, such as accumulation commands.
+
+
### Bug Fixes
- When destructuring for accumulation commands, don't assume that `pcase` binds
@@ -25,6 +34,7 @@ For Loopy Dash, see <https://github.com/okamsn/loopy-dash>.
[#254]: https://github.com/okamsn/loopy/PR/254
[#251]: https://github.com/okamsn/loopy/PR/251
[#256]: https://github.com/okamsn/loopy/PR/256
+[#265]: https://github.com/okamsn/loopy/PR/265
## 0.15.0
diff --git a/doc/loopy-doc.org b/doc/loopy-doc.org
index 9ac407f5d03..8571096145c 100644
--- a/doc/loopy-doc.org
+++ b/doc/loopy-doc.org
@@ -81,6 +81,8 @@ libraries =seq= ([[info:elisp#Sequence Functions]]) and
=cl-lib= ([[info:cl]])).
- [[#default-bare-names-in-loopy-iter][Default Bare Names in ~loopy-iter~]]
- [[#customizing-macro-behavior][Customizing Macro Behavior]]
- [[#using-flags][Using Flags]]
+ - [[#destructuring-flags][Destructuring Flags]]
+ - [[#the-no-loop-flag][The =no-loop= Flag]]
- [[#custom-aliases][Custom Aliases]]
- [[#custom-commands][Custom Commands]]
- [[#background-info][Background Info]]
@@ -4876,7 +4878,7 @@ use of Loopy internally and wishes to modify its behavior
would use the
A {{{dfn(flag)}}} is a symbol passed to the =flag= or =flags= special macro
argument, changing the macro's behavior. Currently, flags affect what method
~loopy~ uses to perform destructuring (=pcase=, =seq=, =dash=, or the default
-method).
+method) and whether ~loopy~ expands with or without the default ~while~-loop.
Flags are applied in order. If you specify =(flags seq pcase)=, then ~loopy~
will use ~pcase-let~ for destructuring, not ~seq-let~.
@@ -4884,14 +4886,18 @@ will use ~pcase-let~ for destructuring, not ~seq-let~.
The following flags are currently supported:
#+cindex: pcase flag
+#+cindex: seq flag
+#+cindex: dash flag
+#+cindex: no-loop flag
+#+cindex: default flag
- =pcase= :: Use ~pcase-let~ for destructuring
([[info:elisp#Destructuring with pcase Patterns]]).
-#+cindex: seq flag
- =seq= :: Use ~seq-let~ for destructuring ([[info:elisp#seq-let]]).
-#+cindex: dash flag
- =dash= :: Use the style of destructuring found in the =dash= library
([[info:dash#-let]]).
-#+cindex: default flag
+- =no-loop= :: Expand the looping macros without creating a ~while~-loop.
This
+ is intended for wrapping macros that don't want to work around the default
+ ~while~-loop.
- =default= :: Use the default behavior for all options.
@@ -4905,6 +4911,24 @@ in the list. Similarly, =(flags -dash dash)= and
=(flags -dash +dash)= leave
=dash= destructuring enabled, and =(flags +dash -dash)= disables =dash=
destructuring and uses the default behavior.
+The flags are described in more detail in the following subsections.
+
+*** Destructuring Flags
+:PROPERTIES:
+:CUSTOM_ID: destructuring-flags
+:DESCRIPTION: Using different destructuring systems.
+:END:
+
+There are four flags that change the destructuring system used by Loopy:
+
+- =pcase= :: Use ~pcase-let~ for destructuring
+ ([[info:elisp#Destructuring with pcase Patterns]]).
+- =seq= :: Use ~seq-let~ for destructuring ([[info:elisp#seq-let]]).
+- =dash= :: Use the style of destructuring found in the =dash= library
+ ([[info:dash#-let]]).
+- =default= :: Use the default behavior for all options.
+
+
#+cindex: loopy-dash
#+cindex: loopy-pcase
#+cindex: loopy-seq
@@ -4912,7 +4936,7 @@ The destructuring flags (=pcase=, =seq=, and =dash=) are
separate libraries
(respectively, =loopy-pcase=, =loopy-seq=, and =loopy-dash=) that must be
loaded after =loopy=. Currently, =loopy-dash= is a separate package.
-Below are some example of using the destructuring flags. These flags affect
+Below are some examples of using the destructuring flags. These flags affect
the destructuring of:
- iteration variables
- accumulation variables
@@ -4962,11 +4986,11 @@ new variables as they please, which can be interpreted
as accumulation
variables.
#+end_quote
-Consider the below example in which a hypothetical ~pcase~ pattern creates the
-variable ~temporary?~ for destructuring. Loopy has no way of knowing whether
it
-was the user who create the variable, or the destructuring system. As a
result,
-~temporary?~ is treated as an accumulation variable. Such cases can be
unwanted
-and produce inefficient code.
+For the warning, consider the below example in which a hypothetical ~pcase~
+pattern creates the variable ~temporary?~ for destructuring. Loopy has no way
+of knowing whether it was the user or the destructuring system that created the
+variable. As a result, ~temporary?~ is treated as a user-facing accumulation
+variable. Such cases can be unwanted and produce inefficient code.
#+begin_src emacs-lisp
;; Possibly unexpected behavior:
@@ -5011,13 +5035,85 @@ Therefore, uses of =flag=, including aliases, can be
identified by checking
;; Ignores the `seq' flag as expected:
;;
- ;; => ( 1 2 3 4)
+ ;; => (1 2 3 4)
(my-loopy-flag-wrapper (flag seq)
(list `(,i . ,j) '((1 . 2) (3 . 4)))
(collect i)
(collect j))
#+end_src
+
+*** The =no-loop= Flag
+:PROPERTIES:
+:CUSTOM_ID: no-loop-flag
+:DESCRIPTION: Avoiding creating the loop.
+:END:
+
+Some users of Loopy's macros may prefer using Loopy's non-looping constructs
+(such as accumulation commands) inside Emacs Lisp's default looping features
+(such as ~dolist~ or ~mapc~). By default, Loopy creates an infinite
+~while~-loop, which can interfere with this approach.
+
+One way of avoiding the infinite ~while~-loop is to use a command like =cycle=
+or =leave=, as in the below examples. This does not prevent the creation of
the
+~while~-loop; it just causes Loopy to exit the loop after one step.
+
+#+begin_src emacs-lisp
+ ;; => 10
+ (loopy-iter (cycling 1)
+ (dolist (i '(1 2 3 4))
+ (summing i)))
+
+ ;; => 10
+ (loopy-iter (dolist (i '(1 2 3 4))
+ (summing i))
+ (leaving))
+#+end_src
+
+#+cindex: no-loop flag
+Another way to avoid the ~while~-loop is to use the flag =no-loop=. The
benefit
+of using the =no-loop= flag is that, because its meaning is clear during macro
+expansion, it can trigger additional error checking. Currently, the =no-loop=
+flag will cause Loopy to signal an error when used with the following features:
+- Iteration commands like =list= or =array=
+- Commands like =skip=
+
+#+begin_src emacs-lisp
+ ;; `listing' doesn't make sense with `no-loop', so Loopy
+ ;; signals an error here:
+ ;;
+ (loopy-iter (flag no-loop)
+ (listing i '(1 2 3 4))
+ (summing i))
+#+end_src
+
+Currently, all other Loopy features can be used with the =no-loop= flag,
+including:
+- Special macro arguments
+- Accumulation commands, such as =sum= and =collect=
+- Early-exit commands, such as =leave= and =return=
+- sub-loop commands
+
+One might wish to use this feature to implement something like ~cl-block~ using
+~loopy-iter~, as in the below example.
+
+#+begin_src emacs-lisp
+ (defmacro my-loopy-block (&rest args)
+ `(loopy-iter (flag no-loop)
+ ,@(butlast args)
+ (finally-return ,(car (last args)))))
+
+ ;; => 6
+ (my-loopy-block (dolist (i '(1 2 3))
+ (summing i))
+ loopy-result)
+
+ ;; => 6
+ (my-loopy-block (mapc (lambda (i) (summing i))
+ '(1 2 3))
+ loopy-result)
+#+end_src
+
** Custom Aliases
:PROPERTIES:
:CUSTOM_ID: custom-aliases
diff --git a/doc/loopy.texi b/doc/loopy.texi
index 406b9aeb86e..92bf6b942dc 100644
--- a/doc/loopy.texi
+++ b/doc/loopy.texi
@@ -97,6 +97,11 @@ Customizing Macro Behavior
* Custom Commands:: Extending `loopy' with personal commands.
* Locally Overriding Behavior:: Using the `overrides' special macro argument.
+Using Flags
+
+* Destructuring Flags:: Using different destructuring systems.
+* The @samp{no-loop} Flag:: Avoiding the loop.
+
Custom Commands
* Background Info:: The internals of `loopy'.
@@ -746,7 +751,7 @@ You should keep in mind that commands are evaluated in
order. This means that
attempting something like the below example might not do what you expect, as
@samp{i}
is assigned a value from the list after collecting @samp{i} into @samp{coll}.
-@float Listing,orgb677e2e
+@float Listing,org96b983b
@lisp
;; => (nil 1 2)
(loopy (collect coll i)
@@ -935,7 +940,7 @@ the flag @samp{dash} provided by the package
@samp{loopy-dash}.
Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
-@float Listing,org1e2c3e4
+@float Listing,org86f1547
@lisp
;; => (1 2 3 4)
(cl-loop for (i . j) in '((1 . 2) (3 . 4))
@@ -950,7 +955,7 @@ Below are two examples of destructuring in @code{cl-loop}
and @code{loopy}.
@caption{Destructuring values in a list.}
@end float
-@float Listing,org3b108b1
+@float Listing,org18a3b65
@lisp
;; => (1 2 3 4)
(cl-loop for elem in '((1 . 2) (3 . 4))
@@ -4986,7 +4991,7 @@ using the @code{let*} special form.
This method recognizes all commands and their aliases in the user option
@code{loopy-parsers}.
-@float Listing,org50bbb64
+@float Listing,org0ab40f0
@lisp
;; => ((-9 -8 -7 -6 -5 -4 -3 -2 -1)
;; (0)
@@ -5382,7 +5387,7 @@ use of Loopy internally and wishes to modify its behavior
would use the
A @dfn{flag} is a symbol passed to the @samp{flag} or @samp{flags} special
macro
argument, changing the macro's behavior. Currently, flags affect what method
@code{loopy} uses to perform destructuring (@samp{pcase}, @samp{seq},
@samp{dash}, or the default
-method).
+method) and whether @code{loopy} expands with or without the default
@code{while}-loop.
Flags are applied in order. If you specify @samp{(flags seq pcase)}, then
@code{loopy}
will use @code{pcase-let} for destructuring, not @code{seq-let}.
@@ -5390,24 +5395,23 @@ will use @code{pcase-let} for destructuring, not
@code{seq-let}.
The following flags are currently supported:
@cindex pcase flag
+@cindex seq flag
+@cindex dash flag
+@cindex no-loop flag
+@cindex default flag
@table @asis
@item @samp{pcase}
Use @code{pcase-let} for destructuring
(@ref{Destructuring with pcase Patterns,,,elisp,}).
-@end table
-@cindex seq flag
-@table @asis
@item @samp{seq}
Use @code{seq-let} for destructuring (@ref{seq-let,,,elisp,}).
-@end table
-@cindex dash flag
-@table @asis
@item @samp{dash}
Use the style of destructuring found in the @samp{dash} library
(@ref{-let,,,dash,}).
-@end table
-@cindex default flag
-@table @asis
+@item @samp{no-loop}
+Expand the looping macros without creating a @code{while}-loop. This
+is intended for wrapping macros that don't want to work around the default
+@code{while}-loop.
@item @samp{default}
Use the default behavior for all options.
@end table
@@ -5423,6 +5427,32 @@ in the list. Similarly, @samp{(flags -dash dash)} and
@samp{(flags -dash +dash)
@samp{dash} destructuring enabled, and @samp{(flags +dash -dash)} disables
@samp{dash}
destructuring and uses the default behavior.
+The flags are described in more detail in the following subsections.
+
+@menu
+* Destructuring Flags:: Using different destructuring systems.
+* The @samp{no-loop} Flag:: Avoiding the loop.
+@end menu
+
+@node Destructuring Flags
+@subsection Destructuring Flags
+
+There are four flags that change the destructuring system used by Loopy:
+
+@table @asis
+@item @samp{pcase}
+Use @code{pcase-let} for destructuring
+(@ref{Destructuring with pcase Patterns,,,elisp,}).
+@item @samp{seq}
+Use @code{seq-let} for destructuring (@ref{seq-let,,,elisp,}).
+@item @samp{dash}
+Use the style of destructuring found in the @samp{dash} library
+(@ref{-let,,,dash,}).
+@item @samp{default}
+Use the default behavior for all options.
+@end table
+
+
@cindex loopy-dash
@cindex loopy-pcase
@cindex loopy-seq
@@ -5430,7 +5460,7 @@ The destructuring flags (@samp{pcase}, @samp{seq}, and
@samp{dash}) are separate
(respectively, @samp{loopy-pcase}, @samp{loopy-seq}, and @samp{loopy-dash})
that must be
loaded after @samp{loopy}. Currently, @samp{loopy-dash} is a separate package.
-Below are some example of using the destructuring flags. These flags affect
+Below are some examples of using the destructuring flags. These flags affect
the destructuring of:
@itemize
@item
@@ -5486,11 +5516,11 @@ variables.
@end quotation
-Consider the below example in which a hypothetical @code{pcase} pattern
creates the
-variable @code{temporary?} for destructuring. Loopy has no way of knowing
whether it
-was the user who create the variable, or the destructuring system. As a
result,
-@code{temporary?} is treated as an accumulation variable. Such cases can be
unwanted
-and produce inefficient code.
+For the warning, consider the below example in which a hypothetical
@code{pcase}
+pattern creates the variable @code{temporary?} for destructuring. Loopy has
no way
+of knowing whether it was the user or the destructuring system that created the
+variable. As a result, @code{temporary?} is treated as a user-facing
accumulation
+variable. Such cases can be unwanted and produce inefficient code.
@lisp
;; Possibly unexpected behavior:
@@ -5535,13 +5565,91 @@ Therefore, uses of @samp{flag}, including aliases, can
be identified by checking
;; Ignores the `seq' flag as expected:
;;
-;; => ( 1 2 3 4)
+;; => (1 2 3 4)
(my-loopy-flag-wrapper (flag seq)
(list `(,i . ,j) '((1 . 2) (3 . 4)))
(collect i)
(collect j))
@end lisp
+@node The @samp{no-loop} Flag
+@subsection The @samp{no-loop} Flag
+
+Some users of Loopy's macros may prefer using Loopy's non-looping constructs
+(such as accumulation commands) inside Emacs Lisp's default looping features
+(such as @code{dolist} or @code{mapc}). By default, Loopy creates an infinite
+@code{while}-loop, which can interfere with this approach.
+
+One way of avoiding the infinite @code{while}-loop is to use a command like
@samp{cycle}
+or @samp{leave}, as in the below examples. This does not prevent the creation
of the
+@code{while}-loop; it just causes Loopy to exit the loop after one step.
+
+@lisp
+;; => 10
+(loopy-iter (cycling 1)
+ (dolist (i '(1 2 3 4))
+ (summing i)))
+
+;; => 10
+(loopy-iter (dolist (i '(1 2 3 4))
+ (summing i))
+ (leaving))
+@end lisp
+
+@cindex no-loop flag
+Another way to avoid the @code{while}-loop is to use the flag @samp{no-loop}.
The benefit
+of using the @samp{no-loop} flag is that, because its meaning is clear during
macro
+expansion, it can trigger additional error checking. Currently, the
@samp{no-loop}
+flag will cause Loopy to signal an error when used with the following features:
+@itemize
+@item
+Iteration commands like @samp{list} or @samp{array}
+@item
+Commands like @samp{skip}
+@end itemize
+
+@lisp
+;; `listing' doesn't make sense with `no-loop', so Loopy
+;; signals an error here:
+;;
+(loopy-iter (flag no-loop)
+ (listing i '(1 2 3 4))
+ (summing i))
+@end lisp
+
+Currently, all other Loopy features can be used with the @samp{no-loop} flag,
+including:
+@itemize
+@item
+Special macro arguments
+@item
+Accumulation commands, such as @samp{sum} and @samp{collect}
+@item
+Early-exit commands, such as @samp{leave} and @samp{return}
+@item
+sub-loop commands
+@end itemize
+
+One might wish to use this feature to implement something like @code{cl-block}
using
+@code{loopy-iter}, as in the below example.
+
+@lisp
+(defmacro my-loopy-block (&rest args)
+ `(loopy-iter (flag no-loop)
+ ,@@(butlast args)
+ (finally-return ,(last args))))
+
+;; => 6
+(my-loopy-block (dolist (i '(1 2 3))
+ (summing i))
+ loopy-result)
+
+;; => 6
+(my-loopy-block (mapc (lambda (i) (summing i))
+ '(1 2 3))
+ loopy-result)
+@end lisp
+
@node Custom Aliases
@section Custom Aliases
@@ -5568,7 +5676,7 @@ between aliases and preferred names (which are the ones
commonly used and listed
first in this document). We continue to use the word ``alias'' for its common
definition rather than to suggest a difference in the code.
-@float Listing,org23481af
+@float Listing,org70362c3
@lisp
;; => ("a" "b" "c" "d")
(loopy (array i "abcd")
diff --git a/lisp/loopy-misc.el b/lisp/loopy-misc.el
index 5222964876c..6febcc1d15a 100644
--- a/lisp/loopy-misc.el
+++ b/lisp/loopy-misc.el
@@ -272,8 +272,17 @@
'loopy-error)
(define-error 'loopy-bad-quoted-form
- "Loopy: Unrecognized quoted form"
- 'loopy-error)
+ "Loopy: Unrecognized quoted form"
+ 'loopy-error)
+
+;;;;; Errors on No-Loop Expansions
+(define-error 'loopy-no-loop-skip
+ "Loopy: `skip'-like commands with `no-loop' flag are not allowed
(no loop step to skip)"
+ 'loopy-error)
+
+(define-error 'loopy-no-loop-iteration
+ "Loopy: Iteration commands with `no-loop' flag are not allowed"
+ 'loopy-error)
;;;; List Processing
diff --git a/lisp/loopy-vars.el b/lisp/loopy-vars.el
index d68d645cb3e..0e5367ce227 100644
--- a/lisp/loopy-vars.el
+++ b/lisp/loopy-vars.el
@@ -357,6 +357,14 @@ true names and lists of aliases.
;;;; Flags
;;;;; Variables that can be set by flags
+(defvar loopy--no-loop nil
+ "Whether to avoid the `while' loop in `loopy--expand-to-loop'.
+
+When this is non-`nil', Loopy macros will not use a `while' loop
+and some features that interact with the loop,
+such as iteration commands and commands like `skip' and `leave'
+will trigger an error when used.")
+
(defvar loopy--destructuring-for-with-vars-function nil
"The function used for destructuring `with' variables.
@@ -730,6 +738,7 @@ known to fall into the first group.")
loopy--in-sub-level
;; -- Flag Variables --
+ loopy--no-loop
loopy--destructuring-for-with-vars-function
loopy--destructuring-for-iteration-function
loopy--destructuring-accumulation-parser)
diff --git a/lisp/loopy.el b/lisp/loopy.el
index f5c2eea7845..c3db723bf6c 100644
--- a/lisp/loopy.el
+++ b/lisp/loopy.el
@@ -141,10 +141,24 @@
(setq loopy--destructuring-for-with-vars-function
#'loopy--destructure-for-with-vars-default
loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command-default))
+ #'loopy--parse-destructuring-accumulation-command-default
+ loopy--no-loop nil))
(cl-callf map-insert loopy--flag-settings 'default
#'loopy--enable-flag-default)
+;;;;;; No-Loop
+(defun loopy--enable-flag-no-loop ()
+ "Set `loopy--no-loop' to `t'."
+ (setq loopy--no-loop t))
+
+(defun loopy--disable-flag-no-loop ()
+ "Set `loopy--no-loop' to `nil'."
+ (setq loopy--no-loop nil))
+
+(cl-callf map-insert loopy--flag-settings 'no-loop
#'loopy--enable-flag-no-loop)
+(cl-callf map-insert loopy--flag-settings '+no-loop
#'loopy--enable-flag-no-loop)
+(cl-callf map-insert loopy--flag-settings '-no-loop
#'loopy--disable-flag-no-loop)
+
;;;; Miscellaneous and Utility Functions
(defun loopy--validate-binding (binding)
"Validate the form of BINDING. Signal error if invalid.
@@ -245,34 +259,43 @@ The function creates quoted code that should be used by a
macro."
result-is-one-expression (zerop (length result)))
(when (eq loopy--skip-used loopy--skip-tag-name)
- (setq result `(catch (quote ,loopy--skip-tag-name) ,@result)
- result-is-one-expression t))
+ (if loopy--no-loop
+ (signal 'loopy-no-loop-skip (list loopy--loop-name))
+ (setq result `(catch (quote ,loopy--skip-tag-name) ,@result)
+ result-is-one-expression t)))
(when loopy--latter-body
- (setq result `(,@(get-result) ,@loopy--latter-body)
- result-is-one-expression nil))
+ (if loopy--no-loop
+ (signal 'loopy-no-loop-iteration (list loopy--loop-name))
+ (setq result `(,@(get-result) ,@loopy--latter-body)
+ result-is-one-expression nil)))
(when loopy--post-conditions
- (setq result
- (append result
- `((unless ,(cl-case (length loopy--post-conditions)
- (0 t)
- (1 (car loopy--post-conditions))
- (t (cons 'and loopy--post-conditions)))
- ;; If the loop exits early, we should still use the
- ;; implicit return. That isn't a problem for the
- ;; `while' loop, but we need to be more explicit
- ;; here.
- (cl-return-from ,loopy--loop-name
- ,loopy--implicit-return))))))
-
- ;; Now wrap loop body in the `while' form.
- (setq result `(while ,(cl-case (length loopy--pre-conditions)
- (0 t)
- (1 (car loopy--pre-conditions))
- (t (cons 'and loopy--pre-conditions)))
- ,@(get-result))
- result-is-one-expression t)
+ (if loopy--no-loop
+ (signal 'loopy-no-loop-iteration (list loopy--loop-name))
+ (setq result
+ (append result
+ `((unless ,(cl-case (length loopy--post-conditions)
+ (0 t)
+ (1 (car loopy--post-conditions))
+ (t (cons 'and loopy--post-conditions)))
+ ;; If the loop exits early, we should still use the
+ ;; implicit return. That isn't a problem for the
+ ;; `while' loop, but we need to be more explicit
+ ;; here.
+ (cl-return-from ,loopy--loop-name
+ ,loopy--implicit-return)))))))
+
+ ;; Now, if looping, wrap loop body in the `while' form.
+ (if loopy--no-loop
+ (when loopy--pre-conditions
+ (signal 'loopy-no-loop-iteration (list loopy--loop-name)))
+ (setq result `(while ,(cl-case (length loopy--pre-conditions)
+ (0 t)
+ (1 (car loopy--pre-conditions))
+ (t (cons 'and loopy--pre-conditions)))
+ ,@(get-result))
+ result-is-one-expression t))
;; Make sure that the implicit accumulation variable is correctly
;; updated after the loop, if need be. Note that to avoid errors,
@@ -377,8 +400,10 @@ The function creates quoted code that should be used by a
macro."
;; Declare the loop variables.
(when loopy--iteration-vars
- (setq result `(let* ,loopy--iteration-vars ,@(get-result))
- result-is-one-expression t))
+ (if loopy--no-loop
+ (signal 'loopy-no-loop-iteration (list loopy--loop-name))
+ (setq result `(let* ,loopy--iteration-vars ,@(get-result))
+ result-is-one-expression t)))
(when loopy--other-vars
(setq result `(let* ,loopy--other-vars ,@(get-result))
diff --git a/tests/tests.el b/tests/tests.el
index 5686a7529c4..6815a3c740a 100644
--- a/tests/tests.el
+++ b/tests/tests.el
@@ -648,6 +648,113 @@ Make sure that it does not break early returns."
(_collect . collect)
(_do . do)))
+;;;; Flag No-Loop
+
+(loopy-deftest flag-no-loop
+ :doc "Test that flag `no-loop' prevents creating the `while' loop."
+ :result 1
+ :body ((flag no-loop)
+ (with (i 0))
+ (if (< i 5)
+ (set i (1+ i))
+ (do (error "Looping in `no-loop' flag.")))
+ (finally-return i))
+ :loopy t
+ :iter-keyword (set do)
+ :iter-bare ((set . setting)
+ (do . progn)))
+
+(loopy-deftest flag-no-loop-skip-errors
+ :doc "Using `skip' with `no-loop' is illogical and should error."
+ :error loopy-no-loop-skip
+ :macroexpand t
+ :body ((flag no-loop)
+ (with (i 0))
+ (if (< i 5)
+ (set i (1+ i))
+ (do (error "Looping in `no-loop' flag.")))
+ (skip)
+ (finally-return i))
+ :loopy t
+ :iter-keyword (set do skip)
+ :iter-bare ((set . setting)
+ (skip . skipping)
+ (do . progn)))
+
+(loopy-deftest flag-no-loop-iterate-errors
+ :doc "Using iteration commands with `no-loop' are illogical and should
error."
+ :error loopy-no-loop-iteration
+ :macroexpand t
+ :body ((flag no-loop)
+ (with (i 0))
+ (list x '(1 2 3))
+ (if (< i 5)
+ (set i (1+ i))
+ (do (error "Looping in `no-loop' flag.")))
+ (finally-return i))
+ :loopy t
+ :iter-keyword (set do list)
+ :iter-bare ((set . setting)
+ (do . progn)
+ (list . listing)))
+
+(loopy-deftest flag-no-loop-accumulate-works
+ :doc "Accumulation commands should work with `no-loop'."
+ :result 3
+ :body ((flag no-loop)
+ (with (i 1))
+ (sum i)
+ (sum i)
+ (sum i))
+ :loopy t
+ :iter-keyword (sum)
+ :iter-bare ((sum . summing)))
+
+(loopy-deftest flag-no-loop-find-works
+ :doc "`find' command should work with `no-loop'.
+The `find' command uses a non-returning exit, like `leave'."
+ :result '(27 nil)
+ :body ((flag no-loop)
+ (with (a nil)
+ (b nil))
+ (set a 3)
+ (find 27 (> a 2))
+ (set b 45)
+ (finally-return loopy-result b))
+ :loopy t
+ :iter-keyword (set find)
+ :iter-bare ((set . setting)
+ (find . finding)))
+
+(loopy-deftest flag-no-loop-leave-works
+ :doc "`leave' command should work with `no-loop'.
+The `leave' command uses a non-returning exit."
+ :result 6
+ :body ((flag no-loop)
+ (dolist (a '(1 2 3 4 5))
+ (if (> a 3) (leave))
+ (sum a)))
+ :loopy nil
+ :iter-keyword (sum leave)
+ :iter-bare ((sum . summing)
+ (leave . leaving)))
+
+(ert-deftest flag-no-loop-wrapping-example ()
+ "Test the example from the documentation of the `no-loop' flag."
+ (eval (quote (cl-macrolet ((my-loopy-block (&rest args)
+ `(loopy-iter (flag no-loop)
+ ,@(butlast args)
+ (finally-return ,(car (last
args))))))
+
+ (should (equal 6 (my-loopy-block (dolist (i '(1 2 3))
+ (summing i))
+ loopy-result)))
+
+ (should (equal 6 (my-loopy-block (dolist (i '(1 2 3))
+ (summing i))
+ loopy-result)))))
+ t))
+
;;;; Changing the order of macro arguments.
(loopy-deftest change-order-of-commands
:result 7