From ce14e0daea81b80f45fbfb49a874a78dd3325dc4 Mon Sep 17 00:00:00 2001 From: okamsn Date: Sat, 17 Aug 2024 22:21:30 -0400 Subject: [PATCH] Improve `set-prev`. - Allow `back` to no be known at compile time. Use a simple implementation of a queue, adapted from https://irreal.org/blog/?p=40. - Add a test of a `with`-bound `back`. - State clearly in the documentation that `set-prev` stores the value of `VAL` found at the end of the loop cycle and that it does not modify `VAR` until the specified loop cycle. --- CHANGELOG.md | 11 +++++ doc/loopy-doc.org | 25 ++++++++--- loopy-commands.el | 103 ++++++++++++++++++++++++++++------------------ tests/tests.el | 17 ++++++-- 4 files changed, 108 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c51f5e1f..3a219ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,20 @@ This document describes the user-facing changes to Loopy. ([#197], [#145]). These commands no longer take multiple conditions in the same command. +### Bugs Fixed + +- Allow `back` in `set-prev` to not be known at compile time ([#202]). + +### Documentation Improvements + +- State explicitly that `set-prev` records values from the end + of each loop cycle and that it does not modify its variable + until the specified cycle ([#202]). + [#195]: https://github.com/okamsn/loopy/pull/195 [#196]: https://github.com/okamsn/loopy/pull/196 [#197]: https://github.com/okamsn/loopy/pull/197 +[#202]: https://github.com/okamsn/loopy/pull/202 ## 0.12.2 diff --git a/doc/loopy-doc.org b/doc/loopy-doc.org index c7c5dc4b..95bac5ca 100644 --- a/doc/loopy-doc.org +++ b/doc/loopy-doc.org @@ -1370,9 +1370,11 @@ value and do no affect how the loop iterates. #+findex: prev - =(set-prev|prev-expr VAR VAL &key back)= :: Bind =VAR= to a value =VAL= from a previous cycle in the loop. With =BACK= (default: 1), use the value from that - many cycles previous. This command /does not/ work like a queue; it always - uses the value from the =BACK=-th previous cycle, regardless of when the - command is run. + many cycles previous. _If not enough cycles have passed yet, then the value + of =VAR= is not modified._ This command /does not/ work like a queue for + recording =VAL=; it always uses the value from the =BACK=-th previous cycle, + regardless of when the command is run. The value used is always the value at + the end of the cycle. This command also has the aliases =setting-prev=, =prev-set=, and =prev=. @@ -1383,10 +1385,13 @@ value and do no affect how the loop iterates. (collect j)) ;; => (nil nil nil 1 2) - (loopy (list i '(1 2 3 4 5)) - (set-prev j i :back 3) + (loopy (with (n 3)) + (list i '(1 2 3 4 5)) + (set-prev j i :back n) (collect j)) + ;; NOTE: `j' isn't overwritten until the correct cycle: + ;; ;; => ((first-val nil) (first-val nil) (1 2) (3 4)) (loopy (with (j 'first-val)) (list i '((1 . 2) (3 . 4) (5 . 6) (7 . 8))) @@ -1402,6 +1407,16 @@ value and do no affect how the loop iterates. (when (cl-oddp i) (set-prev j i)) (collect j)) + + ;; NOTE: `j' is always bound to the previous value of `i' + ;; from the end of the specified cycle. + ;; + ;; => (nil 101 102 103) + (loopy (numbers i :from 1 :to 4) + (set i2 i) + (set-prev j i2) + (set i2 (+ i 100)) + (collect j)) #+end_src ** Iteration diff --git a/loopy-commands.el b/loopy-commands.el index 90f1e18e..8d0a5cdc 100644 --- a/loopy-commands.el +++ b/loopy-commands.el @@ -190,52 +190,75 @@ handled by `loopy-iter'." ;; being known at compile time (but still only being evaluated once.) ;; (#194) (cl-defun loopy--parse-set-prev-command - ((_ var val &key back)) + ((_ var val &key (back 1))) "Parse the `set-prev' command as (set-prev VAR VAL &key back). VAR is set to a version of VAL in a past loop cycle. With BACK, wait that many cycle before beginning to update VAR. +This command records the value of VAL at the end of the cycle, +not when the command is run. + This command does not wait for VAL to change before updating VAR." - (let* ((holding-vars (cl-loop for i from 1 to (or back 1) - collect (gensym "set-prev-hold"))) - (using-destructuring (seqp var)) - (with-bound (if using-destructuring - (cl-some #'loopy--with-bound-p - (cl-second (loopy--destructure-for-iteration var val))) - (loopy--with-bound-p var))) - ;; We don't use `cl-shiftf' in the main body because we want the - ;; holding variables to update regardless of whether we update - ;; VAR. - (holding-vars-setq `(loopy--latter-body - (cl-shiftf ,@holding-vars ,val)))) - (if with-bound - (if using-destructuring - (let ((cnt-holder (gensym "count")) - (back-holder (gensym "back"))) - `((loopy--other-vars (,cnt-holder 0)) - (loopy--latter-body (setq ,cnt-holder (1+ ,cnt-holder))) - (loopy--other-vars (,back-holder ,back)) - ,@(mapcar (lambda (x) `(loopy--other-vars (,x nil))) - holding-vars) - ,@(loopy--bind-main-body (main-exprs rest-instr) - (loopy--destructure-for-other-command - var (car holding-vars)) - `((loopy--main-body (when (>= ,cnt-holder ,back-holder) - ,@main-exprs)) - ,@rest-instr)) - ,holding-vars-setq)) - (let ((val-holder (gensym "set-prev-val"))) - `((loopy--other-vars (,val-holder ,var)) - ,@(mapcar (lambda (x) `(loopy--other-vars (,x ,val-holder))) - holding-vars) - (loopy--main-body (setq ,var ,(car holding-vars))) - ,holding-vars-setq))) - `(,@(mapcar (lambda (x) `(loopy--other-vars (,x nil))) - holding-vars) - ,@(loopy--destructure-for-other-command - var (car holding-vars)) - ,holding-vars-setq)))) + (if (not (numberp back)) + ;; When we don't know ahead of time how far back we need to go, we have to + ;; use a queue. This code is adapted from Irreal's blog + ;; (https://irreal.org/blog/?p=40) where they give an example of a simple + ;; FIFO queue in Scheme. It uses two lists. The "front" lists contains + ;; values for popping off. The "back" list contains values for pushing + ;; on. When the "front" list is exhausted, values are moved from the + ;; "back" list in reverse. + (loopy--instr-let-const* ((prev back)) + loopy--other-vars + (loopy--instr-let-var* ((cnt 0) + (queue-front nil) + (queue-end nil)) + loopy--other-vars + ;; We generate a main-body expression for binding the variables to the + ;; desired values and for setting them to nil. There is overlap in the + ;; remaining expressions, which initialize the variables. + (loopy--bind-main-body (main-exprs init-instr) + (loopy--destructure-for-other-command + var `(or (pop ,queue-front) + (progn + (setq ,queue-front (reverse ,queue-end) + ,queue-end nil) + (pop ,queue-front)))) + `((loopy--main-body (when (>= ,cnt ,prev) + ,(macroexp-progn main-exprs))) + ,@init-instr + (loopy--latter-body (push ,val ,queue-end)) + (loopy--latter-body (setq ,cnt (1+ ,cnt))))))) + + ;; When we know ahead of time how far we need to go back, we can use a chain + ;; of `setq's for storing values instead of queue. However, except when + ;; BACK is 1, we still need to use a count, in case one of the variables is + ;; initialized in `with'. + + (if (= back 1) + (loopy--instr-let-var* ((hold-var nil) + (run nil)) + loopy--other-vars + `(,@(loopy--bind-main-body (main-exprs init-instrs) + (loopy--destructure-for-other-command var hold-var) + `((loopy--main-body (when ,run + ,@main-exprs)) + ,@init-instrs)) + (loopy--latter-body (setq ,hold-var ,val)) + (loopy--latter-body (setq ,run t)))) + (let ((hold-vars (cl-loop for i from 1 to back + collect (gensym "hold-var")))) + (loopy--instr-let-var* ((cnt 0)) + loopy--other-vars + `(,@(mapcar (lambda (x) `(loopy--other-vars (,x nil))) + hold-vars) + ,@(loopy--bind-main-body (main-exprs init-instrs) + (loopy--destructure-for-other-command var (car hold-vars)) + `((loopy--main-body (when (>= ,cnt ,back) + ,@main-exprs)) + ,@init-instrs)) + (loopy--latter-body (cl-shiftf ,@hold-vars ,val)) + (loopy--latter-body (setq ,cnt (1+ ,cnt))))))))) ;;;;;; Group (cl-defun loopy--parse-group-command ((_ &rest body)) diff --git a/tests/tests.el b/tests/tests.el index f254dbcf..4369833a 100644 --- a/tests/tests.el +++ b/tests/tests.el @@ -876,9 +876,20 @@ SYMS-STR are the string names of symbols from `loopy-iter-bare-commands'." (loopy-deftest set-prev-keyword-back :result '(nil nil nil 1 2) - :body ((list i '(1 2 3 4 5)) - (set-prev j i :back 3) - (collect j)) + :multi-body t + :body [((list i '(1 2 3 4 5)) + (set-prev j i :back 3) + (collect j)) + + ((with (n 3) + (first-time t)) + (list i '(1 2 3 4 5)) + (set-prev j i :back (if first-time + (progn + (setq first-time nil) + n) + (error "Evaluated more than once."))) + (collect j))] :loopy t :iter-bare ((list . listing) (collect . collecting)