diff --git a/CHANGELOG.md b/CHANGELOG.md index 822ad367..6b26fa39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,11 @@ This document describes the user-facing changes to Loopy. error, use the `with` special macro argument to explicitly state a starting value for the accumulation variable. +- Remove the deprecated positional arguments to the `numbers` command ([#205]). + [#169]: https://github.com/okamsn/loopy/issues/169 [#203]: https://github.com/okamsn/loopy/pull/203 +[#205]: https://github.com/okamsn/loopy/pull/205 ## 0.13.0 diff --git a/README.org b/README.org index 86314246..a606e467 100644 --- a/README.org +++ b/README.org @@ -33,6 +33,8 @@ please let me know. - Unreleased: - Conflicting initialization values for accumulation variables now signal a warning. In the future, they will signal an error. + - The positional arguments to the =numbers= command have been removed, + being deprecated since version 0.12.0. - Version 0.13.0: - The deprecated =:init= keyword argument has been removed. Use the =with= special macro argument instead. diff --git a/doc/loopy-doc.org b/doc/loopy-doc.org index c07ac38d..8f41e818 100644 --- a/doc/loopy-doc.org +++ b/doc/loopy-doc.org @@ -1643,7 +1643,7 @@ variants =numbers-up= and =numbers-down=. #+begin_src emacs-lisp ;; => (7 8 9 10 11 12 13 14 15 16) (loopy (cycle 10) - (numbers i 7) + (numbers i :from 7) (collect i)) #+end_src @@ -1657,13 +1657,9 @@ variants =numbers-up= and =numbers-down=. (loopy (numbers i :from 1 :to 5 :by 2) (collect i)) - ;; => (1 3 5) - (loopy (numbers i 1 5 2) - (collect i)) - ;; => (7 9 11 13 15 17 19 21 23 25) (loopy (cycle 10) - (numbers i 7 :by 2) + (numbers i :from 7 :by 2) (collect i)) ;; => (1 2.5 4.0) @@ -1675,9 +1671,9 @@ variants =numbers-up= and =numbers-down=. whether the value should be increasing or decreasing when using the =:by= keyword, one can use the keywords =:downfrom=, =:downto=, =:upfrom=, =:upto=, =:above=, and =:below=. The keywords =:from= and =:to= don't by themselves - specify a direction, and they can be used with the keyword arguments that do - without conflict. Using arguments that contradict one another will signal an - error. + specify a direction, and they can be used without conflict with the keyword + arguments that do. Using arguments that contradict one another will signal + an error. #+begin_src emacs-lisp ;; => (3 2 1) @@ -1701,7 +1697,7 @@ variants =numbers-up= and =numbers-down=. (loopy (numbers i :from 1 :upto 7) (collect i)) - ;; => Signals an error: + ;; Signals an error: (loopy (numbers i :downfrom 10 :upto 20) (collect i)) #+end_src @@ -1771,7 +1767,7 @@ are simple wrappers of the above =numbers= command. #+findex: numbers-down #+findex: numbering-down - =(numbers-down|nums-down VAR START [END] &key by)= :: Equivalent to =(numbers - VAR START [:downto END] &key by)=. This command exists only for convenience. + VAR :from START [:downto END] &key by)=. This command exists only for convenience. This command also has the aliases =numsdown= and =numbering-down=. @@ -1789,7 +1785,7 @@ are simple wrappers of the above =numbers= command. #+findex: numbers-up #+findex: numbering-up - =(numbers-up|nums-up VAR START [END] &key by)= :: Equivalent to =(numbers VAR - START [END] &key by)=. This command exists only for convenience. + :from START [END] &key by)=. This command exists only for convenience. This command also has the aliases =numsup= and =numbering-up=. diff --git a/doc/loopy.texi b/doc/loopy.texi index ba8a9411..4a917636 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,org60882dd +@float Listing,org04e7c72 @lisp ;; => (nil 1 2) (loopy (collect coll i) @@ -888,7 +888,7 @@ the flag @samp{dash} provided by the package @samp{loopy-dash}. Below are two examples of destructuring in @code{cl-loop} and @code{loopy}. -@float Listing,orge44397f +@float Listing,orga4486aa @lisp ;; => (1 2 3 4) (cl-loop for (i . j) in '((1 . 2) (3 . 4)) @@ -903,7 +903,7 @@ Below are two examples of destructuring in @code{cl-loop} and @code{loopy}. @caption{Destructuring values in a list.} @end float -@float Listing,org9267070 +@float Listing,orgea59f47 @lisp ;; => (1 2 3 4) (cl-loop for elem in '((1 . 2) (3 . 4)) @@ -1803,7 +1803,7 @@ end. @lisp ;; => (7 8 9 10 11 12 13 14 15 16) (loopy (cycle 10) - (numbers i 7) + (numbers i :from 7) (collect i)) @end lisp @@ -1817,13 +1817,9 @@ whether the variable is incremented or decremented. (loopy (numbers i :from 1 :to 5 :by 2) (collect i)) -;; => (1 3 5) -(loopy (numbers i 1 5 2) - (collect i)) - ;; => (7 9 11 13 15 17 19 21 23 25) (loopy (cycle 10) - (numbers i 7 :by 2) + (numbers i :from 7 :by 2) (collect i)) ;; => (1 2.5 4.0) @@ -1835,9 +1831,9 @@ By default, the variable's value starts at 0 and increases by 1. To specify whether the value should be increasing or decreasing when using the @samp{:by} keyword, one can use the keywords @samp{:downfrom}, @samp{:downto}, @samp{:upfrom}, @samp{:upto}, @samp{:above}, and @samp{:below}. The keywords @samp{:from} and @samp{:to} don't by themselves -specify a direction, and they can be used with the keyword arguments that do -without conflict. Using arguments that contradict one another will signal an -error. +specify a direction, and they can be used without conflict with the keyword +arguments that do. Using arguments that contradict one another will signal +an error. @lisp ;; => (3 2 1) @@ -1861,7 +1857,7 @@ error. (loopy (numbers i :from 1 :upto 7) (collect i)) -;; => Signals an error: +;; Signals an error: (loopy (numbers i :downfrom 10 :upto 20) (collect i)) @end lisp @@ -1933,7 +1929,7 @@ are simple wrappers of the above @samp{numbers} command. @table @asis @item @samp{(numbers-down|nums-down VAR START [END] &key by)} Equivalent to @samp{(numbers - VAR START [:downto END] &key by)}. This command exists only for convenience. + VAR :from START [:downto END] &key by)}. This command exists only for convenience. This command also has the aliases @samp{numsdown} and @samp{numbering-down}. @@ -1953,7 +1949,7 @@ This command also has the aliases @samp{numsdown} and @samp{numbering-down}. @table @asis @item @samp{(numbers-up|nums-up VAR START [END] &key by)} Equivalent to @samp{(numbers VAR - START [END] &key by)}. This command exists only for convenience. + :from START [END] &key by)}. This command exists only for convenience. This command also has the aliases @samp{numsup} and @samp{numbering-up}. @@ -4682,7 +4678,7 @@ using the @code{let*} special form. This method recognizes all commands and their aliases in the user option @code{loopy-aliases}. -@float Listing,org0f10623 +@float Listing,org5fa2cde @lisp ;; => ((1 2 3) (-3 -2 -1) (0)) (loopy-iter (arg accum-opt positives negatives other) diff --git a/loopy-commands.el b/loopy-commands.el index 6b720eb4..107aeff8 100644 --- a/loopy-commands.el +++ b/loopy-commands.el @@ -99,21 +99,25 @@ ;;;;; Working with Plists and Keyword Arguments -;; Loopy uses property lists to handle keyword arguments. -(defun loopy--extract-keywords (list) - "Extract the keywords from LIST according to `keywordp'." - (cl-loop for i in list - if (keywordp i) - collect i)) - -(defun loopy--only-valid-keywords-p (correct list) +(cl-defun loopy--only-valid-keywords-p (correct list) "Return nil if a keyword in LIST is not in CORRECT. -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--extract-keywords list) correct))) +CORRECT is a list of valid keywords. + +Any keyword not in CORRECT is considered invalid. Any element +not in a keyword position that is not a keyword is invalid. If +LIST does not contain an even number of elements, it is invalid." + ;; `cl-loop' is broken for this use-case. See Emacs bug #72753. + (let ((length 0)) + (let ((this-pos nil)) + (dolist (i list) + (cl-incf length) + (setq this-pos (not this-pos)) + (when (let ((kwp (keywordp i))) + (or (and this-pos (not kwp)) + (and kwp (not (memq i correct))))) + (cl-return-from loopy--only-valid-keywords-p nil)))) + (cl-evenp length))) ;;;; Included parsing functions. ;;;;; Sub-Loops @@ -442,6 +446,13 @@ instructions: (when (nlistp keywords) (setq keywords (list keywords))) + (when (or (eq other-vals 0) + (eq other-vals '(0))) + (setq other-vals nil)) + + (when (null required-vals) + (setq required-vals 0)) + ;; Make sure `keywords' are all prefixed with a colon. (setq keywords (mapcar (lambda (x) (if (eq ?: (aref (symbol-name x) 0)) @@ -478,7 +489,7 @@ instructions: ,@(if keywords (if other-vals '(&rest args) - `(&key ,@var-keys)) + `(&rest opts)) (when other-vals '(&rest other-vals))))) ,doc-string @@ -493,29 +504,34 @@ instructions: (opts nil)) ;; These can be referred to directly, but we'll keep ;; the option open for using `opts'. - `((opts (list ,@(cl-loop for sym in keywords - for var in var-keys - append (list sym var)))))) + (cl-loop for sym in keywords + for var in var-keys + collect `(,var (compat-call plist-get opts ,sym #'eq))) + ;; `((opts (list ,@(cl-loop for sym in keywords + ;; for var in var-keys + ;; append (list sym var))))) + ) nil) ;; We only want to run this code if the values of `opts' and ;; `other-vals' are contained in `args'. For other cases, the ;; function arguments perform this step for us. - ,@(when (and other-vals keywords) - `(;; Set `opts' as starting from the first keyword and `other-vals' - ;; as everything before that. - (cl-loop with other-val-holding = nil - for cons-cell on args - for arg = (car cons-cell) - until (keywordp arg) - do (push arg other-val-holding) - finally do (setq opts cons-cell - other-vals (nreverse other-val-holding))) - - ;; Validate any keyword arguments: - (unless (loopy--only-valid-keywords-p (quote ,keywords) opts) - (signal 'loopy-wrong-number-of-command-arguments-or-bad-keywords - (list cmd))))) + ,(when (and other-vals keywords) + ;; Set `opts' as starting from the first keyword and `other-vals' + ;; as everything before that. + `(cl-loop with other-val-holding = nil + for cons-cell on args + for arg = (car cons-cell) + until (keywordp arg) + do (push arg other-val-holding) + finally do (setq opts cons-cell + other-vals (nreverse other-val-holding)))) + + ;; Validate any keyword arguments: + ,(when keywords + `(unless (loopy--only-valid-keywords-p (quote ,keywords) opts) + (signal 'loopy-wrong-number-of-command-arguments-or-bad-keywords + (list cmd)))) ,(when (consp other-vals) `(unless (cl-member (length other-vals) @@ -526,8 +542,9 @@ instructions: (ignore cmd name ;; We can only ignore variables if they're defined. - ,(if other-vals 'other-vals) - ,(if keywords 'opts)) + ,@(when (and keywords (null other-vals)) var-keys) + ,(when other-vals 'other-vals) + ,(when keywords 'opts)) ,instructions)))) @@ -546,7 +563,8 @@ iteration command. The supported keywords are: - `:above' (exclusive end) - `:below' (exclusive end) - `:by' (increment) -- `:test' (comparison function) +- `:test' (comparison function, being already in PLIST or calculated) +- `:test-given' (whether `:test' was given) CMD is the command usage for error reporting." @@ -925,6 +943,7 @@ map's keys. Duplicate keys are ignored." (loopy--latter-body (setq ,key-list (cdr ,key-list)))))) ;;;;;; Numbers + (loopy--defiteration numbers "Parse the `numbers' command as (numbers VAR &key KEYS). @@ -947,108 +966,44 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom', `:downto' and `:downfrom' make the index decrease instead of increase." :keywords (:by :from :downfrom :upfrom :to :downto :upto :above :below :test) :required-vals 0 - :other-vals (0 1 2 3) + :other-vals 0 :instructions - ;; 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) - - (condition-case nil - (loopy--find-start-by-end-dir-vals opts) - (loopy-conflicting-command-arguments - (signal 'loopy-conflicting-command-arguments (list cmd)))) - - ;; 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))) - - (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--plist-bind ( :start key-start :end key-end :by key-by + :decreasing decreasing + :test key-test + :test-given test-given) - (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--find-start-by-end-dir-vals opts cmd) - ,@(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))))))))))) + (loopy--instr-let-const* ((increment-val-holder (or key-by 1)) + (test-val-holder key-test)) + loopy--iteration-vars + (loopy--instr-let-var* ((var-val-holder (or key-start 0) + (if (loopy--with-bound-p var) + (gensym "num-test-var") + var))) + loopy--iteration-vars + `(,(when (loopy--with-bound-p var) + `(loopy--main-body (setq ,var ,var-val-holder))) + + (loopy--latter-body + (setq ,var-val-holder + ,(cond (test-given + `(+ ,var-val-holder ,increment-val-holder)) + ((eq increment-val-holder 1) + (if decreasing + `(1- ,var-val-holder) + `(1+ ,var-val-holder))) + (t + `(,(if decreasing #'- #'+) + ,var-val-holder ,increment-val-holder))))) + + ,@(when key-end + (loopy--instr-let-const* ((end-val-holder key-end)) + loopy--iteration-vars + `((loopy--pre-conditions (funcall ,test-val-holder + ,var-val-holder + ,end-val-holder)))))))))) ;;;;;; Numbers Up diff --git a/tests/tests.el b/tests/tests.el index 94cfdab4..8d1b454a 100644 --- a/tests/tests.el +++ b/tests/tests.el @@ -899,7 +899,7 @@ SYMS-STR are the string names of symbols from `loopy-iter-bare-commands'." (loopy-deftest set-prev-keyword-with :result '(first-val first-val 2 2 4 4 6 6 8 8) :body ((with (j 'first-val)) - (numbers i 1 10) + (numbers i :from 1 :to 10) (when (cl-oddp i) (set-prev j i)) (collect j)) @@ -1833,7 +1833,7 @@ Using numbers directly will use less variables and more efficient code." (loopy-deftest numbers :result '(1 2 3 4 5) :repeat _cmd - :body ((_cmd i 1 5) + :body ((_cmd i :from 1 :to 5) (collect i)) :loopy ((_cmd . (nums numbers num number))) :iter-keyword ((_cmd . (nums numbers num number)) @@ -1841,23 +1841,15 @@ Using numbers directly will use less variables and more efficient code." :iter-bare ((_cmd . (numbering)) (collect . collecting))) -(loopy-deftest numbers-pos-nokey-step - :result '(1 3 5) +(loopy-deftest numbers-keywords-pos-args-should-error + :doc "Make sure an error is signaled when using the now removed positional arguments." + :error loopy-wrong-number-of-command-arguments-or-bad-keywords :body ((numbers i 1 5 2) (collect i)) :loopy t - :iter-keyword (numbers collect) - :iter-bare ((numbers . numbering) - (collect . collecting))) - -(loopy-deftest numbers-neg-nokey-step - :result '(5 3 1) - :body (loopy (numbers i 5 1 -2) - (collect i)) - :loopy t - :iter-keyword (numbers collect) - :iter-bare ((numbers . numbering) - (collect . collecting))) + :iter-keyword (collect numbers) + :iter-bare ((collect . collecting) + (numbers . numbering))) ;;;;; Nums Keywords @@ -1996,17 +1988,13 @@ Using numbers directly will use less variables and more efficient code." :iter-keyword (numbers) :iter-bare ((numbers . numbering))) -;;;;; Nums With Vars +;;;;; Numbers With Vars (loopy-deftest numbers-literal-by-and-literal-end :doc "Check the optimizing for non-variable `:by' and `:to' doesn't fail." :result '(2 4 6 8) - :multi-body t - :body [((with (start 2)) - (numbers i start 8 2) - (collect i)) - ((with (start 2)) - (numbers i start :to 8 :by 2) - (collect i))] + :body ((with (start 2)) + (numbers i :from start :to 8 :by 2) + (collect i)) :loopy t :iter-keyword (numbers collect) :iter-bare ((numbers . numbering) @@ -2015,13 +2003,9 @@ Using numbers directly will use less variables and more efficient code." (loopy-deftest numbers-literal-by-with-var-end :doc "Check the optimizing for non-variable `:by' and variable `:to' doesn't fail." :result '(2 4 6 8) - :multi-body t - :body [((with (start 2) (end 8)) - (numbers i start end 2) - (collect i)) - ((with (start 2) (end 8)) - (numbers i start :to end :by 2) - (collect i))] + :body ((with (start 2) (end 8)) + (numbers i :from start :to end :by 2) + (collect i)) :loopy t :iter-keyword (numbers collect) :iter-bare ((numbers . numbering) @@ -2030,13 +2014,9 @@ Using numbers directly will use less variables and more efficient code." (loopy-deftest numbers-var-by-with-var-end :doc "Check the optimizing for variable `:by' and variable `:to' doesn't fail." :result '(2 4 6 8) - :multi-body t - :body [((with (start 2) (by 2) (end 8)) - (numbers i start end by) - (collect i)) - ((with (start 2) (by 2) (end 8)) - (numbers i start :to end :by by) - (collect i))] + :body ((with (start 2) (by 2) (end 8)) + (numbers i :from start :to end :by by) + (collect i)) :loopy t :iter-keyword (numbers collect) :iter-bare ((numbers . numbering) @@ -2047,7 +2027,7 @@ Using numbers directly will use less variables and more efficient code." :result '(2 4 6 8) :body ((with (start 2)) (cycle 4) - (numbers i start :by 2) + (numbers i :from start :by 2) (collect i)) :loopy t :iter-keyword (numbers collect cycle) @@ -2060,7 +2040,7 @@ Using numbers directly will use less variables and more efficient code." :result '(2 4 6 8) :body ((with (start 2) (by 2)) (cycle 4) - (numbers i start :by by) + (numbers i :from start :by by) (collect i)) :loopy t :iter-keyword (numbers collect cycle)