Skip to content

Commit

Permalink
Review evaluation of keyword arguments during run time and expansion …
Browse files Browse the repository at this point in the history
…time.

- Make `close` in `iter` evaluable.  Make it evaluated at the beginning.

- Make `on-failure` in `find` evaluated at the beginning.

Closes issue #210.
  • Loading branch information
okamsn committed Sep 14, 2024
1 parent a374774 commit 8e97e9c
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 55 deletions.
43 changes: 37 additions & 6 deletions doc/loopy-doc.org
Original file line number Diff line number Diff line change
Expand Up @@ -1507,13 +1507,17 @@ loop. This restriction allows for producing more efficient code.
holds the value yielded by the iterator. The loop ends when the iterator
finishes.

=close= is whether the generator should be closed via ~iter-close~ after the
loop ends. The default is ~t~. Note that Emacs will eventually close
un-closed, un-reachable generators during garbage collection. To be
consistent with other commands, =close= is evaluated at the start of the loop,
even though it's value is only used after the loop finishes.

=yield-result= is the optional second argument to the function ~iter-next~,
which is the value of ~iter-yield~ in the iterator (not to be confused with
the value yielded by calling ~iter-next~).

=close= is whether the generator should be closed via ~iter-close~ after the
loop ends. The default is ~t~. Note that Emacs will eventually close
un-closed, un-reachable generators during garbage collection.
the value yielded by calling ~iter-next~). Unlike =close=, which is evaluated
once, =yield-result= is an expression which is substituted into the loop body.
Therefore, =yield-result= can be used to repeatedly call functions.

For efficiency, when possible, =VAR= is bound to the yielded value before each
step of the loop, which is used to detect whether the iterator signals that it
Expand All @@ -1526,6 +1530,7 @@ loop. This restriction allows for producing more efficient code.

#+begin_src emacs-lisp
;; With var:
;;
;; => ((1 . 4) (2 . 5) (3 . 6))
(loopy (with (iter-maker (iter-lambda (x)
(while x
Expand All @@ -1535,6 +1540,7 @@ loop. This restriction allows for producing more efficient code.
(collect (cons i j)))

;; Without var:
;;
;; => (1 2 3)
(loopy (iter (funcall (iter-lambda ()
;; These yielded values are all ignored.
Expand All @@ -1543,6 +1549,21 @@ loop. This restriction allows for producing more efficient code.
(iter-yield 'third-yield))))
(set i 1 (1+ i))
(collect i))

;; Using `yield-result':
;;
;; => (3 2 1)
(loopy (with (yield-results nil))
(set i 1 (1+ i))
(iter (funcall (iter-lambda ()
;; The value from the expression specified by
;; `:yield-result' is `push'-ed:
(push (iter-yield 'first-yield) yield-results)
(push (iter-yield 'second-yield) yield-results)
(push (iter-yield 'third-yield) yield-results)))
;; Note that the value of `i' evaluated each time:
:yield-result i)
(finally-return yield-results))
#+end_src

#+ATTR_TEXINFO: :tag Warning
Expand Down Expand Up @@ -2123,7 +2144,7 @@ source sequences.
=EXPR=. If =by= is non-nil (default: 1), then move to the next n-th element
during each iteration. This command is a special case of the =substream=
command (described below), setting =VAR= to the first element of each
substream. For more information, see the command =substream=.
substream. For more information on streams, see the command =substream=.

This command also has the alias =streaming=.

Expand Down Expand Up @@ -3396,6 +3417,9 @@ Sequence accumulation commands are used to join lists (such as =union= and
normal failure and =VAR= will be set to the value of =ON-FAILURE=, if
provided.

To be consistent with other commands, =ON-FAILURE= is evaluated at the
start of the loop, even though that is not necessarily where it is used.

#+BEGIN_SRC emacs-lisp
;; => (13 (1 2))
(loopy (list i '(1 2 3 4 5 6 7 8))
Expand All @@ -3419,6 +3443,13 @@ Sequence accumulation commands are used to join lists (such as =union= and
;; => nil
(loopy (list i '(1 2 3 4 5 6))
(find nil (> i 3) :on-failure 27))

;; Value of `:on-failure' gotten at the start of the loop:
;; => 27
(loopy (with (on-fail 27))
(list i '(1 2 3))
(set on-fail 33)
(find i (> i 4) :on-failure on-fail))
#+END_SRC

*** Optimizing Accumulations
Expand Down
51 changes: 41 additions & 10 deletions doc/loopy.texi
Original file line number Diff line number Diff line change
Expand Up @@ -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,org5119497
@float Listing,org57e6a65
@lisp
;; => (nil 1 2)
(loopy (collect coll i)
Expand Down Expand Up @@ -887,7 +887,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,orgf3a9f7c
@float Listing,orgdb6dedf
@lisp
;; => (1 2 3 4)
(cl-loop for (i . j) in '((1 . 2) (3 . 4))
Expand All @@ -902,7 +902,7 @@ Below are two examples of destructuring in @code{cl-loop} and @code{loopy}.
@caption{Destructuring values in a list.}
@end float

@float Listing,org1bf5dc0
@float Listing,orgb7824db
@lisp
;; => (1 2 3 4)
(cl-loop for elem in '((1 . 2) (3 . 4))
Expand Down Expand Up @@ -1668,13 +1668,17 @@ iterator object produced by a calling a generator function. If given, @samp{VAR
holds the value yielded by the iterator. The loop ends when the iterator
finishes.

@samp{close} is whether the generator should be closed via @code{iter-close} after the
loop ends. The default is @code{t}. Note that Emacs will eventually close
un-closed, un-reachable generators during garbage collection. To be
consistent with other commands, @samp{close} is evaluated at the start of the loop,
even though it's value is only used after the loop finishes.

@samp{yield-result} is the optional second argument to the function @code{iter-next},
which is the value of @code{iter-yield} in the iterator (not to be confused with
the value yielded by calling @code{iter-next}).

@samp{close} is whether the generator should be closed via @code{iter-close} after the
loop ends. The default is @code{t}. Note that Emacs will eventually close
un-closed, un-reachable generators during garbage collection.
the value yielded by calling @code{iter-next}). Unlike @samp{close}, which is evaluated
once, @samp{yield-result} is an expression which is substituted into the loop body.
Therefore, @samp{yield-result} can be used to repeatedly call functions.

For efficiency, when possible, @samp{VAR} is bound to the yielded value before each
step of the loop, which is used to detect whether the iterator signals that it
Expand All @@ -1687,6 +1691,7 @@ This command also has the name @samp{iterating}.

@lisp
;; With var:
;;
;; => ((1 . 4) (2 . 5) (3 . 6))
(loopy (with (iter-maker (iter-lambda (x)
(while x
Expand All @@ -1696,6 +1701,7 @@ This command also has the name @samp{iterating}.
(collect (cons i j)))
;; Without var:
;;
;; => (1 2 3)
(loopy (iter (funcall (iter-lambda ()
;; These yielded values are all ignored.
Expand All @@ -1704,6 +1710,21 @@ This command also has the name @samp{iterating}.
(iter-yield 'third-yield))))
(set i 1 (1+ i))
(collect i))
;; Using `yield-result':
;;
;; => (3 2 1)
(loopy (with (yield-results nil))
(set i 1 (1+ i))
(iter (funcall (iter-lambda ()
;; The value from the expression specified by
;; `:yield-result' is `push'-ed:
(push (iter-yield 'first-yield) yield-results)
(push (iter-yield 'second-yield) yield-results)
(push (iter-yield 'third-yield) yield-results)))
;; Note that the value of `i' evaluated each time:
:yield-result i)
(finally-return yield-results))
@end lisp

@quotation Warning
Expand Down Expand Up @@ -2305,7 +2326,7 @@ Iterate through the elements for the stream
@samp{EXPR}. If @samp{by} is non-nil (default: 1), then move to the next n-th element
during each iteration. This command is a special case of the @samp{substream}
command (described below), setting @samp{VAR} to the first element of each
substream. For more information, see the command @samp{substream}.
substream. For more information on streams, see the command @samp{substream}.

This command also has the alias @samp{streaming}.

Expand Down Expand Up @@ -3677,6 +3698,9 @@ If the loop is left early and @samp{TEST} was never non-nil, this is the same as
normal failure and @samp{VAR} will be set to the value of @samp{ON-FAILURE}, if
provided.

To be consistent with other commands, @samp{ON-FAILURE} is evaluated at the
start of the loop, even though that is not necessarily where it is used.

@lisp
;; => (13 (1 2))
(loopy (list i '(1 2 3 4 5 6 7 8))
Expand All @@ -3700,6 +3724,13 @@ provided.
;; => nil
(loopy (list i '(1 2 3 4 5 6))
(find nil (> i 3) :on-failure 27))
;; Value of `:on-failure' gotten at the start of the loop:
;; => 27
(loopy (with (on-fail 27))
(list i '(1 2 3))
(set on-fail 33)
(find i (> i 4) :on-failure on-fail))
@end lisp
@end table

Expand Down Expand Up @@ -4653,7 +4684,7 @@ using the @code{let*} special form.
This method recognizes all commands and their aliases in the user option
@code{loopy-aliases}.

@float Listing,orgb546acb
@float Listing,org36b4a9a
@lisp
;; => ((1 2 3) (-3 -2 -1) (0))
(loopy-iter (arg accum-opt positives negatives other)
Expand Down
91 changes: 52 additions & 39 deletions loopy-commands.el
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,9 @@ is a function by which to update VAR (default `cdr')."
VAR is the variable. VAL is an generator-producing function, as
from `iter-defun' or `iter-lambda'. YIELD-RESULT is the optional
value of `iter-next'. CLOSE is whether the iterator should be
closed after the loop completes."
value of `iter-next' and is an expression, not a value. CLOSE is
whether the iterator should be closed after the loop completes
and is a value."
:required-vals 0 ;; Require values /after/ `var'.
:other-vals (0 1)
:keywords (:yield-result :close)
Expand Down Expand Up @@ -825,11 +826,16 @@ closed after the loop completes."
(iter-next ,obj-holder ,yield-result)
(iter-end-of-sequence nil)
(:success t))))))
,(when close
`(loopy--vars-final-updates
(,obj-holder . (iter-close ,obj-holder))))))))


,@(pcase close
('nil nil)
('t `((loopy--vars-final-updates
(,obj-holder . (iter-close ,obj-holder)))))
(_
(loopy--instr-let-const* ((close2 close))
loopy--iteration-vars
`((loopy--vars-final-updates
(,obj-holder . (when ,close2
(iter-close ,obj-holder))))))))))))

;;;;;; List
;; TODO: Make this a normal function.
Expand Down Expand Up @@ -2161,57 +2167,64 @@ This function is called by `loopy--expand-optimized-accum'."

;;;;;;; Find
(loopy--defaccumulation find
"Parse a command of the form `(finding VAR EXPR TEST &key ON-FAILURE)'."
"Parse a command of the form `(finding VAR EXPR TEST &key ON-FAILURE)'.
ON-FAILURE is evaluated at the beginning of the loop, even though
it is only meaningful at the end."
:num-args 3
:keywords (on-failure)
:explicit (let* ((test-arg (cl-third args))
(test-form (if (loopy--quoted-form-p test-arg)
`(,(loopy--get-function-symbol test-arg) ,val)
test-arg))
(of-used (plist-member opts :on-failure))
(on-failure-given (plist-member opts :on-failure))
(on-failure (plist-get opts :on-failure))
(tag-name (loopy--produce-non-returning-exit-tag-name
loopy--loop-name))
(found (gensym "found")))

`((loopy--non-returning-exit-used ,tag-name)
(loopy--accumulation-vars (,var nil))
,@(if of-used
;; If TEST always nil, bind to ON-FAILURE.
`((loopy--accumulation-vars (,found nil))
(loopy--main-body (when ,test-form
(setq ,var ,val
,found t)
(throw (quote ,tag-name) t)))
(loopy--vars-final-updates
(,var . (unless ,found (setq ,var ,on-failure)))))
`((loopy--main-body (when ,test-form
(setq ,var ,val)
(throw (quote ,tag-name) t)))))))
(loopy--instr-let-const* ((on-failure2 on-failure))
loopy--accumulation-vars
`((loopy--non-returning-exit-used ,tag-name)
(loopy--accumulation-vars (,var nil))
,@(if on-failure-given
;; If TEST always nil, bind to ON-FAILURE.
`((loopy--accumulation-vars (,found nil))
(loopy--main-body (when ,test-form
(setq ,var ,val
,found t)
(throw (quote ,tag-name) t)))
(loopy--vars-final-updates
(,var . (unless ,found (setq ,var ,on-failure2)))))
`((loopy--main-body (when ,test-form
(setq ,var ,val)
(throw (quote ,tag-name) t))))))))
:implicit (let* ((test-arg (cl-second args))
(test-form (if (loopy--quoted-form-p test-arg)
`(,(loopy--get-function-symbol test-arg) ,val)
test-arg))
(of-used (plist-member opts :on-failure))
(on-failure-given (plist-member opts :on-failure))
(on-failure (plist-get opts :on-failure))
(found (gensym "found"))
(tag-name (loopy--produce-non-returning-exit-tag-name
loopy--loop-name)))
`((loopy--non-returning-exit-used ,tag-name)
(loopy--accumulation-vars (,var nil))
,@(if of-used
;; If TEST always nil, bind to ON-FAILURE.
`((loopy--accumulation-vars (,found nil))
(loopy--main-body (when ,test-form
(setq ,var ,val
,found t)
(throw (quote ,tag-name) t)))
(loopy--vars-final-updates
(,var . (unless ,found (setq ,var ,on-failure)))))
`((loopy--main-body (when ,test-form
(setq ,var ,val)
(throw (quote ,tag-name) t)))))
(loopy--implicit-return ,var))))
(loopy--instr-let-const* ((on-failure2 on-failure))
loopy--accumulation-vars
`((loopy--non-returning-exit-used ,tag-name)
(loopy--accumulation-vars (,var nil))
,@(if on-failure-given
;; If TEST always nil, bind to ON-FAILURE.
`((loopy--accumulation-vars (,found nil))
(loopy--main-body (when ,test-form
(setq ,var ,val
,found t)
(throw (quote ,tag-name) t)))
(loopy--vars-final-updates
(,var . (unless ,found (setq ,var ,on-failure2)))))
`((loopy--main-body (when ,test-form
(setq ,var ,val)
(throw (quote ,tag-name) t)))))
(loopy--implicit-return ,var)))))

;;;;;;; Set Accum
(loopy--defaccumulation set-accum
Expand Down
Loading

0 comments on commit 8e97e9c

Please sign in to comment.