Skip to content

Commit

Permalink
Improve set-prev.
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
okamsn committed Aug 18, 2024
1 parent 5b40c0d commit ce14e0d
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 48 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 20 additions & 5 deletions doc/loopy-doc.org
Original file line number Diff line number Diff line change
Expand Up @@ -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=.

Expand All @@ -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)))
Expand All @@ -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
Expand Down
103 changes: 63 additions & 40 deletions loopy-commands.el
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
17 changes: 14 additions & 3 deletions tests/tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit ce14e0d

Please sign in to comment.