Description
If a lambda re-enters a letrec
initialisation expression using a continuation from call/cc
, and specifically that lambda is referred to in both branches of an if-expression, it will have the wrong behaviour.
For background, letrec
differs from letrec*
, in that letrec
assigns all of its variables after all of their corresponding initialisation expressions have been evaluated, whereas letrec*
assigns its variables one at a time after each corresponding initialisation expression has been evaluated. This difference leads to different behaviours with the following program, which mutates one of the letrec
variables before re-entering another variable by its contiuation:
(let ((f (lambda ()
(letrec[*] ((a 4) (j (call/cc (lambda (cc) cc))))
(if (eqv? j 0)
a
(begin
(set! a 9)
(j 0)))))))
(f))
With letrec*
the mutation persists and results in the value 9
, because a
has already been initialised by the point of the call/cc
. With letrec
, the mutation is overwritten back to a 4
, because a
has not yet been initialised by the point of the call/cc
, and is assigned 4
again after re-entering the initialisation expression for j
.
Chez Scheme has the correct behaviour in this example. However, if f
is instead reached through an if-expression, and particularly if f
is referenced in both branches, letrec
has the incorrect behaviour which seems to correspond with letrec*
, and gives 9
rather than 4
.
(let ((f (lambda ()
(letrec ((a 4) (j (call/cc (lambda (cc) cc))))
(if (eqv? j 0)
a
(begin
(set! a 9)
(j 0)))))))
(if #t (f) (f)))
This example incorrectly produces 9
. The correct behaviour should be to produce 4
. This happens regardless if the true or false branch is taken. It does not happen if f
is only referred to in one of the branches, i.e. (if #t (f) 0)
gives 4
.
Tested on 10.2.0, NixOS. Correct behaviour witnessed on Chicken.