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)))