lexical for-in/for-of loose end

Brendan Eich brendan at mozilla.org
Sat Feb 4 18:35:09 PST 2012

Allen Wirfs-Brock wrote:
> On Feb 4, 2012, at 12:55 PM, Brendan Eich wrote:
>> >  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.
> Yes, I support the general principal that that initializers of bindings capture to the left.  But the problem here is that conceptually the let is defining multiple bindings (one per iteration).  I don't think many people are actually going to understanding the details of the proposed semantics and their implications.  Since most uses won't involve closure capture, any of the proposed semantics that have per iteration bindings with forward value propagation are just going to do the "right thing".  That is good.

That's all agreed, yes.

>    However, I doubt that someone who actually codes a function in a for(let;;) initializer is going to be thinking, "of course, this only captures the first iteration bindings".

Why? The initializer runs first. There are two working possibilities: 
bindings of the same name in an implicit block around the loop (the "0th 
iteration scope"), immediately shadowed on first (if the condition is 
true) by fresh bindings of the same names with values forwarded; or what 
I propose, the first iteration's bindings.

Why is the 0th iteration scope better than the 1st iteration scope? It 
may be that some people think of the INIT part in for (INIT; TEST; UPD) 
STMT as being "before the loop starts", but others may see it as part of 
the first iteration.

If we really do have evidence for the 0th iteration scope (hard to 
imagine), then that is doable but it costs a bit more in the 
closure-in-initializer case. All else equal I go with lower cost in that 
rare case.

> With the TDZ alternative I proposed, there  would still be equivalence for:
>       for(let x=x;;)...;
> and
>       for(let x={|| x}();;)...;
> Both throw for accessing an initialized variable.

Yippee. :-|

Making a dynamic error trap for no good reason is a mistake. Where's the 
evidence that closures in initializers that capture loop control 
variables are "wrong"? There isn't any, AFAIK.

> But you're right that equivalence is lost for
>       for(let x=n, y=x;;)...;
> and
>       for(let x=n, y={|| x}();;)...;
> Whether this is better or worse than the "wrong capture" issue complete depends upon the actual programmer intent.

It's worse for the language's consistency of binding rules and 
equivalences. It's a botch. I think we should not equivocate here.

> >  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.
> For the latter, I strongly suspect that they won't know and will be WTF surprised when they encounter it.

Why? What binding did the think they captured, given the assumption we 
share (since you didn't reject it, we do share this, right?) that they 
know about fresh binding(s) per iteration? Are you implying users will 
expect the 0th-iteration bindings? Could be, but I rather suspect they'd 
want 1st. There isn't another choice. They don't want throwing upvars 
(throwing-up vars :-P).

> >  Different how?
> Different from subsequent iterations...
> Take Jason's example
>     for (let i = 0; i<  n; ) {
>         setTimeout(...closure using i...);
>         if (shouldAdvance())
>             i++;
>     }
> If somebody decided to abstract the increment:
>     for (let i = 0, advance={|| i++}; i<  n; ) {
>         setTimeout(...closure using i...);
>         if (shouldAdvance())
>           advance();
>     }
> advance does what is intended if called on the first iteration, but not on subsequent one iterations. I'd soon get an error when I ran this than having it get stuck in a loop.

(I s/;/,/ on the for line to fix the typo.)

Anyone who writes that must be thinking the advance block-lambda is in 
the scope of the body, if they understand fresh-binding-per-iteration. 
But the advance block-lambda is not in the body. It's not before the 
loop either (wrong [outer] or no i in scope there). This is simply 
erroneous code by definition -- the assumption of 
fresh-binding-per-iteration having been learned already is violated by 
the structure of the code.

Rather than a dynamic error (might not test this), anyone writing this 
might rather get a static error. A warning would be justifed by a high 
quality implementation, based on analysis of the advance() call possibly 
coming from later iterations.

So here we return to Grant's thought of banning closures that capture 
loop control variables from the INIT part of for(;;) loops. We could do 
that, but I would want an early error. A closure could use eval to 
frustrate my desire for such an early error, but that would not make me 
want only a runtime error for all cases.

Anyway, I don't think we have enough evidence to make this an error case 

> Well, we don't have a lot of evidence for any of this discussion. Does 
> any C-syntax language currently implement for(;;) in the manner that 
> is being proposed?

Dart, and for good cause: because JS with for(var;;) {... closure 
capture ...} is a design flaw that bites people over and over. It's why 
CoffeeScript added "do".

> Actually, I'm  more concerned about un-savy users who won't really understand the full semantics, whatever we decide they are.

That's the wrong target of concern. http://wtfjs.org is full of stuff 
already, this is not even going to make the top ten, or top 100.

The crucial target of concern should be the savvy users and especially 
the semanticists and smarties who want consistent and minimal binding 
rules. I agree that error on loop variable capture from closure in INIT 
avoids doing violence to binding rules, but errors without justification 
do their own kind of damage. Rather have no errors and consistent 
binding rules, which is why I favor 1st iteration capture.

We should see if there are fuzz-testing cases that target JS1.7+ (Jason 
mentioned some) that would be broken by 1st iteration capture. Not 
because we cater to fuzzers, but to get a feel for the testable surface.


More information about the es-discuss mailing list