Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

let init forms and unwind-protect

25 views
Skip to first unread message

D Herring

unread,
Apr 22, 2011, 1:07:24 AM4/22/11
to
Hi all,

In an effort to ease binding to some C++ code, I'm working on a macro
that encapsulates the following basic form.

(let ((var1 init1)
(var2 init2)
...
(varN initN)
(abort t))
(unwind-protect
(multiple-value-prog1
(progn ,@body)
(setf abort nil))
(cleanupN varN abort)
...
(cleanup2 var2 abort)
(cleanup1 var1 abort)))

That shows the proper lexical scoping, however it has one fatal flaw.

If one of initX aborts, the unwind-protect is bypassed entirely,
skipping any cleanup forms that may need to run. C++ semantics
require destructor functions for completed constructors to run during
an unwind.

To resolve this, my form is becoming the following monster.

(let ((var1 var2 ... varN
gensym1 gensym2 ... gensymN
(varcount 0)
(abort t)))
(unwind-protect
(multiple-value-prog1
(progn
;; move init-forms into the unwind-protect
;; preserve let's parallel evaluation
(setf gensym1 init1
varcount 1
gensym2 init2
varcount 2
...
gensymN initN
varcount N
var1 gensym1
var2 gensym2
...
varN gensymN)
,@body)
;; prep for unwinding; preserve any changes in ,@body
(setf
gensym1 var1
gensym2 var2
...
gensymN varN)
(setf abort nil))
;; make sure cleanup forms see bound vars
;; forced need for prep above
(setf
var1 gensym1
var2 gensym2
...
varN gensymN)
;; unwind only what was wound
(tagbody ; could also be list of (when (>= varcount X) cleanX)
(case varcount
(0 (go clean0))
(1 (go clean1))
(2 (go clean2))
...
(N (go cleanN))
cleanN (cleanupN varN abort)
...
clean2 (cleanup2 var2 abort)
clean1 (cleanup1 var1 abort)))
clean0)))

That might have the proper semantics (it catches unwinds from any init
form, all init forms are evaluated before lexicals are bound,
unwinding only covers initialized variables). BUT IT STINKS!

Is there an easier way to have
- LET's binding semantics
- unwind-protect around LET's init forms and the body
- protected forms conditioned on which init forms completed
?


Thanks,
Daniel

P.S. For bonus points: I have the belief that unwind-protect is
fairly costly. Am I wrong? Is there an efficient way to nest them?
Some trick that eliminates the nesting and allows one top
unwind-protect to emulate them, but still protects against exits in
the cleanup forms...

so (unwind-protect (unwind-protect ...))
becomes (unwind-protect ... magic)

D Herring

unread,
Apr 22, 2011, 1:35:21 AM4/22/11
to
On 04/22/2011 01:07 AM, D Herring wrote:
> To resolve this, my form is becoming the following monster.

Improved form:

(let ((var1 var2 ... varN
gensym1 gensym2 ... gensymN

(abortp t)))


(unwind-protect
(multiple-value-prog1
(progn
;; move init-forms into the unwind-protect
;; preserve let's parallel evaluation
(setf gensym1 init1

gensym2 init2
...
gensymN initN


var1 gensym1
var2 gensym2
...
varN gensymN)
,@body)
;; prep for unwinding; preserve any changes in ,@body
(setf
gensym1 var1
gensym2 var2
...
gensymN varN)

(setf abortp nil))


;; unwind only what was wound

(when gensymN (cleanupN gensymN abortp))
...
(when gensym2 (cleanup2 gensym2 abortp))
(when gensym1 (cleanup1 gensym1 abortp))))

Still unpleasant with the duplicated variable bindings, but workable
if need be.

- Daniel

Madhu

unread,
Apr 22, 2011, 4:31:23 AM4/22/11
to

* D Herring <ior42r$kt6$1...@dont-email.me> :
Wrote on Fri, 22 Apr 2011 01:35:21 -0400:

| On 04/22/2011 01:07 AM, D Herring wrote:
|> To resolve this, my form is becoming the following monster.
|

| ;; move init-forms into the unwind-protect
| ;; preserve let's parallel evaluation

This seems to be your primary concern: To preserve LET's parallel
evaluation. Otherwise the patten is no different from WITH-OPEN-FILE or
WITH-RESOURCE.

However I think you had a typo and meant to use PSETF instead of SETF
here.

| Still unpleasant with the duplicated variable bindings, but workable
| if need be.

It seems your second (aesthetic) concern is to avoid code duplicaton,
and you want to collect the cleanup forms at one textual location.

Without being sure if I understood the desired semantics, (based on the
first code you presented in your original post) here is what I'd do to
avoid duplicate variable bindings, by duplicating code in the init forms
instead:

(let (var1
var2 ... varN bork (abortp t))
(psetq var1 (let (tmp) (unwind-protect (setq tmp init1) (unless tmp (setq bork t))))
var1 (let (tmp) (unwind-protect (setq tmp init2) (unless tmp (setq bork t))))
...
varN (let (tmp) (unwind-protect (setq tmp initN) (unless tmp (setq bork t)))))
(unwind-protect
(multiple-value-prog1
(unless bork (progn ,@body))
(setq abort nil))
(when varN (cleanupN varN abortp))
...
(when var2 (cleanup2 var2 abortp))
(when var1 (cleanup2 var1 abortp))))

---
Madhu

Madhu

unread,
Apr 22, 2011, 4:37:30 AM4/22/11
to

I noticed an error in my post immediately after posting: The PSETQ has
to go INSIDE the UNWIND-PROTECT, for the idea to work, of course.

* Madhu <m3tydqe...@leonis4.robolove.meer.net> :
Wrote on Fri, 22 Apr 2011 14:01:23 +0530:

| Without being sure if I understood the desired semantics, (based on the
| first code you presented in your original post) here is what I'd do to
| avoid duplicate variable bindings, by duplicating code in the init forms
| instead:
|
| (let (var1
| var2 ... varN bork (abortp t))
| (psetq var1 (let (tmp) (unwind-protect (setq tmp init1) (unless tmp (setq bork t))))
| var1 (let (tmp) (unwind-protect (setq tmp init2) (unless tmp (setq bork t))))
| ...
| varN (let (tmp) (unwind-protect (setq tmp initN) (unless tmp (setq bork t)))))
| (unwind-protect
| (multiple-value-prog1
| (unless bork (progn ,@body))
| (setq abort nil))
| (when varN (cleanupN varN abortp))
| ...
| (when var2 (cleanup2 var2 abortp))
| (when var1 (cleanup2 var1 abortp))))
|

(let (var1
var2 ... varN bork (abortp t))

(unwind-protect
(progn
(psetf var1 (let (tmp) (unwind-protect (setq tmp init1) (unless tmp (setq bork t))))


var1 (let (tmp) (unwind-protect (setq tmp init2) (unless tmp (setq bork t))))
...
varN (let (tmp) (unwind-protect (setq tmp initN) (unless tmp (setq bork t)))))

(multiple-value-prog1
(unless bork (progn ,@body))

(setq abort nil)))

Carlos

unread,
Apr 22, 2011, 12:55:43 PM4/22/11
to
[D Herring <dher...@at.tentpost.dot.com>, 2011-04-22 01:35]

You can build a list of things to cleanup as you go initializing them.
Something like

(let ((to-clean '()))
(unwind-protect
(progn
(let ((var1 (caar (push (cons (init1) #'cleanup1) to-clean)))
(var2 (caar (push (cons (init2) #'cleanup2) to-clean))))
,@body))
(mapc (lambda (pair) (funcall (cdr pair) (car pair))))))

Carlos.
--

Carlos

unread,
Apr 22, 2011, 12:59:18 PM4/22/11
to
[Carlos <an...@quovadis.com.ar>, 2011-04-22 18:55]

> (let ((to-clean '()))
> (unwind-protect
> (progn
> (let ((var1 (caar (push (cons (init1) #'cleanup1)
> to-clean))) (var2 (caar (push (cons (init2) #'cleanup2) to-clean))))
> ,@body))
> (mapc (lambda (pair) (funcall (cdr pair) (car pair))))))

The missing parameter to mapc should be "to-clean", of course.

>
> Carlos.

D Herring

unread,
Apr 22, 2011, 9:45:18 PM4/22/11
to
On 04/22/2011 04:31 AM, Madhu wrote:
>
> * D Herring<ior42r$kt6$1...@dont-email.me> :
> Wrote on Fri, 22 Apr 2011 01:35:21 -0400:
>
> | On 04/22/2011 01:07 AM, D Herring wrote:
> |> To resolve this, my form is becoming the following monster.
> |
> | ;; move init-forms into the unwind-protect
> | ;; preserve let's parallel evaluation
>
> This seems to be your primary concern: To preserve LET's parallel
> evaluation. Otherwise the patten is no different from WITH-OPEN-FILE or
> WITH-RESOURCE.
>
> However I think you had a typo and meant to use PSETF instead of SETF
> here.

No. The SETF was intentional. I want the init forms evaluated in
order, and each bound to its variable before the next is evaluated.
Copying the gensyms to the variables could happen in a SETF or PSETF.


> | Still unpleasant with the duplicated variable bindings, but workable
> | if need be.
>
> It seems your second (aesthetic) concern is to avoid code duplicaton,
> and you want to collect the cleanup forms at one textual location.

Efficiency more than aesthetic. Don't want to double my stack
allocation if it can be avoided.


> Without being sure if I understood the desired semantics, (based on the
> first code you presented in your original post) here is what I'd do to
> avoid duplicate variable bindings, by duplicating code in the init forms
> instead:

[from your followup]


> (let (var1
> var2 ... varN bork (abortp t))

> (unwind-protect
> (progn
> (psetf var1 (let (tmp) (unwind-protect (setq tmp init1) (unless tmp (setq bork t))))


> var1 (let (tmp) (unwind-protect (setq tmp init2) (unless tmp (setq bork t))))
> ...
> varN (let (tmp) (unwind-protect (setq tmp initN) (unless tmp (setq bork t)))))

> (multiple-value-prog1
> (unless bork (progn ,@body))

> (setq abort nil)))


> (when varN (cleanupN varN abortp))
> ...
> (when var2 (cleanup2 var2 abortp))
> (when var1 (cleanup2 var1 abortp))))

That tmp/bork thing is an interesting idea. Not quite what I was
looking for (it doesn't stop evaluation of init forms) but it might
give me an idea.

Also, psetf on SBCL does something very similar to my temporary
gensyms; it just hides the mess. Haven't looked; maybe the compiler
can optimize away the temporaries in both cases.

Thanks for your suggestions,
Daniel

D Herring

unread,
Apr 22, 2011, 9:51:42 PM4/22/11
to
On 04/22/2011 12:55 PM, Carlos wrote:
> [D Herring<dher...@at.tentpost.dot.com>, 2011-04-22 01:35]
>> On 04/22/2011 01:07 AM, D Herring wrote:
>>> To resolve this, my form is becoming the following monster.
>>
>> Improved form:
...

> You can build a list of things to cleanup as you go initializing them.
> Something like
>
> (let ((to-clean '()))
> (unwind-protect
> (progn
> (let ((var1 (caar (push (cons (init1) #'cleanup1) to-clean)))
> (var2 (caar (push (cons (init2) #'cleanup2) to-clean))))
> ,@body))
> (mapc (lambda (pair) (funcall (cdr pair) (car pair))))))

Interesting idea.

I had actually considered a similar cleanup list for dynamic forms,
but hadn't noticed how clean it was in this case. However, I still
want a minimal overhead, non-consing solution for the static case.
Also the cleanup list doesn't "properly" handle the case where varX is
set to nil by the body, but I might need to reconsider those semantics.

Thanks,
Daniel

Madhu

unread,
Apr 22, 2011, 10:50:30 PM4/22/11
to

* D Herring <iotavg$mb0$1...@dont-email.me> :
Wrote on Fri, 22 Apr 2011 21:45:18 -0400:

|> (let (var1
|> var2 ... varN bork (abortp t))
|> (unwind-protect
|> (progn
|> (psetf var1 (let (tmp) (unwind-protect (setq tmp init1) (unless tmp (setq bork t))))
|> var1 (let (tmp) (unwind-protect (setq tmp init2) (unless tmp (setq bork t))))
|> ...
|> varN (let (tmp) (unwind-protect (setq tmp initN) (unless tmp (setq bork t)))))
|> (multiple-value-prog1
|> (unless bork (progn ,@body))
|> (setq abort nil)))
|> (when varN (cleanupN varN abortp))
|> ...
|> (when var2 (cleanup2 var2 abortp))
|> (when var1 (cleanup2 var1 abortp))))
|
| That tmp/bork thing is an interesting idea. Not quite what I was
| looking for (it doesn't stop evaluation of init forms) but it might
| give me an idea.

I suspected it was not what you wanted---It struck my fancy to allow the
initX to run "conceptually in parallel"*, while still cleaning up after
non-local exits in some initX correctly. (* which, of course, is not
possible in Common Lisp since SETQ/PSETQ will always evaluate initX
sequentially). If the initX had to run sequentially, the codetransform
would just change (ugly now)

(setq tmp initX) => (unless bork (setq tmp initX))

| Also, psetf on SBCL does something very similar to my temporary
| gensyms; it just hides the mess. Haven't looked; maybe the compiler
| can optimize away the temporaries in both cases.

Maybe PSETF support is not required at all (if you are generating all
code which uses it) and you would just use SETQ, and you could
explicitly disclaim support for LET (like the infamous ITERATE)

---
Madhu

0 new messages