From 3aebae95ea1a7689715cc62bbf04e7a248194808 Mon Sep 17 00:00:00 2001 From: okamsn Date: Wed, 25 Oct 2023 22:01:21 -0400 Subject: [PATCH] Improve efficiency and various clean-ups. - Add `loopy--instr-let-var*`, `loopy--instr-let-const*`, `loopy--instr-let-var`, `loopy--instr-let-const`. These macros are similar to `macroexp-let2*` in that the `const` versions try to pass constant values directly without creating a variable in the Loopy expansion. - Update `array`, `array-ref`, `cons`, `list`, `list-ref`, `seq`, `seq-ref`, `seq-index`, `adjoin`, `union`, `nunion` - Don't update `numbers` until after we remove the non-keyword args. - Replace some uses of `seq-let` with `cl-destructuring-bind`. - Use `loopy--bind-main-body` in some places. - Add some TODOs. - Fix `list-ref` tests to not modify literal constant list. - Fix `seq-ref` tests to not modify literal constant list. - Make `loopy--find-start-by-end-dir-vals` return the test function. - Add `:test` to `array`, `array-ref`, `sequence`, `sequence-index`, and `sequence-ref`. - When going up on lists, use `nthcdr` instead of `elt`. - Add `sequence`, `sequence-ref`, `sequence-index`, `array`, and `array-ref` tests for `:downfrom` and `:upfrom` as needed. See also issue #176 and this PR #180. --- CHANGELOG.md | 23 +- doc/loopy-doc.org | 82 ++++- loopy-commands.el | 826 +++++++++++++++++++++++++--------------------- loopy-misc.el | 84 ++++- tests/tests.el | 419 ++++++++++++++++++++--- 5 files changed, 979 insertions(+), 455 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d1c79f..d46702d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,9 +79,10 @@ This document describes the user-facing changes to Loopy. (list i '(4 5 6))) ``` -- In `adjoin`, `nunion`, and `union`, the `test` and `key` keywords are now - evaluated only once. This is now consistent with passing function values of - other loop commands. See [#170] and [#177]. +- The keyword arguments of commands are now evaluated only once. This is now + consistent with passing function values of other loop commands. If constant + according to `macroexp-const-p`, then they are used directly. Otherwise, the + value is first stored in a variable. See [#170], [#177], [#176], and [#180]. - In accumulation commands using the `test` keyword argument, the argument order of the two-argument test function is now document as `(SEQUENCE-ITEM, @@ -161,9 +162,11 @@ This document describes the user-facing changes to Loopy. be more convenient and consistent with other commands ([#144]). - The commands now exit the loop without forcing a return value, which allows implicit return values to be finalized. + - The commands now use variables to store the implicit return values of the loop, defaulting to `loopy-result` and which can be specified via `:into`, similar to accumulation commands. + ```elisp ;; => "hello there" (loopy (list i '(1 1 1 1)) @@ -176,12 +179,15 @@ This document describes the user-facing changes to Loopy. (thereis i) (finally-return (+ loopy-result 4))) ``` + - As with other incompatible commands, an error is now signaled when trying to use `thereis` with `always` or `never` **when using the same variable** ([#144]). -- Add a `:test` keyword argument to `numbers` ([#172]). This is useful when the - direction of the iteration is not known ahead of time. +- Add a `:test` keyword argument to `numbers`, `array`, `array-ref`, `sequence`, + `sequence-ref`, and `sequence-index` ([#172], [#180]). This is useful when + the direction of the iteration is not known ahead of time. + ```elisp ;; => (10 9.5 9.0 8.5 8.0 7.5 7.0 6.5 6.0 5.5) (loopy (with (start 10) @@ -192,6 +198,11 @@ This document describes the user-facing changes to Loopy. (collect i)) ``` +- By using `macroexp-const-p`, Loopy now better uses constant values ([#176], + [#180]). Instead of always creating a variable in some cases, it now better + uses the constant value directly, which Emacs can optimize to avoid some uses + of `funcall`. + ### Other Changes - Add `loopy--other-vars`, given the more explicit restriction on @@ -213,7 +224,9 @@ This document describes the user-facing changes to Loopy. [#171]: https://github.com/okamsn/loopy/pull/171 [#172]: https://github.com/okamsn/loopy/pull/172 [#173]: https://github.com/okamsn/loopy/pull/173 +[#176]: https://github.com/okamsn/loopy/issues/176 [#177]: https://github.com/okamsn/loopy/pull/177 +[#180]: https://github.com/okamsn/loopy/pull/180 ## 0.11.2 diff --git a/doc/loopy-doc.org b/doc/loopy-doc.org index a662d17f..f4900c97 100644 --- a/doc/loopy-doc.org +++ b/doc/loopy-doc.org @@ -705,7 +705,46 @@ For simplicity, the commands are described using the following notation: Generally, =VAR= is initialized to ~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 ~nil~ if it is set to ~nil~ using the =with= special +macro argument. These special cases allow for more efficient code and less +indirection. + +#+begin_src emacs-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_src + +Unlike ~cl-loop~ in some cases, in Loopy, the values passed as keyword arguments +are evaluated only once. For example, the command =(list i some-list :by +(get-function))= evaluates ~(get-function)~ only once. It does not evaluate it +repeatedly for each step of the loop. + +#+begin_src emacs-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_src ** Basic Destructuring :PROPERTIES: @@ -1102,6 +1141,14 @@ error to use the same iteration variable for multiple iteration commands. (finally-return t)) #+end_src +Unlike ~cl-loop~ and like Common Lisp's ~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 =list= command is the same for the entire +loop. This restriction allows for producing more efficient code. + + *** Generic Iteration :PROPERTIES: :CUSTOM_ID: generic-iteration @@ -1364,7 +1411,8 @@ variants =numbers-up= and =numbers-down=. =from= and =to=; it cannot be used with keywords that already describe a direction and ending condition. To match the behavior of ~cl-loop~, the default testing function is ~#'<=~. When =test= is given, =by= can be - negative. + negative. As there is no default end value when =test= is given, =to= must + also be given. #+begin_src emacs-lisp ;; => (10 9.5 9.0 8.5 8.0 7.5 7.0 6.5 6.0 5.5) @@ -1714,9 +1762,19 @@ source sequences. This command also has the aliases =seqing= and =sequencing=. =KEYS= is one or several of =from=, =upfrom=, =downfrom=, =to=, =upto=, - =downto=, =above=, =below=, =by=, and =index=. =index= names the variable - used to store the index being accessed. For others, see the =numbers= - command. + =downto=, =above=, =below=, =by=, =test=, and =index=. =index= names the + variable used to store the index being accessed. For the others, see the + =numbers= command. + + #+ATTR_TEXINFO: :tag Warning + #+begin_quote + Array elements can be accessed in constant time, but not list elements. For + lists, the =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 =sequence= command can be + noticeably slower for lists when working backwards or when the =test= + parameter (for which direction cannot be assumed) is provided. + #+end_quote If multiple sequences are given, then these keyword arguments apply to the resulting sequence of distributed elements. @@ -1794,10 +1852,10 @@ iterate. aliases =seqf=, =arrayf=, =listf=, and =stringf= of the =seq-ref= command. =KEYS= is one or several of =from=, =upfrom=, =downfrom=, =to=, =upto=, - =downto=, =above=, =below=, and =by=. For their meaning, see the =numbers= - command. This command is very similar to =numbers=, except that it can - automatically end the loop when the final element is reached. With - =numbers=, one would first need to explicitly calculate the length of the + =downto=, =above=, =below=, =by=, and =test=. For their meaning, see the + =numbers= command. This command is very similar to =numbers=, except that it + can automatically end the loop when the index of the final element is reached. + With =numbers=, one would first need to explicitly calculate the length of the sequence. Similar to =numbers=, for efficiency, =VAR= is initialized to the starting @@ -2028,9 +2086,9 @@ the accessed index during the loop. ~setf~-able place. =KEYS= is one or several of =from=, =upfrom=, =downfrom=, =to=, =upto=, - =downto=, =above=, =below=, =by=, and =index=. =index= names the variable - used to store the index being accessed. For others, see the =numbers= - command. + =downto=, =above=, =below=, =by=, =test=, and =index=. =index= names the + variable used to store the index being accessed. For others, see the + =numbers= command. #+BEGIN_SRC emacs-lisp ;; => (7 7 7 7) diff --git a/loopy-commands.el b/loopy-commands.el index 4987db66..fd806e8a 100644 --- a/loopy-commands.el +++ b/loopy-commands.el @@ -318,10 +318,9 @@ Warning trigger: %s" cmd)) (loopy--other-vars (,back-holder ,back)) ,@(mapcar (lambda (x) `(loopy--other-vars (,x nil))) holding-vars) - ,@(seq-let (main-exprs rest-instr) - (loopy--extract-main-body - (loopy--destructure-for-other-command - var (car (last holding-vars)))) + ,@(loopy--bind-main-body (main-exprs rest-instr) + (loopy--destructure-for-other-command + var (car (last holding-vars))) `((loopy--main-body (when (>= ,cnt-holder ,back-holder) ,@main-exprs)) ,@rest-instr)) @@ -638,7 +637,7 @@ instructions: ,instructions)))) -(defun loopy--find-start-by-end-dir-vals (plist) +(defun loopy--find-start-by-end-dir-vals (plist &optional cmd) "Find the numeric start, end, and step, direction, and inclusivity. The values are returned in a list in that order as a plist. @@ -653,7 +652,9 @@ iteration command. The supported keywords are: - `:above' (exclusive end) - `:below' (exclusive end) - `:by' (increment) -- `:test' (comparison function)" +- `:test' (comparison function) + +CMD is the command usage for error reporting." (loopy--plist-bind ( :from from :upfrom upfrom :downfrom downfrom :to to :upto upto :downto downto @@ -665,27 +666,56 @@ iteration command. The supported keywords are: (< 1 (cl-count-if #'identity (list to upto downto above below))) (and (or downfrom downto above) (or upfrom upto below))) - (signal 'loopy-conflicting-command-arguments (list plist))) + (signal 'loopy-conflicting-command-arguments (list (or cmd plist)))) - (let ((decreasing (or downfrom downto above))) + (let ((dir-given (or above below downfrom downto upfrom upto)) + (end-given (or to downto above below upto)) + (start-given (or from downfrom upfrom)) + (decreasing (or downfrom downto above)) + (inclusive (not (or above below)))) ;; Check directions for above and below. ;; :above is only for when the value is decreasing. ;; :below is only for when the value in increasing. (when (or (and below decreasing) (and above (not decreasing))) - (signal 'loopy-conflicting-command-arguments (list plist))) - - `(,@(when-let ((start (or from upfrom downfrom))) - `(:start ,start)) + (signal 'loopy-conflicting-command-arguments (list (or cmd plist)))) + + ;; If we're using a directional word, then we shouldn't be giving a test. + (when (and dir-given test) + (signal 'loopy-conflicting-command-arguments (list (or cmd plist)))) + + ;; The first guess is that if we're continuing indefinitely, then there is + ;; no test that we can run. The second guess is that this is incorrect + ;; because the commands use a default value for the start and ends, so a + ;; testing function is still valid. The answer is that without knowing + ;; the direction, then there is no way to know the correct default values, + ;; and if the direction is known, then the test is not needed. + (when (and test (null end-given)) + (signal 'loopy-conflicting-command-arguments (list (or cmd plist)))) + + (when (and test (null start-given)) + (signal 'loopy-conflicting-command-arguments (list (or cmd plist)))) + + `(,@(when start-given + `(:start ,start-given)) ,@(when by `(:by ,by)) - ,@(when-let ((end (or to downto above below upto))) - `(:end ,end)) - :dir-given ,(or above below downfrom downto upfrom upto) - :test ,test + ,@(when end-given + `(:end ,end-given)) + :dir-given ,dir-given + :test-given ,test + :test ,(or test (if inclusive + ;; Functions are double quoted to match as if the + ;; user passed a sharp-quoted function to the macro. + (if decreasing + (quote #'>=) + (quote #'<=)) + (if decreasing + (quote #'>) + (quote #'<)))) :decreasing ,decreasing - :inclusive ,(not (or above below)))))) + :inclusive ,inclusive)))) ;;;;;; Array (defmacro loopy--distribute-array-elements (&rest arrays) @@ -712,7 +742,8 @@ For example, [1 2] and [3 4] gives [(1 3) (1 4) (2 3) (2 4)]." "Parse the `array' command as (array VAR VAL [VALS] &key KEYS). KEYS is one or several of `:index', `:by', `:from', `:downfrom', -`:upfrom', `:to', `:downto', `:upto', `:above', or `:below'. +`:upfrom', `:to', `:downto', `:upto', `:above', `:below', and +`:test'. - `:index' names a variable used to store the accessed index of the array. @@ -720,50 +751,49 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', - `:from', `:downfrom', and `:upfrom' name the starting index - `:to', `:downto', and `:upto' name the ending index (inclusive) - `:below' and `:above' name an exclusive ending index. +- `:test' is the test function. `:downto' and `:downfrom' make the index decrease instead of increase. If multiple values are given, their elements are distributed using the function `loopy--distribute-array-elements'." :other-vals t - :keywords (:index - :by :from :downfrom :upfrom :to :downto :upto :above :below) + :keywords ( :index :test + :by :from :downfrom :upfrom :to :downto :upto :above :below) :instructions - (let ((value-holder (gensym "array-")) - (index-holder (or (plist-get opts :index) - (gensym "array-index-"))) - (end-holder (gensym "array-end-")) - (increment-holder (gensym "array-increment"))) - (loopy--plist-bind ( :start key-start :end key-end :by (by 1) - :decreasing decreasing :inclusive inclusive) - - (loopy--find-start-by-end-dir-vals opts) - - `((loopy--iteration-vars (,value-holder ,(if (null other-vals) - val - `(loopy--distribute-array-elements - ,val ,@other-vals)))) - (loopy--iteration-vars (,end-holder ,(or key-end - (if decreasing - -1 - `(length ,value-holder))))) - (loopy--iteration-vars (,index-holder ,(or key-start - (if decreasing - `(1- (length ,value-holder)) - 0)))) - ,@(loopy--destructure-for-iteration-command - var `(aref ,value-holder ,index-holder)) - - ,@(loopy--generate-inc-idx-instructions - index-holder increment-holder by decreasing) - - (loopy--pre-conditions (,(if (or (null key-end) - (not inclusive)) - (if decreasing #'> #'<) - (if decreasing #'>= #'<=)) - ,index-holder - ,end-holder)))))) + (loopy--plist-bind ( :start key-start :end key-end :by (by 1) + :decreasing decreasing + :test-given test-given :test test) + (loopy--find-start-by-end-dir-vals opts) + (loopy--instr-let-const* ((value-holder (if (null other-vals) + val + `(loopy--distribute-array-elements + ,val ,@other-vals))) + (end-holder (or key-end + (if decreasing + 0 + `(1- (length ,value-holder))))) + (increment-holder by) + (test test)) + loopy--iteration-vars + (loopy--instr-let-var* ((index-holder (or key-start + (if decreasing + `(1- (length ,value-holder)) + 0)) + (plist-get opts :index))) + loopy--iteration-vars + `(,@(loopy--destructure-for-iteration-command + var `(aref ,value-holder ,index-holder)) + (loopy--latter-body + (setq ,index-holder (,(cond + (test-given #'+) + (decreasing #'-) + (t #'+)) + ,index-holder ,increment-holder))) + (loopy--pre-conditions (funcall ,test + ,index-holder + ,end-holder))))))) ;;;;;; Array Ref (loopy--defiteration array-ref @@ -780,42 +810,37 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', - `:below' and `:above' name an exclusive ending index. `:downto' and `:downfrom' make the index decrease instead of increase." - :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below) + :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below :test) :instructions - (let ((value-holder (gensym "array-ref-")) - (index-holder (or (plist-get opts :index) - (gensym "array-ref-index-"))) - (end-holder (gensym "array-ref-end-")) - (increment-holder (gensym "array-ref-increment-"))) - - (loopy--plist-bind ( :start key-start :end key-end :by (by 1) - :decreasing decreasing :inclusive inclusive) - - (loopy--find-start-by-end-dir-vals opts) - - `((loopy--iteration-vars (,value-holder ,val)) - - (loopy--iteration-vars (,end-holder ,(or key-end - (if decreasing - -1 - `(length ,value-holder))))) - - (loopy--iteration-vars (,index-holder ,(or key-start - (if decreasing - `(1- (length ,value-holder)) - 0)))) - - ,@(loopy--destructure-for-generalized-command - var `(aref ,value-holder ,index-holder)) - - ,@(loopy--generate-inc-idx-instructions - index-holder increment-holder by decreasing) - - (loopy--pre-conditions (,(if (or (null key-end) - (not inclusive)) - (if decreasing #'> #'<) - (if decreasing #'>= #'<=)) - ,index-holder ,end-holder)))))) + (loopy--plist-bind ( :start key-start :end key-end :by (by 1) + :decreasing decreasing + :test-given test-given :test test) + (loopy--find-start-by-end-dir-vals opts) + (loopy--instr-let-const* ((value-holder val) + (end-holder (or key-end + (if decreasing + 0 + `(1- (length ,value-holder))))) + (increment-holder by) + (test test)) + loopy--iteration-vars + (loopy--instr-let-var* ((index-holder (or key-start + (if decreasing + `(1- (length ,value-holder)) + 0)) + (plist-get opts :index))) + loopy--iteration-vars + `(,@(loopy--destructure-for-generalized-command + var `(aref ,value-holder ,index-holder)) + (loopy--latter-body + (setq ,index-holder (,(cond + (test-given #'+) + (decreasing #'-) + (t #'+)) + ,index-holder ,increment-holder))) + (loopy--pre-conditions (funcall ,test + ,index-holder + ,end-holder))))))) ;;;;;; Cons (loopy--defiteration cons @@ -825,23 +850,19 @@ VAR is a variable name. VAL is a cons cell value. Keyword BY is a function by which to update VAR (default `cdr')." :keywords (:by) :instructions - (if (or (loopy--with-bound-p var) - (sequencep var)) - (let ((value-holder (gensym "cons-"))) - `((loopy--iteration-vars (,value-holder ,val)) - ,@(loopy--destructure-for-iteration-command var value-holder) - (loopy--latter-body - (setq ,value-holder ,(loopy--apply-function (or by (quote #'cdr)) - value-holder))) - ;; NOTE: The benchmarks show that `consp' is faster than no `consp', + (loopy--instr-let-const* ((cons-by (or by (quote #'cdr)))) + loopy--iteration-vars + (let ((indirect (or (loopy--with-bound-p var) + (sequencep var)))) + (loopy--instr-let-var* ((cons-value val (unless indirect var))) + loopy--iteration-vars + `(;; NOTE: The benchmarks show that `consp' is faster than no `consp', ;; at least for some commands. - (loopy--pre-conditions (consp ,value-holder)))) - `((loopy--iteration-vars (,var ,val)) - ;; NOTE: The benchmarks show that `consp' is faster than no `consp', - ;; at least for some commands. - (loopy--pre-conditions (consp ,var)) - (loopy--latter-body - (setq ,var ,(loopy--apply-function (or by (quote #'cdr)) var)))))) + (loopy--pre-conditions (consp ,cons-value)) + ,@(when indirect + (loopy--destructure-for-iteration-command var cons-value)) + (loopy--latter-body + (setq ,cons-value (funcall ,cons-by ,cons-value)))))))) ;;;;;; Iter (loopy--defiteration iter @@ -899,10 +920,12 @@ closed after the loop completes." ;;;;;; List +;; TODO: Make this a normal function. (defmacro loopy--distribute-list-elements (&rest lists) "Distribute the elements of LISTS into a list of lists. For example, (1 2) and (3 4) would give ((1 3) (1 4) (2 3) (2 4))." + (let ((vars (cl-loop for _ in lists collect (gensym "list-var-"))) (reverse-order (reverse lists))) @@ -928,22 +951,20 @@ using the function `loopy--distribute-list-elements'." :other-vals t :keywords (:by) :instructions - (let* ((by-func (or (plist-get opts :by) - ;; Need to quote as if passed in to macro - (quote #'cdr)))) - (let ((value-holder (gensym "list-"))) - `((loopy--iteration-vars - (,value-holder ,(if (null other-vals) - val - `(loopy--distribute-list-elements - ,val ,@other-vals)))) - (loopy--latter-body - (setq ,value-holder ,(loopy--apply-function by-func value-holder))) - ;; NOTE: The benchmarks show that `consp' is faster than no `consp', + (loopy--instr-let-const* ((list-func (or (plist-get opts :by) + (quote #'cdr)))) + loopy--iteration-vars + (loopy--instr-let-var* ((list-val (if (null other-vals) + val + `(loopy--distribute-list-elements + ,val ,@other-vals)))) + loopy--iteration-vars + `(;; NOTE: The benchmarks show that `consp' is faster than no `consp', ;; at least for some commands. - (loopy--pre-conditions (consp ,value-holder)) - ,@(loopy--destructure-for-iteration-command - var `(car ,value-holder)))))) + (loopy--pre-conditions (consp ,list-val)) + ,@(loopy--destructure-for-iteration-command var `(car ,list-val)) + (loopy--latter-body + (setq ,list-val (funcall ,list-func ,list-val))))))) ;;;;;; List Ref (loopy--defiteration list-ref @@ -952,18 +973,20 @@ using the function `loopy--distribute-list-elements'." BY is the function to use to move through the list (default `cdr')." :keywords (:by) :instructions - (let ((val-holder (gensym "list-ref")) - ;; Need to quote as if passed in to macro - (by-func (or by (quote #'cdr)))) - `((loopy--iteration-vars (,val-holder ,val)) - ,@(loopy--destructure-for-generalized-command var `(car ,val-holder)) - (loopy--latter-body - (setq ,val-holder ,(loopy--apply-function by-func val-holder))) - ;; NOTE: The benchmarks show that `consp' is faster than no `consp', - ;; at least for some commands. - (loopy--pre-conditions (consp ,val-holder))))) + (loopy--instr-let-const* ((list-func (or by (quote #'cdr)))) + loopy--iteration-vars + (loopy--instr-let-var* ((list-val val)) + loopy--iteration-vars + `(;; NOTE: The benchmarks show that `consp' is faster than no `consp', + ;; at least for some commands. + (loopy--pre-conditions (consp ,list-val)) + ,@(loopy--destructure-for-generalized-command var `(car ,list-val)) + (loopy--latter-body + (setq ,list-val (funcall ,list-func ,list-val))))))) ;;;;;; Map +;; TODO: Instead of using `seq-uniq' at the start, +;; check as we go. (cl-defun loopy--parse-map-command ((name var val &key (unique t))) "Parse the `map' loop command. @@ -1009,7 +1032,7 @@ map's keys. Duplicate keys are ignored." ;;;;;; Numbers (loopy--defiteration numbers - "Parse the `numbers' command as (nums VAR &key KEYS). + "Parse the `numbers' command as (numbers VAR &key KEYS). - START is the starting index, if given. - END is the ending index (inclusive), if given. @@ -1032,104 +1055,106 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', :required-vals 0 :other-vals (0 1 2 3) :instructions - ;; TODO: `cl-destructuring-bind' signals error here. Why? - (seq-let (explicit-start explicit-end explicit-by) + ;; TODO: Use `loopy--instr-let-const*' to simplify, after the non-keyword arguments + ;; have been removed. + (cl-destructuring-bind (&optional explicit-start explicit-end explicit-by) other-vals (loopy--plist-bind ( :start key-start :end key-end :by key-by - :decreasing decreasing :inclusive inclusive - :dir-given dir-given :test test) + :decreasing decreasing :inclusive inclusive) (condition-case nil (loopy--find-start-by-end-dir-vals opts) (loopy-conflicting-command-arguments (signal 'loopy-conflicting-command-arguments (list cmd)))) - ;; Warn that the non-keyword arguments are deprecated. - (when (or explicit-start - explicit-end - explicit-by) - (warn "`loopy': `numbers': The non-keyword arguments are deprecated. -Instead, use the keyword arguments, possibly including the new `:test' argument. -Warning trigger: %s" cmd)) - - ;; Check that nothing conflicts. - (when (or (and explicit-start key-start) - (and explicit-end key-end) - (and explicit-by key-by)) - (signal 'loopy-conflicting-command-arguments (list cmd))) - - (let* ((end (or explicit-end key-end)) - (end-val-holder (gensym "nums-end")) - (start (or explicit-start key-start 0)) - (by (or explicit-by key-by 1)) - (number-by (numberp by)) - (number-by-and-end (and number-by (numberp end))) - (increment-val-holder (gensym "nums-increment")) - (var-val-holder (if (loopy--with-bound-p var) - (gensym "num-test-var") - var))) - - (when (and test (or dir-given (null end))) + ;; We have to do this here because of how we treat the explicit arguments. + ;; Once they are removed, we can move this into the above + ;; `loopy--plist-bind'. + (let ((key-test (plist-get opts :test))) + + ;; Warn that the non-keyword arguments are deprecated. + (when (or explicit-start + explicit-end + explicit-by) + (warn "`loopy': `numbers': The non-keyword arguments are deprecated. + Instead, use the keyword arguments, possibly including the new `:test' argument. + Warning trigger: %s" cmd)) + + ;; Check that nothing conflicts. + (when (or (and explicit-start key-start) + (and explicit-end key-end) + (and explicit-by key-by)) (signal 'loopy-conflicting-command-arguments (list cmd))) - `((loopy--iteration-vars (,var-val-holder ,start)) - ,(when (loopy--with-bound-p var) - `(loopy--main-body (setq ,var ,var-val-holder))) + (let* ((end (or explicit-end key-end)) + (end-val-holder (gensym "nums-end")) + (start (or explicit-start key-start 0)) + (by (or explicit-by key-by 1)) + (number-by (numberp by)) + (number-by-and-end (and number-by (numberp end))) + (increment-val-holder (gensym "nums-increment")) + (var-val-holder (if (loopy--with-bound-p var) + (gensym "num-test-var") + var))) + + `((loopy--iteration-vars (,var-val-holder ,start)) + ,(when (loopy--with-bound-p var) + `(loopy--main-body (setq ,var ,var-val-holder))) + + (loopy--latter-body + (setq ,var-val-holder + ,(let ((inc (if number-by + by + increment-val-holder))) + (cond (explicit-by `(+ ,var-val-holder ,inc)) + (key-test `(+ ,var-val-holder ,inc)) + (key-by `(,(if decreasing #'- #'+) + ,var-val-holder ,inc)) + (decreasing `(1- ,var-val-holder)) + (t `(1+ ,var-val-holder)))))) - (loopy--latter-body - (setq ,var-val-holder - ,(let ((inc (if number-by - by - increment-val-holder))) - (cond (explicit-by `(+ ,var-val-holder ,inc)) - (test `(+ ,var-val-holder ,inc)) - (key-by `(,(if decreasing #'- #'+) - ,var-val-holder ,inc)) - (decreasing `(1- ,var-val-holder)) - (t `(1+ ,var-val-holder)))))) - - ,@(cond - (number-by-and-end - `((loopy--pre-conditions ,(loopy--apply-function - (cond - (test test) - (explicit-by - (if (cl-plusp by) '#'<= '#'>=)) - (inclusive - (if decreasing '#'>= '#'<=)) - (t (if decreasing '#'> '#'<))) - var-val-holder end)))) - ;; `end' is not a number. `by' might be a number. - (end - `((loopy--iteration-vars (,end-val-holder ,end)) - ,(when (and (not number-by) - (or key-by explicit-by)) - `(loopy--iteration-vars (,increment-val-holder ,by))) - ,@(cond - (test `((loopy--pre-conditions - ,(loopy--apply-function - test var-val-holder end-val-holder)))) - ((not explicit-by) ; `key-by' or default - `((loopy--pre-conditions ,(loopy--apply-function - (if inclusive - (if decreasing '#'>= '#'<=) - (if decreasing '#'> '#'<)) - var-val-holder end-val-holder)))) - (number-by - `((loopy--pre-conditions ,(loopy--apply-function - (if (cl-plusp by) '#'<= '#'>=) - var-val-holder end-val-holder)))) - ;; Ambiguous, so need to check - (t - (let ((fn (gensym "nums-fn"))) - `((loopy--iteration-vars - (,fn (if (cl-plusp ,increment-val-holder) #'<= #'>=))) - (loopy--pre-conditions (funcall ,fn ,var-val-holder - ,end-val-holder)))))))) + ,@(cond + (number-by-and-end + `((loopy--pre-conditions (funcall + ,(cond + (key-test key-test) + (explicit-by + (if (cl-plusp by) '#'<= '#'>=)) + (inclusive + (if decreasing '#'>= '#'<=)) + (t (if decreasing '#'> '#'<))) + ,var-val-holder ,end)))) + ;; `end' is not a number. `by' might be a number. + (end + `((loopy--iteration-vars (,end-val-holder ,end)) + ,(when (and (not number-by) + (or key-by explicit-by)) + `(loopy--iteration-vars (,increment-val-holder ,by))) + ,@(cond + (key-test `((loopy--pre-conditions + ,(loopy--apply-function + key-test var-val-holder end-val-holder)))) + ((not explicit-by) ; `key-by' or default + `((loopy--pre-conditions ,(loopy--apply-function + (if inclusive + (if decreasing '#'>= '#'<=) + (if decreasing '#'> '#'<)) + var-val-holder end-val-holder)))) + (number-by + `((loopy--pre-conditions ,(loopy--apply-function + (if (cl-plusp by) '#'<= '#'>=) + var-val-holder end-val-holder)))) + ;; Ambiguous, so need to check + (t + (let ((fn (gensym "nums-fn"))) + `((loopy--iteration-vars + (,fn (if (cl-plusp ,increment-val-holder) #'<= #'>=))) + (loopy--pre-conditions (funcall ,fn ,var-val-holder + ,end-val-holder)))))))) - ;; No `end'. We gave a non-number as `by', so we need a holding var. - ((and by (not number-by)) - `((loopy--iteration-vars (,increment-val-holder ,by)))))))))) + ;; No `end'. We gave a non-number as `by', so we need a holding var. + ((and by (not number-by)) + `((loopy--iteration-vars (,increment-val-holder ,by))))))))))) ;;;;;; Numbers Up @@ -1173,10 +1198,10 @@ This is for decreasing indices. :downto ,(cl-first other-vals) :by ,(or by (cl-second other-vals)))))) -;;;;;; Repeat +;;;;;; Cycle/repeat (cl-defun loopy--parse-cycle-command ((name var-or-count &optional (count nil count-given))) - "Parse the `repeat' loop command as (repeat [VAR] VAL). + "Parse the `cycle' loop command as (repeat [VAR] VAL). VAR-OR-COUNT is a variable name or an integer. Optional COUNT is an integer, to be used if a variable name is provided. @@ -1199,6 +1224,7 @@ NAME is the name of the command." (loopy--pre-conditions (< ,value-holder ,num-steps))))) ;;;;;; Seq +;; TODO: Turn this into a function. (defmacro loopy--distribute-sequence-elements (&rest sequences) "Distribute the elements of SEQUENCES into a vector of lists. @@ -1223,7 +1249,8 @@ For example, [1 2] and (3 4) give [(1 3) (1 4) (2 3) (2 4)]." "Parse the `seq' command as (seq VAR EXPR [EXPRS] &key KEYS). KEYS is one or several of `:index', `:by', `:from', `:downfrom', -`:upfrom', `:to', `:downto', `:upto', `:above', or `:below'. +`:upfrom', `:to', `:downto', `:upto', `:above', `:below', and +`:test'. - `:index' names a variable used to store the accessed index of the sequence. @@ -1236,75 +1263,88 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', If multiple sequence values are given, their elements are distributed using the function `loopy--distribute-sequence-elements'." - :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below) + :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below :test) :other-vals t :instructions - (let ((value-holder (gensym "seq-")) - (index-holder (or (plist-get opts :index) - (gensym "seq-index-"))) - (end-index-holder (gensym "seq-end-index-")) - (increment-holder (gensym "seq-increment-"))) - - (loopy--plist-bind ( :start starting-index :end ending-index :by (by 1) - :decreasing going-down :inclusive inclusive) - - (loopy--find-start-by-end-dir-vals opts) - - ;; Optimize for the case of traversing from start to end, as done in - ;; `cl-loop'. Currently, all other case use `elt'. - (let ((optimize (and (not going-down) - (and (numberp by) (= 1 by)) - (or (and (numberp starting-index) - (zerop starting-index)) - (null starting-index))))) - - `((loopy--iteration-vars - (,value-holder ,(if other-vals - `(loopy--distribute-sequence-elements - ,val ,@other-vals) - val))) - - (loopy--iteration-vars - (,index-holder ,(cond - (starting-index) - (going-down `(1- (length ,value-holder))) - (t 0)))) - - (loopy--iteration-vars - (,end-index-holder ,(cond - (ending-index) - (going-down -1) - ;; Only calculate length when actually needed. - (optimize `(when (arrayp ,value-holder) - (length ,value-holder))) - (t `(length ,value-holder))))) - - ,@(loopy--generate-inc-idx-instructions - index-holder increment-holder by going-down) - - ,@(cond - (optimize - `(,@(loopy--destructure-for-iteration-command - var `(if (consp ,value-holder) - (pop ,value-holder) - (aref ,value-holder ,index-holder))) - (loopy--pre-conditions - (and ,value-holder - ,(if ending-index - `(,(if inclusive #'<= #'<) - ,index-holder ,end-index-holder) - `(or (consp ,value-holder) - (< ,index-holder ,end-index-holder))))))) - - (t - `(,@(loopy--destructure-for-iteration-command - var `(elt ,value-holder ,index-holder)) - (loopy--pre-conditions (,(if (or (null ending-index) - (not inclusive)) - (if going-down '> '<) - (if going-down '>= '<=)) - ,index-holder - ,end-index-holder)))))))))) + (loopy--plist-bind ( :start starting-index :end ending-index :by (by 1) + :decreasing going-down :test test :test-given test-given) + + (loopy--find-start-by-end-dir-vals opts) + + ;; If we are going up, then we can use `nthcdr' for lists instead of + ;; searching from the beginning each time with `elt'. + ;; + ;; It's a bit weird in that if we're going up, we want to know the starting + ;; index before we calculate the initial value, so that we can call `nthcdr' + ;; if needed. However, if we're going down, then the starting index is the + ;; 1 minus the length of the initial value, so we would like to have the + ;; initial value first. To compromise, we just use a maybe-variable holding + ;; the the declared start or zero, which we may or may not use in the + ;; expansion. Another option would be to duplicate most of the code and + ;; branch on that single condition, but the cost of the variable should be + ;; negligible. + (let ((optimize (and (not going-down) (not test-given)))) + (loopy--instr-let-const* ((by by) + (maybe-start (or starting-index 0)) + (test test)) + loopy--iteration-vars + (loopy--instr-let-var* ((temp-val (if other-vals + `(loopy--distribute-sequence-elements + ,val ,@other-vals) + val)) + (is-list (if optimize + `(consp ,temp-val) + nil)) + (seq-val (if optimize + `(if (and ,is-list + (> ,maybe-start 0)) + (nthcdr ,maybe-start ,temp-val) + ,temp-val) + temp-val)) + (seq-index (if optimize + maybe-start + (if starting-index + maybe-start + `(1- (length ,seq-val)))) + (plist-get opts :index))) + loopy--iteration-vars + (loopy--instr-let-const* ((end (cond + (ending-index) + (going-down 0) + ;; Only calculate length when actually needed. + (t `(unless ,is-list + (1- (length ,seq-val))))))) + loopy--iteration-vars + `((loopy--pre-conditions ,(if optimize + `(if ,is-list + (consp ,seq-val) + (funcall ,test ,seq-index ,end)) + `(funcall ,test ,seq-index ,end))) + ,@(loopy--destructure-for-iteration-command + var (if optimize + `(if ,is-list + (car ,seq-val) + (aref ,seq-val ,seq-index)) + `(elt ,seq-val ,seq-index))) + (loopy--latter-body + ,(cond + (test-given `(setq ,seq-index (+ ,seq-index ,by))) + (going-down `(setq ,seq-index (- ,seq-index ,by))) + ;; If the user intends to use the index, we need + ;; to make sure that we're always updating it. + ((plist-member opts :index) + `(progn + (when ,is-list + (setq ,seq-val (nthcdr ,by ,seq-val))) + (setq ,seq-index (,(if going-down #'- #'+) + ,seq-index ,by)))) + ;; Otherwise, we only have to update it + ;; when not using the list. + (t + `(if ,is-list + (setq ,seq-val (nthcdr ,by ,seq-val)) + (setq ,seq-index (,(if going-down #'- #'+) + ,seq-index ,by))))))))))))) ;;;;;; Seq Index (loopy--defiteration seq-index @@ -1320,39 +1360,33 @@ KEYS is one or several of `:by', `:from', `:downfrom', `:upfrom', `:downto' and `:downfrom' make the index decrease instead of increase." - :keywords (:by :from :downfrom :upfrom :to :downto :upto :above :below) + :keywords (:by :from :downfrom :upfrom :to :downto :upto :above :below :test) :instructions - (let ((value-holder (gensym "array-")) - (end-holder (gensym "array-end-")) - (index-holder (gensym "seq-index-index-")) - (increment-holder (gensym "array-increment")) - (with-bound (loopy--with-bound-p var))) - + (let ((with-bound (loopy--with-bound-p var))) (loopy--plist-bind ( :start key-start :end key-end :by (by 1) - :decreasing decreasing :inclusive inclusive) - + :decreasing decreasing :test key-test) (loopy--find-start-by-end-dir-vals opts) - - (unless with-bound (setq index-holder var)) - `(,@(when with-bound - `((loopy--main-body (setq ,var ,index-holder)) - (loopy--iteration-vars (,var nil)))) - (loopy--iteration-vars (,value-holder ,val)) - (loopy--iteration-vars (,end-holder ,(or key-end - (if decreasing - -1 - `(length ,value-holder))))) - (loopy--iteration-vars (,index-holder ,(or key-start - (if decreasing - `(1- (length ,value-holder)) - 0)))) - ,@(loopy--generate-inc-idx-instructions - index-holder increment-holder by decreasing) - (loopy--pre-conditions (,(if (or (null key-end) - (not inclusive)) - (if decreasing #'> #'<) - (if decreasing #'>= #'<=)) - ,index-holder ,end-holder)))))) + (loopy--instr-let-var* ((seq-index-val val) + (seq-index-index (cond (key-start) + (decreasing `(1- (length ,seq-index-val))) + (t 0)) + (unless with-bound var))) + loopy--iteration-vars + (loopy--instr-let-const* ((seq-index-index-end (or key-end + (if decreasing + 0 + `(1- (length ,seq-index-val))))) + (test key-test) + (by by)) + loopy--iteration-vars + `((loopy--pre-conditions (funcall ,test ,seq-index-index ,seq-index-index-end)) + ,@(when with-bound + `((loopy--iteration-vars (,var nil)) + (loopy--main-body (setq ,var ,seq-index-index)))) + (loopy--latter-body + ,(cond + (decreasing `(setq ,seq-index-index (- ,seq-index-index ,by))) + (t `(setq ,seq-index-index (+ ,seq-index-index ,by))))))))))) ;;;;;; Seq Ref (loopy--defiteration seq-ref @@ -1367,42 +1401,88 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', - `:from', `:downfrom', and `:upfrom' name the starting index - `:to', `:downto', and `:upto' name the ending index (inclusive) - `:below' and `:above' name an exclusive ending index. +- `:test' is the test function. `:downto' and `:downfrom' make the index decrease instead of increase." - :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below) + :keywords (:index :by :from :downfrom :upfrom :to :downto :upto :above :below :test) :instructions - (let ((value-holder (gensym "seq-ref-")) - (index-holder (or (plist-get opts :index) - (gensym "seq-ref-index-"))) - (end-index-holder (gensym "seq-ref-end-index-")) - (increment-holder (gensym "seq-ref-increment-"))) - - (loopy--plist-bind ( :start starting-index :end ending-index :by (by 1) - :decreasing going-down :inclusive inclusive) - - (loopy--find-start-by-end-dir-vals opts) - - `((loopy--iteration-vars (,value-holder ,val)) - (loopy--iteration-vars - (,index-holder ,(or starting-index (if going-down - `(1- (length ,value-holder)) - 0)))) - (loopy--iteration-vars - (,end-index-holder ,(or ending-index (if going-down - -1 - `(length ,value-holder))))) - - ,@(loopy--generate-inc-idx-instructions - index-holder increment-holder by going-down) - - ,@(loopy--destructure-for-generalized-command - var `(elt ,value-holder ,index-holder)) - (loopy--pre-conditions (,(if (or (null ending-index) - (not inclusive)) - (if going-down '> '<) - (if going-down '>= '<=)) - ,index-holder - ,end-index-holder)))))) + (loopy--plist-bind ( :start starting-index :end ending-index :by (by 1) + :decreasing going-down :test test :test-given test-given) + + (loopy--find-start-by-end-dir-vals opts) + + ;; If we are going up, then we can use `nthcdr' for lists instead of + ;; searching from the beginning each time with `elt'. + ;; + ;; It's a bit weird in that if we're going up, we want to know the starting + ;; index before we calculate the initial value, so that we can call `nthcdr' + ;; if needed. However, if we're going down, then the starting index is the + ;; 1 minus the length of the initial value, so we would like to have the + ;; initial value first. To compromise, we just use a maybe-variable holding + ;; the the declared start or zero, which we may or may not use in the + ;; expansion. Another option would be to duplicate most of the code and + ;; branch on that single condition, but the cost of the variable should be + ;; negligible. + (let ((optimize (and (not going-down) (not test-given)))) + (loopy--instr-let-const* ((by by) + (maybe-start (or starting-index 0)) + (test test)) + loopy--iteration-vars + (loopy--instr-let-var* ((temp-val val) + (is-list (if optimize + `(consp ,temp-val) + nil)) + (seq-val (if optimize + `(if (and ,is-list + (> ,maybe-start 0)) + (nthcdr ,maybe-start ,temp-val) + ,temp-val) + temp-val)) + (seq-index (if optimize + maybe-start + (if starting-index + maybe-start + `(1- (length ,seq-val)))) + (plist-get opts :index))) + loopy--iteration-vars + (loopy--instr-let-const* ((end (cond + (ending-index) + (going-down 0) + ;; Only calculate length when actually needed. + (t `(unless ,is-list + (1- (length ,seq-val))))))) + loopy--iteration-vars + `((loopy--pre-conditions ,(if optimize + `(if ,is-list + (consp ,seq-val) + (funcall ,test ,seq-index ,end)) + `(funcall ,test ,seq-index ,end))) + ;; NOTE: Yes, we can use `if' with `setf', apparently. + ,@(loopy--destructure-for-generalized-command + var (if optimize + `(if ,is-list + (car ,seq-val) + (aref ,seq-val ,seq-index)) + `(elt ,seq-val ,seq-index))) + (loopy--latter-body + ,(cond + (test-given `(setq ,seq-index (+ ,seq-index ,by))) + (going-down `(setq ,seq-index (- ,seq-index ,by))) + ;; If the user intends to use the index, we need + ;; to make sure that we're always updating it. + ((plist-member opts :index) + `(progn + (when ,is-list + (setq ,seq-val (nthcdr ,by ,seq-val))) + (setq ,seq-index (,(if going-down #'- #'+) + ,seq-index ,by)))) + ;; Otherwise, we only have to update it + ;; when not using the list. + (t + `(if ,is-list + (setq ,seq-val (nthcdr ,by ,seq-val)) + (setq ,seq-index (,(if going-down #'- #'+) + ,seq-index ,by))))))))))))) ;;;;; Accumulation ;;;;;; Compatibility @@ -1449,7 +1529,7 @@ like the `:result-type' keyword argument of commands like (if-let ((existing-description (alist-get key loopy--accumulation-variable-info nil nil #'equal))) - (seq-let (existing-category existing-command) + (cl-destructuring-bind (existing-category existing-command) existing-description (unless (eq category existing-category) (signal 'loopy-incompatible-accumulations @@ -1521,8 +1601,8 @@ more efficient than repeatedly traversing the list." ;; for longer lists. (let ((last-link (loopy--get-accumulation-list-end-var loopy--loop-name var))) `((loopy--accumulation-vars (,last-link nil)) - ,@(loopy--instr-let2* ((test-val test) - (key-val key)) + ,@(loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body ,(cl-once-only ((adjoin-value val)) @@ -1590,8 +1670,8 @@ more efficient than repeatedly traversing the list." ;; lists, but much faster for longer lists. (let ((last-link (loopy--get-accumulation-list-end-var loopy--loop-name var))) `((loopy--accumulation-vars (,last-link nil)) - ,@(loopy--instr-let2* ((test-val test) - (key-val key)) + ,@(loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body ,(cl-with-gensyms (new-items) @@ -1868,6 +1948,9 @@ Warning trigger: %s" ;;;;;;; Accumulate +;; TODO: Should the function be evaluated only once for `accumulate'? +;; It would produce faster code, by as an accumulation argument/value, +;; should it be able to change during the loop? (loopy--defaccumulation accumulate "Parse the `accumulate command' as (accumulate VAR VAL FUNC &key init)." :keywords (init) @@ -1895,8 +1978,8 @@ Warning trigger: %s" ('end end)) (loopy--get-accum-counts loop var 'adjoin) (let* ((at-start-instrs - (loopy--instr-let2* ((test-val test) - (key-val key)) + (loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body ,(cl-once-only ((adjoin-value val)) @@ -1958,8 +2041,8 @@ RESULT-TYPE can be used to `cl-coerce' the return value." `((loopy--accumulation-vars (,var nil)) ,@(cond ((member pos '(start beginning 'start 'beginning)) - (loopy--instr-let2* ((test-val test) - (key-val key)) + (loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body ,(cl-once-only ((adjoin-value val)) @@ -2040,8 +2123,8 @@ RESULT-TYPE can be used to `cl-coerce' the return value." `((loopy--accumulation-vars (,var nil)) (loopy--main-body (loopy--optimized-accum '( :loop ,loopy--loop-name - :var ,var :val ,val - :cmd ,cmd :name ,name :at ,pos))))) + :var ,var :val ,val + :cmd ,cmd :name ,name :at ,pos))))) (loopy--check-accumulation-compatibility loopy--loop-name var 'list cmd) `((loopy--accumulation-vars (,var nil)) ,@(cond @@ -2066,7 +2149,7 @@ RESULT-TYPE can be used to `cl-coerce' the return value." `((loopy--accumulation-vars (,var nil)) (loopy--main-body (loopy--optimized-accum '( :loop ,loopy--loop-name :var ,var :val ,val - :cmd ,cmd :name ,name :at ,pos))) + :cmd ,cmd :name ,name :at ,pos))) (loopy--implicit-return ,var)))) ;;;;;;; Collect @@ -2437,8 +2520,8 @@ This function is used by `loopy--expand-optimized-accum'." :key key :test test) plist (cl-flet ((make-at-start (reverse) - (loopy--instr-let2* ((key-val key) - (test-val test)) + (loopy--instr-let-const* ((key-val key) + (test-val test)) loopy--accumulation-vars `((loopy--main-body (setq ,var @@ -2501,8 +2584,8 @@ This function is used by `loopy--expand-optimized-accum'." `((loopy--accumulation-vars (,var nil)) ,@(cond ((member pos '(start beginning 'start 'beginning)) - (loopy--instr-let2* ((test-val test) - (key-val key)) + (loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body (setq ,var (nconc (cl-delete-if @@ -2556,6 +2639,9 @@ This function is used by `loopy--expand-optimized-accum'." (loopy--parse-collect-command `(collect ,@(cdr arg) :at start))) ;;;;;;; Reduce +;; TODO: Should the function be evaluated only once for `reduce'? +;; It would produce faster code, by as an accumulation argument/value, +;; should it be able to change during the loop? (loopy--defaccumulation reduce "Parse the `reduce' command as (reduce VAR VAL FUNC &key init). @@ -2626,8 +2712,8 @@ This function is used by `loopy--expand-optimized-accum'." :key key :test test) plist (cl-flet ((make-at-start (reverse) - (loopy--instr-let2* ((key-val key) - (test-val test)) + (loopy--instr-let-const* ((key-val key) + (test-val test)) loopy--accumulation-vars `((loopy--main-body (setq ,var @@ -2686,8 +2772,8 @@ This function is used by `loopy--expand-optimized-accum'." `((loopy--accumulation-vars (,var nil)) ,@(cond ((member pos '(start beginning 'start 'beginning)) - (loopy--instr-let2* ((test-val test) - (key-val key)) + (loopy--instr-let-const* ((test-val test) + (key-val key)) loopy--accumulation-vars `((loopy--main-body (setq ,var (nconc (cl-delete-if diff --git a/loopy-misc.el b/loopy-misc.el index 6965ac08..010d3f45 100644 --- a/loopy-misc.el +++ b/loopy-misc.el @@ -1062,6 +1062,9 @@ If not, then it is possible that FORM is a variable." (eq (car form-or-symbol) 'function) (eq (car form-or-symbol) 'cl-function)))) +;; TODO: Byte optimization for `funcall' with a quoted argument +;; should expand to (FUNC ARGS...), so we shouldn't need +;; this function. (defun loopy--apply-function (func &rest args) "Return an expansion to appropriately apply FUNC to ARGS. @@ -1149,42 +1152,87 @@ KEY transforms those elements and ELEMENT." ;;;; Variable binding for instructions -(defmacro loopy--instr-let2 (place sym exp &rest body) +(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))) + (let ((res (macroexp-progn body))) + (pcase-dolist ((or `(,var ,val) + `(,var ,val ,name)) + (reverse bindings)) + (setq res `(loopy--instr-let-var ,place ,var ,val ,name ,res))) + res)) + +(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. -See also `macroexp-let2'." - (declare (indent 3) - (debug (sexp sexp form body))) +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")) - (val-holder (gensym (format "new-%s" sym)))) + (expsym (gensym "exp"))) `(let* ((,expsym ,exp) - (,sym (if (macroexp-const-p ,expsym) + (,sym (if (or (null ,expsym) + (macroexp-const-p ,expsym)) ,expsym - (quote ,val-holder))) + (or ,name + (gensym (symbol-name (quote ,sym)))))) (,bodysym (progn ,@body))) (if (eq ,sym ,expsym) ,bodysym (cons (list (quote ,place) - (list (quote ,val-holder) ,expsym)) + (list ,sym ,expsym)) ,bodysym))))) -(defmacro loopy--instr-let2* (bindings place &rest body) - "A multi-binding version of `loopy--instr-let2'. +(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)) + (debug ((&rest (gate symbol form &optional form)) symbol body))) - (cl-loop with res = (macroexp-progn body) - for (var val) in (reverse bindings) - do (setq res - `(loopy--instr-let2 ,place ,var ,val - ,res)) - finally return res)) + (let ((res (macroexp-progn body))) + (pcase-dolist ((or `(,var ,val) + `(,var ,val ,name)) + (reverse bindings)) + (setq res `(loopy--instr-let-const ,place ,var ,val ,name ,res))) + res)) + (provide 'loopy-misc) ;;; loopy-misc.el ends here diff --git a/tests/tests.el b/tests/tests.el index f1376971..dadbc596 100644 --- a/tests/tests.el +++ b/tests/tests.el @@ -1069,6 +1069,16 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((collect . collecting) (array . arraying))) +(loopy-deftest array-keywords-:downfrom + :result '(10 8 6 4 2 0) + :body ((array i (vector 0 1 2 3 4 5 6 7 8 9 10) + :downfrom 10 :by 2) + (collect i)) + :loopy t + :iter-keyword (collect array) + :iter-bare ((collect . collecting) + (array . arraying))) + (loopy-deftest array-keywords-:from-:downto-:by :result '(8 6 4 2) @@ -1127,6 +1137,35 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((collect . collecting) (array . arraying))) +(loopy-deftest array-:test + :result '(8 6 4 2) + :body ((with (start 8) + (end 2) + (step -2)) + (array i [0 1 2 3 4 5 6 7 8 9 10] + :from start :to end :by step + :test #'>=) + (collect i)) + :loopy t + :iter-keyword (array collect) + :iter-bare ((array . arraying) + (collect . collecting))) + +(loopy-deftest array-:test-just-once + :result '(2 4 6 8) + :body ((with (times 0)) + (array i [0 1 2 3 4 5 6 7 8 9 10] + :from 2 :to 8 :by 2 + :test (progn + (cl-assert (= times 0)) + (cl-incf times) + #'<=)) + (collect i)) + :loopy t + :iter-keyword (array collect) + :iter-bare ((array . arraying) + (collect . collecting))) + ;;;;; Array Ref (loopy-deftest array-ref :result "aaa" @@ -1262,6 +1301,39 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((array-ref . arraying-ref) (do . ignore))) +(loopy-deftest array-ref-:test + :result [0 1 22 3 22 5 22 7 22 9 10] + :body ((with (start 8) + (end 2) + (step -2) + (arr (vector 0 1 2 3 4 5 6 7 8 9 10))) + (array-ref i arr + :from start :to end :by step + :test #'>=) + (do (setf i 22)) + (finally-return arr)) + :loopy t + :iter-keyword (array-ref do) + :iter-bare ((array-ref . arraying-ref) + (do . ignore))) + +(loopy-deftest array-ref-:test-just-once + :result [0 1 22 3 22 5 22 7 22 9 10] + :body ((with (times 0) + (arr (vector 0 1 2 3 4 5 6 7 8 9 10))) + (array-ref i arr + :from 2 :to 8 :by 2 + :test (progn + (cl-assert (= times 0)) + (cl-incf times) + #'<=)) + (do (setf i 22)) + (finally-return arr)) + :loopy t + :iter-keyword (array-ref do) + :iter-bare ((array-ref . arraying-ref) + (do . ignore))) + ;;;;; Cons (loopy-deftest cons :result '((1 2 3 4) (2 3 4) (3 4) (4)) @@ -1293,6 +1365,22 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((cons . consing) (collect . collecting))) +(loopy-deftest cons-:by-once-only + :doc "The function should be evaluated only once." + :result '((1 2 3 4) (3 4)) + :body ((with (times 0)) + (cons x '(1 2 3 4) :by (progn + (if (> times 0) + (error "Evaluated more than once") + (cl-incf times) + #'cddr))) + (collect coll x) + (finally-return coll)) + :loopy t + :iter-keyword (cons collect) + :iter-bare ((cons . consing) + (collect . collecting))) + (loopy-deftest cons-destr :doc "Check that `cons' implements destructuring, not destructuring itself." :result '((1 (2 3 4)) (2 (3 4)) (3 (4)) (4 nil)) @@ -1449,6 +1537,22 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((list . listing) (collect . collecting))) +(loopy-deftest list-:by-once-only + :doc "The function should be evaluated only once." + :result '(1 3) + :body ((with (times 0)) + (list x '(1 2 3 4) :by (progn + (if (> times 0) + (error "Evaluated more than once") + (cl-incf times) + #'cddr))) + (collect coll x) + (finally-return coll)) + :loopy t + :iter-keyword (list collect) + :iter-bare ((list . listing) + (collect . collecting))) + (loopy-deftest list-destructuring :doc "Check that `list' implements destructuring, not destructuring itself." :result '(5 6) @@ -1491,7 +1595,7 @@ Using numbers directly will use less variables and more efficient code." ;;;;; List Ref (loopy-deftest list-ref :result '(7 7 7) - :body ((with (my-list '(1 2 3))) + :body ((with (my-list (list 1 2 3))) (list-ref i my-list) (do (setf i 7)) (finally-return my-list)) @@ -1503,15 +1607,15 @@ Using numbers directly will use less variables and more efficient code." (loopy-deftest list-ref-:by :result '(7 2 7) :multi-body t - :body [((with (my-list '(1 2 3))) + :body [((with (my-list (list 1 2 3))) (list-ref i my-list :by #'cddr) (do (setf i 7)) (finally-return my-list)) - ((with (my-list '(1 2 3))) + ((with (my-list (list 1 2 3))) (list-ref i my-list :by (lambda (x) (cddr x))) (do (setf i 7)) (finally-return my-list)) - ((with (my-list '(1 2 3)) (f (lambda (x) (cddr x)))) + ((with (my-list (list 1 2 3)) (f (lambda (x) (cddr x)))) (list-ref i my-list :by f) (do (setf i 7)) (finally-return my-list))] @@ -1520,10 +1624,28 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((list-ref . listing-ref) (do . ignore))) +(loopy-deftest list-ref-:by-once-only + :doc "The function should be evaluated only once." + :result '(7 2 7) + :body ((with (my-list (list 1 2 3)) + (f (lambda (x) (cddr x))) + (times 0)) + (list-ref i my-list :by (progn + (if (> times 0) + (error "Evaluated more than once") + (cl-incf times) + #'cddr))) + (do (setf i 7)) + (finally-return my-list)) + :loopy t + :iter-keyword (list-ref do) + :iter-bare ((list-ref . listing-ref) + (do . ignore))) + (loopy-deftest list-ref-destructuring :doc "Check that `list-ref' implements destructuring, not destructuring itself." :result '((7 8 9) (7 8 9)) - :body ((with (my-list '((1 2 3) (4 5 6)))) + :body ((with (my-list (list (list 1 2 3) (list 4 5 6)))) (list-ref (i j k) my-list) (do (setf i 7) (setf j 8) @@ -2089,58 +2211,78 @@ Using numbers directly will use less variables and more efficient code." :iter-keyword (seq) :iter-bare ((seq . sequencing))) -(loopy-deftest seq-opt-type-explicit-movement - :doc "Use type-specific movement when -- step is 1 -- start is 0 -- going up -Othe cases use `elt'." - :result '(1 2 3 4 5) +(loopy-deftest seq-:by + :result '(0 2 4 6 8 10) :multi-body t - :body [((sequence i '(1 2 3 4 5) :from 0 :by 1) (collect i)) - ((sequence i [1 2 3 4 5] :from 0 :by 1) (collect i))] + :body [((sequence i (list 0 1 2 3 4 5 6 7 8 9 10) :by 2) + (collect i)) + ((sequence i [0 1 2 3 4 5 6 7 8 9 10] :by 2) + (collect i))] :loopy t :iter-keyword (sequence collect) :iter-bare ((sequence . sequencing) (collect . collecting))) -(loopy-deftest seq-vars-literal-:by - :doc "Literal `:by' should be used directly, even when not 1." - :result '(2 4 6 8 10) - :body (loopy (with (start 2) (arr (cl-coerce (number-sequence 0 10) 'vector))) - (sequence i arr :from start :by 2) - (collect i)) - :loopy t - :iter-keyword (sequence collect) - :iter-bare ((sequence . sequencing) - (collect . collecting))) +(loopy-deftest seq-:by-just-once + :doc "`:by' should only be evaluated once." + :result '(1 3 5) + :multi-body t + :body [((with (times 0)) + (sequence i (vector 1 2 3 4 5 6) :by (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i)) -(loopy-deftest seq-vars-literal-:by-variable-:end - :result '(2 4 6 8) - :body (loopy (with (start 2) (end 8) - (arr (cl-coerce (number-sequence 0 10) 'vector))) - (sequence i arr :from start :to end :by 2) - (collect i)) + ((with (times 0)) + (sequence i (list 1 2 3 4 5 6) :by (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i))] :loopy t :iter-keyword (sequence collect) :iter-bare ((sequence . sequencing) (collect . collecting))) -(loopy-deftest seq-vars-variable-:by-variable-:end - :result '(2 4 6 8) - :body (loopy (with (start 2) (end 8) (by 2) - (arr (cl-coerce (number-sequence 0 10) 'vector))) - (sequence i arr :from start :to end :by by) - (collect i)) - :loopy t - :iter-keyword (sequence collect) - :iter-bare ((sequence . sequencing) - (collect . collecting))) +(loopy-deftest seq-end-just-once + :doc "`:by' should only be evaluated once." + :result '(0 1 2) + :multi-body t + :body [((with (times 0)) + (sequence i (vector 0 1 2 3 4 5 6) :to (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i)) -(loopy-deftest seq-:by - :result '(0 2 4 6 8 10) - :body ((sequence i [0 1 2 3 4 5 6 7 8 9 10] :by 2) - (collect i)) + ((with (times 0)) + (sequence i (vector 0 1 2 3 4 5 6) :upto (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i)) + + ((with (times 0)) + (sequence i (vector 2 1 0) :downto (progn + (cl-assert (= times 0)) + (cl-incf times) + 0)) + (collect i)) + + ((with (times 0)) + (sequence i (vector 0 1 2 3 4 5 6) :below (progn + (cl-assert (= times 0)) + (cl-incf times) + 3)) + (collect i)) + + (loopy (with (times 0)) + (sequence i (vector 2 1 0) :above (progn + (cl-assert (= times 0)) + (cl-incf times) + -1)) + (collect i))] :loopy t :iter-keyword (sequence collect) :iter-bare ((sequence . sequencing) @@ -2148,8 +2290,11 @@ Othe cases use `elt'." (loopy-deftest seq-:index :result '((0 . 4) (1 . 3) (2 . 2) (3 . 1) (4 . 0)) - :body ((sequence i [4 3 2 1 0] :index cat) - (collect (cons cat i))) + :multi-body t + :body [((sequence i [4 3 2 1 0] :index cat) + (collect (cons cat i))) + ((sequence i (list 4 3 2 1 0) :index cat) + (collect (cons cat i)))] :loopy t :iter-keyword (sequence collect) :iter-bare ((sequence . sequencing) @@ -2210,6 +2355,24 @@ Othe cases use `elt'." :iter-bare ((sequence . sequencing) (collect . collecting))) +(loopy-deftest seq-:downfrom + :result '(5 4 3 2 1 0) + :body ((sequence i [0 1 2 3 4 5] :downfrom 5) + (collect i)) + :loopy t + :iter-keyword (sequence collect) + :iter-bare ((sequence . sequencing) + (collect . collecting))) + +(loopy-deftest seq-:upfrom + :result '(2 3 4 5) + :body ((sequence i [0 1 2 3 4 5] :upfrom 2) + (collect i)) + :loopy t + :iter-keyword (sequence collect) + :iter-bare ((sequence . sequencing) + (collect . collecting))) + (loopy-deftest seq-multi-seq :result '((1 3) (1 4) (2 3) (2 4)) :body ((sequence i [1 2] '(3 4)) @@ -2228,6 +2391,53 @@ Othe cases use `elt'." :iter-bare ((sequence . sequencing) (collect . collecting))) +(loopy-deftest sequence-:test + :result '(8 6 4 2) + :multi-body t + :body [((with (start 8) + (end 2) + (step -2)) + (sequence i [0 1 2 3 4 5 6 7 8 9 10] + :from start :to end :by step + :test #'>=) + (collect i)) + ((with (start 8) + (end 2) + (step -2)) + (sequence i '(0 1 2 3 4 5 6 7 8 9 10) + :from start :to end :by step + :test #'>=) + (collect i))] + :loopy t + :iter-keyword (sequence collect) + :iter-bare ((sequence . sequencing) + (collect . collecting))) + +(loopy-deftest sequence-:test-just-once + :result '(2 4 6 8) + :multi-body t + :body [((with (times 0)) + (sequence i [0 1 2 3 4 5 6 7 8 9 10] + :from 2 :to 8 :by 2 + :test (progn + (cl-assert (= times 0)) + (cl-incf times) + #'<=)) + (collect i)) + + ((with (times 0)) + (sequence i '(0 1 2 3 4 5 6 7 8 9 10) + :from 2 :to 8 :by 2 + :test (progn + (cl-assert (= times 0)) + (cl-incf times) + #'<=)) + (collect i))] + :loopy t + :iter-keyword (sequence collect) + :iter-bare ((sequence . sequencing) + (collect . collecting))) + ;;;;; Seq Index (loopy-deftest seq-index :result '(97 98 99) @@ -2262,6 +2472,69 @@ Othe cases use `elt'." :iter-bare ((seq-index . sequencing-index) (collect . collecting))) +(loopy-deftest seq-index-:by-only-once + :result '(0 2 4 6 8 10) + :body ((with (my-seq [0 1 2 3 4 5 6 7 8 9 10]) + (times 0)) + (seq-index i my-seq :by (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect (elt my-seq i))) + :loopy t + :iter-keyword (seq-index collect) + :iter-bare ((seq-index . sequencing-index) + (collect . collecting))) + +(loopy-deftest seq-index-end-just-once-up + :result '(0 1 2) + :multi-body t + :body [((with (times 0)) + (sequence-index i (vector 0 1 2 3 4 5 6) :to (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i)) + + ((with (times 0)) + (sequence-index i (vector 0 1 2 3 4 5 6) :upto (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (collect i)) + + ((with (times 0)) + (sequence-index i (vector 0 1 2 3 4 5 6) :below (progn + (cl-assert (= times 0)) + (cl-incf times) + 3)) + (collect i))] + :loopy t + :iter-keyword (sequence-index collect) + :iter-bare ((sequence-index . sequencing) + (collect . collecting))) + +(loopy-deftest seq-index-end-just-once-down + :result '(2 1 0) + :multi-body t + :body [((with (times 0)) + (sequence-index i (vector 0 1 2) :downto (progn + (cl-assert (= times 0)) + (cl-incf times) + 0)) + (collect i)) + + ((with (times 0)) + (sequence-index i (vector 0 1 2) :above (progn + (cl-assert (= times 0)) + (cl-incf times) + -1)) + (collect i))] + :loopy t + :iter-keyword (sequence-index collect) + :iter-bare ((sequence-index . sequencing) + (collect . collecting))) + (loopy-deftest seq-index-:from-:downto-:by :result '(8 6 4 2) :body ((with (my-seq [0 1 2 3 4 5 6 7 8 9 10])) @@ -2323,6 +2596,15 @@ Othe cases use `elt'." :iter-bare ((seq-index . sequencing-index) (collect . collecting))) +(loopy-deftest seq-index-:downfrom + :result '(5 4 3 2 1 0) + :body ((sequence-index i [0 1 2 3 4 5 6 7 8 9] :downfrom 5) + (collect i)) + :loopy t + :iter-keyword (sequence-index collect) + :iter-bare ((sequence-index . sequencing-index) + (collect . collecting))) + (loopy-deftest seq-index-step-var :doc "If `:by' is a numeric literal, `seq-index' can use it directly." :result '(2 4 6 8) @@ -2377,7 +2659,7 @@ Othe cases use `elt'." :iter-bare ((_cmd . (sequencing-ref)) (do . ignore))) -(loopy-deftest seq-ref-:by +(loopy-deftest seq-ref-:by-array :result "a1a3a5a7a9" :body ((with (my-str "0123456789")) (seq-ref i my-str :by 2) @@ -2388,6 +2670,32 @@ Othe cases use `elt'." :iter-bare ((seq-ref . sequencing-ref) (do . ignore))) +(loopy-deftest seq-ref-:by-list + :result '(99 1 99 3 99 5 99 7 99 9 99) + :body ((with (my-list (list 0 1 2 3 4 5 6 7 8 9 10) )) + (seq-ref i my-list :by 2) + (do (setf i 99)) + (finally-return my-list)) + :loopy t + :iter-keyword (seq-ref do) + :iter-bare ((seq-ref . sequencing-ref) + (do . ignore))) + +(loopy-deftest seq-ref-:by-just-once + :result "a1a3a5a7a9" + :body ((with (my-str "0123456789") + (times 0)) + (seq-ref i my-str :by (progn + (cl-assert (= times 0)) + (cl-incf times) + 2)) + (do (setf i ?a)) + (finally-return my-str)) + :loopy t + :iter-keyword (seq-ref do) + :iter-bare ((seq-ref . sequencing-ref) + (do . ignore))) + (loopy-deftest seq-ref-:by-:index :result "a1a3a5a7a9" :body ((with (my-str "0123456789")) @@ -2401,7 +2709,7 @@ Othe cases use `elt'." (loopy-deftest seq-ref-:from-:by :result '(0 cat 2 cat 4 cat 6 cat 8 cat) - :body ((with (my-list '(0 1 2 3 4 5 6 7 8 9))) + :body ((with (my-list (list 0 1 2 3 4 5 6 7 8 9))) (seq-ref i my-list :from 1 :by 2 ) (do (setf i 'cat)) (finally-return my-list)) @@ -2445,7 +2753,7 @@ Othe cases use `elt'." (loopy-deftest seq-ref-:above-list :result '(0 1 2 3 4 5 cat cat cat cat) - :body ((with (my-list '(0 1 2 3 4 5 6 7 8 9))) + :body ((with (my-list (list 0 1 2 3 4 5 6 7 8 9))) (seq-ref i my-list :above 5) (do (setf i 'cat)) (finally-return my-list)) @@ -2478,7 +2786,7 @@ Othe cases use `elt'." (loopy-deftest seq-ref-:upfrom-:by-string :result '(0 cat 2 cat 4 cat 6 cat 8 cat) - :body ((with (my-list '(0 1 2 3 4 5 6 7 8 9))) + :body ((with (my-list (list 0 1 2 3 4 5 6 7 8 9))) (seq-ref i my-list :upfrom 1 :by 2) (do (setf i 'cat)) (finally-return my-list)) @@ -2487,6 +2795,17 @@ Othe cases use `elt'." :iter-bare ((seq-ref . sequencing-ref) (do . ignore))) +(loopy-deftest sequence-ref-:downfrom + :result '(0 cat 2 cat 4 cat 6 7 8 9) + :body ((with (my-list (list 0 1 2 3 4 5 6 7 8 9))) + (sequence-ref i my-list :downfrom 5 :by 2) + (do (setf i 'cat)) + (finally-return my-list)) + :loopy t + :iter-keyword (sequence-ref do) + :iter-bare ((sequence-ref . sequencing-ref) + (do . ignore))) + (loopy-deftest seq-ref-:by-literal :doc "`seq-ref' can use literal `:by' directly." :result [0 1 22 3 22 5 22 7 22 9 10]