diff --git a/.github/workflows/emacs-matrix-tests.yml b/.github/workflows/emacs-matrix-tests.yml
index ae9aa30c..73a12375 100644
--- a/.github/workflows/emacs-matrix-tests.yml
+++ b/.github/workflows/emacs-matrix-tests.yml
@@ -15,10 +15,11 @@ jobs:
strategy:
matrix:
emacs-version:
- - '27.1'
+ # - '27.1'
- '27.2'
- - '28.1'
+ # - '28.1'
- '28.2'
+ - '29.2'
- 'release-snapshot'
# - 'snapshot'
steps:
@@ -38,8 +39,11 @@ jobs:
- name: Make folder for base package.
run: |
mkdir 'test-install'
- cp loopy-commands.el loopy.el loopy-vars.el loopy-misc.el loopy-pkg.el test-install
- cp loopy-iter.el loopy-pcase.el loopy-seq.el test-install
+ # Match all files of "loopy*.el" except for "loopy-dash.el".
+ echo Shell is $SHELL
+ shopt -s extglob
+ cp loopy!(-dash).el test-install
+ shopt -u extglob
- name: Install packages
run: emacs -batch -l tests/install-script.el
- name: Basic tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d46702d2..8449c233 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,11 @@ This document describes the user-facing changes to Loopy.
(collecting (10+ j))))))
```
+- The documentation describes Loopy's default destructuring style as a super-set
+ of that of `cl-lib`. `&key` now behaves more like it does in `cl-lib`,
+ signaling an error when appropriate ([#182]) and supporting the full form
+ `((KEY VAR) DEFAULT SUPPLIED)`.
+
### Breaking Changes
- Fix how the first accumulated value is used in `reduce`. See [#164] and the
@@ -89,6 +94,14 @@ This document describes the user-facing changes to Loopy.
TESTED-ITEM)`, similar to `seq-contains-p`. The argument order was previously
undocumented and not guaranteed. See [#170] and [#177].
+- Like in `cl-lib`, destructuring with `&key` will now signal an error if there
+ are unmatched keys and `&allow-other-keys` was not given or
+ `:allow-other-keys` is not present in the property list with a non-nil value
+ ([#182]).
+
+- The default destructuring style now uses `pcase` underneath ([#182]). To
+ accomodate this, some of the defined errors and error detections have changed.
+
#### Removals
- The deprecated flag `split` was removed ([#165], [#131], [#124]). Instead,
@@ -203,12 +216,28 @@ This document describes the user-facing changes to Loopy.
uses the constant value directly, which Emacs can optimize to avoid some uses
of `funcall`.
+### Destructuring Improvements
+
+- A `loopy` `pcase` pattern has been added ([#182]). Destructuring is now based
+ on `pcase`.
+- A `&map` construct was added for destructuring, analogous to `&key` but using
+ `map-elt` and the `map` `pcase` pattern ([#182]). Extending the `map`
+ pattern, `&map` also has a `SUPPLIED` parameter, as in `(KEY VAR DEFAULT
+ SUPPLIED)`.
+- An `&optional` construct was added, like in `cl-lib` ([#182]).
+- `&key` now works like it does in `cl-lib`, including the `DEFAULT`,
+ `SUPPLIED`, and `KEY` values in the full form and signaling an error
+ when appropriate ([#182]).
+
### Other Changes
- Add `loopy--other-vars`, given the more explicit restriction on
`loopy--iteration-vars` ([#144]). For example, these are the variables bound
by the `set` command, which are allowed to occur in more than one command.
+- To reduce the maintenance burden, destructuring was re-implemented using
+ `pcase` ([#182]).
+
[#144]: https://github.com/okamsn/loopy/issue/142
[#144]: https://github.com/okamsn/loopy/pull/144
[#145]: https://github.com/okamsn/loopy/issue/145
@@ -227,6 +256,7 @@ This document describes the user-facing changes to Loopy.
[#176]: https://github.com/okamsn/loopy/issues/176
[#177]: https://github.com/okamsn/loopy/pull/177
[#180]: https://github.com/okamsn/loopy/pull/180
+[#182]: https://github.com/okamsn/loopy/pull/182
## 0.11.2
diff --git a/Makefile b/Makefile
index ce5a54bb..56e6b275 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,16 @@
+EMACS ?= emacs
+
.PHONY: tests
tests:
- emacs -Q -batch -l ert -l tests/load-path.el -l tests/tests.el -f ert-run-tests-batch-and-exit
+ $(EMACS) -Q -batch -l ert -l tests/load-path.el -l tests/tests.el -f ert-run-tests-batch-and-exit
.PHONY: iter-tests
iter-tests:
- emacs -Q -batch -l ert -l tests/load-path.el -l tests/iter-tests.el -f ert-run-tests-batch-and-exit
+ $(EMACS) -Q -batch -l ert -l tests/load-path.el -l tests/iter-tests.el -f ert-run-tests-batch-and-exit
+
+.PHONY: misc-tests
+
+misc-tests:
+ $(EMACS) -Q -batch -l ert -l tests/load-path.el -l tests/misc-tests.el -f ert-run-tests-batch-and-exit
diff --git a/README.org b/README.org
index 422c863d..a17b2290 100644
--- a/README.org
+++ b/README.org
@@ -23,7 +23,7 @@ overview.
-----
#+begin_center
-*NOTE*: Loopy is still in its middle stages.\\
+*NOTE*: Loopy is still in its latter middle stages.\\
Constructive criticism is welcome. If you see a place for improvement,
please let me know.
#+end_center
@@ -63,6 +63,8 @@ please let me know.
- Change the argument order of =test= to be (1) the sequence element then (2)
the tested value, like in ~seq-contains-p~ and _unlike_ in ~cl-member~, and
explicitly state this order in the documentation.
+ - =&key= now signals an error when there are unmatched keys in the plist, as
+ in =cl-lib=. =&allow-other-keys= has been added.
- See the [[https://github.com/okamsn/loopy/blob/master/CHANGELOG.md][change log]] for less recent changes.
# This auto-generated by toc-org.
@@ -171,7 +173,7 @@ iteration.
* Similar Libraries
Loopy is not the only Lisp library that uses parenthetical expressions instead of
-keyword clauses (like in ~cl-loop~). [[https://common-lisp.net/project/iterate/][Iterate]] and [[https://github.com/Shinmera/for/][For]] are two examples from
+keyword clauses (as in ~cl-loop~). [[https://common-lisp.net/project/iterate/][Iterate]] and [[https://github.com/Shinmera/for/][For]] are two examples from
Common Lisp.
#+begin_src emacs-lisp
@@ -256,9 +258,10 @@ Commands in Arbitrary Code]]), use
The default destructuring system is a super-set of what =cl-lib= provides
and is described in the section [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#basic-destructuring][Basic Destructuring]] in the documentation.
-~loopy~ can optionally use destructuring provided by ~pcase-let~, ~seq-let~, the
-=dash= library, as well as its own kind. This provides greater flexibility and
-allows you to use destructuring patterns that you're already familiar with.
+In addition to the built-in destructuring style, ~loopy~ can optionally use
+destructuring provided by ~pcase-let~, ~seq-let~, the =dash= library. This
+provides greater flexibility and allows you to use destructuring patterns that
+you're already familiar with.
These features can be enabled with "flags", described in the section [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#using-flags][Using Flags]]
in the documentation.
diff --git a/doc/loopy-doc.org b/doc/loopy-doc.org
index f91799ca..1c17e087 100644
--- a/doc/loopy-doc.org
+++ b/doc/loopy-doc.org
@@ -760,17 +760,34 @@ provided in =cl-lib=.
Some differences include:
- Destructuring arrays
+
- Destructuring in accumulation commands ([[#accumulation-commands]])
+
- Destructuring in commands iterating through ~setf~-able places in a sequence
([[#sequence-reference-iteration]])
-In addition to what can be done in loop commands, several macros are available
-for using Loopy's destructuring outside of ~loopy~ loops ([[#destr-macros]]).
+- The extended forms of the =&optional= and =&key= variables (such as default
+ values like in ~... &optional (var default) ...~) can be specified using
+ square brackets as well as parentheses (such as ~... &optional [var default]
+ ...~). Since such variables can be further destructured by being written as
+ sequences themselves, allowing both parentheses and brackets reduces confusion
+ and improves consistency.
+
+- A =&map= construct, similar to =&key=, but using ~map-elt~ instead of
+ ~plist-get~ and which does not error when the map contains keys which aren't
+ matched (in other words, there is no need for an equivalent of
+ =&allow-other-keys=).
+
This section describes the basic built-in destructuring used by most loop
-commands, such as =set= and =list=. Destructuring in accumulation commands and
-sequence-reference commands works slightly differently, and is described more in
-those sections.
+commands, such as =set= and =list=. Destructuring in accumulation commands
+([[#accumulation-commands]]) and sequence-reference commands
+([[#sequence-reference-iteration]]) works slightly differently, and is described
+more in those sections.
+
+In addition to what can be done in loop commands, several features are available
+for using Loopy's destructuring outside of ~loopy~ loops ([[#destr-macros]]),
+including the ~pcase~ pattern =loopy=.
The last thing to note is that ~loopy~ loops can be made to use alternative
destructuring systems, such as ~seq-let~ or ~pcase-let~. This is done by using
@@ -778,6 +795,7 @@ the =flag= special macro argument ([[#flags]]). If you are familiar with the
package =dash= [fn:dash] and its Clojure-style destructuring, consider trying
the flag =dash= provided by the package =loopy-dash=.
+
Below are two examples of destructuring in ~cl-loop~ and ~loopy~.
#+caption: Destructuring values in a list.
@@ -812,13 +830,64 @@ Below are two examples of destructuring in ~cl-loop~ and ~loopy~.
You can use destructured assignment by passing an unquoted sequence of symbols
as the =VAR= argument of a loop command. Loopy supports destructuring lists and
arrays (which includes strings and vectors).
-- To destructure lists, use a list.
-- To destructure arrays, use a vector.
+- To destructure lists, use a list, as in =(a b c)=.
+- To destructure arrays, use a vector, as in =[a b c]=.
+
This sequence of symbols can be shorter than the destructured sequence, /but not
-longer/. If shorter, the unassigned elements of the list are simply ignored.
+longer/. If shorter, the unassigned elements of the destructured sequence are
+simply ignored.
+
+The content of this destructuring sequence is similar to =cl-lib=, and is
+
+#+begin_example
+POSITIONAL-VARIABLES
+&optional OPTIONAL-VARIABLES
+&rest REST-VARIABLE
+&key KEY-VARIABLES [&allow-other-keys]
+&map MAP-VARIABLES
+&aux AUXILLIARY-VARIABLES
+#+end_example
+
+in which at least one of the above constructs must be provided.
+
+#+begin_src emacs-lisp
+ ;; => (1 2 3
+ ;; 4 5 t
+ ;; (:k1 111 :k2 222)
+ ;; 111 t
+ ;; 222
+ ;; 111
+ ;; 333 nil
+ ;; 4444 5555)
+ (pcase (list 1 2 3 4 5 :k1 111 :k2 222)
+ ((loopy ( a b c
+ &optional
+ d
+ (e nil e-supplied)
+ &rest
+ r
+ &key
+ ((:k1 k1) nil k1-supplied)
+ k2
+ &map
+ (:k1 map1)
+ [:k3 map3 333 map3-supplied]
+ &aux
+ [x1 4444] (x2 5555)))
+ (list a b c
+ d
+ e e-supplied
+ r
+ k1 k1-supplied
+ k2
+ map1
+ map3 map3-supplied
+ x1 x2)))
+#+end_src
+
-An element in the sequence =VAR= can be one of the following:
+In more detail, the elements of the destructuring sequence can be:
- A positional variable which will be bound to the corresponding element in the
sequence. These variables can themselves be sequences, but must be of the
@@ -831,8 +900,9 @@ An element in the sequence =VAR= can be one of the following:
(collect (list i j k)))
#+end_src
-- The symbol =_= (an underscore): The symbol =_= means to avoid creating a
- variable. This can be more efficient.
+#+cindex: _
+- The symbol =_= (an underscore) or a symbol beginning with an underscore: This
+ means to ignore the element at this location. This can be more efficient.
#+begin_src emacs-lisp
;; Only creates the variables `a' and `d':
@@ -848,10 +918,11 @@ An element in the sequence =VAR= can be one of the following:
(collect a))
;; => (1 3)
- (loopy (array (a . _) [(1 2) (3 4)])
+ (loopy (array (a . _ignored) [(1 2) (3 4)])
(collect a))
#+end_src
+#+cindex: &whole
- The symbol =&whole=: If =&whole= is the first element in the sequence, then
the second element of the sequence names a variable that holds the entire
value of what is destructured.
@@ -872,9 +943,34 @@ An element in the sequence =VAR= can be one of the following:
'((1 2) (3 4)))
#+end_src
+#+cindex: &rest
- The symbol =&rest=: A variable named after =&rest= contains the remaining
- elements of the destructured value. When destructuring lists, one can also
- use dotted notation. These variables can themselves be sequences.
+ elements of the destructured value after any positional and optional values.
+ When destructuring lists, one can also use dotted notation, as in a CL
+ ~lambda~ list. These variables can themselves be sequences to be further
+ destructured.
+
+ When used after optional values, the =&rest= value is the subsequence starting
+ at the index after any possible optional values, even when those optional
+ values are not actually present. If the sequence is not long enough, than the
+ sub-sequence is empty.
+
+ #+begin_src emacs-lisp
+ ;; => (1 2 (3))
+ (pcase (list 1 2 3)
+ ((loopy (a &optional b &rest c))
+ (list a b c)))
+
+ ;; => (1 nil nil)
+ (pcase (list 1)
+ ((loopy (a &optional b &rest c))
+ (list a b c)))
+
+ ;; => (1 [])
+ (pcase (vector 1)
+ ((loopy [a &optional _ _ _ _ &rest c])
+ (list a c)))
+ #+end_src
This =&rest= is the same as when used in ~seq-let~.
@@ -908,6 +1004,60 @@ An element in the sequence =VAR= can be one of the following:
(collect (list i j)))
#+end_src
+#+cindex: &optional
+- The symbol =&optional=: A variable named after =&optional= is optional. If
+ the list is not long enough to bind the variable, then the variable is bound
+ to ~nil~ or, if specified, a default value. Additionally, one may bind a
+ variable to record whether the list was long enough to contain the optional
+ value.
+
+ As in a CL ~lambda~ list, the variable has the one of the following forms:
+
+ - =(VAR DEFAULT SUPPLIED)= or =[VAR DEFAULT SUPPLIED]=, in which =VAR= itself
+ can be a sequence
+
+ - =(VAR DEFAULT)= or =[VAR DEFAULT]=, in which =VAR= itself can be a sequence
+
+ - =(VAR)= or =[VAR]=, in which =VAR= itself can be a sequence
+
+ - a symbol =VAR=
+
+ #+begin_src emacs-lisp
+ ;; => (1 2 88 t nil)
+ (loopy (array (a &optional ((b &optional (c 88 c-supplied))
+ (list 77)
+ bc-supplied))
+ [(1 (2))])
+ (collect (list a b c bc-supplied c-supplied)))
+
+ ;; => (1 2 3 t t)
+ (loopy (array (a &optional ((b &optional (c 88 c-supplied))
+ (list 77)
+ bc-supplied))
+ [(1 (2 3))])
+ (collect (list a b c bc-supplied c-supplied)))
+ #+end_src
+
+ =&optional= cannot be used after =&rest=.
+
+ #+begin_src emacs-lisp
+ ;; => ((1 2 3 4 5)
+ ;; 1
+ ;; 2
+ ;; 3
+ ;; (4 5))
+ (loopy (array (&whole all a b &optional c &rest d)
+ [(1 2 3 4 5)])
+ (collect (list all a b c d)))
+
+ ;; Same as above:
+ (loopy (array (&whole all a b &rest (c &rest d))
+ [(1 2 3 4 5)])
+ (collect (list all a b c d)))
+ #+end_src
+
+#+cindex: &key
+#+cindex: &keys
- The symbol =&key= or =&keys=: Variables named after =&key= are transformed
into keys whose values will be sought using ~plist-get~, which returns ~nil~
if the key isn't found in the list.
@@ -921,10 +1071,29 @@ An element in the sequence =VAR= can be one of the following:
(collect (list a b missing)))
#+end_src
- If the key is not in the list, a default value can be provided by using a
- two-item list of the variable and the default value. If a default value is
- provided, then keys are sought using ~plist-member~. That way, a value of
- ~nil~ for a key is not the same as a missing key.
+ Variables after =&key= can be of the following forms:
+
+ - =((VAR KEY) DEFAULT SUPPLIED)=, =[[VAR KEY] DEFAULT SUPPLIED]=, =([VAR KEY]
+ DEFAULT SUPPLIED)=, or =[(VAR KEY) DEFAULT SUPPLIED]=, in which =VAR= itself
+ can be a sequence
+
+ - =((VAR KEY) DEFAULT)=, =[[VAR KEY] DEFAULT]=, =([VAR KEY] DEFAULT)=, or
+ =[(VAR KEY) DEFAULT]=, in which =VAR= itself can be a sequence
+
+ - =((VAR KEY))=, =[[VAR KEY]]=, =([VAR KEY])=, or =[(VAR KEY)]=, in which
+ =VAR= itself can be a sequence
+
+ - =(VAR DEFAULT SUPPLIED)= or =[VAR DEFAULT SUPPLIED]=, in which =VAR= is a
+ symbol
+
+ - =(VAR DEFAULT)= or =[VAR DEFAULT]=, in which =VAR= is a symbol
+
+ - =(VAR)= or =[VAR]=, in which =VAR= is a symbol
+
+ - a symbol =VAR=
+
+ If a default value is provided, then keys are sought using ~plist-member~.
+ That way, a value of ~nil~ for a key is not the same as a missing key.
#+begin_src emacs-lisp
;; Note that `nil' is not the same as a missing value:
@@ -935,6 +1104,19 @@ An element in the sequence =VAR= can be one of the following:
(collect (list a b c missing)))
#+end_src
+ By default, the sought key is made by prepending a colon (":") to the symbol
+ name. For example, =a= searches for =:a= and =b= searches for =:b=. Like in
+ =cl-lib=, an evaluated key can be sought by using a sub-sequence as the first
+ element of the list. When =VAR= is a sequence, the key must be provided
+ separately.
+
+ #+begin_src emacs-lisp
+ ;; => ((1 nil t))
+ (loopy (list (&key ((:cat c)) ((:dog d) 27 dog-found))
+ '((:cat 1 :dog nil)))
+ (collect (list c d dog-found)))
+ #+end_src
+
Keys are sought in values after those bound to positional variables, which can
be the same values bound to the variable named by =&rest= when both are
used.
@@ -968,6 +1150,115 @@ An element in the sequence =VAR= can be one of the following:
(collect (list a b k1)))
#+end_src
+ Like in =cl-lib=, if, after searching for the other keys, there remains an
+ unmatched key in the destructured value, an error is signaled unless
+ =&allow-other-keys= is also used, or unless the key =:allow-other-keys= is
+ associated with a non-nil value in the property list, even when using =&rest=.
+
+ #+begin_src emacs-lisp
+ ;; Error due to presence of `:k3':
+ (cl-destructuring-bind (a b &rest c &key k1 k2)
+ (list 1 2 :k1 3 :k2 4 :k3 5)
+ (list a b c k1 k2))
+
+ ;; Works as expected:
+ ;;
+ ;; => (1 2 (:k1 3 :k2 4 :k3 5) 3 4)
+ (cl-destructuring-bind (a b &rest c &key k1 k2 &allow-other-keys)
+ (list 1 2 :k1 3 :k2 4 :k3 5)
+ (list a b c k1 k2))
+ #+end_src
+
+#+cindex: &map
+- The symbol =&map=: Variables after =&map= are bound similar to ~map-let~ from
+ the library =map.el=. =&map= works similarly to =&key=, but has a few
+ important differences:
+
+ 1. Maps are more generic than property lists ("plists"). A "map" is a generic
+ structure which supports the function ~map-elt~. The built-in maps are
+ arrays, property lists ("plists"), association lists ("alists"), and hash
+ tables. This generality means that it is slower than =&key= for property
+ lists, though the difference should be small.
+
+ 2. =&map= will not signal an error if there are unused keys inside the
+ destructured value; there is no =&allow-other-keys= for =map=. In the same
+ vein, it cannot be made to signal an error if there are unused keys.
+
+ Variables after =&map= can be of the following forms:
+
+ - =(KEY VAR DEFAULT SUPPLIED)= or =[KEY VAR DEFAULT SUPPLIED]=, in which =VAR=
+ itself can be a sequence
+
+ - =(KEY VAR DEFAULT)= or =[KEY VAR DEFAULT]=, in which =VAR= itself can be a
+ sequence
+
+ - =(KEY VAR)= or =[KEY VAR]=, in which =VAR= itself can be a sequence
+
+ - =(VAR)= or =[VAR]=, in which =VAR= is a symbol
+
+ - a symbol =VAR=
+
+ When specifying =KEY=, =VAR= can be a sequence to perform further
+ destructuring. When =KEY= is not given, then the key is the symbol =VAR=, as
+ in ~(quote VAR)~.
+
+ #+begin_src emacs-lisp
+ ;; => ((1 2 3 4 27))
+ (loopy (array (a b &map c ('dog d) (:elephant e 27))
+ [(1 2 c 3 dog 4)])
+ (collect (list a b c d e)))
+
+ ;; => ((1 2 3 4 27 33 nil))
+ (loopy (array ( a b
+ &map
+ c
+ ('dog d)
+ (:elephant e 27)
+ (:fox f 33 fox-found))
+ [(1 2 (c . 3) (dog . 4))])
+ (collect (list a b c d e f fox-found)))
+
+ ;; => ((1 2 5 t))
+ (loopy (array (a b &map (:fox f 33 fox-found))
+ [(1 2 (c . 3) (dog . 4) (:fox . 5))])
+ (collect (list a b f fox-found)))
+
+ ;; For arrays, the key is the index:
+ ;;
+ ;; => ((20 50))
+ (loopy (list (&map (2 two-times-ten) (5 five-times-ten))
+ (list [00 10 20 30 40 50 60 70 80 90 100]))
+ (collect (list two-times-ten five-times-ten)))
+ #+end_src
+
+ When =&map= and =&key= are used together, they search through the same
+ values. The use of both is normally redundant.
+
+ #+begin_src emacs-lisp
+ ;; => (1 2 (:k1 3 :k2 4)
+ ;; 3 4
+ ;; 3 4)
+ (loopy (array ( a b
+ &rest c
+ &key ((:k1 key-k1)) ((:k2 key-k2))
+ &map (:k1 map-k1) (:k2 map-k2))
+ [(1 2 :k1 3 :k2 4)])
+ (collect (list a b c
+ key-k1 key-k2
+ map-k1 map-k2)))
+ #+end_src
+
+- The symbol =&aux=: Variables named after =&aux= are bound to the given values.
+ Like in CL Lib, =&aux= must come last in the list.
+
+ #+begin_src emacs-lisp
+ ;; => (7 7 7)
+ (loopy (cycle 3)
+ (collect (&aux [coll 7]) 'ignored)
+ (finally-return coll))
+ #+end_src
+
+
** Generic Evaluation
:PROPERTIES:
:DESCRIPTION: Setting variables, evaluating expressions, etc.
@@ -1928,16 +2219,22 @@ in the example above.
#+attr_texinfo: :tag Caution
#+begin_quote
Be aware that using ~setf~ on an array sub-sequence named by =&rest=
-will only overwrite values, not truncate or grow the array.
+will only overwrite values, not truncate or grow the array.
#+end_quote
#+attr_texinfo: :tag Warning
#+begin_quote
Unfortunately, not all kinds of recursive destructuring work on references.
-This is a limitation of how generic setters are implemented, and is not
-specific to ~loopy~.
-
-Currently, the variable after =&rest= in arrays cannot be recursive.
+This is a limitation of how generic setters are implemented, and not all
+limitations are specific to ~loopy~.
+
+Currently:
+- The variable after =&rest= in arrays cannot be recursive.
+- The variables after =&map= cannot be recursive due to the current
+ implementation of =map.el= upstream.
+- =&optional= variables are not supported
+- =SUPPLIED= variables are not supported for =&key= and =&map=.
+- Non-nil default values for =&optional=, =&key=, and =&map= are not supported.
#+end_quote
As with the =array= and =seq= commands, the =array-ref= and =seq-ref=
@@ -4219,12 +4516,9 @@ the destructuring of:
- accumulation variables
- variables bound by the special macro argument =with=
-#+attr_texinfo: :tag Note
-#+begin_quote
These flags do not affect the destructuring of generalized variables
(~setf~-able places) as the libraries =pcase.el=, =seq.el=, and =dash.el= do not
yet provide the required functionality.
-#+end_quote
#+begin_src emacs-lisp
;; => ((1 4) coll1
@@ -4257,6 +4551,36 @@ yet provide the required functionality.
(finally-return (+ sum1 v1) (+ sum2 v2)))
#+end_src
+
+#+attr_texinfo: :tag Warning
+#+begin_quote
+For accumulation commands, there is no guarantee that a variable that was used
+in destructuring was meant to be user-facing. Destructuring systems can create
+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.
+
+
+#+begin_src emacs-lisp
+ ;; Possibly unexpected behavior:
+ ;;
+ ;; => ((1 2 3) (2 4 6))
+ (loopy (flag +pcase)
+ (list i '(1 2 3))
+ (collect (and whole
+ (let temporary? (* 2 whole)))
+ i)
+ (finally-return whole temporary?))
+#+end_src
+
+
* Custom Aliases
:PROPERTIES:
:CUSTOM_ID: custom-aliases
diff --git a/doc/loopy.texi b/doc/loopy.texi
index 3d0e482d..f31ea7d8 100644
--- a/doc/loopy.texi
+++ b/doc/loopy.texi
@@ -705,7 +705,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,org9e6a997
+@float Listing,org668e9bc
@lisp
;; => (nil 1 2)
(loopy (collect coll i)
@@ -780,7 +780,46 @@ value using @samp{:by SOME-EXPRESSION}.
Generally, @samp{VAR} is initialized to @code{nil}, but not always. This document tries
-to note when that is not the case.
+to note when that is not the case. For when that is not the case, the variable
+can still be initialized to @code{nil} if it is set to @code{nil} using the @samp{with} special
+macro argument. These special cases allow for more efficient code and less
+indirection.
+
+@lisp
+;; => (0 1 2 3)
+(loopy (collect i)
+ (numbers i :from 0 :to 3))
+
+;; => (nil 0 1 2)
+(loopy (with (i nil))
+ (collect i)
+ (numbers i :from 0 :to 3))
+@end lisp
+
+Unlike @code{cl-loop} in some cases, in Loopy, the values passed as keyword arguments
+are evaluated only once. For example, the command @samp{(list i some-list :by
+(get-function))} evaluates @code{(get-function)} only once. It does not evaluate it
+repeatedly for each step of the loop.
+
+@lisp
+;; Passes the assertion:
+;;
+;; => (0 1 2 3 4 5 6 7 8 9 10)
+(loopy (with (times 0))
+ (list i (number-sequence 0 10) :by (progn
+ (cl-assert (= times 0))
+ (cl-incf times)
+ #'cdr))
+ (collect i))
+
+;; => Fails the assertion on the second step of the loop:
+(cl-loop with times = 0
+ for i in (number-sequence 0 10) by (progn
+ (cl-assert (= times 0))
+ (cl-incf times)
+ #'cdr)
+ collect i)
+@end lisp
@menu
* Basic Destructuring:: How to destructure variables and values in loop commands.
@@ -805,20 +844,39 @@ Some differences include:
@itemize
@item
Destructuring arrays
+
@item
Destructuring in accumulation commands (@ref{Accumulation})
+
@item
Destructuring in commands iterating through @code{setf}-able places in a sequence
(@ref{Sequence Reference Iteration})
+
+@item
+The extended forms of the @samp{&optional} and @samp{&key} variables (such as default
+values like in @code{... &optional (var default) ...}) can be specified using
+square brackets as well as parentheses (such as @code{... &optional [var default]
+ ...}). Since such variables can be further destructured by being written as
+sequences themselves, allowing both parentheses and brackets reduces confusion
+and improves consistency.
+
+@item
+A @samp{&map} construct, similar to @samp{&key}, but using @code{map-elt} instead of
+@code{plist-get} and which does not error when the map contains keys which aren't
+matched (in other words, there is no need for an equivalent of
+@samp{&allow-other-keys}).
@end itemize
-In addition to what can be done in loop commands, several macros are available
-for using Loopy's destructuring outside of @code{loopy} loops (@ref{Destructuring Macros}).
This section describes the basic built-in destructuring used by most loop
-commands, such as @samp{set} and @samp{list}. Destructuring in accumulation commands and
-sequence-reference commands works slightly differently, and is described more in
-those sections.
+commands, such as @samp{set} and @samp{list}. Destructuring in accumulation commands
+(@ref{Accumulation}) and sequence-reference commands
+(@ref{Sequence Reference Iteration}) works slightly differently, and is described
+more in those sections.
+
+In addition to what can be done in loop commands, several features are available
+for using Loopy's destructuring outside of @code{loopy} loops (@ref{Destructuring Macros}),
+including the @code{pcase} pattern @samp{loopy}.
The last thing to note is that @code{loopy} loops can be made to use alternative
destructuring systems, such as @code{seq-let} or @code{pcase-let}. This is done by using
@@ -826,9 +884,10 @@ the @samp{flag} special macro argument (@ref{Using Flags}). If you are familiar
package @samp{dash} @footnote{@uref{https://github.com/magnars/dash.el}} and its Clojure-style destructuring, consider trying
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,org866b582
+@float Listing,orgcba0f7c
@lisp
;; => (1 2 3 4)
(cl-loop for (i . j) in '((1 . 2) (3 . 4))
@@ -843,7 +902,7 @@ Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
@caption{Destructuring values in a list.}
@end float
-@float Listing,orgedbe07f
+@float Listing,org87acf60
@lisp
;; => (1 2 3 4)
(cl-loop for elem in '((1 . 2) (3 . 4))
@@ -866,15 +925,66 @@ as the @samp{VAR} argument of a loop command. Loopy supports destructuring list
arrays (which includes strings and vectors).
@itemize
@item
-To destructure lists, use a list.
+To destructure lists, use a list, as in @samp{(a b c)}.
@item
-To destructure arrays, use a vector.
+To destructure arrays, use a vector, as in @samp{[a b c]}.
@end itemize
+
This sequence of symbols can be shorter than the destructured sequence, @emph{but not
-longer}. If shorter, the unassigned elements of the list are simply ignored.
+longer}. If shorter, the unassigned elements of the destructured sequence are
+simply ignored.
-An element in the sequence @samp{VAR} can be one of the following:
+The content of this destructuring sequence is similar to @samp{cl-lib}, and is
+
+@example
+POSITIONAL-VARIABLES
+&optional OPTIONAL-VARIABLES
+&rest REST-VARIABLE
+&key KEY-VARIABLES [&allow-other-keys]
+&map MAP-VARIABLES
+&aux AUXILLIARY-VARIABLES
+@end example
+
+in which at least one of the above constructs must be provided.
+
+@lisp
+;; => (1 2 3
+;; 4 5 t
+;; (:k1 111 :k2 222)
+;; 111 t
+;; 222
+;; 111
+;; 333 nil
+;; 4444 5555)
+(pcase (list 1 2 3 4 5 :k1 111 :k2 222)
+ ((loopy ( a b c
+ &optional
+ d
+ (e nil e-supplied)
+ &rest
+ r
+ &key
+ ((:k1 k1) nil k1-supplied)
+ k2
+ &map
+ (:k1 map1)
+ [:k3 map3 333 map3-supplied]
+ &aux
+ [x1 4444] (x2 5555)))
+ (list a b c
+ d
+ e e-supplied
+ r
+ k1 k1-supplied
+ k2
+ map1
+ map3 map3-supplied
+ x1 x2)))
+@end lisp
+
+
+In more detail, the elements of the destructuring sequence can be:
@itemize
@item
@@ -888,10 +998,13 @@ syntax for sequences.
(loopy (list [i (j k)] '([1 (2 3)] [4 (5 6)]))
(collect (list i j k)))
@end lisp
+@end itemize
+@cindex _
+@itemize
@item
-The symbol @samp{_} (an underscore): The symbol @samp{_} means to avoid creating a
-variable. This can be more efficient.
+The symbol @samp{_} (an underscore) or a symbol beginning with an underscore: This
+means to ignore the element at this location. This can be more efficient.
@lisp
;; Only creates the variables `a' and `d':
@@ -907,10 +1020,13 @@ variable. This can be more efficient.
(collect a))
;; => (1 3)
-(loopy (array (a . _) [(1 2) (3 4)])
+(loopy (array (a . _ignored) [(1 2) (3 4)])
(collect a))
@end lisp
+@end itemize
+@cindex &whole
+@itemize
@item
The symbol @samp{&whole}: If @samp{&whole} is the first element in the sequence, then
the second element of the sequence names a variable that holds the entire
@@ -931,11 +1047,38 @@ This is the same as when used in a CL @code{lambda} list.
(list both i j)))
'((1 2) (3 4)))
@end lisp
+@end itemize
+@cindex &rest
+@itemize
@item
The symbol @samp{&rest}: A variable named after @samp{&rest} contains the remaining
-elements of the destructured value. When destructuring lists, one can also
-use dotted notation. These variables can themselves be sequences.
+elements of the destructured value after any positional and optional values.
+When destructuring lists, one can also use dotted notation, as in a CL
+@code{lambda} list. These variables can themselves be sequences to be further
+destructured.
+
+When used after optional values, the @samp{&rest} value is the subsequence starting
+at the index after any possible optional values, even when those optional
+values are not actually present. If the sequence is not long enough, than the
+sub-sequence is empty.
+
+@lisp
+;; => (1 2 (3))
+(pcase (list 1 2 3)
+ ((loopy (a &optional b &rest c))
+ (list a b c)))
+
+;; => (1 nil nil)
+(pcase (list 1)
+ ((loopy (a &optional b &rest c))
+ (list a b c)))
+
+;; => (1 [])
+(pcase (vector 1)
+ ((loopy [a &optional _ _ _ _ &rest c])
+ (list a c)))
+@end lisp
This @samp{&rest} is the same as when used in @code{seq-let}.
@@ -968,7 +1111,72 @@ This @samp{&rest} is the same as when used in @code{seq-let}.
(loopy (list (i . [j k]) '((1 . [2 3]) (4 . [5 6])))
(collect (list i j)))
@end lisp
+@end itemize
+@cindex &optional
+@itemize
+@item
+The symbol @samp{&optional}: A variable named after @samp{&optional} is optional. If
+the list is not long enough to bind the variable, then the variable is bound
+to @code{nil} or, if specified, a default value. Additionally, one may bind a
+variable to record whether the list was long enough to contain the optional
+value.
+
+As in a CL @code{lambda-list}, the variable has the one of the following forms:
+
+@itemize
+@item
+@samp{(VAR DEFAULT SUPPLIED)} or @samp{[VAR DEFAULT SUPPLIED]}, in which @samp{VAR} itself
+can be a sequence
+
+@item
+@samp{(VAR DEFAULT)} or @samp{[VAR DEFAULT]}, in which @samp{VAR} itself can be a sequence
+
+@item
+@samp{(VAR)} or @samp{[VAR]}, in which @samp{VAR} itself can be a sequence
+
+@item
+a symbol @samp{VAR}
+@end itemize
+
+@lisp
+;; => (1 2 88 t nil)
+(loopy (array (a &optional ((b &optional (c 88 c-supplied))
+ (list 77)
+ bc-supplied))
+ [(1 (2))])
+ (collect (list a b c bc-supplied c-supplied)))
+
+;; => (1 2 3 t t)
+(loopy (array (a &optional ((b &optional (c 88 c-supplied))
+ (list 77)
+ bc-supplied))
+ [(1 (2 3))])
+ (collect (list a b c bc-supplied c-supplied)))
+@end lisp
+
+@samp{&optional} cannot be used after @samp{&rest}.
+
+@lisp
+;; => ((1 2 3 4 5)
+;; 1
+;; 2
+;; 3
+;; (4 5))
+(loopy (array (&whole all a b &optional c &rest d)
+ [(1 2 3 4 5)])
+ (collect (list all a b c d)))
+
+;; Same as above:
+(loopy (array (&whole all a b &rest (c &rest d))
+ [(1 2 3 4 5)])
+ (collect (list all a b c d)))
+@end lisp
+@end itemize
+
+@cindex &key
+@cindex &keys
+@itemize
@item
The symbol @samp{&key} or @samp{&keys}: Variables named after @samp{&key} are transformed
into keys whose values will be sought using @code{plist-get}, which returns @code{nil}
@@ -983,10 +1191,38 @@ Currently, only lists support this destructuring.
(collect (list a b missing)))
@end lisp
-If the key is not in the list, a default value can be provided by using a
-two-item list of the variable and the default value. If a default value is
-provided, then keys are sought using @code{plist-member}. That way, a value of
-@code{nil} for a key is not the same as a missing key.
+Variables after @samp{&key} can be of the following forms:
+
+@itemize
+@item
+@samp{((VAR KEY) DEFAULT SUPPLIED)}, @samp{[[VAR KEY] DEFAULT SUPPLIED]}, @samp{([VAR KEY]
+ DEFAULT SUPPLIED)}, or @samp{[(VAR KEY) DEFAULT SUPPLIED]}, in which @samp{VAR} itself
+can be a sequence
+
+@item
+@samp{((VAR KEY) DEFAULT)}, @samp{[[VAR KEY] DEFAULT]}, @samp{([VAR KEY] DEFAULT)}, or
+@samp{[(VAR KEY) DEFAULT]}, in which @samp{VAR} itself can be a sequence
+
+@item
+@samp{((VAR KEY))}, @samp{[[VAR KEY]]}, @samp{([VAR KEY])}, or @samp{[(VAR KEY)]}, in which
+@samp{VAR} itself can be a sequence
+
+@item
+@samp{(VAR DEFAULT SUPPLIED)} or @samp{[VAR DEFAULT SUPPLIED]}, in which @samp{VAR} is a
+symbol
+
+@item
+@samp{(VAR DEFAULT)} or @samp{[VAR DEFAULT]}, in which @samp{VAR} is a symbol
+
+@item
+@samp{(VAR)} or @samp{[VAR]}, in which @samp{VAR} is a symbol
+
+@item
+a symbol @samp{VAR}
+@end itemize
+
+If a default value is provided, then keys are sought using @code{plist-member}.
+That way, a value of @code{nil} for a key is not the same as a missing key.
@lisp
;; Note that `nil' is not the same as a missing value:
@@ -997,6 +1233,19 @@ provided, then keys are sought using @code{plist-member}. That way, a value of
(collect (list a b c missing)))
@end lisp
+By default, the sought key is made by prepending a colon (``:'') to the symbol
+name. For example, @samp{a} searches for @samp{:a} and @samp{b} searches for @samp{:b}. Like in
+@samp{cl-lib}, an evaluated key can be sought by using a sub-sequence as the first
+element of the list. When @samp{VAR} is a sequence, the key must be provided
+separately.
+
+@lisp
+;; => ((1 nil t))
+(loopy (list (&key ((:cat c)) ((:dog d) 27 dog-found))
+ '((:cat 1 :dog nil)))
+ (collect (list c d dog-found)))
+@end lisp
+
Keys are sought in values after those bound to positional variables, which can
be the same values bound to the variable named by @samp{&rest} when both are
used.
@@ -1029,6 +1278,129 @@ the dot in dotted lists.
(loopy (array (a &key k1 . b) [(1 :k1 3)])
(collect (list a b k1)))
@end lisp
+
+Like in @samp{cl-lib}, if, after searching for the other keys, there remains an
+unmatched key in the destructured value, an error is signaled unless
+@samp{&allow-other-keys} is also used, or unless the key @samp{:allow-other-keys} is
+associated with a non-nil value in the property list, even when using @samp{&rest}.
+
+@lisp
+;; Error due to presence of `:k3':
+(cl-destructuring-bind (a b &rest c &key k1 k2)
+ (list 1 2 :k1 3 :k2 4 :k3 5)
+ (list a b c k1 k2))
+
+;; Works as expected:
+;;
+;; => (1 2 (:k1 3 :k2 4 :k3 5) 3 4)
+(cl-destructuring-bind (a b &rest c &key k1 k2 &allow-other-keys)
+ (list 1 2 :k1 3 :k2 4 :k3 5)
+ (list a b c k1 k2))
+@end lisp
+@end itemize
+
+@cindex &map
+@itemize
+@item
+The symbol @samp{&map}: Variables after @samp{&map} are bound similar to @code{map-let} from
+the library @samp{map.el}. @samp{&map} works similarly to @samp{&key}, but has a few
+important differences:
+
+@enumerate
+@item
+Maps are more generic than property lists (``plists''). A ``map'' is a generic
+structure which supports the function @code{map-elt}. The built-in maps are
+arrays, property lists (``plists''), association lists (``alists''), and hash
+tables. This generality means that it is slower than @samp{&key} for property
+lists, though the difference should be small.
+
+@item
+@samp{&map} will not signal an error if there are unused keys inside the
+destructured value; there is no @samp{&allow-other-keys} for @samp{map}. In the same
+vein, it cannot be made to signal an error if there are unused keys.
+@end enumerate
+
+Variables after @samp{&map} can be of the following forms:
+
+@itemize
+@item
+@samp{(KEY VAR DEFAULT SUPPLIED)} or @samp{[KEY VAR DEFAULT SUPPLIED]}, in which @samp{VAR}
+itself can be a sequence
+
+@item
+@samp{(KEY VAR DEFAULT)} or @samp{[KEY VAR DEFAULT]}, in which @samp{VAR} itself can be a
+sequence
+
+@item
+@samp{(KEY VAR)} or @samp{[KEY VAR]}, in which @samp{VAR} itself can be a sequence
+
+@item
+@samp{(VAR)} or @samp{[VAR]}, in which @samp{VAR} is a symbol
+
+@item
+a symbol @samp{VAR}
+@end itemize
+
+When specifying @samp{KEY}, @samp{VAR} can be a sequence to perform further
+destructuring. When @samp{KEY} is not given, then the key is the symbol @samp{VAR}, as
+in @code{(quote VAR)}.
+
+@lisp
+;; => ((1 2 3 4 27))
+(loopy (array (a b &map c ('dog d) (:elephant e 27))
+ [(1 2 c 3 dog 4)])
+ (collect (list a b c d e)))
+
+;; => ((1 2 3 4 27 33 nil))
+(loopy (array ( a b
+ &map
+ c
+ ('dog d)
+ (:elephant e 27)
+ (:fox f 33 fox-found))
+ [(1 2 (c . 3) (dog . 4))])
+ (collect (list a b c d e f fox-found)))
+
+;; => ((1 2 5 t))
+(loopy (array (a b &map (:fox f 33 fox-found))
+ [(1 2 (c . 3) (dog . 4) (:fox . 5))])
+ (collect (list a b f fox-found)))
+
+;; For arrays, the key is the index:
+;;
+;; => ((20 50))
+(loopy (list (&map (2 two-times-ten) (5 five-times-ten))
+ (list [00 10 20 30 40 50 60 70 80 90 100]))
+ (collect (list two-times-ten five-times-ten)))
+@end lisp
+
+When @samp{&map} and @samp{&key} are used together, they search through the same
+values. The use of both is normally redundant.
+
+@lisp
+;; => (1 2 (:k1 3 :k2 4)
+;; 3 4
+;; 3 4)
+(loopy (array ( a b
+ &rest c
+ &key ((:k1 key-k1)) ((:k2 key-k2))
+ &map (:k1 map-k1) (:k2 map-k2))
+ [(1 2 :k1 3 :k2 4)])
+ (collect (list a b c
+ key-k1 key-k2
+ map-k1 map-k2)))
+@end lisp
+
+@item
+The symbol @samp{&aux}: Variables named after @samp{&aux} are bound to the given values.
+Like in CL Lib, @samp{&aux} must come last in the list.
+
+@lisp
+;; => (7 7 7)
+(loopy (cycle 3)
+ (collect (&aux [coll 7]) 'ignored)
+ (finally-return coll))
+@end lisp
@end itemize
@node Generic Evaluation
@@ -1210,6 +1582,13 @@ error to use the same iteration variable for multiple iteration commands.
(finally-return t))
@end lisp
+Unlike @code{cl-loop} and like Common Lisp's @code{iterate}, arguments of the iteration
+commands are evaluated only once. For example, while iterating through numbers,
+you can't suddenly change the direction of the iteration in the middle of the
+loop, nor can you change the final numeric value. Similarly, the function used
+to iterate through the list in the @samp{list} command is the same for the entire
+loop. This restriction allows for producing more efficient code.
+
@menu
* Generic Iteration:: Looping a certain number of times.
* Numeric Iteration:: Iterating through numbers.
@@ -1481,7 +1860,8 @@ argument, as in @code{(funcall TEST VAR FINAL-VAL)}. @samp{test} can only be us
@samp{from} and @samp{to}; it cannot be used with keywords that already describe a
direction and ending condition. To match the behavior of @code{cl-loop}, the
default testing function is @code{#'<=}. When @samp{test} is given, @samp{by} can be
-negative.
+negative. As there is no default end value when @samp{test} is given, @samp{to} must
+also be given.
@lisp
;; => (10 9.5 9.0 8.5 8.0 7.5 7.0 6.5 6.0 5.5)
@@ -1850,9 +2230,19 @@ library @samp{seq.el}.
This command also has the aliases @samp{seqing} and @samp{sequencing}.
@samp{KEYS} is one or several of @samp{from}, @samp{upfrom}, @samp{downfrom}, @samp{to}, @samp{upto},
-@samp{downto}, @samp{above}, @samp{below}, @samp{by}, and @samp{index}. @samp{index} names the variable
-used to store the index being accessed. For others, see the @samp{numbers}
-command.
+@samp{downto}, @samp{above}, @samp{below}, @samp{by}, @samp{test}, and @samp{index}. @samp{index} names the
+variable used to store the index being accessed. For the others, see the
+@samp{numbers} command.
+
+@quotation Warning
+Array elements can be accessed in constant time, but not list elements. For
+lists, the @samp{sequence} command is fastest when moving forwards through the
+list. In that case, the command does not have to search from the beginning of
+the list each time to find the next element. The @samp{sequence} command can be
+noticeably slower for lists when working backwards or when the @samp{test}
+parameter (for which direction cannot be assumed) is provided.
+
+@end quotation
If multiple sequences are given, then these keyword arguments apply to the
resulting sequence of distributed elements.
@@ -1937,10 +2327,10 @@ The aliases @samp{seqi}, @samp{arrayi}, @samp{listi}, and @samp{stringi} are sim
aliases @samp{seqf}, @samp{arrayf}, @samp{listf}, and @samp{stringf} of the @samp{seq-ref} command.
@samp{KEYS} is one or several of @samp{from}, @samp{upfrom}, @samp{downfrom}, @samp{to}, @samp{upto},
-@samp{downto}, @samp{above}, @samp{below}, and @samp{by}. For their meaning, see the @samp{numbers}
-command. This command is very similar to @samp{numbers}, except that it can
-automatically end the loop when the final element is reached. With
-@samp{numbers}, one would first need to explicitly calculate the length of the
+@samp{downto}, @samp{above}, @samp{below}, @samp{by}, and @samp{test}. For their meaning, see the
+@samp{numbers} command. This command is very similar to @samp{numbers}, except that it
+can automatically end the loop when the index of the final element is reached.
+With @samp{numbers}, one would first need to explicitly calculate the length of the
sequence.
Similar to @samp{numbers}, for efficiency, @samp{VAR} is initialized to the starting
@@ -2009,16 +2399,29 @@ in the example above.
@quotation Caution
Be aware that using @code{setf} on an array sub-sequence named by @samp{&rest}
-will only overwrite values, not truncate or grow the array.
+will only overwrite values, not truncate or grow the array.
@end quotation
@quotation Warning
Unfortunately, not all kinds of recursive destructuring work on references.
-This is a limitation of how generic setters are implemented, and is not
-specific to @code{loopy}.
+This is a limitation of how generic setters are implemented, and not all
+limitations are specific to @code{loopy}.
-Currently, the variable after @samp{&rest} in arrays cannot be recursive.
+Currently:
+@itemize
+@item
+The variable after @samp{&rest} in arrays cannot be recursive.
+@item
+The variables after @samp{&map} cannot be recursive due to the current
+implementation of @samp{map.el} upstream.
+@item
+@samp{&optional} variables are not supported
+@item
+@samp{SUPPLIED} variables are not supported for @samp{&key} and @samp{&map}.
+@item
+Non-nil default values for @samp{&optional}, @samp{&key}, and @samp{&map} are not supported.
+@end itemize
@end quotation
@@ -2179,9 +2582,9 @@ through the elements of the sequence @samp{EXPR}, binding @samp{VAR} as a
@code{setf}-able place.
@samp{KEYS} is one or several of @samp{from}, @samp{upfrom}, @samp{downfrom}, @samp{to}, @samp{upto},
-@samp{downto}, @samp{above}, @samp{below}, @samp{by}, and @samp{index}. @samp{index} names the variable
-used to store the index being accessed. For others, see the @samp{numbers}
-command.
+@samp{downto}, @samp{above}, @samp{below}, @samp{by}, @samp{test}, and @samp{index}. @samp{index} names the
+variable used to store the index being accessed. For others, see the
+@samp{numbers} command.
@lisp
;; => (7 7 7 7)
@@ -2645,11 +3048,13 @@ first argument and @samp{EXPR} as the second argument. This is unlike
This command also has the alias @samp{reducing}.
-When @samp{VAR} does not have an explicit starting value (given with the special
-macro argument @samp{with}), the first accumulated value is @samp{EXPR}. The first
-accumulated value is not the result of passing @samp{VAR} and @samp{EXPR} to @samp{FUNC}.
-Using the @samp{with} special macro argument is similar to using @code{cl-reduce}'s
-@samp{:initial-value} keyword argument.
+Note that the first accumulated value depends on the initial value of @samp{VAR}.
+By default, the first accumulated value is the value of @samp{EXPR}, not a result
+of calling @samp{FUNC}. However, if @samp{VAR} has an initial value given by the @samp{with}
+special macro argument, then the first accumulated value is the result of
+@code{(funcall FUNC VAR EXPR)}, as also done in the subsequent steps of the loop.
+This use of @samp{with} is similar to the @samp{:initial-value} keyword argument used by
+@code{cl-reduce}.
@lisp
;; => 6
@@ -4183,7 +4588,7 @@ using the @code{let*} special form.
This method recognizes all commands and their aliases in the user option
@code{loopy-aliases}.
-@float Listing,org0290e68
+@float Listing,org748b4ce
@lisp
;; => ((1 2 3) (-3 -2 -1) (0))
(loopy-iter (arg accum-opt positives negatives other)
@@ -4608,13 +5013,10 @@ accumulation variables
variables bound by the special macro argument @samp{with}
@end itemize
-@quotation Note
These flags do not affect the destructuring of generalized variables
(@code{setf}-able places) as the libraries @samp{pcase.el}, @samp{seq.el}, and @samp{dash.el} do not
yet provide the required functionality.
-@end quotation
-
@lisp
;; => ((1 4) coll1
;; ((2 3) (5 6)) whole
@@ -4646,6 +5048,35 @@ yet provide the required functionality.
(finally-return (+ sum1 v1) (+ sum2 v2)))
@end lisp
+
+@quotation Warning
+For accumulation commands, there is no guarantee that a variable that was used
+in destructuring was meant to be user-facing. Destructuring systems can create
+new variables as they please, which can be interpreted as accumulation
+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.
+
+
+@lisp
+;; Possibly unexpected behavior:
+;;
+;; => ((1 2 3) (2 4 6))
+(loopy (flag +pcase)
+ (list i '(1 2 3))
+ (collect (and whole
+ (let temporary? (* 2 whole)))
+ i)
+ (finally-return whole temporary?))
+@end lisp
+
@node Custom Aliases
@chapter Custom Aliases
@@ -5810,4 +6241,4 @@ special form @code{cond}.
@printindex cp
-@bye
\ No newline at end of file
+@bye
diff --git a/loopy-commands.el b/loopy-commands.el
index dfb1e55b..29ce9a01 100644
--- a/loopy-commands.el
+++ b/loopy-commands.el
@@ -75,6 +75,8 @@
(require 'generator)
(require 'gv)
(require 'loopy-misc)
+(require 'loopy-destructure)
+(require 'loopy-instrs)
(require 'loopy-vars)
(require 'map)
(require 'macroexp)
@@ -93,44 +95,6 @@
;; If Emacs Lisp ever gets support for true multiple values (via `cl-values'),
;; this function might be a good candidate for use.
-(defun loopy--extract-main-body (instructions)
- "Extract main-body expressions from INSTRUCTIONS.
-
-This returns a list of two sub-lists:
-
-1. A list of expressions (not instructions) that are meant to be
- use in the main body of the loop.
-
-2. A list of instructions for places other than the main body.
-
-The lists will be in the order parsed (correct for insertion)."
- (let ((wrapped-main-body)
- (other-instructions))
- (dolist (instruction instructions)
- (if (eq (cl-first instruction) 'loopy--main-body)
- (push (cl-second instruction) wrapped-main-body)
- (push instruction other-instructions)))
-
- ;; 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'.
-
-INSTRUCTIONS is a list of instructions, which don't all have to be
-for `loopy--iteration-vars'."
- (loopy--substitute-using-if
- (cl-function (lambda ((_ init)) (list 'loopy--other-vars init)))
- (lambda (x) (eq (car x) 'loopy--iteration-vars))
- instructions))
;;;;; Working with Plists and Keyword Arguments
@@ -148,7 +112,6 @@ Any keyword not in CORRECT is considered invalid.
CORRECT is a list of valid keywords. The first item in LIST is
assumed to be a keyword."
- ;; (null (cl-set-difference (loopy--every-other list) correct))
(null (cl-set-difference (loopy--extract-keywords list) correct)))
;;;;; Miscellaneous
@@ -1927,9 +1890,9 @@ Warning trigger: %s"
;; If we need to destructure the sequence `var', we use the
;; function named by
;; `loopy--destructuring-accumulation-parser' or the function
- ;; `loopy--parse-destructuring-accumulation-command'.
+ ;; `loopy--parse-destructuring-accumulation-command-default'.
(funcall (or loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command)
+ #'loopy--parse-destructuring-accumulation-command-default)
cmd)
(when (and (loopy--with-bound-p var)
@@ -3078,7 +3041,7 @@ Return a list of instructions for naming these `setf'-able places.
VAR are the variables into to which to destructure the value of
VALUE-EXPRESSION."
(let ((destructurings
- (loopy--destructure-generalized-variables var value-expression))
+ (loopy--destructure-generalized-sequence var value-expression))
(instructions nil))
(dolist (destructuring destructurings)
(push (list 'loopy--generalized-vars
@@ -3086,9 +3049,6 @@ VALUE-EXPRESSION."
instructions))
(nreverse instructions)))
-(defalias 'loopy--destructure-generalized-variables
- #'loopy--destructure-generalized-sequence)
-
(defun loopy--destructure-for-iteration-default (var val)
"Destructure VAL according to VAR.
@@ -3097,9 +3057,10 @@ Returns a list. The elements are:
in VAL.
2. A list of variables which exist outside of this expression and
need to be `let'-bound."
- (let ((bindings (loopy--destructure-sequence var val)))
- (list (cons 'setq (apply #'append bindings))
- (cl-remove-duplicates (mapcar #'cl-first bindings)))))
+ (let ((res (loopy--pcase-destructure-for-iteration `(loopy ,var) val :error t)))
+ (if (null (cl-second res))
+ (signal 'loopy-destructure-vars-missing (list var val))
+ res)))
;; TODO: Rename these so that the current "iteration" features
;; are "generic" and the new "iteration" features
@@ -3138,130 +3099,17 @@ A wrapper around `loopy--destructure-for-iteration-command'."
(loopy--convert-iteration-vars-to-other-vars
(loopy--destructure-for-iteration-command var value-expression)))
-(cl-defun loopy--parse-destructuring-accumulation-command
+(cl-defun loopy--parse-destructuring-accumulation-command-default
((name var val &rest args))
"Return instructions for destructuring accumulation commands.
-Unlike `loopy--basic-builtin-destructuring', this function
+Unlike `loopy--destructure-for-iteration-command', this function
does destructuring and returns instructions.
NAME is the name of the command. VAR is a variable name. VAL is a value."
- (let* ((remaining-var var)
- (value-holder (gensym (format "%s-destructured-seq-" name)))
- (instructions `((loopy--iteration-vars (,value-holder nil))
- (loopy--main-body (setq ,value-holder ,val)))))
-
- ;; Handle the whole var.
- (when (eq (seq-first var) '&whole)
- (dolist (instr (loopy--parse-loop-command
- `(,name ,(seq-elt var 1) ,value-holder ,@args)))
- (push instr instructions))
- (setq remaining-var (seq-drop remaining-var 2)))
-
- ;; How variables are set depends on type. For lists, we wish to use `pop'
- ;; to avoid traversing the list more than once. For arrays, we must use
- ;; `aref'.
- (cl-etypecase remaining-var
- (symbol
- (push `(loopy--accumulation-vars (,remaining-var ,value-holder))
- instructions))
- (list
- (let ((key-vars)
- (this-var)
- (looking-at-key-vars)
- (var-is-dotted (not (proper-list-p remaining-var))))
-
- (while (car-safe remaining-var)
-
- (setq this-var (car remaining-var))
- (cond
- ((eq this-var '_) ; Do nothing in this case.
- (push `(loopy--main-body (setq ,value-holder (cdr ,value-holder)))
- instructions)
- (setq remaining-var (cdr remaining-var)))
-
- ((eq this-var '&rest)
- (setq looking-at-key-vars nil)
- (when var-is-dotted
- (signal 'loopy-&rest-dotted (list var)))
- (dolist (instr (loopy--parse-loop-command
- `( ,name ,(cl-second remaining-var)
- ,value-holder ,@args)))
- (push instr instructions))
- (setq remaining-var (cddr remaining-var)))
-
- ((memq this-var '(&key &keys))
- (setq looking-at-key-vars t
- remaining-var (cdr remaining-var)))
-
- (looking-at-key-vars
- (push this-var key-vars)
- (setq remaining-var (cdr remaining-var)))
-
- (t
- (dolist (instr (loopy--parse-loop-command
- `(,name ,this-var (pop ,value-holder) ,@args)))
- (push instr instructions))
- (setq remaining-var (cdr remaining-var)))))
-
- ;; If `remaining-var' is not nil, then it is now the final atom of an
- ;; improper list.
- (when remaining-var
- (dolist (instr (loopy--parse-loop-command
- `(,name ,remaining-var ,value-holder ,@args)))
- (push instr instructions)))
-
- ;; TODO: In Emacs 28, `pcase' was changed so that all named variables
- ;; are at least bound to nil. Before that version, we should make sure
- ;; that `default' is bound.
- (let ((default nil))
- (ignore default)
- (pcase-dolist ((or `(,kvar ,default)
- kvar)
- key-vars)
- (dolist (instr
- (loopy--parse-loop-command
- `( ,name ,kvar
- ,(let ((key (intern (format ":%s" kvar))))
- (if default
- `(if-let ((key-found (plist-member ,value-holder
- ,key)))
- (cl-second key-found)
- ,default)
- `(plist-get ,value-holder ,key)))
- ,@args)))
- (push instr instructions))))))
-
- (array
- (cl-loop named loop
- with array-length = (length remaining-var)
- for symbol-or-seq across remaining-var
- for index from 0
- do (cond
- ((eq symbol-or-seq '_))
- ((eq symbol-or-seq '&rest)
- (let* ((next-idx (1+ index))
- (next-var (aref remaining-var next-idx)))
- ;; Check that the var after `&rest' is the last:
- (when (> (1- array-length) next-idx)
- (signal 'loopy-&rest-multiple (list var)))
-
- (dolist (instr
- (loopy--parse-loop-command
- `( ,name ,next-var
- (cl-subseq ,value-holder ,index) ,@args)))
- (push instr instructions)))
- ;; Exit the loop.
- (cl-return-from loop))
- (t
- (dolist (instr
- (loopy--parse-loop-command
- `( ,name ,symbol-or-seq
- (aref ,value-holder ,index) ,@args)))
- (push instr instructions)))))))
-
- ;; Return the instructions in the correct order.
- (nreverse instructions)))
+ (loopy--pcase-parse-for-destructuring-accumulation-command
+ `(,name (loopy ,var) ,val ,@args)
+ :error t))
;;;; Selecting parsers
(defun loopy--parse-loop-command (command)
diff --git a/loopy-dash.el b/loopy-dash.el
index 78f0d6e9..45071bda 100644
--- a/loopy-dash.el
+++ b/loopy-dash.el
@@ -81,7 +81,7 @@
(if (eq loopy--destructuring-accumulation-parser
#'loopy-dash--parse-destructuring-accumulation-command)
(setq loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command)))
+ #'loopy--parse-destructuring-accumulation-command-default)))
(add-to-list 'loopy--flag-settings (cons 'dash #'loopy-dash--enable-flag-dash))
(add-to-list 'loopy--flag-settings (cons '+dash #'loopy-dash--enable-flag-dash))
diff --git a/loopy-destructure.el b/loopy-destructure.el
new file mode 100644
index 00000000..6d48ee72
--- /dev/null
+++ b/loopy-destructure.el
@@ -0,0 +1,998 @@
+;;; loopy-destructure.el --- Miscellaneous functions used with Loopy. -*- lexical-binding: t; -*-
+
+;; Copyright (c) 2024 Earl Hyatt
+
+;;; Disclaimer:
+;; This file is not part of GNU Emacs.
+;;
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this file. If not, see .
+
+;;; Commentary:
+;; `loopy' is a macro that is used similarly to `cl-loop'. It provides "loop
+;; commands" that define a loop body and it's surrounding environment, as well
+;; as exit conditions.
+;;
+;; This library provides features for destructuring for the features provided in
+;; the main file. This separation exists for better organization.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'map)
+(require 'compat)
+(require 'loopy-misc)
+(require 'loopy-instrs)
+(require 'pcase)
+(require 'seq)
+(require 'subr-x)
+
+(declare-function loopy--parse-loop-command "ext:loopy-commands" (command))
+
+;; This better allows for things to change in the future.
+(defun loopy--var-ignored-p (var)
+ "Return whether VAR should be ignored for destructuring."
+ (and (symbolp var)
+ (eq (aref (symbol-name var) 0) ?_)))
+
+(defconst loopy--destructure-symbols '( &whole &optional &rest &body
+ &key &keys &allow-other-keys
+ &aux &map)
+ "Symbols affecting how following elements destructure.")
+
+;; Having a single function for all categories allows us to have most of the
+;; ordering rules in once place.
+(defun loopy--get-var-groups (var-seq)
+ "Return the alist of variable groups in sequence VAR-SEQ.
+Type is one of `list' or `array'."
+ (let* ((whole-var) (processing-whole)
+ (pos-var)
+ (opt-var) (processing-opts)
+ (rest-var) (processing-rest) (dotted-rest-var)
+ (key-var) (processing-keys) (allow-other-keys)
+ (map-var) (processing-maps)
+ (aux-var) (processing-auxs)
+ (proper-list-p (proper-list-p var-seq))
+ (type (cl-etypecase var-seq
+ (list 'list)
+ (array 'array)))
+ (improper-list (and (eq type 'list)
+ (not proper-list-p)))
+ (remaining-seq (if improper-list
+ (cl-copy-list var-seq)
+ (copy-sequence var-seq))))
+
+ (when improper-list
+ (cl-shiftf dotted-rest-var
+ (cdr (last remaining-seq))
+ nil))
+
+ (cl-flet ((missing-after (seq) (or (seq-empty-p seq)
+ (memq (seq-elt seq 0)
+ loopy--destructure-symbols)))
+ (stop-processing () (setq processing-whole nil
+ processing-opts nil
+ processing-rest nil
+ processing-keys nil
+ processing-maps nil)))
+
+ ;; Use `seq' functions to support arrays now and maybe other things later.
+ (while (not (seq-empty-p remaining-seq))
+ (seq-let [first &rest rest]
+ remaining-seq
+ (pcase first
+ ('&whole (cond
+ ;; Make sure there is a variable named.
+ ((missing-after rest)
+ (signal 'loopy-&whole-missing (list var-seq)))
+ ;; Make sure `&whole' is before all else.
+ ((or whole-var pos-var opt-var rest-var key-var
+ allow-other-keys aux-var map-var)
+ (signal 'loopy-&whole-bad-position (list var-seq)))
+ (t
+ (stop-processing)
+ (setq processing-whole t))))
+
+ ('&optional (cond
+ ((missing-after rest)
+ (signal 'loopy-&optional-missing
+ (list var-seq)))
+ ;; Make sure `&optional' does not occur after
+ ;; `&rest'.
+ ((or opt-var rest-var key-var map-var aux-var)
+ (signal 'loopy-&optional-bad-position
+ (list var-seq)))
+ (t
+ (stop-processing)
+ (setq processing-opts t))))
+
+ ((or '&rest '&body) (cond
+ (dotted-rest-var
+ (signal 'loopy-&rest-dotted
+ (list var-seq)))
+ ((missing-after rest)
+ (signal 'loopy-&rest-missing
+ (list var-seq)))
+ ((and (> (seq-length rest) 1)
+ (let ((after-var (seq-elt rest 1)))
+ (not (memq after-var loopy--destructure-symbols))))
+ (signal 'loopy-&rest-multiple (list var-seq)))
+ ;; In CL Lib, `&rest' must come before `&key',
+ ;; but we decided to allow it to come after.
+ ((or aux-var rest-var)
+ (signal 'loopy-&rest-bad-position
+ (list var-seq)))
+ (t
+ (stop-processing)
+ (setq processing-rest t))))
+
+ ((or '&key '&keys) (cond
+ ((not (eq type 'list))
+ (signal 'loopy-&key-array
+ (list var-seq)))
+ ((missing-after rest)
+ (signal 'loopy-&key-missing
+ (list var-seq)))
+ ((or aux-var key-var)
+ (signal 'loopy-&key-bad-position
+ (list var-seq)))
+ (t
+ (stop-processing)
+ (setq processing-keys t))))
+
+ ('&allow-other-keys (cond
+ ((not (eq type 'list))
+ (signal 'loopy-&key-array
+ (list var-seq)))
+ ((not processing-keys)
+ (signal 'loopy-&allow-other-keys-without-&key
+ (list var-seq)))
+ (t
+ (stop-processing)
+ (setq allow-other-keys t))))
+
+ ('&map (cond
+ ((missing-after rest)
+ (signal 'loopy-&map-missing (list var-seq)))
+ ((or aux-var map-var)
+ (signal 'loopy-&map-bad-position
+ (list var-seq)))
+ (t
+ (stop-processing)
+ (setq processing-maps t))))
+
+ ('&aux
+ (if (or (missing-after rest)
+ aux-var)
+ (signal 'loopy-&aux-bad-position (list var-seq))
+ (stop-processing)
+ (setq processing-auxs t)))
+
+ ('&environment
+ (signal 'loopy-bad-desctructuring (list var-seq)))
+
+ ((guard processing-whole)
+ (cond
+ ((loopy--var-ignored-p first)
+ (signal 'loopy-&whole-missing (list var-seq)))
+ (t
+ (setq whole-var first
+ processing-whole nil))))
+
+ ((guard processing-rest)
+ ;; `&rest' var can be ignored for clarity,
+ ;; but it is probably an error to ignore it
+ ;; when there are no positional or optional variables.
+ (if (and (loopy--var-ignored-p first)
+ (null pos-var)
+ (null opt-var))
+ (signal 'loopy-&rest-missing
+ (list var-seq))
+ (setq rest-var first
+ processing-rest nil)))
+
+ ((guard processing-opts)
+ (if (and (consp first)
+ (cdr first)
+ (loopy--var-ignored-p (car first)))
+ (signal 'loopy-&optional-ignored-default-or-supplied
+ (list var-seq))
+ (push first opt-var)))
+
+ ((guard processing-keys)
+ (push first key-var))
+
+ ((guard processing-maps)
+ (push first map-var))
+
+ ((guard processing-auxs)
+ (push first aux-var))
+
+ (_
+ (if (or opt-var rest-var key-var map-var aux-var
+ allow-other-keys)
+ (signal 'loopy-bad-desctructuring (list var-seq))
+ (push first pos-var)))))
+
+ (setq remaining-seq (seq-rest remaining-seq))))
+
+ `((whole . ,whole-var)
+ (pos . ,(nreverse pos-var))
+ (opt . ,(nreverse opt-var))
+ (rest . ,(or dotted-rest-var rest-var))
+ (key . ,(nreverse key-var))
+ (allow-other-keys . ,allow-other-keys)
+ (map . ,(nreverse map-var))
+ (aux . ,(nreverse aux-var)))))
+
+(defun loopy--get-&key-spec (var-form)
+ "Get the spec of `&key' VAR-FORM as (KEY VAR DEFAULT SUPPLIED)."
+ (loopy--pcase-let-workaround (key var default supplied)
+ (pcase-let (((or (or (seq (seq key var) default supplied)
+ (seq (seq key var) default)
+ (seq (seq key var)))
+ (and (or (seq var default supplied)
+ (seq var default)
+ (seq var)
+ (and (pred symbolp)
+ var))
+ ;; Strip a leading underscore, since it
+ ;; only means that this argument is
+ ;; unused, but shouldn't affect the
+ ;; key's name (bug#12367).
+ (let key (if (seqp var)
+ (signal 'loopy-&key-key-from-sequence
+ (list var-form))
+ (intern
+ (format ":%s"
+ (let ((name (symbol-name var)))
+ (if (eq ?_ (aref name 0))
+ (substring name 1)
+ name))))))))
+ var-form))
+ (unless var
+ (signal 'loopy-&key-var-malformed
+ (list var-form)))
+ (list key var default supplied))))
+
+(defun loopy--get-&map-spec (var-form)
+ "Get the spec of `&map' VAR-FORM as (KEY VAR DEFAULT SUPPLIED)."
+ (loopy--pcase-let-workaround (key var default supplied)
+ (pcase-let (((or (seq key var default supplied)
+ (seq key var default)
+ (seq key var)
+ (and (or (seq var)
+ (and (pred symbolp)
+ var))
+ (let key
+ (if (seqp var-form)
+ (signal 'loopy-&map-key-from-sequence
+ (list var-form))
+ `(quote ,var)))))
+ var-form))
+ (unless var
+ (signal 'loopy-&map-var-malformed
+ (list var-form)))
+ (list key var default supplied))))
+
+(defun loopy--get-&aux-spec (var-form)
+ "Get the spec of `&aux' VAR-FORM as (VAR VAL)."
+ (loopy--pcase-let-workaround (var val)
+ (pcase-let (((or (seq var val)
+ (seq var)
+ (and (pred symbolp)
+ var))
+ var-form))
+ (unless var
+ (signal 'loopy-&aux-malformed-var (list var-form)))
+ (list var val))))
+
+;;;; Pcase pattern
+
+(defun loopy--get-var-pattern (var)
+ "Get the correct variable pattern.
+
+If VAR is ignored according to `loopy--var-ignored-p', return
+`_'. Otherwise, if VAR is a sequence according to `seqp',
+return `(loopy VAR)'. In all other cases, VAR is returned."
+ (cond
+ ((loopy--var-ignored-p var) '_)
+ ((seqp var) `(loopy ,var))
+ (t var)))
+
+(defun loopy--pcase-pat-positional-list-pattern (pos-vars opt-vars rest-var map-or-key-vars)
+ "Build a pattern for the positional, `&optional', and `&rest' variables.
+
+POS-VARS is the list of the positional variables. OPT-VARS is the list of
+the optional variables. REST-VAR is the `&rest' variable.
+MAP-OR-KEY-VARS is whether there are map or key variables."
+ ;; A modified version of the back-quote pattern to better work with
+ ;; optional values.
+ (cond
+ (pos-vars `(and (pred consp)
+ (app car-safe ,(let ((var (car pos-vars)))
+ (loopy--get-var-pattern var)))
+ (app cdr-safe ,(loopy--pcase-pat-positional-list-pattern
+ (cdr pos-vars) opt-vars
+ rest-var map-or-key-vars))))
+ (opt-vars (loopy--pcase-let-workaround (var default supplied)
+ (pcase-let* (((or (seq var default supplied)
+ (seq var default)
+ (seq var)
+ var)
+ (car opt-vars))
+ (var2 (loopy--get-var-pattern var)))
+ `(and (pred listp)
+ (app car-safe (or (and (pred null)
+ ,@(when supplied `((let ,supplied nil)))
+ ;; To destructure `nil' for a sequence, we need to
+ ;; mark the `&optional' variables as optional.
+ ,(if default
+ `(let ,var2 ,default)
+ (if (seqp var)
+ `(let (loopy (&optional
+ ,@(seq-into (cl-second var2)
+ 'list)))
+ nil)
+ `(let ,var2 nil))))
+ ,(if supplied
+ `(and (let ,supplied t)
+ ,var2)
+ var2)))
+ (app cdr-safe ,(loopy--pcase-pat-positional-list-pattern
+ nil (cdr opt-vars)
+ rest-var map-or-key-vars))))))
+ (rest-var (loopy--get-var-pattern rest-var))
+ ;; `pcase' allows `(,a ,b) to match (1 2 3), so we need to make
+ ;; sure there aren't more values left. However, if we are using
+ ;; `&key', then we allow more values.
+ (map-or-key-vars '_)
+ ;; Unlike `cl-lib', we don't require that all of the positional values have a
+ ;; corresponding variable/pattern. Instead, we do like `seq' and allow the
+ ;; destructuring pattern to be shorter than the sequence.
+ (t '_)))
+
+(defun loopy--pcase-pat-positional-array-pattern (pos-vars opt-vars rest-var map-or-key-vars)
+ "Build a pattern for the positional, `&optional', and `&rest' variables.
+
+POS-VARS is the list of the positional variables. OPT-VARS is the list of
+the optional variables. REST-VAR is the `&rest' variable.
+MAP-OR-KEY-VARS is whether there are map or key variables."
+ (let ((pos-len (length pos-vars))
+ (opt-len (length opt-vars)))
+ ;; We allow the variable form to be shorter than the
+ ;; destructured sequence.
+ `(and (pred (pcase--flip ,(compat-function length>) ,(1- pos-len)))
+ ,@(cl-loop for var in pos-vars
+ for idx from 0
+ collect `(app (pcase--flip aref ,idx)
+ ,(loopy--get-var-pattern var)))
+ ,@(when opt-vars
+ (let ((opt-var-specs (seq-into (mapcar (loopy--pcase-let-workaround (var default supplied)
+ (pcase-lambda ((or (seq var default supplied)
+ (seq var default)
+ (seq var)
+ (and (pred symbolp)
+ var)))
+ (list var default supplied)))
+ opt-vars)
+ 'vector)))
+ `((or ,@(cl-loop with use->= = (or rest-var map-or-key-vars)
+ and pat-idx-low = pos-len
+ and spec-idx-max = (1- opt-len)
+ for checked-len from (+ pos-len opt-len) downto pos-len
+ for spec-idx-high downfrom (1- opt-len) to 0
+ collect
+ ;; If the length matches, then all of the
+ ;; remaining variables were supplied, then
+ ;; the one variable was not supplied and we
+ ;; need to check the remaining ones.
+ `(and ,(if use->=
+ `(pred (pcase--flip ,(compat-function length>) ,(1- checked-len)))
+ `(pred (pcase--flip ,(compat-function length=) ,checked-len)))
+ ;; Variables that should be bound with the value in
+ ;; the array.
+ ,@(cl-loop
+ for spec-idx2 from 0 to spec-idx-high
+ for arr-idx from pat-idx-low
+ append (pcase-let* ((`(,var2 ,_ ,supplied2) (aref opt-var-specs spec-idx2))
+ (var3 (loopy--get-var-pattern var2)))
+ (if supplied2
+ `((app (pcase--flip aref ,arr-idx)
+ ,var3)
+ (let ,supplied2 t))
+ `((app (pcase--flip aref ,arr-idx)
+ ,var3)))))
+ ;; Variables that should be bound to nil or their
+ ;; default.
+ ,@(cl-loop
+ for spec-idx2 from (1+ spec-idx-high) to spec-idx-max
+ for arr-idx from pat-idx-low
+ append (pcase-let* ((`(,var2 ,default2 ,supplied2)
+ (aref opt-var-specs spec-idx2))
+ (var3 (loopy--get-var-pattern var2)))
+ (cond
+ (supplied2
+ `((let ,var3 ,default2)
+ (let ,supplied2 nil)))
+ (default2
+ `((let ,var3 ,default2)))
+ (t
+ `((let ,var3 nil))))))))
+ ;; A pattern for when nothing matches.
+ (and ,@(cl-loop for spec across opt-var-specs
+ append (pcase-let* ((`(,var2 ,default2 ,supplied2) spec)
+ (var3 (loopy--get-var-pattern var2)))
+ (cond
+ (supplied2
+ `((let ,var3 ,default2)
+ (let ,supplied2 nil)))
+ (default2
+ `((let ,var3 ,default2)))
+ (t
+ `((let ,var3 nil)))))))))))
+
+ ,@(when rest-var
+ (let ((len-sum (+ pos-len opt-len))
+ (rest-pat (loopy--get-var-pattern rest-var))
+ (seqsym (gensym "seqsym")))
+ ;; Rec-checking the length is fast for arrays.
+ `((or (and (pred (pcase--flip ,(compat-function length>) ,len-sum))
+ (app (pcase--flip substring ,len-sum) ; 0-indexed
+ ,rest-pat))
+ (app (lambda (,seqsym) (substring ,seqsym 0 0))
+ ,rest-pat))))))))
+
+(defun loopy--pcase-pat-&key-pattern (key-vars allow-other-keys)
+ "Build a `pcase' pattern for the `&key' variables.
+
+KEY-VARS are the forms of the key variables. ALLOW-OTHER-KEYS is
+whether `&allow-other-keys' was used. PLIST-VAR is the variable
+holding the property list."
+ ;; If we aren't checking whether all keys in EXPVAL were given,
+ ;; then we can use simpler patterns since we don't need to store the
+ ;; value of the key.
+ (cl-flet ((get-var-data (var-form)
+ (loopy--pcase-let-workaround (key var default supplied)
+ (pcase-let ((`(,key ,var ,default ,supplied)
+ (loopy--get-&key-spec var-form)))
+ (list key (loopy--get-var-pattern var)
+ default supplied)))))
+ (if allow-other-keys
+ `(and ,@(mapcar (lambda (var-form)
+ (pcase-let ((`(,key ,var ,default ,supplied) (get-var-data var-form))
+ (key-found (gensym "key-found"))
+ (plist (gensym "plist")))
+ (cond (supplied
+ `(app (lambda (,plist)
+ (let ((,key-found (plist-member ,plist ,key)))
+ (if ,key-found
+ (cons t (cadr ,plist))
+ (cons nil ,default))))
+ (,'\` ((,'\, ,supplied) . (,'\, ,var)))))
+ (default
+ `(app (lambda (,plist)
+ (let ((,key-found (plist-member ,plist ,key)))
+ (if ,key-found
+ (cadr ,plist)
+ ,default)))
+ ,var))
+ (t
+ `(app (pcase--flip plist-get ,key)
+ ,var)))))
+ key-vars))
+ ;; If we are checking whether there are no other keys in EXPVAL,
+ ;; then we use a single function for extracting the associated
+ ;; values and performing the check, whose output we match against
+ ;; a list of patterns.
+ (let ((res (gensym "res"))
+ (keys (gensym "keys"))
+ (plist (gensym "plist"))
+ (pats nil))
+ `(app (lambda (,plist)
+ (let ((,res nil)
+ (,keys nil))
+ ,@(cl-loop
+ for (key var default supplied) in (mapcar #'get-var-data key-vars)
+ collect (macroexp-let2* nil ((keyval key))
+ `(progn
+ (push ,keyval ,keys)
+ ,(cond
+ (supplied
+ (push supplied pats)
+ (push var pats)
+ (cl-once-only ((key-found `(plist-member ,plist ,keyval)))
+ `(if ,key-found
+ (progn
+ (push t ,res)
+ (push (cadr ,key-found) ,res))
+ (push nil ,res)
+ (push ,default ,res))))
+ (default
+ (push var pats)
+ (cl-once-only ((key-found `(plist-member ,plist ,keyval)))
+ `(if ,key-found
+ (push (cadr ,key-found) ,res)
+ (push ,default ,res))))
+ (t
+ (push var pats)
+ `(push (plist-get ,plist ,keyval)
+ ,res))))))
+ (push (or (plist-get ,plist :allow-other-keys)
+ (cl-loop for (key _val) on ,plist by #'cddr
+ always (memq key ,keys)))
+ ,res)
+ ;; Reverse in case a latter pattern use a variable
+ ;; from an earlier pattern.
+ (nreverse ,res)))
+ (,'\` ,(cl-loop for pat in (reverse (cons
+ ;; Use `identity' instead
+ ;; of `(not null)' to support
+ ;; older version of Emacs.
+ '(pred identity)
+ pats))
+ collect `(,'\, ,pat))))))))
+
+(defun loopy--pcase-pat-&map-pattern (map-vars)
+ "Build a `pcase' pattern for the `&map' variables MAP-VARS."
+ (let ((mapsym (gensym "map")))
+ `(and (pred mapp)
+ ,@(mapcar (loopy--pcase-let-workaround (key var default supplied)
+ (lambda (var-form)
+ (pcase-let ((`(,key ,var ,default ,supplied)
+ (loopy--get-&map-spec var-form)))
+ (unless var
+ (signal 'loopy-&map-var-malformed (list var-form)))
+ (setq var (loopy--get-var-pattern var))
+ (cond
+ (supplied
+ `(app (lambda (,mapsym)
+ ;; The default implementations of `map-elt'
+ ;; uses `map-contains-key' (which might be
+ ;; expensive) when given a default value, so
+ ;; we use a generated default to avoid
+ ;; calling it twice.
+ ,(let ((defsym (list 'quote (gensym "loopy--map-contains-test")))
+ (valsym (gensym "loopy--map-elt")))
+ (macroexp-let2* nil ((keysym key))
+ `(let ((,valsym (map-elt ,mapsym ,keysym ,defsym)))
+ (if (equal ,valsym ,defsym)
+ (cons nil ,default)
+ (cons t ,valsym))))))
+ (,'\` ((,'\, ,supplied) . (,'\, ,var)))))
+ (default
+ `(app (lambda (,mapsym) (map-elt ,mapsym ,key ,default))
+ ,var))
+ (t
+ `(app (pcase--flip map-elt ,key) ,var))))))
+ map-vars))))
+
+(defun loopy--pcase-pat-&aux-pattern (aux-vars)
+ "Build `pcase' pattern for `&aux' variables.
+
+AUX-VARS is the list of bindings."
+ `(and ,@(cl-loop
+ for bind in aux-vars
+ for (var val) = (loopy--get-&aux-spec bind)
+ if (null var)
+ do (signal 'loopy-&aux-malformed-var (list bind))
+ else
+ collect `(let ,(loopy--get-var-pattern var)
+ ,val)
+ end)))
+
+;;;###autoload
+(pcase-defmacro loopy (sequence)
+ "Match for Loopy destructuring.
+
+See the Info node `(loopy)Basic Destructuring'."
+ (cond
+ ((loopy--var-ignored-p sequence) '_)
+ ((symbolp sequence) sequence)
+ (t
+ (let* ((groups (loopy--get-var-groups sequence))
+ (whole-var (alist-get 'whole groups))
+ (pos-vars (alist-get 'pos groups))
+ (opt-vars (alist-get 'opt groups))
+ (rest-var (alist-get 'rest groups))
+ (key-vars (alist-get 'key groups))
+ (allow-other-keys (alist-get 'allow-other-keys groups))
+ (map-vars (alist-get 'map groups))
+ (aux-vars (alist-get 'aux groups)))
+ (remq nil
+ `(and ,(when whole-var
+ whole-var)
+ ,@(when (or pos-vars opt-vars rest-var
+ key-vars map-vars allow-other-keys)
+ (cl-etypecase sequence
+ (list
+ `((pred listp)
+ ,(when (or pos-vars opt-vars rest-var)
+ (loopy--pcase-pat-positional-list-pattern
+ pos-vars opt-vars
+ rest-var (or key-vars map-vars)))
+ ,(when key-vars
+ (cond
+ ((and rest-var
+ (not (loopy--var-ignored-p rest-var)))
+ `(app (lambda (_) ,rest-var)
+ ,(loopy--pcase-pat-&key-pattern
+ key-vars allow-other-keys)))
+ ((or pos-vars opt-vars)
+ `(app (nthcdr ,(+ (length pos-vars)
+ (length opt-vars)))
+ ,(loopy--pcase-pat-&key-pattern
+ key-vars allow-other-keys)))
+ (t (loopy--pcase-pat-&key-pattern
+ key-vars allow-other-keys))))))
+ (array
+ `((pred arrayp)
+ ,(when (or pos-vars opt-vars rest-var)
+ (loopy--pcase-pat-positional-array-pattern
+ pos-vars opt-vars
+ rest-var key-vars))
+ ,(when key-vars
+ (signal 'loopy-&key-array (list sequence)))))))
+ ,(when map-vars
+ (cond
+ ((and rest-var
+ (not (loopy--var-ignored-p rest-var)))
+ `(app (lambda (_) ,rest-var)
+ ,(loopy--pcase-pat-&map-pattern map-vars)))
+ ((or pos-vars opt-vars)
+ `(app (pcase--flip seq-subseq ,(+ (length pos-vars)
+ (length opt-vars)))
+ ,(loopy--pcase-pat-&map-pattern map-vars)))
+ (t
+ (loopy--pcase-pat-&map-pattern map-vars))))
+ ,(when aux-vars
+ (loopy--pcase-pat-&aux-pattern aux-vars))))))))
+
+;;;; Destructuring for Iteration and Accumulation Commands
+
+(cl-defun loopy--pcase-destructure-for-iteration (var val &key error)
+ "Destructure VAL according to VAR as by `pcase-let'.
+
+Returns a list. The elements are:
+1. An expression which binds the variables in VAR to the values
+ in VAL.
+2. A list of variables which exist outside of this expression and
+ need to be `let'-bound.
+
+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)
+ (let ((var-list)
+ (destructuring-expression)
+ (val-holder (gensym "loopy--pcase-workaround")))
+ (cl-flet ((signaler (&rest _)
+ `(signal 'loopy-bad-run-time-destructuring
+ (list (quote ,var)
+ ,val-holder))))
+ ;; This sets `destructuring-expression' and `var-list'.
+ (setq destructuring-expression
+ ;; This holding variable seems to be needed for the older method,
+ ;; before the introduction of `pcase-compile-patterns'. In some cases,
+ ;; it just evaluates `VAL' repeatedly, which is bad for functions
+ ;; that work with state and bad for efficiency.
+ ;;
+ ;; Regardless, we also use it to report the value that caused the
+ ;; error.
+ `(let ((,val-holder ,val))
+ ,(if (fboundp 'pcase-compile-patterns)
+ (pcase-compile-patterns
+ val-holder
+ (remq nil
+ (list (cons var
+ (lambda (varvals &rest _)
+ (cons 'setq (mapcan (cl-function
+ (lambda ((var val &rest rest))
+ (push var var-list)
+ (list var val)))
+ varvals))))
+ (when error
+ (cons '_ #'signaler)))))
+ ;; NOTE: In Emacs versions less than 28, this functionality
+ ;; technically isn't public, but this is what the developers
+ ;; recommend.
+ (pcase--u
+ (remq
+ nil
+ (list (list (pcase--match val-holder
+ (pcase--macroexpand
+ (if error
+ var
+ `(or ,var pcase--dontcare))))
+ (lambda (vars)
+ (cons 'setq
+ (mapcan (lambda (v)
+ (let ((destr-var (car v))
+ ;; Use `cadr' for Emacs 28+, `cdr' for less.
+ (destr-val (funcall (eval-when-compile
+ (if (version< emacs-version "28")
+ #'cdr
+ #'cadr))
+ v)))
+ (push destr-var var-list)
+ (list destr-var destr-val)))
+ vars))))
+ (when error
+ (list (pcase--match val-holder
+ (pcase--macroexpand '_))
+ #'signaler)))))))))
+ (list destructuring-expression
+ var-list))))
+
+(defun loopy--pcase-destructure-for-with-vars (bindings)
+ "Return a way to destructure BINDINGS by `pcase-let*'.
+
+Returns a list of two elements:
+1. The symbol `pcase-let*'.
+2. A new list of bindings."
+ (list 'pcase-let* bindings))
+
+(cl-defun loopy--pcase-parse-for-destructuring-accumulation-command
+ ((name var val &rest args) &key error)
+ "Parse the accumulation loop command using `pcase' for destructuring.
+
+NAME is the name of the command. VAR-OR-VAL is a variable name
+or, if using implicit variables, a value . VAL is a value, and
+should only be used if VAR-OR-VAL is a variable. ERROR is when
+an error should be signaled if the pattern doesn't match."
+ (let* ((instructions)
+ (full-main-body)
+ ;; This holding variable seems to be needed for the older method,
+ ;; before the introduction of `pcase-compile-patterns'. In some cases,
+ ;; it just evaluates `VAL' repeatedly, which is bad for functions
+ ;; that work with state and bad for efficiency.
+ (value-holder (gensym "loopy--pcase-workaround")))
+ (cl-flet ((signaler (&rest _)
+ `(signal 'loopy-bad-run-time-destructuring
+ (list (quote ,var)
+ ,value-holder))))
+ (if (fboundp 'pcase-compile-patterns)
+ (setq full-main-body
+ (pcase-compile-patterns
+ value-holder
+ (remq nil
+ (list (cons var
+ (lambda (varvals &rest _)
+ (let ((destr-main-body))
+ (dolist (varval varvals)
+ (let ((destr-var (cl-first varval))
+ (destr-val (cl-second varval)))
+ (loopy--bind-main-body (main-body other-instructions)
+ (loopy--parse-loop-command
+ `(,name ,destr-var ,destr-val ,@args))
+ ;; Just push the other instructions, but
+ ;; gather the main body expressions.
+ (dolist (instr other-instructions)
+ (push instr instructions))
+ (push main-body destr-main-body))))
+ ;; The lambda returns the destructured main body,
+ ;; which needs to be wrapped by Pcase's
+ ;; destructured bindings.
+ (macroexp-progn (apply #'append destr-main-body)))))
+ (when error
+ (cons '_ #'signaler))))))
+ ;; NOTE: In Emacs versions less than 28, this functionality technically
+ ;; isn't public, but this is what the developers recommend.
+ (setq full-main-body
+ (pcase--u
+ (remq nil `((,(pcase--match value-holder
+ (pcase--macroexpand
+ (if error
+ var
+ `(or ,var pcase--dontcare))))
+ ,(lambda (vars)
+ (let ((destr-main-body))
+ (dolist (v vars)
+ (let ((destr-var (car v))
+ ;; Use `cadr' for Emacs 28+, `cdr' for less.
+ (destr-val (funcall (eval-when-compile
+ (if (version< emacs-version "28")
+ #'cdr
+ #'cadr))
+ v)))
+ (seq-let (main-body other-instructions)
+ (loopy--extract-main-body
+ (loopy--parse-loop-command
+ `(,name ,destr-var ,destr-val ,@args)))
+ ;; Just push the other instructions, but
+ ;; gather the main body expressions.
+ (dolist (instr other-instructions)
+ (push instr instructions))
+ (push main-body destr-main-body))))
+ ;; The lambda returns the destructured main body,
+ ;; which needs to be wrapped by Pcase's
+ ;; destructured bindings.
+ (macroexp-progn (apply #'append destr-main-body)))))
+ ,(when error
+ (list (pcase--match value-holder (pcase--macroexpand '_))
+ #'signaler))))))))
+ ;; Finally, return the instructions.
+ ;; We don't know all of the cases when the value holder is needed,
+ ;; so we just always use it.
+ `((loopy--main-body (let ((,value-holder ,val))
+ ,full-main-body))
+ ,@(nreverse instructions))))
+
+;;;; Destructuring Generalized Variables
+
+(defun loopy--destructure-generalized-sequence (var value-expression)
+ "Destructure VALUE-EXPRESSION according to VAR as `setf'-able places.
+
+VALUE-EXPRESSION should itself be a `setf'-able place.
+
+Returns a list of bindings suitable for `cl-symbol-macrolet'."
+ (cl-typecase var
+ (symbol (unless (loopy--var-ignored-p var)
+ `((,var ,value-expression))))
+ (list (loopy--destructure-generalized-list var value-expression))
+ (array (loopy--destructure-generalized-array var value-expression))
+ (t (signal 'loopy-destructure-type (list var)))))
+
+(defun loopy--destructure-generalized-array (var-form value-expression)
+ "Destructure VALUE-EXPRESSION according to VAR-FORM as `setf'-able places.
+
+VALUE-EXPRESSION should itself be a `setf'-able place.
+
+Returns a list of bindings suitable for `cl-symbol-macrolet'.
+
+- `&rest' references a subsequence place.
+- `&whole' references the entire place.
+- `&optional' is not supported.
+- `&map' references the values in the map.
+- `&key' references the values in the property list."
+ (map-let (('whole whole-var)
+ ('pos pos-vars)
+ ('opt opt-vars)
+ ('rest rest-var)
+ ('key key-vars)
+ ('allow-other-keys allow-other-keys)
+ ('map map-vars)
+ ('aux aux-vars))
+ (loopy--get-var-groups var-form)
+ `(,@(when whole-var
+ `((,whole-var ,value-expression)))
+ ,@(when pos-vars
+ (cl-loop for v in pos-vars
+ for n from 0
+ for expr = `(aref ,value-expression ,n)
+ if (seqp v)
+ append (loopy--destructure-generalized-sequence
+ v expr)
+ else
+ append `((,v ,expr))))
+ ,@(when opt-vars
+ (signal 'loopy-&optional-generalized-variable
+ (list var-form value-expression)))
+ ,@(when rest-var
+ (let ((rest-val `(cl-subseq ,value-expression
+ ,(+ (length pos-vars)
+ (length opt-vars)))))
+ (if (seqp rest-var)
+ (loopy--destructure-generalized-sequence rest-var rest-val)
+ `((,rest-var ,rest-val)))))
+ ,@(when (or key-vars allow-other-keys)
+ (signal 'loopy-&key-array
+ (list var-form value-expression)))
+
+ ,@(when map-vars
+ (cl-loop
+ for elem in map-vars
+ for (key var2 default supplied) = (loopy--get-&map-spec elem)
+ for expr = `(map-elt (cl-subseq ,value-expression
+ ,(+ (length pos-vars)
+ (length opt-vars)))
+ ,key ,default)
+ if default
+ do (signal 'loopy-generalized-default
+ (list var-form value-expression))
+ else if supplied
+ do (signal 'loopy-generalized-supplied
+ (list var-form value-expression))
+ else if (seqp var2)
+ append (loopy--destructure-generalized-sequence
+ var2 expr)
+ else
+ append `((,var2 ,expr))
+ end))
+ ,@(when aux-vars
+ (cl-loop for elem in aux-vars
+ for (var val) = (loopy--get-&aux-spec elem)
+ collect `(,var ,val))))))
+
+(cl-defun loopy--destructure-generalized-list (var-form value-expression)
+ "Destructure list VALUE-EXPRESSION with generalized variables via VAR-FORM."
+ (map-let (('whole whole-var)
+ ('pos pos-vars)
+ ('opt opt-vars)
+ ('rest rest-var)
+ ('key key-vars)
+ ('allow-other-keys allow-other-keys)
+ ('map map-vars)
+ ('aux aux-vars))
+ (loopy--get-var-groups var-form)
+ `(,@(when whole-var
+ `((,whole-var ,value-expression)))
+ ,@(when pos-vars
+ (cl-loop for v in pos-vars
+ for n from 0
+ for expr = `(nth ,n ,value-expression)
+ if (seqp v)
+ append (loopy--destructure-generalized-sequence
+ v expr)
+ else
+ append `((,v ,expr))))
+ ,@(when opt-vars
+ (signal 'loopy-&optional-generalized-variable
+ (list var-form value-expression)))
+ ,@(when rest-var
+ (let ((rest-val `(nthcdr ,(+ (length pos-vars)
+ (length opt-vars))
+ ,value-expression)))
+ (if (seqp rest-var)
+ (loopy--destructure-generalized-sequence rest-var rest-val)
+ `((,rest-var ,rest-val)))))
+
+ ,@(when (or key-vars allow-other-keys)
+ (cl-loop
+ for elem in key-vars
+ for (key var2 default supplied) = (loopy--get-&key-spec elem)
+ for expr = `(compat-call plist-get (nthcdr ,(+ (length pos-vars)
+ (length opt-vars))
+ ,value-expression)
+ ,key)
+ if default
+ do (signal 'loopy-generalized-default
+ (list var-form value-expression))
+ else if supplied
+ do (signal 'loopy-generalized-supplied
+ (list var-form value-expression))
+ else if (seqp var2)
+ append (loopy--destructure-generalized-sequence
+ var2 expr)
+ else
+ append `((,var2 ,expr))
+ end))
+
+ ,@(when map-vars
+ (cl-loop for elem in map-vars
+ for (key var2 default supplied) =
+ (loopy--get-&map-spec elem)
+ for expr = `(map-elt (nthcdr ,(+ (length pos-vars)
+ (length opt-vars))
+ ,value-expression)
+ ,key ,default)
+ if default
+ do (signal 'loopy-generalized-default
+ (list var-form value-expression))
+ else if supplied
+ do (signal 'loopy-generalized-supplied
+ (list var-form value-expression))
+ else if (seqp var2)
+ append (loopy--destructure-generalized-sequence
+ var2 expr)
+ else
+ append `((,var2 ,expr))
+ end))
+ ,@(when aux-vars
+ (cl-loop for elem in aux-vars
+ for (var val) = (loopy--get-&aux-spec elem)
+ collect `(,var ,val))))))
+
+(provide 'loopy-destructure)
+;;; loopy-destructure.el ends here
diff --git a/loopy-instrs.el b/loopy-instrs.el
new file mode 100644
index 00000000..3896e021
--- /dev/null
+++ b/loopy-instrs.el
@@ -0,0 +1,159 @@
+;;; loopy-instrs.el --- Features for working with Loopy's instructions. -*- lexical-binding: t; -*-
+
+;; Copyright (c) 2024 Earl Hyatt
+
+;;; Disclaimer:
+;; This file is not part of GNU Emacs.
+;;
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this file. If not, see .
+
+;;; Commentary:
+;; `loopy' is a macro that is used similarly to `cl-loop'. It provides "loop
+;; commands" that define a loop body and it's surrounding environment, as well
+;; as exit conditions.
+;;
+;; This library provides features for working with the "instructions" that
+;; describe the contents of the macro expansion. This separation exists for
+;; better organization.
+
+;;; Code:
+
+(require 'loopy-misc)
+
+;;;; Variable binding for instructions
+;; TODO: Check not using `pcase' in github errors.
+
+(defvar loopy--iteration-vars)
+(defvar loopy--accumulation-vars)
+(defvar loopy--other-vars)
+
+(defmacro loopy--instr-let-var (place sym exp name &rest body)
+ "Use SYM as EXP for BODY, creating an instruction to bind at PLACE.
+
+Use this for values that should change during iteration.
+
+For normal variables (that is, not needing instructions), see
+also `macroexp-let2' and `cl-once-only'."
+ (declare (indent 4)
+ (debug (sexp sexp form sexp body)))
+ (let ((bodysym (gensym "body"))
+ (expsym (gensym "exp")))
+ `(let* ((,expsym ,exp)
+ (,sym (or ,name (gensym (symbol-name (quote ,sym)))))
+ (,bodysym (progn ,@body)))
+ (cons (list (quote ,place)
+ (list ,sym ,expsym))
+ ,bodysym))))
+
+(defmacro loopy--instr-let-var* (bindings place &rest body)
+ "A multi-binding version of `loopy--instr-let-var'.
+
+BINDINGS are variable-value pairs. A third item in the list is
+an expression that evaluates to a symbol to use to generate a
+name to use in the binding. PLACE is the Loopy variable to use
+as the head of the instruction. BODY are the forms for which the
+binding exists."
+ (declare (indent 2)
+ (debug ((&rest (gate symbol form &optional form))
+ symbol
+ body)))
+ (cl-reduce (cl-function (lambda (res (var val &optional name))
+ `(loopy--instr-let-var ,place ,var ,val ,name ,res)))
+ (reverse bindings)
+ :initial-value (macroexp-progn body)))
+
+(defmacro loopy--instr-let-const (place sym exp name &rest body)
+ "Use SYM as EXP for BODY, maybe creating an instruction to bind at PLACE.
+
+Use for values that are evaluated only once, such as the optional
+arguments to the iteration commands. If the value of EXP is not
+null and is not constant according to `macroexp-const-p', then a
+binding is created.
+
+For normal variables (that is, not needing instructions), see
+also `macroexp-let2' and `cl-once-only'."
+ (declare (indent 4)
+ (debug (sexp sexp form sexp body)))
+ (let ((bodysym (gensym "body"))
+ (expsym (gensym "exp")))
+ `(let* ((,expsym ,exp)
+ (,sym (if (or (null ,expsym)
+ (macroexp-const-p ,expsym))
+ ,expsym
+ (or ,name
+ (gensym (symbol-name (quote ,sym))))))
+ (,bodysym (progn ,@body)))
+ (if (eq ,sym ,expsym)
+ ,bodysym
+ (cons (list (quote ,place)
+ (list ,sym ,expsym))
+ ,bodysym)))))
+
+(defmacro loopy--instr-let-const* (bindings place &rest body)
+ "A multi-binding version of `loopy--instr-let-const'.
+
+BINDINGS are variable-value pairs. PLACE is the Loopy variable to use
+as the head of the instruction. BODY are the forms for which the
+binding exists."
+ (declare (indent 2)
+ (debug ((&rest (gate symbol form &optional form))
+ symbol
+ body)))
+ (cl-reduce (cl-function (lambda (res (var val &optional name))
+ `(loopy--instr-let-const ,place ,var ,val ,name ,res)))
+ (reverse bindings)
+ :initial-value (macroexp-progn body)))
+
+
+(defun loopy--extract-main-body (instructions)
+ "Extract main-body expressions from INSTRUCTIONS.
+
+This returns a list of two sub-lists:
+
+1. A list of expressions (not instructions) that are meant to be
+ use in the main body of the loop.
+
+2. A list of instructions for places other than the main body.
+
+The lists will be in the order parsed (correct for insertion)."
+ (let ((wrapped-main-body)
+ (other-instructions))
+ (dolist (instruction instructions)
+ (if (eq (cl-first instruction) 'loopy--main-body)
+ (push (cl-second instruction) wrapped-main-body)
+ (push instruction other-instructions)))
+
+ ;; 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'.
+
+INSTRUCTIONS is a list of instructions, which don't all have to be
+for `loopy--iteration-vars'."
+ (loopy--substitute-using-if
+ (cl-function (lambda ((_ init)) (list 'loopy--other-vars init)))
+ (lambda (x) (eq (car x) 'loopy--iteration-vars))
+ instructions))
+
+(provide 'loopy-instrs)
+;;; loopy-instrs.el ends here
diff --git a/loopy-iter.el b/loopy-iter.el
index ce6a698e..4c120243 100644
--- a/loopy-iter.el
+++ b/loopy-iter.el
@@ -30,6 +30,7 @@
(require 'loopy-vars)
(require 'loopy-misc)
(require 'loopy-commands)
+(require 'loopy-instrs)
(require 'cl-lib)
(require 'seq)
(require 'map)
diff --git a/loopy-misc.el b/loopy-misc.el
index ec417c87..ab7ab3c7 100644
--- a/loopy-misc.el
+++ b/loopy-misc.el
@@ -30,6 +30,7 @@
;; NOTE: This file can't require any of the other `loopy' files.
(require 'cl-lib)
+(require 'map)
(require 'compat)
(require 'pcase)
(require 'seq)
@@ -109,40 +110,120 @@
;;;;; Errors on Destructuring
(define-error 'loopy-bad-desctructuring
- "Loopy: Bad destructuring"
- 'loopy-error)
+ "Loopy: Bad destructuring"
+ 'loopy-error)
+
+(define-error 'loopy-bad-run-time-destructuring
+ "Loopy: Bad run-time destructuring (value doesn't match)"
+ 'loopy-error)
(define-error 'loopy-&whole-sequence
- "Loopy: `&whole' variable is sequence"
- 'loopy-bad-desctructuring)
+ "Loopy: `&whole' variable is sequence"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-&whole-missing
- "Loopy: `&whole' variable is missing"
- 'loopy-bad-desctructuring)
+ "Loopy: `&whole' variable is missing"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&whole-bad-position
+ "Loopy: `&whole' in bad position"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-&rest-missing
- "Loopy: `&rest' variable is missing"
- 'loopy-bad-desctructuring)
+ "Loopy: `&rest' variable is missing"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-&rest-non-var
- "Loopy: Non-variable item after `&rest'"
- 'loopy-bad-desctructuring)
+ "Loopy: Non-variable item after `&rest'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&optional-bad-position
+ "Loopy: `&optional' in bad position"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&optional-ignored-default-or-supplied
+ "Loopy: Using default or asking whether supplied for ignored `&optional'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&optional-generalized-variable
+ "Loopy: `&optional' variables not implemented for generalized variables"
+ 'loopy-bad-destructuring)
+
+(define-error 'loopy-generalized-default
+ "Loopy: Default values not implemented for generalized variables"
+ 'loopy-bad-destructuring)
+
+(define-error 'loopy-generalized-supplied
+ "Loopy: `SUPPLIED-P' variables not implemented for generalized variables"
+ 'loopy-bad-destructuring)
(define-error 'loopy-&rest-multiple
- "Loopy: Multiple variables after `&rest'"
- 'loopy-bad-desctructuring)
+ "Loopy: Multiple variables after `&rest'"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-&rest-dotted
- "Loopy: Using `&rest' in dotted (improper) list"
- 'loopy-bad-desctructuring)
+ "Loopy: Using `&rest' in dotted (improper) list"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&rest-bad-position
+ "Loopy: `&rest' or `&body' in bad position"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-&key-missing
- "Loopy: `&key' variable is missing"
- 'loopy-bad-desctructuring)
+ "Loopy: `&key' variable is missing"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&key-bad-position
+ "Loopy: `&key' or `&keys' in bad position"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&allow-other-keys-without-&key
+ "Loopy: Used `&allow-other-keys' before or without `&key'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&key-unmatched
+ "Loopy: Value destructured by `&key' not matching without `&allow-other-keys'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&key-var-malformed
+ "Loopy: Malformed variable for `&key'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&key-array
+ "Loopy: Use of `&key' in array"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&key-key-from-sequence
+ "Loopy: Can't create `&key' key from a sequence"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&map-var-malformed
+ "Loopy: Malformed variable for `&map'"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&map-missing
+ "Loopy: `&map' variable is missing"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&map-bad-position
+ "Loopy: `&map'in bad position"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&map-key-from-sequence
+ "Loopy: Can't create `&map' key from a sequence"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&aux-bad-position
+ "Loopy: `&aux' in bad position"
+ 'loopy-bad-desctructuring)
+
+(define-error 'loopy-&aux-var-malformed
+ "Loopy: Malformed variable for `&aux'"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-destructure-type
- "Loopy: Can't destructure type"
- 'loopy-bad-desctructuring)
+ "Loopy: Can't destructure type"
+ 'loopy-bad-desctructuring)
(define-error 'loopy-destructure-vars-missing
"Loopy: No variables bound"
@@ -164,6 +245,7 @@
"Check whether the `car' of A equals the `car' of B."
(equal (car a) (car b)))
+;; Similar to `seq--count-successive'.
(defun loopy--count-while (pred list)
"Count the number of items while PRED is true in LIST.
@@ -192,13 +274,6 @@ For example, applying `cl-oddp' on (2 4 6 7) returns 3."
until (funcall pred i)
sum 1))
-
-(defun loopy--every-other (list)
- "Return a list of every other element in LIST, starting with the first.
-
-This is helpful when working with property lists."
- (cl-loop for i in list by #'cddr collect i))
-
(defmacro loopy--plist-bind (bindings plist &rest body)
"Bind values in PLIST to variables in BINDINGS, surrounding BODY.
@@ -247,767 +322,6 @@ NEW receives the element as its only argument.
Unlike `loopy--substitute-using', the test is required."
(loopy--substitute-using new seq :test test))
-(cl-defun loopy--split-list-before (list element &key key (test #'eq))
- "Split LIST on ELEMENT, so that ELEMENT begins the latter part.
-
-TEST is used to determine equality. KEY is applied to ELEMENT
-and each item in LIST.
-
-For example, using 2 as ELEMENT would split (1 2 3)
-into (1) and (2 3)."
- (let ((first-part nil)
- (second-part nil))
- (setq second-part
- (if key
- (cl-loop for cell on list
- for item = (car cell)
- if (funcall test
- (funcall key item)
- (funcall key element))
- return cell
- else do (push item first-part))
- (cl-loop for cell on list
- for item = (car cell)
- if (funcall test item element)
- return cell
- else do (push item first-part))))
- (list (nreverse first-part)
- second-part)))
-
-(defun loopy--split-off-last-var (var-list)
- "Split VAR-LIST, separating the last variable from the rest.
-
-VAR-LIST can be a proper or dotted list. For example,
-splitting (1 2 3) or (1 2 . 3) returns ((1 2) 3)."
- (let ((var-hold))
- (while (car-safe var-list)
- (push (pop var-list) var-hold))
- (if var-list
- (list (nreverse var-hold) var-list)
- (let ((last (cl-first var-hold)))
- (list (nreverse (cl-rest var-hold)) last)))))
-
-
-;;;; Destructuring
-
-;; This better allows for things to change in the future.
-(defun loopy--var-ignored-p (var)
- "Return whether VAR should be ignored."
- (eq var '_))
-
-;;;;; Destructuring normal values
-;;
-;; Note that functions which are only used for commands are found in
-;; `loopy-commands.el'. The functions found here are used generally.
-
-(defun loopy--destructure-sequence (seq value-expression)
- "Return a list of bindings destructuring VALUE-EXPRESSION according to SEQ.
-
-Return a list of variable-value pairs (not dotted), suitable for
-substituting into a `let*' form or being combined under a `setq'
-form.
-
-If SEQ is `_', then a generated variable name will be used."
- (cl-typecase seq
- (symbol `((,(if (loopy--var-ignored-p seq) (gensym "ignored-value-") seq)
- ,value-expression)))
- (list (loopy--destructure-list seq value-expression))
- (array (loopy--destructure-array seq value-expression))
- (t (signal 'loopy-destructure-type (list seq)))))
-
-(defun loopy--destructure-array (var value-expression)
- "Return a list of bindings destructuring VALUE-EXPRESSION according to VAR.
-
-- If `&rest', bind the remaining values in the array.
-- If `&whole', name a variable holding the whole value."
- (let ((bindings)
- (using-rest-var)
- (remaining-var var)
- (using-whole-var)
- (remaining-length (length var))
- (rest-pos nil)
- (starting-index 0))
-
- (when (eq '&whole (aref var 0))
- (cond ((or (= 1 remaining-length)
- (eq (aref var 1) '&rest))
- (signal 'loopy-&whole-missing (list var)))
- ((sequencep (aref remaining-var 1))
- (signal 'loopy-&whole-sequence (list var)))
- (t
- (let ((whole-var (aref remaining-var 1)))
- (when (= 2 remaining-length)
- (warn "`&whole' variable used alone: %s" var))
-
- (setq remaining-var (substring remaining-var 2)
- remaining-length (max 0 (- remaining-length 2)))
-
- (if (loopy--var-ignored-p whole-var)
- (warn "`&whole' variable ignored: %s" var)
- (setq using-whole-var whole-var))))))
-
- (when-let ((pos (cl-position '&rest remaining-var :test #'eq)))
- (cond
- ((= (1+ pos) remaining-length)
- (signal 'loopy-&rest-missing (list var)))
- ((> remaining-length (+ 2 pos))
- (signal 'loopy-&rest-multiple (list var)))
- (t
- ;; (when (zerop pos)
- ;; (warn "`&rest' variable used like `&whole': %s" var))
- (let ((rest-var (aref remaining-var (1+ pos))))
- (unless (loopy--var-ignored-p rest-var)
- (setq using-rest-var rest-var
- rest-pos pos))
- (setq remaining-length (max 0 (- remaining-length 2))
- remaining-var (substring remaining-var 0 -2))))))
-
- ;; Remove ignored positions from the beginning and end.
- (cl-loop for x across remaining-var
- for ind from 0
- while (loopy--var-ignored-p x)
- finally (setq remaining-var (substring remaining-var ind)
- remaining-length (- remaining-length ind)
- ;; Be sure that we know what is left.
- starting-index ind))
-
- ;; (loopy (array x var :index ind :downfrom (1- remaining-length))
- ;; (unless (eq x '_)
- ;; (do (setq remaining-var (substring remaining-var 0 (1+ ind))
- ;; remaining-length (1+ ind)))
- ;; (leave)))
-
- (cl-loop for x across (reverse remaining-var)
- for ind downfrom (1- remaining-length)
- while (loopy--var-ignored-p x)
- finally do
- ;; `substring' is end exclusive.
- (setq remaining-var (substring remaining-var 0 (1+ ind))
- remaining-length (1+ ind)))
-
- (cond
- ((zerop (length remaining-var))
- (signal 'loopy-destructure-vars-missing (list var)))
- ;; Check to see if we can avoid binding the holding variable.
- ((and (= 1 remaining-length)
- (not using-whole-var)
- (not using-rest-var))
- (push `(,(aref remaining-var 0) (aref ,value-expression ,starting-index))
- bindings))
- (t
- ;; Even if we're not using `whole-var', we still need to bind
- ;; a holding variable in case of `rest-var' or the positional
- ;; variables.
- (let ((holding-var (if using-whole-var
- using-whole-var
- (gensym "destr-array-"))))
-
- ;; Otherwise, bind the holding variable and repeatedly access it.
- (push `(,holding-var ,value-expression) bindings)
-
- (cl-loop named loop
- for v across remaining-var
- for idx from starting-index
- do (cond
- ((loopy--var-ignored-p v)) ; Do nothing if variable is `_'.
- (t (if (sequencep v)
- (dolist (binding (loopy--destructure-sequence
- v `(aref ,holding-var ,idx)))
- (push binding bindings))
- (push `(,v (aref ,holding-var ,idx))
- bindings)))))
-
- ;; Now bind the `&rest' var, if needed.
- (when using-rest-var
- (let ((rest-val `(substring ,holding-var ,rest-pos)))
- (if (sequencep using-rest-var)
- (dolist (binding (loopy--destructure-sequence
- using-rest-var rest-val))
- (push binding bindings))
- (push `(,using-rest-var ,rest-val) bindings))))
-
- (when (null (cdr bindings))
- (signal 'loopy-destructure-vars-missing (list var))))))
-
- (nreverse bindings)))
-
-(cl-defun loopy--destructure-list (var value-expression)
- "Destructure VALUE-EXPRESSION according to VAR.
-
-- If the first element of VAR is `&whole', then the next element
- names a variable containing the entire value.
-- Positional variable names can be next.
-- A variable named after `&rest' or after the dot in a dotted list
- sets that variable to the remainder of the list. If no positional
- variables are given, then this is the same as `&whole'.
-- Variables named after `&key' are values found using plist functions.
- These can optionally be a list of 2 element: (1) a variable name
- and (2) a default value if the corresponding key is not present.
- Keys are only sought in the remainder of the list, be that after
- positional variable names or in a variable named `&rest'.
-
-Only the positional variables and the remainder can be recursive."
- (let ((bindings nil) ; The result of this function.
- (whole-var nil) ; Variable after `&whole'.
- ;; Variable after `&rest' or a generated symbol.
- (rest-var nil)
- ;; Sequence that was after `&rest', if any.
- (rest-var-was-sequence nil)
- ;; A value holder for if we only use keys.
- (key-target-var (gensym "key-target-"))
- ;; Whether we pop positional variables from `key-target-var'.
- ;; This determines whether we need to bind `key-target-var' before
- ;; or after processing the positional variables.
- (popping-key-target-var)
- ;; Variables after `&key' or `&keys'. These are the final values bound,
- ;; but must be detected before the positional variables are processed.
- (key-vars)
- ;; The positional variables processed. This is a copy of `var', since
- ;; `var' is eaten away in a `while' loop while processing those
- ;; variables. This affects where `&key' variables are sought.
- (positional-vars)
- ;; Copy for consumption.
- (remaining-var var))
-
- ;; Find `whole-var'. If found, remove from `remaining-var'.
- (when (eq (cl-first remaining-var) '&whole)
- (if (null (cdr remaining-var))
- ;; Make sure there is a variable named.
- (signal 'loopy-&whole-missing (list var))
- (let ((possible-whole-var (cl-second remaining-var)))
- (cond
- ;; Make sure we have a variable and not a special symbol.
- ((memq possible-whole-var '(&rest &key &keys))
- (signal 'loopy-&whole-missing (list var)))
- ((sequencep possible-whole-var)
- (signal 'loopy-&whole-sequence (list var)))
- ;; If it's the only variable named, just bind it and return.
- ((and (not (cddr remaining-var))
- (not (loopy--var-ignored-p possible-whole-var)))
- (warn "`&whole' used when only one variable listed: %s" var)
- (cl-return-from loopy--destructure-list
- `((,possible-whole-var ,value-expression))))
- (t
- ;; Now just operate on remaining variables.
- (setq remaining-var (cddr remaining-var))
- (if (loopy--var-ignored-p possible-whole-var)
- (warn "`&whole' variable ignored: %s" var)
- (setq whole-var possible-whole-var)
- (push `(,whole-var ,value-expression) bindings)))))))
-
- ;; Find any (_ &rest `rest') or (_ . `rest') variable. If found, set
- ;; `rest-var' and remove them from the variable list `remaining-var'.
- (let ((possible-rest-var))
- (if (not (proper-list-p remaining-var))
- ;; If REMAINING-VAR is not a proper list, then the last cons cell is dotted.
- (seq-let (other-vars last-var)
- (loopy--split-off-last-var remaining-var)
- (setq remaining-var other-vars
- possible-rest-var last-var))
-
- (seq-let (before after)
- (loopy--split-list-before remaining-var '&rest)
-
- (unless before
- (warn "`&rest' being treated same as `&whole': %s" var))
-
- (when after
- (let ((rest-var (cl-second after))
- (vars-after-rest-var (cddr after)))
- (cond ((or (null rest-var)
- (memq (cl-second after) '(&key &keys)))
- (signal 'loopy-&rest-missing (list var)))
- ;; This is the best place to check that argument only uses
- ;; keys after the `rest-var'.
- ((and vars-after-rest-var
- (not (memq (cl-first vars-after-rest-var)
- '(&key &keys))))
- (signal 'loopy-&rest-non-var (list var)))
- (t
- ;; Now just operate on remaining variables.
- (setq remaining-var (append before vars-after-rest-var)
- possible-rest-var rest-var)))))))
-
- ;; Finally, bind the &rest var, if any.
- (when (and possible-rest-var
- (not (loopy--var-ignored-p possible-rest-var)))
- ;; NOTE: For sequence `&rest' vars, we need to destructure
- ;; /after/ the normal variables have been `pop'-ed off
- ;; of the value.
- (if (and possible-rest-var
- (sequencep possible-rest-var))
- (setq rest-var-was-sequence possible-rest-var
- rest-var (gensym "seq-rest-"))
- (setq rest-var possible-rest-var))))
-
- ;; Find the key vars, if any. The key vars must be drawn from
- ;; the remaining part after the normal variables are bound.
- (seq-let (before after)
- (loopy--split-list-before remaining-var '&key)
- ;; We might as well be forgiving of this mistake.
- (unless after
- (seq-let (bef aft)
- (loopy--split-list-before remaining-var '&keys)
- (setq before bef after aft)))
- (when after
- (if (null (cdr after))
- (signal 'loopy-&key-missing (list var))
- (setq key-vars (cdr after)
- remaining-var before))))
-
- ;; Handle the positional variables. Generally, we want to `pop' the
- ;; positional values off of some container variable. This could be the
- ;; `rest' variable, the `whole' variable, the variable in which keys are
- ;; sought, or the last positional variable.
- ;;
- ;; NOTE: By this point, there may still be variable to ignore,
- ;; so we bind the `rest' var inside of here.
- (when remaining-var
- ;; Whether `positional-vars' is non-nil affects where keys are sought. We
- ;; just need to record that they exists before we consume `remaining-var'
- ;; in the `while' list.
- (setq positional-vars remaining-var)
-
- ;; If we can, we should skip over as many ignored values as possible.
- ;;
- ;; We still need to record where ignored values end so that we can
- ;; correctly bind the `rest' var, and where true values being
- ;; so that we can start popping from the correct place.
- (let* ((fist-positional-pos 0)
- (rest-pos (length remaining-var))
- (last-positional-pos (1- rest-pos)))
-
- (cl-loop for v in (copy-sequence remaining-var)
- while (loopy--var-ignored-p v)
- do
- (cl-incf fist-positional-pos)
- (setq remaining-var (cl-rest remaining-var)))
-
- ;; `cl-subseq' uses an exclusive final argument
- (cl-loop with final-exclusive-index = (- rest-pos fist-positional-pos)
- for v in (reverse remaining-var)
- while (loopy--var-ignored-p v)
- do (progn
- (cl-decf final-exclusive-index)
- (cl-decf last-positional-pos))
- finally do
- (cl-callf cl-subseq remaining-var 0 final-exclusive-index))
-
- ;; If need be, bind the `rest' variable. If there are no key vars,
- ;; no positional vars, and the rest var is a sequence, then we can just
- ;; destructure
- (when rest-var
- (let* ((val-expr (or whole-var value-expression))
- (rest-val (if (zerop fist-positional-pos)
- val-expr
- `(nthcdr ,fist-positional-pos ,val-expr))))
- (cond
- (key-vars
- ;; Otherwise, if no positional variables, just bind the &rest var.
- (push `(,rest-var ,rest-val) bindings))
- ((null remaining-var)
- (if (and rest-var-was-sequence (null key-vars))
- (dolist (binding (loopy--destructure-sequence
- rest-var-was-sequence rest-val))
- (push binding bindings))
- (push `(,rest-var ,rest-val) bindings)))
- (t
- (push `(,rest-var ,rest-val) bindings)))))
-
- ;; If we don't need a pop target, then we can take a shortcut
- ;; and consume the single remaining variable.
- (when (and (null rest-var)
- (null key-vars)
- (= 1 (length remaining-var)))
- (let ((single-var (cl-first remaining-var))
- (final-val `(nth ,fist-positional-pos
- ,(or whole-var value-expression))))
- (if (sequencep single-var)
- (dolist (binding (loopy--destructure-sequence
- single-var final-val))
- (push binding bindings))
- (push `(,single-var ,final-val) bindings)))
- ;; Consume final remaining positional var.
- (setq remaining-var nil))
-
- ;; Otherwise, if there are still unignored positional variables,
- ;; we need to decide how to pop them off.
- (when remaining-var
-
- (let (;; The positional variables sans those that can be ignored given the
- ;; destructuring requirements.
- (popped-vars)
- ;; Whence positional values are popped. This can be a generated
- ;; variable.
- (pop-target)
- ;; Whether we'll need to do more destructuring after processing
- ;; the variables in `popped-vars'. This is the orignal sequence,
- ;; not the generated variable.
- (pop-target-was-seq)
- ;; If `pop-target' is the last valid positional variable, then it
- ;; needs to be extracted from a list of remaining values after the
- ;; preceding positional variables are bound. This is not a concern
- ;; when `rest-var' is the `pop-target'.
- (pop-target-is-positional-var))
-
- ;; Choose the variables to bind and whence they will be extracted.
- (cond
- ;; Rest var is bound in its own section, in case there are no
- ;; positional variables. Otherwise, it would be bound here.
- (rest-var (setq pop-target rest-var
- popped-vars remaining-var
- pop-target-was-seq rest-var-was-sequence
- pop-target-is-positional-var nil))
-
- (key-vars (setq pop-target key-target-var
- popped-vars remaining-var
- pop-target-was-seq nil
- pop-target-is-positional-var nil)
-
- ;; `key-target-var' is only used with `&key' without
- ;; `&rest'.
- (let ((val (or whole-var value-expression)))
- (push `(,key-target-var
- ,(if (zerop fist-positional-pos)
- val
- `(nthcdr ,fist-positional-pos ,val)))
- bindings))
- (setq popping-key-target-var t))
-
- ;; TODO: Optimize when final var is pop var.
- (t (seq-let (other-vars last-var)
- (loopy--split-off-last-var remaining-var)
-
- (setq pop-target-is-positional-var t
- pop-target-was-seq (and (sequencep last-var) last-var)
- popped-vars other-vars
- pop-target (if pop-target-was-seq
- (gensym "pop-target-")
- last-var))
-
- (let ((val (or whole-var value-expression)))
- (push `(,pop-target ,(if (zerop fist-positional-pos)
- val
- `(nthcdr ,fist-positional-pos ,val)))
- bindings)))))
-
- ;; Now that variables are decided, pop `popped-vars' off of the value of
- ;; `pop-target'. If there are sublists of ignored variables, we skip
- ;; over all of them and simply set the `pop-target' to some nth `cdr' of
- ;; itself.
- (while popped-vars
- (let ((i (car popped-vars)))
- (setq popped-vars (cdr popped-vars))
- (cond ((sequencep i)
- (dolist (binding (loopy--destructure-sequence
- i `(pop ,pop-target)))
- (push binding bindings)))
- ((loopy--var-ignored-p i)
- ;; Combine multiple ignored popped-vars.
- (let ((count (loopy--count-while
- #'loopy--var-ignored-p popped-vars)))
- ;; `nthcdr' is a C function, so it should be fast enough
- ;; even for high counts.
- (push `(,pop-target (nthcdr ,(1+ count) ,pop-target))
- bindings)
- (setq popped-vars (nthcdr count popped-vars))))
- (t
- (push `(,i (pop ,pop-target))
- bindings)))))
-
-
- ;; Since we can ignore positions between the last unignored
- ;; positional variable and the rest var, we need to make sure that
- ;; the rest var is the correct Nth cdr now that we're done popping.
- ;;
- ;; `rest-pos' is technically just the length of the list of
- ;; positional variables before we started processing them,
- ;; so it is always bound to a number.
- (when (or rest-var key-vars)
- (let ((pos-diff (- rest-pos last-positional-pos))
- (var (or rest-var key-target-var)))
- (when (> pos-diff 1)
- (push `(,var (nthcdr ,(1- pos-diff) ,var))
- bindings))))
-
- ;; Do final update of `pop-target' if need be. We only need to do this
- ;; if it was a sequence (in which case there are more variables to bind)
- ;; or if it was a positional variable.
- (cond
- (pop-target-was-seq
- (dolist (bind (loopy--destructure-sequence
- ;; If `pop-target' is `rest-var', then it is the
- ;; remainder of the current list. Else, `pop-target' is
- ;; an element of that list.
- pop-target-was-seq (if rest-var
- pop-target
- `(car ,pop-target))))
- (push bind bindings)))
- (pop-target-is-positional-var
- (push `(,pop-target (car ,pop-target)) bindings)))))))
-
- ;; Now process the keys.
- (when key-vars
- (let ((target-var (or rest-var
- ;; If we used positional variables, then they can be
- ;; popped off of `key-target-var', which is bound
- ;; to the value expression or `whole-var'.
- (and positional-vars key-target-var)
- whole-var
- key-target-var)))
-
- ;; If we are only using keys, then we need to create a holding variable in
- ;; which to search.
- (when (and (eq target-var key-target-var)
- (null popping-key-target-var))
- (let ((val (or whole-var value-expression)))
- (push `(,key-target-var
- ,(if positional-vars
- `(nthcdr ,(length positional-vars) ,val)
- val))
- bindings)))
-
- ;; TODO: In Emacs 28, `pcase' was changed so that all named variables
- ;; are at least bound to nil. Before that version, we should make sure
- ;; that `default' is bound.
- (let ((default nil))
- (ignore default)
- (pcase-dolist ((or `(,key-var ,default)
- key-var)
- key-vars)
- (let ((key (intern (format ":%s" key-var))))
- (push `(,key-var
- ,(if default
- `(if-let ((key-found (plist-member ,target-var ,key)))
- (cl-second key-found)
- ,default)
- `(plist-get ,target-var ,key)))
- bindings))))))
-
- ;; Check that things were bound.
- (when (null bindings)
- (signal 'loopy-destructure-vars-missing (list var)))
-
- ;; Fix the order of the bindings and return.
- (nreverse bindings)))
-
-;;;;; Destructuring Generalized Variables
-(defun loopy--destructure-generalized-sequence (var value-expression)
- "Destructure VALUE-EXPRESSION according to VAR as `setf'-able places.
-
-VALUE-EXPRESSION should itself be a `setf'-able place.
-
-Returns a list of bindings suitable for `cl-symbol-macrolet'."
- (cl-typecase var
- (symbol (unless (loopy--var-ignored-p var)
- `((,var ,value-expression))))
- (list (loopy--destructure-generalized-list var value-expression))
- (array (loopy--destructure-generalized-array var value-expression))
- (t (signal 'loopy-destructure-type (list var)))))
-
-(defun loopy--destructure-generalized-array (var value-expression)
- "Destructure VALUE-EXPRESSION according to VAR as `setf'-able places.
-
-VALUE-EXPRESSION should itself be a `setf'-able place.
-
-Returns a list of bindings suitable for `cl-symbol-macrolet'.
-
-- `&rest' references a subsequence place.
-- `&whole' references the entire place."
- (let ((bindings)
- (using-rest-var)
- (remaining-var var)
- (using-whole-var)
- (remaining-length (length var)))
-
- (when (eq '&whole (aref var 0))
- (cond ((= 1 remaining-length)
- (signal 'loopy-&whole-missing (list var)))
- ((sequencep (aref remaining-var 1))
- (signal 'loopy-&whole-sequence (list var)))
- (t
- (let ((whole-var (aref remaining-var 1)))
- (when (= 2 remaining-length)
- (warn "`&whole' variable used alone: %s" var))
-
- (setq remaining-var (substring remaining-var 2)
- remaining-length (max 0 (- remaining-length 2)))
-
- (if (loopy--var-ignored-p whole-var)
- (warn "`&whole' variable ignored: %s" var)
- (setq using-whole-var whole-var))))))
-
- (when-let ((pos (cl-position '&rest remaining-var :test #'eq)))
- (cond
- ((= (1+ pos) remaining-length)
- (signal 'loopy-&rest-missing (list var)))
- ((> remaining-length (+ 2 pos))
- (signal 'loopy-&rest-multiple (list var)))
- (t
- (setq using-rest-var (aref remaining-var (1+ pos))
- remaining-length (max 0 (- remaining-length 2))
- remaining-var (substring remaining-var 0 -2)))))
-
- (when using-whole-var
- (push `(,using-whole-var ,value-expression) bindings))
-
- (cl-loop for v across remaining-var
- for idx from 0
- do (cond
- ((loopy--var-ignored-p v)) ; Do nothing if variable is `_'.
- (t (if (sequencep v)
- (dolist (binding (loopy--destructure-generalized-sequence
- v `(aref ,value-expression ,idx)))
- (push binding bindings))
- (push `(,v (aref ,value-expression ,idx))
- bindings)))))
-
- ;; Now bind the `&rest' var, if needed.
- (when (and using-rest-var
- (not (loopy--var-ignored-p using-rest-var)))
- ;; Note: Can't use the more specific `substring' here, as that would
- ;; convert the sequence to a string in `setf'.
- (let ((rest-val `(cl-subseq ,value-expression ,remaining-length)))
- (if (sequencep using-rest-var)
- (dolist (binding (loopy--destructure-sequence
- using-rest-var rest-val))
- (push binding bindings))
- (push `(,using-rest-var ,rest-val) bindings))))
-
- (nreverse bindings)))
-
-(cl-defun loopy--destructure-generalized-list (var value-expression)
- "Destructure VALUE-EXPRESSION according to VAR as `setf'-able places.
-
-VALUE-EXPRESSION should itself be a `setf'-able place.
-
-returns a list of bindings suitable for `cl-symbol-macrolet'.
-
-- `&rest' references a subsequence place.
-- `&whole' references the entire place.
-
-See `loopy--destructure-list' for normal values."
- (let ((var-list var) ; For reporting errors.
- (bindings nil)) ; The result of this function.
-
- (when (eq (cl-first var) '&whole)
- (cond
- ;; Make sure there is a variable named.
- ((null (cdr var))
- (signal 'loopy-&whole-missing (list var-list)))
- ;; If it's the only variable named, just bind it and return.
- ((null (cddr var))
- (warn "`&whole' used when only one variable listed: %s"
- var-list)
- (cl-return-from loopy--destructure-generalized-list
- `((,(cl-second var) ,value-expression))))
- (t
- (let ((possible-whole-var (cl-second var)))
- (if (loopy--var-ignored-p possible-whole-var)
- (warn "`&whole' variable being ignored: %s" var-list)
- ;; Now just operate on remaining variables.
- (push `(,possible-whole-var ,value-expression)
- bindings)))
- (setq var (cddr var)))))
-
- ;; Now handle the remaining variables. Since we're not storing a value,
- ;; we don't need to do any `pop'-ing like in `loopy--destructure-list'.
- ;; However, we still need to keep track of where to look for keys.
- ;;
- ;; Since it's possible for `var' to be a dotted list, we only know
- ;; where to look after processing the entire list `var'.
- (let ((var-is-dotted (not (proper-list-p var)))
- (rest-var-value nil)
- (last-positional-var-index)
- (positional-vars-used)
- (key-vars))
-
- (let ((looking-at-key-vars nil)
- (index 0)
- (v nil))
- (while (car-safe var)
- (setq v (car var))
- (cond
- ((loopy--var-ignored-p v)
- (setq var (cdr var))
- (cl-incf index)) ; Do nothing in this case.
-
- ((eq v '&rest)
- (setq looking-at-key-vars nil)
- (let ((rest-var (cl-second var))
- (vars-after-rest-var (cddr var)))
- (cond
- (var-is-dotted
- (signal 'loopy-&rest-dotted (list var-list)))
- ((or (null rest-var)
- (memq rest-var '(&key &keys)))
- (signal 'loopy-&rest-missing (list var-list)))
- ((and vars-after-rest-var
- (not (memq (cl-first vars-after-rest-var)
- '(&key &keys))))
- (signal 'loopy-&rest-non-var (list var-list)))
- (t
- (unless (loopy--var-ignored-p rest-var)
- (setq rest-var-value `(nthcdr ,index ,value-expression))
- (if (sequencep rest-var)
- (dolist (bind (loopy--destructure-generalized-sequence
- rest-var rest-var-value))
- (push bind bindings))
- (push `(,rest-var ,rest-var-value)
- bindings)))
- (setq var (cddr var))
- (cl-incf index 2)))))
-
- ;; For keys, we don't want to increase the index, just skip over
- ;; them. Key variables stop once `&rest' or the last cdr of a
- ;; dotted list is reached (at which point the loop exits).
- ((memq v '(&key &keys))
- (setq looking-at-key-vars t
- var (cl-rest var)))
-
- (looking-at-key-vars
- (push v key-vars)
- (setq var (cl-rest var)))
-
- (t
- (if (sequencep v)
- (dolist (bind (loopy--destructure-generalized-sequence
- v `(nth ,index ,value-expression)))
- (push bind bindings))
- (push `(,v (nth ,index ,value-expression)) bindings))
- (setq var (cl-rest var)
- last-positional-var-index index
- positional-vars-used t)
- (cl-incf index))))
-
- ;; If it was a dotted list, then `var' is now an atom.
- (when var
- ;; The first `cdr' is 1, not 0, so we must add 1 here to get the
- ;; remainder of the list after the last positional variable.
- (let ((cdr-value `(nthcdr ,(1+ last-positional-var-index)
- ,value-expression)))
- (setq rest-var-value cdr-value)
- (push `(,var ,cdr-value) bindings))))
-
- ;; Decide where to look for keys, if any.
- (when key-vars
- (let ((key-target-value (or rest-var-value
- (and positional-vars-used
- ;; The first `cdr' is 1, not 0, so we
- ;; must add 1 here to get the remainder
- ;; of the list after the last
- ;; positional variable.
- `(nthcdr ,(1+ last-positional-var-index)
- ,value-expression))
- value-expression)))
- (dolist (k key-vars)
- (push `(,k (compat-call plist-get ,key-target-value
- ,(intern (format ":%s" k))))
- bindings)))))
-
- ;; Fix the order of the bindings and return.
- (nreverse bindings)))
-
;;;; Loop Tag Names
(defun loopy--produce-non-returning-exit-tag-name (&optional loop-name)
@@ -1073,31 +387,6 @@ This expansion can apply FUNC directly or via `funcall'."
`(,(loopy--get-function-symbol func) ,@args)
`(funcall ,func ,@args)))
-
-;;;; Indexing
-
-(defun loopy--generate-inc-idx-instructions
- (index-holder increment-holder by decreasing)
- "Generate instructions for incrementing an index variable.
-
-If possible, directly use a number in the code instead of storing
-it in a variable, since that seems to be faster.
-
-INDEX-HOLDER is the variable use for index.
-INCREMENT-HOLDER is the variable to store the increment.
-BY is the increment passed in the parsing function.
-DECREASING is whether the increment should be decreasing.
-
-Returns a list of instructions."
- (if (numberp by)
- `((loopy--latter-body
- (setq ,index-holder (,(if decreasing #'- #'+)
- ,index-holder ,by))))
- `((loopy--iteration-vars (,increment-holder ,by))
- (loopy--latter-body
- (setq ,index-holder (,(if decreasing #'- #'+)
- ,index-holder ,increment-holder))))))
-
;;;; Membership
@@ -1150,89 +439,18 @@ KEY transforms those elements and ELEMENT."
('eq `(memq ,element ,list))
(_ form))))
-;;;; Variable binding for instructions
-;; TODO: Check not using `pcase' in github errors.
-
-(defvar loopy--iteration-vars)
-(defvar loopy--accumulation-vars)
-(defvar loopy--other-vars)
-
-(defmacro loopy--instr-let-var (place sym exp name &rest body)
- "Use SYM as EXP for BODY, creating an instruction to bind at PLACE.
-
-Use this for values that should change during iteration.
-
-For normal variables (that is, not needing instructions), see
-also `macroexp-let2' and `cl-once-only'."
- (declare (indent 4)
- (debug (sexp sexp form sexp body)))
- (let ((bodysym (gensym "body"))
- (expsym (gensym "exp")))
- `(let* ((,expsym ,exp)
- (,sym (or ,name (gensym (symbol-name (quote ,sym)))))
- (,bodysym (progn ,@body)))
- (cons (list (quote ,place)
- (list ,sym ,expsym))
- ,bodysym))))
-
-(defmacro loopy--instr-let-var* (bindings place &rest body)
- "A multi-binding version of `loopy--instr-let-var'.
-
-BINDINGS are variable-value pairs. A third item in the list is
-an expression that evaluates to a symbol to use to generate a
-name to use in the binding. PLACE is the Loopy variable to use
-as the head of the instruction. BODY are the forms for which the
-binding exists."
- (declare (indent 2)
- (debug ((&rest (gate symbol form &optional form))
- symbol
- body)))
- (cl-reduce (cl-function (lambda (res (var val &optional name))
- `(loopy--instr-let-var ,place ,var ,val ,name ,res)))
- (reverse bindings)
- :initial-value (macroexp-progn body)))
-
-(defmacro loopy--instr-let-const (place sym exp name &rest body)
- "Use SYM as EXP for BODY, maybe creating an instruction to bind at PLACE.
-
-Use for values that are evaluated only once, such as the optional
-arguments to the iteration commands. If the value of EXP is not
-null and is not constant according to `macroexp-const-p', then a
-binding is created.
-
-For normal variables (that is, not needing instructions), see
-also `macroexp-let2' and `cl-once-only'."
- (declare (indent 4)
- (debug (sexp sexp form sexp body)))
- (let ((bodysym (gensym "body"))
- (expsym (gensym "exp")))
- `(let* ((,expsym ,exp)
- (,sym (if (or (null ,expsym)
- (macroexp-const-p ,expsym))
- ,expsym
- (or ,name
- (gensym (symbol-name (quote ,sym))))))
- (,bodysym (progn ,@body)))
- (if (eq ,sym ,expsym)
- ,bodysym
- (cons (list (quote ,place)
- (list ,sym ,expsym))
- ,bodysym)))))
-
-(defmacro loopy--instr-let-const* (bindings place &rest body)
- "A multi-binding version of `loopy--instr-let-const'.
-
-BINDINGS are variable-value pairs. PLACE is the Loopy variable to use
-as the head of the instruction. BODY are the forms for which the
-binding exists."
- (declare (indent 2)
- (debug ((&rest (gate symbol form &optional form))
- symbol
- body)))
- (cl-reduce (cl-function (lambda (res (var val &optional name))
- `(loopy--instr-let-const ,place ,var ,val ,name ,res)))
- (reverse bindings)
- :initial-value (macroexp-progn body)))
+(cl-defmacro loopy--pcase-let-workaround (variables form)
+ "Wrap FORM in a `let' with VARIABLES bound to nil on Emacs less than 28.
+
+Prior to Emacs 28, it was not guaranteed that `pcase-let' bound
+unmatched variables."
+ (declare (indent 1))
+ (if (eval-when-compile (< emacs-major-version 28))
+ `(let ,(mapcar (lambda (sym) `(,sym nil))
+ variables)
+ ,(cons 'ignore variables)
+ ,form)
+ form))
(provide 'loopy-misc)
;;; loopy-misc.el ends here
diff --git a/loopy-pcase.el b/loopy-pcase.el
index 96eff7d4..7f520215 100644
--- a/loopy-pcase.el
+++ b/loopy-pcase.el
@@ -31,21 +31,17 @@
;;; Code:
(require 'loopy)
-(require 'loopy-misc)
+(require 'loopy-destructure)
(require 'loopy-vars)
-(require 'macroexp)
-(require 'pcase)
-(require 'cl-lib)
(defun loopy-pcase--enable-flag-pcase ()
"Make this `loopy' loop use `pcase' destructuring."
- (setq
- loopy--destructuring-for-iteration-function
- #'loopy-pcase--destructure-for-iteration
- loopy--destructuring-for-with-vars-function
- #'loopy-pcase--destructure-for-with-vars
- loopy--destructuring-accumulation-parser
- #'loopy-pcase--parse-destructuring-accumulation-command))
+ (setq loopy--destructuring-for-iteration-function
+ #'loopy-pcase--destructure-for-iteration
+ loopy--destructuring-for-with-vars-function
+ #'loopy-pcase--destructure-for-with-vars
+ loopy--destructuring-accumulation-parser
+ #'loopy-pcase--parse-destructuring-accumulation-command))
(defun loopy-pcase--disable-flag-pcase ()
"Make this `loopy' loop use `pcase' destructuring."
@@ -60,7 +56,7 @@
(if (eq loopy--destructuring-accumulation-parser
#'loopy-pcase--parse-destructuring-accumulation-command)
(setq loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command)))
+ #'loopy--parse-destructuring-accumulation-command-default)))
(add-to-list 'loopy--flag-settings
(cons 'pcase #'loopy-pcase--enable-flag-pcase))
@@ -69,123 +65,14 @@
(add-to-list 'loopy--flag-settings
(cons '-pcase #'loopy-pcase--disable-flag-pcase))
-(defun loopy-pcase--destructure-for-iteration (var val)
- "Destructure VAL according to VAR as by `pcase-let'.
+(defalias 'loopy-pcase--destructure-for-iteration
+ #'loopy--pcase-destructure-for-iteration)
-Returns a list. The elements are:
-1. An expression which binds the variables in VAR to the values
- in VAL.
-2. A list of variables which exist outside of this expression and
- need to be `let'-bound."
- (let ((var-list)
- (destructuring-expression))
- ;; This sets `destructuring-expression' and `var-list'.
- (setq destructuring-expression
- (if (fboundp 'pcase-compile-patterns)
- (pcase-compile-patterns
- val
- (list
- (cons var
- (lambda (varvals &rest _)
- (cons 'setq (mapcan (cl-function
- (lambda ((var val &rest rest))
- (push var var-list)
- (list var val)))
- varvals))))))
- ;; NOTE: In Emacs versions less than 28, this functionality
- ;; technically isn't public, but this is what the developers
- ;; recommend.
- (pcase--u
- `((,(pcase--match val
- (pcase--macroexpand
- `(or ,var pcase--dontcare)))
- ,(lambda (vars)
- (cons 'setq
- (mapcan (lambda (v)
- (let ((destr-var (car v))
- ;; Use `cadr' for Emacs 28+, `cdr' for less.
- (destr-val (if (version< emacs-version "28")
- (cdr v)
- (warn "loopy-pcase: Update Emacs 28 to use `pcase-compile-patterns'.")
- (cadr v))))
- (push destr-var var-list)
- (list destr-var destr-val)))
- vars))))))))
- (list destructuring-expression var-list)))
+(defalias 'loopy-pcase--destructure-for-with-vars
+ #'loopy--pcase-destructure-for-with-vars)
-(defun loopy-pcase--destructure-for-with-vars (bindings)
- "Return a way to destructure BINDINGS by `pcase-let*'.
-
-Returns a list of two elements:
-1. The symbol `pcase-let*'.
-2. A new list of bindings."
- (list 'pcase-let* bindings))
-
-(cl-defun loopy-pcase--parse-destructuring-accumulation-command
- ((name var val &rest args))
- "Parse the accumulation loop command using `pcase' for destructuring.
-
-NAME is the name of the command. VAR-OR-VAL is a variable name
-or, if using implicit variables, a value . VAL is a value, and
-should only be used if VAR-OR-VAL is a variable."
- (let* ((instructions)
- (full-main-body))
- (if (fboundp 'pcase-compile-patterns)
- (setq full-main-body
- (pcase-compile-patterns
- val
- (list
- (cons var
- (lambda (varvals &rest _)
- (let ((destr-main-body))
- (dolist (varval varvals)
- (let ((destr-var (cl-first varval))
- (destr-val (cl-second varval)))
- (seq-let (main-body other-instructions)
- (loopy--extract-main-body
- (loopy--parse-loop-command
- `(,name ,destr-var ,destr-val ,@args)))
- ;; Just push the other instructions, but
- ;; gather the main body expressions.
- (dolist (instr other-instructions)
- (push instr instructions))
- (push main-body destr-main-body))))
-
- ;; The lambda returns the destructured main body,
- ;; which needs to be wrapped by Pcase's
- ;; destructured bindings.
- (macroexp-progn (apply #'append destr-main-body))))))))
- ;; NOTE: In Emacs versions less than 28, this functionality technically
- ;; isn't public, but this is what the developers recommend.
- (setq full-main-body
- (pcase--u `((,(pcase--match val
- (pcase--macroexpand
- `(or ,var pcase--dontcare)))
- ,(lambda (vars)
- (let ((destr-main-body))
- (dolist (v vars)
- (let ((destr-var (car v))
- ;; Use `cadr' for Emacs 28+, `cdr' for less.
- (destr-val (if (version< emacs-version "28")
- (cdr v)
- (warn "loopy-pcase: Update Emacs 28 to use `pcase-compile-patterns'.")
- (cadr v))))
- (seq-let (main-body other-instructions)
- (loopy--extract-main-body
- (loopy--parse-loop-command
- `(,name ,destr-var ,destr-val ,@args)))
- ;; Just push the other instructions, but
- ;; gather the main body expressions.
- (dolist (instr other-instructions)
- (push instr instructions))
- (push main-body destr-main-body))))
- ;; The lambda returns the destructured main body,
- ;; which needs to be wrapped by Pcase's
- ;; destructured bindings.
- (macroexp-progn (apply #'append destr-main-body)))))))))
- ;; Finally, return the instructions.
- `((loopy--main-body ,full-main-body)
- ,@(nreverse instructions))))
+(defalias 'loopy-pcase--parse-destructuring-accumulation-command
+ #'loopy--pcase-parse-for-destructuring-accumulation-command)
(provide 'loopy-pcase)
;;; loopy-pcase.el ends here
diff --git a/loopy-pkg.el b/loopy-pkg.el
index 54888509..579d35eb 100644
--- a/loopy-pkg.el
+++ b/loopy-pkg.el
@@ -1,7 +1,7 @@
(define-package "loopy" "0.11.2"
"A looping macro"
'((emacs "27.1")
- (map "3.0")
+ (map "3.3.1")
(seq "2.22")
(compat "29.1.3"))
:homepage "https://github.com/okamsn/loopy"
diff --git a/loopy-seq.el b/loopy-seq.el
index 237d2a2c..7fed5fa2 100644
--- a/loopy-seq.el
+++ b/loopy-seq.el
@@ -36,24 +36,21 @@
;; `seq-let' to produce values (which in turn uses `pcase-let') instead of
;; directly passing the variable list to `pcase-let'.
+(require 'cl-lib)
+(require 'seq)
+
(require 'loopy)
-(require 'loopy-misc)
+(require 'loopy-destructure)
(require 'loopy-vars)
-(require 'seq)
-(require 'pcase)
-(require 'loopy-pcase)
-(require 'macroexp)
-(require 'cl-lib)
(defun loopy-seq--enable-flag-seq ()
"Make this `loopy' loop use `seq-let' destructuring."
- (setq
- loopy--destructuring-for-iteration-function
- #'loopy-seq--destructure-for-iteration
- loopy--destructuring-for-with-vars-function
- #'loopy-seq--destructure-for-with-vars
- loopy--destructuring-accumulation-parser
- #'loopy-seq--parse-destructuring-accumulation-command))
+ (setq loopy--destructuring-for-iteration-function
+ #'loopy-seq--destructure-for-iteration
+ loopy--destructuring-for-with-vars-function
+ #'loopy-seq--destructure-for-with-vars
+ loopy--destructuring-accumulation-parser
+ #'loopy-seq--parse-destructuring-accumulation-command))
(defun loopy-seq--disable-flag-seq ()
"Make this `loopy' loop use `seq-let' destructuring."
@@ -68,14 +65,21 @@
(if (eq loopy--destructuring-accumulation-parser
#'loopy-seq--parse-destructuring-accumulation-command)
(setq loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command)))
-
-(add-to-list 'loopy--flag-settings
- (cons 'seq #'loopy-seq--enable-flag-seq))
-(add-to-list 'loopy--flag-settings
- (cons '+seq #'loopy-seq--enable-flag-seq))
-(add-to-list 'loopy--flag-settings
- (cons '-seq #'loopy-seq--disable-flag-seq))
+ #'loopy--parse-destructuring-accumulation-command-default)))
+
+(add-to-list 'loopy--flag-settings (cons 'seq #'loopy-seq--enable-flag-seq))
+(add-to-list 'loopy--flag-settings (cons '+seq #'loopy-seq--enable-flag-seq))
+(add-to-list 'loopy--flag-settings (cons '-seq #'loopy-seq--disable-flag-seq))
+
+;; Same as `seq--make-pcase-patterns', copied in case of future changes.
+(defun loopy-seq--make-pcase-pattern (args)
+ "Return a list of `(seq ...)' pcase patterns from the argument list ARGS."
+ (cons 'seq
+ (seq-map (lambda (elt)
+ (if (seqp elt)
+ (seq--make-pcase-patterns elt)
+ elt))
+ args)))
(defun loopy-seq--destructure-for-with-vars (bindings)
"Return a way to destructure BINDINGS as if by a `seq-let*'.
@@ -106,7 +110,7 @@ variables."
result-is-one-expression t))))
result))
-(cl-defun loopy-seq--destructure-for-iteration (var val)
+(defun loopy-seq--destructure-for-iteration (var val)
"Destructure VAL according to VAR, as if by `seq-let'.
Returns a list. The elements are:
@@ -114,7 +118,7 @@ Returns a list. The elements are:
in VAL.
2. A list of variables which exist outside of this expression and
need to be `let'-bound."
- (loopy-pcase--destructure-for-iteration (seq--make-pcase-patterns var) val))
+ (loopy--pcase-destructure-for-iteration (loopy-seq--make-pcase-pattern var) val))
(cl-defun loopy-seq--parse-destructuring-accumulation-command
((name var val &rest args))
@@ -126,8 +130,8 @@ the value to accumulate."
;; Pcase macro, so we can use functions from loopy-pcase.el. The `setq'
;; bindings in the instruction should not be order-sensitive for accumulation
;; commands; the bindings should be independent.
- (loopy-pcase--parse-destructuring-accumulation-command
- `(,name ,(seq--make-pcase-patterns var) ,val ,@args)))
+ (loopy--pcase-parse-for-destructuring-accumulation-command
+ `(,name ,(loopy-seq--make-pcase-pattern var) ,val ,@args)))
(provide 'loopy-seq)
;;; loopy-seq.el ends here
diff --git a/loopy-vars.el b/loopy-vars.el
index ad576394..7d5aa503 100644
--- a/loopy-vars.el
+++ b/loopy-vars.el
@@ -285,7 +285,7 @@ Unlike `loopy--destructuring-for-iteration-function', the
function named by this variable returns instructions, not a list
of variable-value pairs.
-If nil, use `loopy--parse-destructuring-accumulation-command'.")
+If nil, use `loopy--parse-destructuring-accumulation-command-default'.")
;;;;; For setting up flags
(defvar loopy--flag-settings nil
diff --git a/loopy.el b/loopy.el
index 1b172232..7130db86 100644
--- a/loopy.el
+++ b/loopy.el
@@ -6,7 +6,7 @@
;; Created: November 2020
;; URL: https://github.com/okamsn/loopy
;; Version: 0.11.2
-;; Package-Requires: ((emacs "27.1") (map "3.0") (seq "2.22") (compat "29.1.3"))
+;; Package-Requires: ((emacs "27.1") (map "3.3.1") (seq "2.22") (compat "29.1.3"))
;; Keywords: extensions
;; LocalWords: Loopy's emacs Edebug
@@ -121,6 +121,7 @@
(require 'cl-lib)
(require 'gv)
+(require 'macroexp)
(require 'map)
(require 'pcase)
(require 'seq)
@@ -128,6 +129,8 @@
(require 'loopy-misc)
(require 'loopy-commands)
(require 'loopy-vars)
+(require 'loopy-destructure)
+(require 'loopy-instrs)
;;;; Built-in flags
@@ -138,7 +141,7 @@
(setq loopy--destructuring-for-with-vars-function
#'loopy--destructure-for-with-vars-default
loopy--destructuring-accumulation-parser
- #'loopy--parse-destructuring-accumulation-command))
+ #'loopy--parse-destructuring-accumulation-command-default))
(cl-callf map-insert loopy--flag-settings 'default #'loopy--enable-flag-default)
@@ -156,90 +159,6 @@ this means that an explicit \"nil\" is always required."
"Ensure BINDINGS valid according to `loopy--validate-binding'."
(mapc #'loopy--validate-binding bindings))
-
-;;;###autoload
-(defmacro loopy-setq (&rest args)
- "Use Loopy destructuring in a `setq' form.
-
-This macro supports only the built-in style of destructuring, and
-is unaffected by flags like `seq' or `pcase'. For example, if
-you wish to use `pcase' destructuring, you should use `pcase-let'
-instead of this macro.
-
-\(fn SYM VAL SYM VAL ...)"
- (declare (debug (&rest [sexp form])))
- `(setq ,@(apply #'append
- (cl-loop for (var val . _) on args by #'cddr
- append (loopy--destructure-sequence var val)))))
-;;;###autoload
-(defalias 'loopy-dsetq 'loopy-setq) ; Named for Iterate's `dsetq'.
-
-;;;###autoload
-(defmacro loopy-let* (bindings &rest body)
- "Use Loopy destructuring on BINDINGS in a `let*' form wrapping BODY.
-
-This macro supports only the built-in style of destructuring, and
-is unaffected by flags like `seq' or `pcase'. For example, if
-you wish to use `pcase' destructuring, you should use `pcase-let'
-instead of this macro."
- (declare (debug ((&rest [sexp form]) body))
- (indent 1))
- `(let* ,(cl-loop for (var val) in bindings
- append (loopy--destructure-sequence var val))
- ,@body))
-
-;;;###autoload
-(defmacro loopy-ref (bindings &rest body)
- "Destructure BINDINGS as `setf'-able places around BODY.
-
-This macro only creates references to those places via
-`cl-symbol-macrolet'. It does /not/ create new variables or bind
-values. Its behavior should not be mistaken with that of
-`cl-letf*', which temporarily binds values to those places.
-
-As these places are not true variable, BINDINGS is not
-order-sensitive.
-
-This macro supports only the built-in style of destructuring,
-and is unaffected by flags like `pcase' and `seq'."
- (declare (debug ((&rest [sexp form]) body))
- (indent 1))
- `(cl-symbol-macrolet
- ,(cl-loop for (var val) in bindings
- append (loopy--destructure-generalized-sequence
- var val))
- ,@body))
-
-;;;###autoload
-(defmacro loopy-lambda (args &rest body)
- "Create a `lambda' using `loopy' destructuring in the argument list.
-
-ARGS are the arguments of the lambda, which can be `loopy'
-destructuring patterns. See the info node `(loopy)Loop Commands'
-for more on this.
-
-BODY is the `lambda' body."
- (declare (debug (lambda-list body))
- (indent 1))
- (let ((lambda-args)
- (destructurings))
- (dolist (arg args)
- (if (symbolp arg)
- (push arg lambda-args)
- (let ((arg-var (gensym)))
- (push arg-var lambda-args)
- (push (list arg arg-var) destructurings))))
- `(lambda ,(nreverse lambda-args)
- (loopy-let* ,(nreverse destructurings)
- ,@body))))
-
-(defalias 'loopy--basic-builtin-destructuring #'loopy--destructure-sequence
- "Destructure VALUE-EXPRESSION according to VAR.
-
-Return a list of variable-value pairs (not dotted), suitable for
-substituting into a `let*' form or being combined under a `setq'
-form.")
-
(defun loopy--destructure-for-with-vars (bindings)
"Destructure BINDINGS into bindings suitable for something like `let*'.
@@ -264,13 +183,46 @@ which will be used to wrap the loop and other code."
"Destructure BINDINGS into bindings suitable for something like `let*'.
Returns a list of two elements:
-1. The symbol `let*'.
+1. The symbol `pcase-let*'.
2. A new list of bindings."
- (list 'let*
- (mapcan (cl-function
- (lambda ((var val))
- (loopy--destructure-sequence var val)))
- 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))))
(cl-defun loopy--find-special-macro-arguments (names body)
"Find any usages of special macro arguments NAMES in BODY, given aliases.
@@ -1023,9 +975,109 @@ see the Info node `(loopy)' distributed with this package."
;; in the correct order.
(loopy--correct-var-structure)
-
;; Constructing/Creating the returned code.
(loopy--expand-to-loop)))
+;;;;; Other features
+
+;; TODO: We didn't implement these using `loopy' to avoid a weird error about
+;; `loopy--process-special-arg-loop-name' not being defined. This error
+;; doesn't seem to occur in `loopy-iter.el', in which we already use
+;; `loopy'.
+
+;;;###autoload
+(defalias 'loopy-dsetq 'loopy-setq) ; Named for Iterate's `dsetq'.
+
+;;;###autoload
+(defmacro loopy-setq (&rest args)
+ "Use Loopy destructuring in a `setq' form.
+
+This macro supports only the built-in style of destructuring, and
+is unaffected by flags like `seq' or `pcase'. For example, if
+you wish to use `pcase' destructuring, you should use `pcase-let'
+instead of this macro.
+
+\(fn SYM VAL SYM VAL ...)"
+ (declare (debug (&rest [sexp form])))
+ (macroexp-progn
+ (cl-loop for (var val) on args by #'cddr
+ collect (car (loopy--destructure-for-iteration-default var val)))))
+
+;;;###autoload
+(defmacro loopy-let* (bindings &rest body)
+ "Use Loopy destructuring on BINDINGS in a `let*' form wrapping BODY.
+
+This macro supports only the built-in style of destructuring, and
+is unaffected by flags like `seq' or `pcase'. For example, if
+you wish to use `pcase' destructuring, you should use `pcase-let'
+instead of this macro."
+ (declare (debug ((&rest [sexp form]) body))
+ (indent 1))
+ ;; Because Emacs versions less than 28 weren't guaranteed to bind all
+ ;; variables in Pcase, we need to use the same approach we do for
+ ;; destructuring `with' bindings, instead of just passing the bindings to
+ ;; `pcase' directly.
+ (let ((new-binds))
+ (dolist (bind bindings)
+ (cl-destructuring-bind (var val)
+ bind
+ (if (symbolp var)
+ (push bind new-binds)
+ (let ((sym (gensym)))
+ (push `(,sym ,val) new-binds)
+ (cl-destructuring-bind (var-set-expr var-list)
+ (loopy--pcase-destructure-for-iteration `(loopy ,var) sym :error t)
+ (dolist (var var-list)
+ (push var new-binds))
+ (push `(_ ,var-set-expr) new-binds))))))
+ `(let* ,(nreverse new-binds)
+ ,@body)))
+
+;;;###autoload
+(defmacro loopy-ref (bindings &rest body)
+ "Destructure BINDINGS as `setf'-able places around BODY.
+
+This macro only creates references to those places via
+`cl-symbol-macrolet'. It does /not/ create new variables or bind
+values. Its behavior should not be mistaken with that of
+`cl-letf*', which temporarily binds values to those places.
+
+As these places are not true variable, BINDINGS is not
+order-sensitive.
+
+This macro supports only the built-in style of destructuring,
+and is unaffected by flags like `pcase' and `seq'."
+ (declare (debug ((&rest [sexp form]) body))
+ (indent 1))
+ `(cl-symbol-macrolet
+ ,(cl-loop for (var val) in bindings
+ append (loopy--destructure-generalized-sequence
+ var val))
+ ,@body))
+
+;;;###autoload
+(defmacro loopy-lambda (args &rest body)
+ "Create a `lambda' using `loopy' destructuring in the argument list.
+
+ARGS are the arguments of the lambda, which can be `loopy'
+destructuring patterns. See the info node `(loopy)Loop Commands'
+for more on this.
+
+BODY is the `lambda' body."
+ (declare (debug (lambda-list body))
+ (indent 1))
+ (let ((lambda-args)
+ (destructurings))
+ (dolist (arg args)
+ (if (symbolp arg)
+ (push arg lambda-args)
+ (let ((arg-var (gensym)))
+ (push arg-var lambda-args)
+ (push (list arg arg-var) destructurings))))
+ `(lambda ,(nreverse lambda-args)
+ (loopy-let* ,(nreverse destructurings)
+ ,@body))))
+
+
(provide 'loopy)
;;; loopy.el ends here
diff --git a/tests/iter-tests.el b/tests/iter-tests.el
index d3c93bfd..8224e1d0 100644
--- a/tests/iter-tests.el
+++ b/tests/iter-tests.el
@@ -1,11 +1,17 @@
;;; Tests for `loopy-iter' -*- lexical-binding: t; -*-
+(require 'cl-lib)
+
+(require 'package)
+(unless (featurep 'compat)
+ (dolist (dir (cl-remove-if-not #'file-directory-p (directory-files (expand-file-name package-user-dir) t "compat")))
+ (push dir load-path)))
+
(eval-when-compile (require 'loopy)
(require 'loopy-iter))
(require 'loopy)
(require 'loopy-iter)
(require 'ert)
-(require 'cl-lib)
(require 'generator)
(defmacro liq (&rest body)
diff --git a/tests/load-path.el b/tests/load-path.el
index 554e80c1..69221f0a 100644
--- a/tests/load-path.el
+++ b/tests/load-path.el
@@ -1,6 +1,9 @@
;; Add installed packages to load path.
;; Don't use Seq, as we want to load the right version.
(require 'cl-lib)
-(cl-loop for i in (directory-files-recursively "~/.emacs.d/elpa/" "" t)
- when (file-directory-p i)
- do (add-to-list 'load-path i))
+(let ((dir (expand-file-name (if (require 'package nil t)
+ package-user-dir
+ "~/.emacs.d/elpa"))))
+ (cl-loop for i in (directory-files-recursively dir "" t)
+ when (file-directory-p i)
+ do (add-to-list 'load-path i)))
diff --git a/tests/misc-tests.el b/tests/misc-tests.el
index 3ba63707..c5e0ae0b 100644
--- a/tests/misc-tests.el
+++ b/tests/misc-tests.el
@@ -1,27 +1,35 @@
;; Tests of secondary features and helper functions.
+(push (expand-file-name ".")
+ load-path)
+
(require 'cl-lib)
+
+(require 'package)
+(unless (featurep 'compat)
+ (dolist (dir (cl-remove-if-not #'file-directory-p (directory-files (expand-file-name package-user-dir) t "compat")))
+ (push dir load-path)))
+
(require 'map)
(require 'ert)
(require 'pcase)
(require 'map)
(require 'loopy)
+;; (require 'loopy-destructure)
+
+(ert-deftest pcase-pat-defined ()
+ (should (get 'loopy 'pcase-macroexpander)))
(defmacro loopy-test-structure (input output-pattern)
"Use `pcase' to check a destructurings bindings.
INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
+ (declare (indent 1))
`(pcase ,input
(,output-pattern
t)
(_ nil)))
;;; Minor Functions
-(ert-deftest split-off-last-var ()
- (should (equal '((a b c) d)
- (loopy--split-off-last-var '(a b c d))))
-
- (should (equal '((a b c) d)
- (loopy--split-off-last-var '(a b c . d)))))
(ert-deftest loopy--member-p ()
(should (loopy--member-p '((a . 1) (b . 2))
@@ -38,63 +46,59 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
;;; Destructuring
+(ert-deftest destructure-with ()
+ (should-error (eval (quote (loopy (with ((a b) [1 2]))
+ (cycle 1)
+ (collect a)
+ (collect b)))
+ t)
+ :type 'loopy-bad-run-time-destructuring)
+
+ (should (equal '(1 2 3 6)
+ (eval (quote (loopy (with ([a b] [1 2])
+ (c (1+ b))
+ (d (+ 3 c)))
+ (cycle 1)
+ (collect a)
+ (collect b)
+ (collect c)
+ (collect d)))
+ t))))
+
(ert-deftest destructure-array-errors ()
- (should-error (loopy--destructure-array [a b &rest] 'val))
- (should-error (loopy--destructure-array [a b &rest c d] 'val))
- (should-error (loopy--destructure-array [&rest] 'val))
- (should-error (loopy--destructure-array [&whole &rest] 'val))
- (should-error (loopy--destructure-array [&whole] 'val))
- (should-error (loopy--destructure-array [&whole _] 'val))
- (should-error (loopy--destructure-array [&rest _] 'val))
- (should-error (loopy--destructure-array [_ _] 'val)))
-
-(ert-deftest destructure-arrays-steps-output ()
- "Test for ideal output."
- (should (loopy-test-structure (loopy--destructure-array [a] 'val)
- `((a (aref val 0)))))
-
- (should (loopy-test-structure (loopy--destructure-array [a _] 'val)
- `((a (aref val 0)))))
-
- (should (loopy-test-structure (loopy--destructure-array [_ b] 'val)
- `((b (aref val 1)))))
-
- (should (loopy-test-structure (loopy--destructure-array [_ b _] 'val)
- `((b (aref val 1)))))
-
- (should (loopy-test-structure (loopy--destructure-array [a b] 'val)
- `((,_ val)
- (a (aref ,_ 0))
- (b (aref ,_ 1)))))
-
- (should (loopy-test-structure (loopy--destructure-array [_ b c] 'val)
- `((,_ val)
- (b (aref ,_ 1))
- (c (aref ,_ 2)))))
-
- (should (loopy-test-structure (loopy--destructure-array [_ b c _ _] 'val)
- `((,_ val)
- (b (aref ,_ 1))
- (c (aref ,_ 2)))))
-
- (should (loopy-test-structure (loopy--destructure-array [a b &rest c] 'val)
- `((,_ val)
- (a (aref ,_ 0))
- (b (aref ,_ 1))
- (c (substring ,_ 2)))))
-
- (should (loopy-test-structure (loopy--destructure-array [a _ &rest c] 'val)
- `((,_ val)
- (a (aref ,_ 0))
- (c (substring ,_ 2)))))
-
- (should (loopy-test-structure (loopy--destructure-array [a b &rest _] 'val)
- `((,_ val)
- (a (aref ,_ 0))
- (b (aref ,_ 1)))))
-
- (should (loopy-test-structure (loopy--destructure-array [_ b _ &rest _] 'val)
- `((b (aref val 1))))))
+ (should-error (loopy--destructure-for-iteration-default [a b &rest] 'val)
+ :type 'loopy-&rest-missing)
+ (should-error (loopy--destructure-for-iteration-default [a b &rest c d] 'val)
+ :type 'loopy-&rest-multiple)
+ (should-error (loopy--destructure-for-iteration-default [&rest] 'val)
+ :type 'loopy-&rest-missing)
+ (should-error (loopy--destructure-for-iteration-default [&whole &rest] 'val)
+ :type 'loopy-&whole-missing)
+ (should-error (loopy--destructure-for-iteration-default [&whole] 'val)
+ :type 'loopy-&whole-missing)
+ (should-error (loopy--destructure-for-iteration-default [&whole _] 'val)
+ :type 'loopy-&whole-missing)
+ (should-error (loopy--destructure-for-iteration-default [&rest _] 'val)
+ :type 'loopy-&rest-missing)
+ (should-error (loopy--destructure-for-iteration-default [_ _] 'val)
+ :type 'loopy-destructure-vars-missing))
+
+(ert-deftest loopy-let*-prev-val ()
+ "Make sure we don't shadow values.
+Later bindings can have access to the values of earlier bindings.
+Later variables in the same destructuring should not use the
+new values of the earlier variables."
+ (should (equal '(2 3 13 107)
+ (eval (quote (let ((a 1)
+ (b 2)
+ (c 7)
+ (d 33))
+ (loopy-let* (((a b) (list (1+ a) (1+ b)))
+ (f (lambda (x) (+ 100 x)))
+ ([c d] (vector (+ 10 b)
+ (funcall f c))))
+ (list a b c d))))
+ t))))
(ert-deftest destructure-arrays ()
(should (equal '(1 2 3)
@@ -122,245 +126,23 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
(list cat a b c)))))))
(ert-deftest destructure-list-errors ()
- (should-error (loopy--destructure-list '(a b &rest) 'val))
- (should-error (loopy--destructure-list '(a b &rest c d) 'val))
- (should-error (loopy--destructure-list '(&rest) 'val))
- (should-error (loopy--destructure-list '(&whole &rest) 'val))
- (should-error (loopy--destructure-list '(&whole) 'val))
- (should-error (loopy--destructure-list '(&whole _) 'val))
- (should-error (loopy--destructure-list '(&rest _) 'val))
- (should-error (loopy--destructure-list '(&key) 'val))
- (should-error (loopy--destructure-list '(&keys) 'val))
- (should-error (loopy--destructure-list '(_ _) 'val)))
-
-(ert-deftest destructure-lists-steps-output-rest ()
- (should (loopy-test-structure (loopy--destructure-list '(a b &rest c) 'val)
- `((c val)
- (a (pop c))
- (b (pop c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole a b &rest c) 'val)
- `((whole val)
- (c whole)
- (a (pop c))
- (b (pop c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(a _ &rest c) 'val)
- `((c val)
- (a (pop c))
- (c (nthcdr 1 c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole a _ &rest c) 'val)
- `((whole val)
- (c whole)
- (a (pop c))
- (c (nthcdr 1 c)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ a _ _ &rest c) 'val)
- `((c (nthcdr 2 val))
- (a (pop c))
- (c (nthcdr 2 c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(a b &rest _) 'val)
- `((b val)
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole a b &rest _) 'val)
- `((whole val)
- (b whole)
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ &rest (a b)) 'val)
- `((b (nthcdr 2 val))
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ &rest (a b)) 'val)
- `((whole val)
- (b (nthcdr 2 whole))
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ b _ &rest _) 'val)
- `((b (nth 1 val)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ b _ &rest _) 'val)
- `((whole val)
- (b (nth 1 whole)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ b _ &rest (c d e)) 'val)
- `((,_ (nthcdr 1 val))
- (b (pop ,_))
- (,_ (nthcdr 1 ,_))
- (e ,_)
- (c (pop ,_))
- (d (pop ,_))
- (e (car e)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ b _ &rest (c d e)) 'val)
- `((whole val)
- (,_ (nthcdr 1 whole))
- (b (pop ,_))
- (,_ (nthcdr 1 ,_))
- (e ,_)
- (c (pop ,_))
- (d (pop ,_))
- (e (car e)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ &rest b) 'val)
- `((b (nthcdr 2 val)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ &rest b) 'val)
- `((whole val)
- (b (nthcdr 2 whole))))))
-
-(ert-deftest destructure-lists-steps-output-key ()
- (should (loopy-test-structure
- (loopy--destructure-list '(&key k1 k2) 'val)
- `((,_ val)
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ &rest b &key k1 k2) 'val)
- `((b (nthcdr 2 val))
- (k1 (plist-get b :k1))
- (k2 (plist-get b :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ &rest b &key k1 k2) 'val)
- `((whole val)
- (b (nthcdr 2 whole))
- (k1 (plist-get b :k1))
- (k2 (plist-get b :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ &rest _ &key k1 k2) 'val)
- `((,_ (nthcdr 2 val))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ &rest _ &key k1 k2) 'val)
- `((whole val)
- (,_ (nthcdr 2 whole))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ &key k1 k2) 'val)
- `((,_ (nthcdr 2 val))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ &key k1 k2) 'val)
- `((whole val)
- (,_ (nthcdr 2 whole))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(a _ &key k1 k2) 'val)
- `((,_ val)
- (a (pop ,_))
- (,_ (nthcdr 1 ,_))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ a _ &key k1 k2) 'val)
- `((,_ (nthcdr 2 val))
- (a (pop ,_))
- (,_ (nthcdr 1 ,_))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(&whole whole _ _ a _ &key k1 k2) 'val)
- `((whole val)
- (,_ (nthcdr 2 whole))
- (a (pop ,_))
- (,_ (nthcdr 1 ,_))
- (k1 (plist-get ,_ :k1))
- (k2 (plist-get ,_ :k2)))))
-
- (should (loopy-test-structure
- (loopy--destructure-list '(_ _ a _ &key k1 (k2 25) k3) 'val)
- `((,_ (nthcdr 2 val))
- (a (pop ,_))
- (,_ (nthcdr 1 ,_))
- (k1 (plist-get ,_ :k1))
- (k2 (if-let ((key-found (plist-member ,_ :k2)))
- (cl-second key-found)
- 25))
- (k3 (plist-get ,_ :k3))))))
-
-(ert-deftest destructure-lists-steps-output ()
- "Test for ideal output."
- (should (loopy-test-structure (loopy--destructure-list '(a) 'val)
- `((a (nth 0 val)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole a) 'val)
- `((whole val)
- (a (nth 0 whole)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(a _) 'val)
- `((a (nth 0 val)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(_ b) 'val)
- `((b (nth 1 val)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole _ b) 'val)
- `((whole val)
- (b (nth 1 whole)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(_ b _) 'val)
- `((b (nth 1 val)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(a b) 'val)
- `((b val)
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(_ b c) 'val)
- `((c (nthcdr 1 val))
- (b (pop c))
- (c (car c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(_ b c _ _) 'val)
- `((c (nthcdr 1 val))
- (b (pop c))
- (c (car c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole _ b c _ _) 'val)
- `((whole val)
- (c (nthcdr 1 whole))
- (b (pop c))
- (c (car c)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(_ (a b) _) 'val)
- `((b (nth 1 val))
- (a (pop b))
- (b (car b)))))
-
- (should (loopy-test-structure (loopy--destructure-list '(&whole whole _ (a b) _) 'val)
- `((whole val)
- (b (nth 1 whole))
- (a (pop b))
- (b (car b))))))
+ (should-error (loopy--get-var-groups '(a b &rest)) :type 'loopy-&rest-missing)
+ (should-error (loopy--get-var-groups '(a b &rest c d)) :type 'loopy-&rest-multiple)
+ (should-error (loopy--get-var-groups '(&rest)) :type 'loopy-&rest-missing)
+ (should-error (loopy--get-var-groups '(&whole &rest)) :type 'loopy-&whole-missing)
+ (should-error (loopy--get-var-groups '(&whole)) :type 'loopy-&whole-missing)
+ (should-error (loopy--get-var-groups '(&whole _)) :type 'loopy-&whole-missing)
+ (should-error (loopy--get-var-groups '(&rest _)) :type 'loopy-&rest-missing)
+ (should-error (loopy--get-var-groups '(&rest rest &optional a)) :type 'loopy-&optional-bad-position)
+ (should-error (loopy--get-var-groups '(&key a b &optional c)) :type 'loopy-&optional-bad-position)
+ (should-error (loopy--get-var-groups '(&optional a (_ 27) c)) :type 'loopy-&optional-ignored-default-or-supplied)
+ (should-error (loopy--get-var-groups '(&optional a (_ nil b-supplied) c)) :type 'loopy-&optional-ignored-default-or-supplied)
+ (should-error (loopy--get-var-groups '(&key)) :type 'loopy-&key-missing)
+ (should-error (loopy--get-var-groups '(&keys)) :type 'loopy-&key-missing)
+ (should-error (loopy--get-var-groups '(&map)) :type 'loopy-&map-missing)
+ ;; TODO: This test is expensive with `pcase.el'.
+ ;; (should-error (loopy--get-var-groups '(_ _)) )
+ )
(ert-deftest destructure-lists ()
(should (equal '(1 2 3)
@@ -375,30 +157,89 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
(eval (quote (loopy-let* (((a b c &rest d) '(1 2 3 4 5)))
(list a b c d))))))
+
+ (should (equal '(1 2 3 4 5)
+ (eval (quote (loopy-let* (((a b c &optional d e) '(1 2 3 4 5)))
+ (list a b c d e))))))
+
+ (should (equal '(1 2 3 4 5 nil nil)
+ (eval (quote (loopy-let* (((a b c &optional d e (f nil f-supp)) '(1 2 3 4 5)))
+ (list a b c d e f f-supp))))))
+
+ (should (equal '(1 2 3 4 5 27 nil)
+ (eval (quote (loopy-let* (((a b c &optional d e (f 27 f-supp)) '(1 2 3 4 5)))
+ (list a b c d e f f-supp))))))
+
+ (should (equal '(1 2 3 4 5 6 t)
+ (eval (quote (loopy-let* (((a b c &optional d e (f 27 f-supp)) '(1 2 3 4 5 6)))
+ (list a b c d e f f-supp))))))
+
+ (should (equal '(1 2 3 4 5 6 t (7 8))
+ (eval (quote (loopy-let* ((( a b c &optional d e (f 27 f-supp)
+ &rest g)
+ '(1 2 3 4 5 6 7 8)))
+ (list a b c d e f f-supp g))))))
+
+ (should (equal '(1 2 3 t)
+ (eval (quote (loopy-let* ((( a &optional ((b c) nil bc-supp))
+ '(1 (2 3))))
+ (list a b c bc-supp))))))
+
+ (should (equal '(1 77 88 nil)
+ (eval (quote (loopy-let* ((( a &optional ((b c) (list 77 88) bc-supp))
+ '(1)))
+ (list a b c bc-supp))))))
+
+ (should (equal '(1 77 88 nil nil)
+ (eval (quote (loopy-let* ((( a &optional ((b &optional (c 88 c-supp))
+ (list 77)
+ bc-supp))
+ '(1)))
+ (list a b c bc-supp c-supp))))))
+
(should (equal '(1 2 3 4 5)
(eval (quote (loopy-let* (((a b c &key d e) '(1 2 3 :e 5 :d 4)))
(list a b c d e))))))
+ (should (equal '(1 2 3 5 t 27 nil)
+ (eval (quote (loopy-let* (( (a b c &key (e nil e-supp)
+ (f 27 f-supp)
+ &allow-other-keys)
+ '(1 2 3 :e 5 :d 4)))
+ (list a b c e e-supp f f-supp))))))
+
+ (should (equal '(1 2 3 5 t nil nil)
+ (eval (quote (loopy-let* (((a b c &key
+ ((:elephant e) nil e-supp)
+ ((:fox f) nil f-supp)
+ &allow-other-keys)
+ '(1 2 3 :elephant 5 :d 4)))
+ (list a b c e e-supp f f-supp))))))
+
(should (equal '(1 2 3 4 5 (:e 5 :d 4))
(eval (quote (loopy-let* (((a b c &key d e . f) '(1 2 3 :e 5 :d 4)))
(list a b c d e f))))))
- (should (equal '(1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((a b c &key d e . f) '(1 2 3 4 5 :e 6 :d 7)))
+ (should (equal '(1 2 3 7 6 (:e 6 :d 7))
+ (eval (quote (loopy-let* (((a b c _ _ &key d e . f) '(1 2 3 4 5 :e 6 :d 7)))
(list a b c d e f))))))
- (should (equal '(1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((a b c &key d e &rest f)
+ (should (equal '(1 2 3 7 6 (4 5 :e 6 :d 7) 5)
+ (eval (quote (loopy-let* (((a b c &key ((4 key4)) d e &rest f)
'(1 2 3 4 5 :e 6 :d 7)))
- (list a b c d e f))))))
+ (list a b c d e f key4))))))
- (should (equal '(1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((a b c &rest f &key d e)
+ (should (equal '(1 2 3 7 6 (4 5 :e 6 :d 7) 5)
+ (eval (quote (loopy-let* (((a b c &rest f &key ((4 key4)) d e)
'(1 2 3 4 5 :e 6 :d 7)))
- (list a b c d e f))))))
+ (list a b c d e f key4))))))
+
+ (should-error (eval (quote (loopy-let* (((&key d e) '(:a 7 :e 5 :d 4)))
+ (list d e a))))
+ :type 'loopy-bad-run-time-destructuring)
(should (equal '(4 5)
- (eval (quote (loopy-let* (((&key d e) '(:a 7 :e 5 :d 4)))
+ (eval (quote (loopy-let* (((&key d e &allow-other-keys) '(:a 7 :e 5 :d 4)))
(list d e))))))
(should (= 4 (eval (quote (loopy-let* (((_ _ _ a _ _ _) '(1 2 3 4 5 6 7)))
@@ -430,24 +271,50 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
'(1 2 3 :e 5 :d 4)))
(list cat a b c d e f))))))
- (should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((&whole cat a b c &key d e . f)
+ (should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7) 5)
+ (eval (quote (loopy-let* (((&whole cat a b c &key d e ((4 key4)). f)
'(1 2 3 4 5 :e 6 :d 7)))
+ (list cat a b c d e f key4))))))
+
+ (should (equal '((1 2 3 4 5 e 6 d 7) 1 2 3 7 6 (4 5 e 6 d 7))
+ (eval (quote (loopy-let* (((&whole cat a b c &map d e . f)
+ '(1 2 3 4 5 e 6 d 7)))
(list cat a b c d e f))))))
(should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((&whole cat a b c &key d e &rest f)
+ (eval (quote (loopy-let* (((&whole cat a b c &map (:d d) (:e e) . f)
+ '(1 2 3 4 5 :e 6 :d 7)))
+ (list cat a b c d e f))))))
+
+ (should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7) 5)
+ (eval (quote (loopy-let* (((&whole cat a b c &key d e ((4 key4)) &rest f)
'(1 2 3 4 5 :e 6 :d 7)))
+ (list cat a b c d e f key4))))))
+
+ (should (equal '((1 2 3 4 5 e 6 d 7) 1 2 3 7 6 (4 5 e 6 d 7))
+ (eval (quote (loopy-let* (((&whole cat a b c &map d e &rest f)
+ '(1 2 3 4 5 e 6 d 7)))
(list cat a b c d e f))))))
+ (should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7) 5)
+ (eval (quote (loopy-let* (((&whole cat a b c &rest f &key d e ((4 key4)))
+ '(1 2 3 4 5 :e 6 :d 7)))
+ (list cat a b c d e f key4))))))
+
(should (equal '((1 2 3 4 5 :e 6 :d 7) 1 2 3 7 6 (4 5 :e 6 :d 7))
- (eval (quote (loopy-let* (((&whole cat a b c &rest f &key d e)
+ (eval (quote (loopy-let* (((&whole cat a b c &rest f
+ &map (:d d) (:e e))
'(1 2 3 4 5 :e 6 :d 7)))
(list cat a b c d e f))))))
(should (equal '((:a 7 :e 5 :d 4) 4 5)
- (eval (quote (loopy-let* (((&whole cat &key d e)
+ (eval (quote (loopy-let* (((&whole cat &key d e &allow-other-keys)
'(:a 7 :e 5 :d 4)))
+ (list cat d e))))))
+
+ (should (equal '((:a 7 :e 5 :d 4 :allow-other-keys t) 4 5)
+ (eval (quote (loopy-let* (((&whole cat &key d e)
+ '(:a 7 :e 5 :d 4 :allow-other-keys t)))
(list cat d e)))))))
;; This only tests the getting of values.
@@ -570,6 +437,25 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
whole (mapcar #'1+ whole)))
l))))))
+(ert-deftest generalized-array-should-error ()
+ ;; TODO: Having trouble with `should-error' here?
+ ;; (should-error (loopy--destructure-generalized-array [a b &optional c] 'val)
+ ;; :type '(loopy-&optional-generalized-variable))
+ (should (condition-case err
+ (loopy--destructure-generalized-array [a b &optional c] 'val)
+ (loopy-&optional-generalized-variable
+ t)))
+
+ (should (condition-case err
+ (loopy--destructure-generalized-array [a b &map ('c c 27)] 'val)
+ (loopy-generalized-default
+ t)))
+
+ (should (condition-case err
+ (loopy--destructure-generalized-array [a b &map ('c c nil c-supp)] 'val)
+ (loopy-generalized-supplied
+ t))))
+
(ert-deftest destructure-array-refs ()
(should (equal [1 2 3]
(let ((arr [7 7 7]))
@@ -611,6 +497,15 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
(setf a 1 b 2 c 3 d [4]))
arr)))
+ ;; NOTE: This currently doesn't work due to upstream implementations.
+ ;; See issue #184.
+ ;; (should (equal [1 2 3 4 0 0 16]
+ ;; (let ((arr (vector 7 7 7 7 0 0 6)))
+ ;; (loopy-ref (([a b c &rest d &map (3 sub-idx-3)] arr))
+ ;; (setf a 1 b 2 c 3 d [4])
+ ;; (cl-incf sub-idx-3 10))
+ ;; arr)))
+
(should (equal [2 3]
(let ((arr [7 7]))
(loopy-ref (([&whole cat a b] arr))
@@ -714,3 +609,827 @@ INPUT is the destructuring usage. OUTPUT-PATTERN is what to match."
(eval (quote (loopy (list elem '((1 2 3 :k1 4 :k2 5) (4 5 6 :k2 8)))
(collect (i j k &key (k1 27) k2 . rest) elem)
(finally-return i j k rest k1 k2)))))))
+
+;;;;; Pcase Pattern
+(defmacro loopy--pcase-exhaustive-wrapper (vars val &rest branches)
+ "Wrap variables to make sure that they're bound on earlier versions of Emacs.
+
+Prior to Emacs 28, `pcase' didn't guarantee binding all variables.
+
+- VARS is the list of variables.
+- VAL is the value to match against.
+- BRANCHES are the `pcase' branches."
+ (declare (indent 2))
+ `(eval (quote (let ,(mapcar (lambda (v)
+ `(,v 'intentionally-bad-test-val))
+ vars)
+ (pcase-exhaustive ,val
+ ,@branches)))
+ t))
+
+(ert-deftest pcase-tests-loopy-&whole-should-error ()
+ "`&whole' must come first if given, and must be followed by a patter."
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&whole))
+ (list a b c)))
+ :type 'loopy-&whole-missing)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&whole &rest))
+ (list a b c)))
+ :type 'loopy-&whole-missing)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&whole _ &rest))
+ (list a b c)))
+ :type 'loopy-&whole-missing)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a b &whole c))
+ (list a b c)))
+ :type 'loopy-&whole-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&rest a &whole c))
+ (list a b c)))
+ :type 'loopy-&whole-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&key a &whole c))
+ (list a b c)))
+ :type 'loopy-&whole-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&aux (a 1) &whole c))
+ (list a b c)))
+ :type 'loopy-&whole-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&optional (a 1) &whole c))
+ (list a b c)))
+ :type 'loopy-&whole-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (whole1 whole2)
+ (list 1 2 3)
+ ((loopy (&whole whole1 &whole whole2))
+ (list whole1 whole2)))
+ :type 'loopy-&whole-bad-position))
+
+(ert-deftest pcase-tests-loopy-&whole ()
+ "`&whole' can be a `pcase' pattern."
+ (should (equal (list (list 1 2 3) 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (whole a b c)
+ (list 1 2 3)
+ ((loopy (&whole whole a b c))
+ (list whole a b c)))))
+
+ (should (equal (list 1 2 3 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a0 b0 c0 a b c)
+ (list 1 2 3)
+ ((loopy (&whole `(,a0 ,b0 ,c0) a b c))
+ (list a0 b0 c0 a b c))))))
+
+(ert-deftest pcase-tests-loopy-pos ()
+ "Positional variables must match the length of EXPVAL."
+ (should (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a b c))
+ (list a b c)))))
+
+ (should (equal nil
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list (list 1))
+ ((loopy (a b)) (list a b))
+ (_ nil))))
+
+ (should (equal nil
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list (list 1 2 3))
+ ((loopy (a b)) (list a b))
+ (_ nil)))))
+
+(ert-deftest pcase-tests-loopy-pos-sub-seq ()
+ (should (equal (list 1 2 3 4)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 (list 3 4))
+ ((loopy (a b (c d)))
+ (list a b c d)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list (list 1 2))
+ ((loopy ((a b)))
+ (list a b))))))
+
+(ert-deftest pcase-tests-loopy-&optional-should-error ()
+ "`&optional' cannot be used after `&optional', `&rest', `&key', and `&aux'."
+ (should-error (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&rest a &optional b c))
+ (list a b c))))
+ :type 'loopy-&optional-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&body a &optional b c))
+ (list a b c)))
+ :type 'loopy-&optional-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&key a &optional b c))
+ (list a b c)))
+ :type 'loopy-&optional-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&aux (a 1) &optional b c))
+ (list a b c)))
+ :type 'loopy-&optional-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&optional a &optional b c))
+ (list a b c)))
+ :type 'loopy-&optional-bad-position))
+
+(ert-deftest pcase-tests-loopy-&optional ()
+ (should (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a b &optional c))
+ (list a b c)))))
+
+ (should (equal (list 1 2 nil)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2)
+ ((loopy (a b &optional c))
+ (list a b c)))))
+
+ (should (equal (list 1 2 13)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2)
+ ((loopy (a b &optional (c 13)))
+ (list a b c)))))
+
+ (should (equal (list 1 2 13)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2)
+ ((loopy (a b &optional [c 13]))
+ (list a b c)))))
+
+ (should (equal (list 1 2 13 nil)
+ (loopy--pcase-exhaustive-wrapper (a b c c-supplied)
+ (list 1 2)
+ ((loopy (a b &optional [c 13 c-supplied]))
+ (list a b c c-supplied)))))
+
+ (should (equal (list 1 2 3 t)
+ (loopy--pcase-exhaustive-wrapper (a b c c-supplied)
+ (list 1 2 3)
+ ((loopy (a b &optional [c 13 c-supplied]))
+ (list a b c c-supplied))))))
+
+(ert-deftest pcase-tests-loopy-&optional-ignored ()
+ (should (equal (list 1 2 nil)
+ (loopy--pcase-exhaustive-wrapper (a b d)
+ (list 1 2)
+ ((loopy (a b &optional _ d))
+ (list a b d)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list 1 2)
+ ((loopy (a b &optional _ _))
+ (list a b)))))
+
+ (should (equal (list 1 2 13 nil)
+ (loopy--pcase-exhaustive-wrapper (a b k1 k2)
+ (list 1 2)
+ ((loopy (a b &optional _ _ &key [k1 13] k2))
+ (list a b k1 k2)))))
+
+ (should (equal (list 1 2 nil 14 nil)
+ (loopy--pcase-exhaustive-wrapper (a b e k1 k2)
+ (list 1 2)
+ ((loopy (a b &optional _ _ &rest e &key [k1 14] k2))
+ (list a b e k1 k2)))))
+
+ (should (equal (list 1 2 nil 14 nil)
+ (loopy--pcase-exhaustive-wrapper (a b e k1 k2)
+ (list 1 2)
+ ((loopy (a b &optional _ _ &rest e &map [:k1 k1 14] (:k2 k2)))
+ (list a b e k1 k2)))))
+
+ (should (equal (list 1 2 nil)
+ (loopy--pcase-exhaustive-wrapper (a b d)
+ (vector 1 2)
+ ((loopy [a b &optional _ d])
+ (list a b d)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (vector 1 2)
+ ((loopy [a b &optional _ _])
+ (list a b)))))
+
+ ;; FIXME: This test fails on Emacs 27 because the tests don't install the
+ ;; correct version of Map.el.
+ (when (> emacs-major-version 27)
+ (should (equal (list 1 2 [] 14 nil)
+ (loopy--pcase-exhaustive-wrapper (a b e k1 k2)
+ (vector 1 2)
+ ((loopy [a b &optional _ _ &rest e &map [:k1 k1 14] (:k2 k2)])
+ (list a b e k1 k2)))))))
+
+(ert-deftest pcase-tests-loopy-&optional-sub-seq ()
+ "Test using sub-seq in `loopy' pattern.
+sub-seq must be contained within a sub-list, since a sub-list
+also provides a default value."
+ (should (equal (list 1 2 3 4)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 (list 3 4))
+ ((loopy (a b &optional ((c d))))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 3 4)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 (list 3 4))
+ ((loopy (a b &optional [(c d)]))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 nil nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy (a b &optional ((c d))))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 nil nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy (a b &optional [(c d)]))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy (a b &optional ((c d) (list 13 14))))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy (a b &optional [(c d) (list 13 14)]))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy ( a b
+ &optional ((c &optional (d 14))
+ (list 13))))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy ( a b
+ &optional ((c &optional [d 14])
+ (list 13))))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy ( a b
+ &optional [(c &optional (d 14))
+ (list 13)]))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2)
+ ((loopy ( a b
+ &optional [(c &optional [d 14])
+ (list 13)]))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 13 14 nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d cd-supplied)
+ (list 1 2)
+ ((loopy (a b &optional ((c d) (list 13 14) cd-supplied)))
+ (list a b c d cd-supplied)))))
+
+ (should (equal (list 1 2 13 14 nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d cd-supplied)
+ (list 1 2)
+ ((loopy (a b &optional [(c d) (list 13 14) cd-supplied]))
+ (list a b c d cd-supplied)))))
+
+ (should (equal (list 1 2 13 14 nil t nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d cd-supplied c-sub-sup d-sub-sup)
+ (list 1 2)
+ ((loopy ( a b
+ &optional
+ ((&optional (c 27 c-sub-sup)
+ (d 14 d-sub-sup))
+ (list 13)
+ cd-supplied)))
+ (list a b c d cd-supplied c-sub-sup d-sub-sup)))))
+
+ (should (equal (list 1 2 13 14 nil t nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d cd-supplied c-sub-sup d-sub-sup)
+ (list 1 2)
+ ((loopy ( a b
+ &optional
+ [(&optional (c 27 c-sub-sup)
+ [d 14 d-sub-sup])
+ (list 13)
+ cd-supplied]))
+ (list a b c d cd-supplied c-sub-sup d-sub-sup))))))
+
+(ert-deftest pcase-tests-loopy-&rest-should-error ()
+ "`&rest' (`&body', `.') cannot be used after `&rest', `&body', `&key',and `&aux'."
+ (should-error (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&rest a &rest b))
+ (list a b c))))
+ :type 'loopy-&rest-bad-position)
+
+ (should-error (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&body a &body b))
+ (list a b c))))
+ :type 'loopy-&rest-bad-position)
+
+ (should-error (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&body a . b))
+ (list a b c))))
+ :type 'loopy-&rest-dotted)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&body a &rest b))
+ (list a b c)))
+ :type 'loopy-&rest-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&rest a &body b))
+ (list a b c)))
+ :type 'loopy-&rest-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&aux (a 1) &rest b))
+ (list a b c)))
+ :type 'loopy-&rest-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (&aux (a 1) &body b))
+ (list a b c)))
+ :type 'loopy-&rest-bad-position))
+
+(ert-deftest pcase-tests-loopy-&rest-ignored ()
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ [1 2 3]
+ ((loopy [a b &rest _])
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ '(1 2 3)
+ ((loopy (a b &rest _))
+ (list a b)))))
+
+ (should (equal (list 1 2 3 11 12)
+ (loopy--pcase-exhaustive-wrapper (a b c k1 k2)
+ '(1 2 3 :k1 11 :k2 12)
+ ((loopy (a b c &rest _ &key k1 k2))
+ (list a b c k1 k2)))))
+
+ (should (equal (list 1 2 3 11 12)
+ (loopy--pcase-exhaustive-wrapper (a b c k1 k2)
+ '(1 2 3 :k1 11 :k2 12)
+ ((loopy (a b c &rest _ &map (:k1 k1) (:k2 k2)))
+ (list a b c k1 k2))))))
+
+(ert-deftest pcase-tests-loopy-&rest-nonlist-cdr ()
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (cons 1 2)
+ ((loopy (a &rest b))
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (cons 1 2)
+ ((loopy (a &body b))
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (cons 1 2)
+ ((loopy (a . b))
+ (list a b))))))
+
+(ert-deftest pcase-tests-loopy-&rest-with-&whole ()
+ (should (equal (list (cons 1 2) 1 2)
+ (loopy--pcase-exhaustive-wrapper (whole a b)
+ (cons 1 2)
+ ((loopy (&whole whole a &rest b))
+ (list whole a b)))))
+
+ (should (equal (list (cons 1 2) 1 2)
+ (loopy--pcase-exhaustive-wrapper (whole a b)
+ (cons 1 2)
+ ((loopy (&whole whole a &body b))
+ (list whole a b)))))
+
+ (should (equal (list (cons 1 2) 1 2)
+ (loopy--pcase-exhaustive-wrapper (whole a b)
+ (cons 1 2)
+ ((loopy (&whole whole a . b))
+ (list whole a b))))))
+
+(ert-deftest pcase-tests-loopy-&rest-only ()
+ "Using only `&rest' should work like `&whole'."
+ (should (equal (list (list 1 2))
+ (loopy--pcase-exhaustive-wrapper (a)
+ (list 1 2)
+ ((loopy (&rest a))
+ (list a)))))
+
+ (should (equal (list (cons 1 2))
+ (loopy--pcase-exhaustive-wrapper (a)
+ (cons 1 2)
+ ((loopy (&body a))
+ (list a))))))
+
+(ert-deftest pcase-tests-loopy-&rest-after-&optional ()
+ (should (equal (list 1 2 3 (list 4 5))
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 3 4 5)
+ ((loopy (&optional a b c &rest d))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 3 (list 4 5))
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 3 4 5)
+ ((loopy (&optional a b c &body d))
+ (list a b c d)))))
+
+ (should (equal (list 1 2 3 (list 4 5))
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ (list 1 2 3 4 5)
+ ((loopy (&optional a b c . d))
+ (list a b c d))))))
+
+(ert-deftest pcase-tests-loopy-&rest-sub-seq ()
+ (should (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a &rest (b c)))
+ (list a b c)))))
+
+ (should (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a . (b c)))
+ (list a b c)))))
+
+ (should (equal (list 1 2 3)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list 1 2 3)
+ ((loopy (a &body (b c)))
+ (list a b c))))))
+
+(ert-deftest pcase-tests-loopy-&key-should-error ()
+ "`&key' cannot be used after `&key', `&allow-other-keys', and `&aux'."
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&key a &key b))
+ (list a b)))
+ :type 'loopy-&key-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&aux (a 1) &key b))
+ (list a b)))
+ :type 'loopy-&key-bad-position))
+
+(ert-deftest pcase-tests-loopy-&map-should-error ()
+ "`&map' cannot be used after `&map' and `&aux'."
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&map a &map b))
+ (list a b)))
+ :type 'loopy-&map-bad-position)
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&aux (a 1) &map b))
+ (list a b)))
+ :type 'loopy-&map-bad-position))
+
+(ert-deftest pcase-tests-&allow-other-keys ()
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&allow-other-keys &key b))
+ (list a b)))
+ :type 'loopy-&allow-other-keys-without-&key))
+
+(ert-deftest pcase-tests-loopy-&key-exact ()
+ "`&key' doesn't match unspecified keys unless `&allow-other-keys' or `:allow-other-keys' is given."
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&key a b))
+ (list a b)))))
+
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2 :c 3)
+ ((loopy (&key a b))
+ (list a b))))
+
+ (should (equal (list 1 2 nil)
+ (loopy--pcase-exhaustive-wrapper (a b c)
+ (list :a 1 :b 2)
+ ((loopy (&key a b c))
+ (list a b c))))))
+
+(ert-deftest pcase-tests-loopy-&key-permissive ()
+ "`&key' doesn't match unspecified keys unless `&allow-other-keys' or `:allow-other-keys' is given."
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2 :c 3)
+ ((loopy (&key a b &allow-other-keys))
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2 :c 3 :allow-other-keys t)
+ ((loopy (&key a b))
+ (list a b))))))
+
+(ert-deftest pcase-tests-loopy-&map-permissive ()
+ "`&map' should not require a construct like `&allow-other-keys'."
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list 'a 1 'b 2 'c 3)
+ ((loopy (&map a b))
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2 :c 3)
+ ((loopy (&map (:a a) (:b b)))
+ (list a b))))))
+
+(ert-deftest pcase-tests-loopy-&key-not-first ()
+ "The plist should be after positional values and equal to `&rest'."
+ (should (equal (list 1 2 3 11 22)
+ (loopy--pcase-exhaustive-wrapper (a b c k1 k2)
+ (list 1 2 3 :k1 11 :k2 22)
+ ((loopy (a b c &key k1 k2))
+ (list a b c k1 k2)))))
+
+ (should (equal (list 1 2 3 (list :k1 11 :k2 22) 11 22)
+ (loopy--pcase-exhaustive-wrapper (a b c r1 k1 k2)
+ (list 1 2 3 :k1 11 :k2 22)
+ ((loopy (a b c &rest r1 &key k1 k2))
+ (list a b c r1 k1 k2))))))
+
+(ert-deftest pcase-tests-loopy-&map-not-first ()
+ "The map should be after positional values and equal to `&rest'."
+ (should (equal (list 1 2 3 11 22)
+ (loopy--pcase-exhaustive-wrapper (a b c k1 k2)
+ (list 1 2 3 'k1 11 'k2 22)
+ ((loopy (a b c &map k1 k2))
+ (list a b c k1 k2)))))
+
+ (should (equal (list 1 2 3 (list :k1 11 :k2 22) 11 22)
+ (loopy--pcase-exhaustive-wrapper (a b c r1 k1 k2)
+ (list 1 2 3 :k1 11 :k2 22)
+ ((loopy (a b c &rest r1 &map (:k1 k1) (:k2 k2)))
+ (list a b c r1 k1 k2))))))
+
+(ert-deftest pcase-tests-loopy-&key-full-form ()
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :b 2)
+ ((loopy (&key a (b 13)))
+ (list a b)))))
+
+ (should (equal (list 1 13)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1)
+ ((loopy (&key a (b 13)))
+ (list a b)))))
+
+ (should (equal (list 1 13 nil)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1)
+ ((loopy (&key a (b 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2 t)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1 :b 2)
+ ((loopy (&key a (b 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2 t)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1 :bat 2)
+ ((loopy (&key a ((:bat b) 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :bat 2)
+ ((loopy (&key a ((:bat b) 13)))
+ (list a b)))))
+
+ (should (equal (list 1 13)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1)
+ ((loopy (&key a ((:bat b) 13)))
+ (list a b)))))
+
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1 :bat 2)
+ ((loopy (&key a ((:bat b))))
+ (list a b)))))
+
+ (should (equal (list 1 nil)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list :a 1)
+ ((loopy (&key a ((:bat b))))
+ (list a b)))))
+
+ (should (equal (list 1 2 t)
+ (let ((key :bat))
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1 :bat 2)
+ ((loopy (&key a ((key b) 13 b-supplied)))
+ (list a b b-supplied)))))))
+
+(ert-deftest pcase-tests-loopy-&map-full-form ()
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list 'a 1 'b 2)
+ ((loopy (&map a ('b b 13)))
+ (list a b)))))
+
+ (should (equal (list 1 13)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ (list 'a 1)
+ ((loopy (&map a ('b b 13)))
+ (list a b)))))
+
+ (should (equal (list 1 13 nil)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list 'a 1)
+ ((loopy (&map a ('b b 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2 t)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list 'a 1 'b 2)
+ ((loopy (&map a ('b b 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2 t)
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1 :bat 2)
+ ((loopy (&map (:a a) (:bat b 13 b-supplied)))
+ (list a b b-supplied)))))
+
+ (should (equal (list 1 2 t)
+ (let ((key :bat))
+ (loopy--pcase-exhaustive-wrapper (a b b-supplied)
+ (list :a 1 :bat 2)
+ ((loopy (&map (:a a) (key b 13 b-supplied)))
+ (list a b b-supplied)))))))
+
+(ert-deftest pcase-tests-loopy-&key-sub-seq ()
+ (should (equal '(1 2 (:c 77 :e should-ignore) nil 77 t 99 nil)
+ (loopy--pcase-exhaustive-wrapper
+ (a b cd cd-supp c c-supp d d-supp)
+ '(:ab (1 2))
+ ((loopy (&key
+ ((:ab (a b)))
+ ((:cd ( &whole cd
+ &key
+ (c 88 c-supp)
+ ((:d d) 99 d-supp)
+ &allow-other-keys))
+ (list :c 77 :e 'should-ignore)
+ cd-supp)))
+ (list a b cd cd-supp c c-supp d d-supp)))))
+
+ (should (equal '( 1 2 (:c 77 :e should-ignore :allow-other-keys t) nil
+ 77 t 99 nil)
+ (loopy--pcase-exhaustive-wrapper
+ (a b cd cd-supp c c-supp d d-supp)
+ '(:ab (1 2))
+ ((loopy (&key
+ ((:ab (a b)))
+ ((:cd ( &whole cd
+ &key
+ (c 88 c-supp)
+ ((:d d) 99 d-supp)))
+ (list :c 77 :e 'should-ignore
+ :allow-other-keys t)
+ cd-supp)))
+ (list a b cd cd-supp c c-supp d d-supp)))))
+
+ (should (equal nil
+ (loopy--pcase-exhaustive-wrapper
+ (a b cd cd-supp c c-supp d d-supp)
+ '(:ab (1 2))
+ ((loopy (&key
+ ((:ab (a b)))
+ ((:cd ( &whole cd
+ &key
+ (c 88 c-supp)
+ ((:d d) 99 d-supp)))
+ (list :c 77 :e 'should-fail)
+ cd-supp)))
+ (list a b cd cd-supp c c-supp d d-supp))
+ (_ nil)))))
+
+(ert-deftest pcase-tests-loopy-&map-sub-seq ()
+ (should (equal '(1 2 (:c 77 :e should-ignore) nil 77 t 99 nil)
+ (loopy--pcase-exhaustive-wrapper
+ (a b cd cd-supp c c-supp d d-supp)
+ '(:ab (1 2))
+ ((loopy (&map
+ (:ab (a b))
+ (:cd ( &whole cd
+ &map
+ (:c c 88 c-supp)
+ (:d d 99 d-supp))
+ (list :c 77 :e 'should-ignore)
+ cd-supp)))
+ (list a b cd cd-supp c c-supp d d-supp))))))
+
+(ert-deftest pcase-tests-loopy-&aux-should-error ()
+ "`&aux' cannot be used after `&aux'."
+ (should-error (loopy--pcase-exhaustive-wrapper (a b)
+ nil
+ ((loopy (&aux a &aux b))
+ (list a b)))
+ :type 'loopy-&aux-bad-position))
+
+(ert-deftest pcase-tests-loopy-&aux ()
+ (should (equal (list 1 2 nil nil)
+ (loopy--pcase-exhaustive-wrapper (a b c d)
+ nil
+ ((loopy (&aux (a 1) (b 2) (c) d))
+ (list a b c d)))))
+
+ (should (equal (list 0 1 2 nil nil)
+ (loopy--pcase-exhaustive-wrapper (z0 a b c d)
+ (list 0)
+ ((loopy (z0 &aux (a 1) (b 2) (c) d))
+ (list z0 a b c d))))))
+
+(ert-deftest pcase-tests-loopy-&aux-sub-seq ()
+ (should (equal (list 1 2)
+ (loopy--pcase-exhaustive-wrapper (a b)
+ nil
+ ((loopy (&aux ((a b) (list 1 2))))
+ (list a b))))))
+
+(ert-deftest pcase-tests-loopy-all ()
+ (should (equal '(1 2 3 4 5 (:k1 111 :k2 222) 111 222 111 222 333 444)
+ (loopy--pcase-exhaustive-wrapper
+ (a b c d e r k1 k2 map1 map2 x1 x2)
+ (list 1 2 3 4 5 :k1 111 :k2 222)
+ ((loopy ( a b c
+ &optional d e
+ &rest r
+ &key k1 k2
+ &map (:k1 map1) (:k2 map2)
+ &aux (x1 333) (x2 444)))
+ (list a b c d e r k1 k2 map1 map2 x1 x2))))))
diff --git a/tests/pcase-tests.el b/tests/pcase-tests.el
index 8988a2f9..4741b036 100644
--- a/tests/pcase-tests.el
+++ b/tests/pcase-tests.el
@@ -2,7 +2,18 @@
;; Run these tests from project dir using:
;; emacs -Q --batch -l ert -l tests.el -f ert-run-tests-batch-and-exit
+;; NOTE: Tests the `pcase' flag, not the `pcase' implementation of destructuring.
+
+(push (expand-file-name ".")
+ load-path)
+
(require 'cl-lib)
+
+(require 'package)
+(unless (featurep 'compat)
+ (dolist (dir (seq-filter #'file-directory-p (directory-files (expand-file-name package-user-dir) t "compat")))
+ (push dir load-path)))
+
(require 'ert)
(require 'pcase)
(require 'loopy)
diff --git a/tests/seq-tests.el b/tests/seq-tests.el
index 874c4c07..8165f485 100644
--- a/tests/seq-tests.el
+++ b/tests/seq-tests.el
@@ -3,6 +3,12 @@
;; emacs -Q --batch -l ert -l tests.el -f ert-run-tests-batch-and-exit
(require 'cl-lib)
+
+(require 'package)
+(unless (featurep 'compat)
+ (dolist (dir (cl-remove-if-not #'file-directory-p (directory-files (expand-file-name package-user-dir) t "compat")))
+ (push dir load-path)))
+
(require 'ert)
(require 'seq)
(require 'loopy)
diff --git a/tests/tests.el b/tests/tests.el
index dadbc596..b3d3fa56 100644
--- a/tests/tests.el
+++ b/tests/tests.el
@@ -11,6 +11,15 @@
load-path)
(require 'cl-lib)
+
+(require 'package)
+(unless (featurep 'compat)
+ (dolist (dir (cl-remove-if-not #'file-directory-p (directory-files (expand-file-name package-user-dir) t "compat")))
+ (push dir load-path)))
+
+(require 'subr-x)
+(require 'package)
+(require 'compat)
(require 'map)
(require 'ert)
(require 'generator)
@@ -250,10 +259,13 @@ SYMS-STR are the string names of symbols from `loopy-iter-bare-commands'."
(loopy-deftest with-destructuring
:result -2
+ :wrap ((x . `(let ((e 7)) ,x)))
:body ((with ((a b) '(1 2))
- ([c d] `[,(1+ a) ,(1+ b)]))
+ ([c d] `[,(1+ a) ,(1+ b)])
+ ((e f) (list (1+ e) (1+ e))))
(return (+ (- a b)
- (- c d))))
+ (- c d)
+ (- e f))))
:loopy t
:iter-bare ((return . returning)))