lexical for-in/for-of loose end

Allen Wirfs-Brock allen at wirfs-brock.com
Sat Feb 4 14:19:45 PST 2012


On Feb 4, 2012, at 12:55 PM, Brendan Eich wrote:

> 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.
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.  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".  

> It would be bad for the eta conversion to break equivalence (use a block-lambda instead of a function expression for full TCP).

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.

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.

> 
> 
>> 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.

For the latter, I strongly suspect that they won't know and will be WTF surprised when they encounter it.  The saving grace is that this will probably be very rare, although its possible that the introduction of block lambdas might somewhat change that.  Just don't know...

> 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.
> 
My TDZ solution is such a restriction.  But I don't see how it is any more ad hoc than any of the other changes we are talking about here in order to give for(;;) per iteration bindings.  Its wartiness  actually seems small and is restricted  to a situation where the programmer probably is actually expecting C=style per loop rather than per iteration binding. 

>> 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.

My comment wasn't about the bind to first unrolling.  It was about the "extra hidden binding" alternative in the first list of alternatives and is probably also applicable to by TDZ alternative.

> 
>> I really don't like the first iteration is different semantics
> 
> 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.

> 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.

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?
> 
>> 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.
That was the point I have had in mind.  While function expressions in for(;;) initializers are now rare, that might change with the use of block-lambdas
> 
>> 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"?
yes
> 
>> 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>
>        INIT
>        goto L2
>    L1: STMT
>        reenterblock
>        UPD
>    L2: TEST
>        iftrue L1
>        leaveblock
> 
> 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.
Actually, I'm  more concerned about un-savy users who won't really understand the full semantics, whatever we decide they are.

Allen




More information about the es-discuss mailing list