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

Reply via email to