lexical for-in/for-of loose end

Brendan Eich brendan at mozilla.org
Sat Feb 4 12:55:17 PST 2012

Allen Wirfs-Brock wrote:
> On Feb 4, 2012, at 9:49 AM, Brendan Eich wrote:
>> I agree we want to capture the first-iteration bindings in any 
>> closures in those declarators' initializers.
> It isn't clear to me why capture first-iteration is abstractly any 
> better than "capture a hidden second x".  In both cases, in most 
> iterations of the loop, evaluation of any such captures is going to 
> reference the "wrong" binding.

The argument is as follows:

   for (let i = 0, a = some.array, n = a.length; i < n; i++) { ... }

here we definitely want the a in a.length (n's initializer) to be scoped 
by the head let -- to be the a declared by the second declarator.

Now consider a bit of eta conversion:

   for (let i = 0, a = some.array, n = (function(){return a})().length; 
i < n; i++) { ... }

It would be quite wrong for the a captured by the anonymous function 
expression to be other than the a binding declared and initialized 
immediately to the left. It would be bad for the eta conversion to break 
equivalence (use a block-lambda instead of a function expression for 
full TCP).

>  From a user perspective, the main advantage I see for capture first 
> iteration is that it has a slightly smaller window of wrongness.  The 
> captures evaluated in the first iteration will reference the 
> correction binding, while latter iterations reference the wrong binding.
Users expect and even (now that they know, and Dart raises the ante) 
demand that each iteration gets fresh let bindings. Any who do capture 
an initial binding in a closure must know, or will learn, that it's just 
the first one, which fits the model.

If this were really a footgun (I don't believe it is without actual 
evidence from the field) we could try to ban closures capturing the 
initial bindings. That ad-hoc restriction would be quite a wart. It 
doesn't seem warranted.

>  From an implementation perspective, it is probably a bit simpler to 
> not have the extra hidden binding for capture.

I don't think so. The unrolling I showed was to use a tail-recursive 
block-lambda helper. But real implementations will do closure analysis 
and optimization (flat AKA display closures, e.g.) and use branch 
instructions for loops, jumps for breaks, etc. Having the first binding 
rib open a bit earlier than subsequent ribs is (I think) a small or 
zero-cost issue.

> I really don't like the first iteration is different semantics

Different how? Making the first iteration's binding initialization 
capture guaranteed errors would be different semantics. Capturing the 
first iteration's bindings from closures in their initializers is not 
"different" any more than having initializers is "different". The 
initialization part of the for loop is already special. It's not like 
the update part.

> and think we should think about the above alternative.

Eta equivalence matters. Given that we want n = a.length to use the a 
declared to the left in the same for-head declaration, I don't see how 
we can make closures in a right-ward initializer capture some outer binding.

Capturing an error-only binding would need evidence of the footgun not 
being useful for shooting other things. We don't have such evidence, not 
by a long shot.

> However, such closure capture is very rare (could use of block lambda 
> based patterns change that??)

I don't think so -- equivalences are stronger, not weaker or different, 
with block-lambdas vs. functions, due to TCP.

> so it may come down to judgements about implementation costs.  Is 
> capture first going to be significantly easier to implement than my 
> alternative scoping? The answer is obvious to me.

Did you mean "isn't"?

>  In either case an implementation is like to special case loops with 
> closure capture in their initializers.

Varying a sketch Jason posted to the SpiderMonkey internals list:

     for (let V = INIT; TEST; UPD)  STMT

compiles to

         enterblock <V>
         goto L2
     L1: STMT
     L2: TEST
         iftrue L1

This is very close to how SpiderMonkey compiles for(let;;) already -- 
the only new instruction is reenterblock, which exits the current block 
and does the equivalent of enterblock <V> again.

<V> is an immediate operand, in SpiderMonkey a block object literal 
created by the compiler. It holds all the bindings and their stack 
offsets, however many declarators with or without destructuring occur 
after let.

I claim implementation is not the driver here. User expectations, esp. 
savvy users who might make some practical or theoretical (testing) use 
of eta conversion, matter more.


More information about the es-discuss mailing list