lexical for-in/for-of loose end

Brendan Eich brendan at mozilla.org
Sat Feb 4 09:49:24 PST 2012


I want off this merry-go-round! Let's recap:

 From the January 19th 2012 notes Waldemar took:

Discussion about scope of for-bindings.
for (var x = ...;;) {...}  will, of course, retain ES1 semantics.
for (let x = ...;;) {...}
Allen: This will behave as in C++: x is bound once in a new scope 
immediately surrounding just the for statement.
DaveH: Strangely enough, this creates a new x binding in Dart at each 
iteration.
There's an alternative semantics that creates an iteration-local second 
x inside the loop and copies it back and forth.  Debate about whether to 
go to such complexity.  Many of us are on the fence.
Waldemar: What happens in the forwarding semantics if you capture the x 
inside a lambda in any of the three expressions in the head?
If this happens in the initializer:
DaveH's option: The lambda would capture an outer x.
Alternative: The lambda captures a hidden second x.
Waldemar's option: The lambda would capture the x from the first 
iteration.  The let variable x is bound once through each iteration, 
just before the test, if
   for (let x = expr1; expr2;) {...}
were:
   while (true) {
     let x = first_iteration ? expr1 : value_of_x_from_previous_iteration;
     if (!expr2)
       break;
     ...
   }
MarkM:  Just discovered that his desugaring has the same semantics as 
Waldemar's option.

--- end waldemar notes ---

Mark's desugaring 
(https://mail.mozilla.org/pipermail/es-discuss/2008-October/007819.html):

You're right. However, the desugaring is more complex than I expected.
Thanks for asking me to write it down.

   for (<keyword>  <varName>  =<initExpr>;<testExpr>;<updateExpr>) {<body>  }

desugars to (hygienic  renaming aside):

breakTarget: {
   const loop = lambda(iter =<initExpr>) {
     <keyword>  <varName>  = iter;
     if (!<testExpr>) { break breakTarget; }
     continueTarget: {<body>  }
     lambda(iter2 =<varName>) {
       <keyword>  <varName>  = iter2;
       <updateExpr>;
       loop(<varName>);
     }();
   };
   loop();
}

I believe this meets all your requirements. However, in contradiction
to my original claim, one couldn't usefully say "const" instead of
"let" with a for(;;) loop.

--- end mark desugaring ---

But then Jon Zeppieri wrote in reply (https://mail.mozilla.org/pipermail/es-discuss/2008-October/007826.html):

>/  I believe this meets all your requirements.
/
I believe it does.  Very cool.  It won't handle the fully general
for(;;).  E.g.,

for (let fn = lambda(n) { ... fn(...) ... };<testExpr>;<updateExpr>) ...

Also,

for (let i = 0, j = i + 1; ...) ...

But the modifications needed to make these work are pretty straightforward.

--- end jon citation ---

Then Grant Husbands wrote (https://mail.mozilla.org/pipermail/es-discuss/2012-January/019804.html):

How about something like this?
(given for (let<varName>  =<initExpr>;<testExpr>;<updateExpr>) {<body>  } )

{
   let<varName>  =<initExpr>;
   while(true) {
     if (!<testExpr>) { break breakTarget; }
     let<tempVar>  =<varName>;
     {
       // There might be a better way to copy values to/from shadowed variables
       // (using temporaries seems a bit weak)
       let<varName>  =<tempVar>;
       continueTarget: {<body>  }
       <tempVar>  =<varName>;
     }
     <varName>  =<tempVar>;
     <updateExpr>;
   }
}

...

Maybe disallowing capture in the for (let ...;...;...) head would be easier.

--- end grant citation ---


Ok, Brendan here. I agree we want to support multiple declarators for the let or const in the for-head's initialization part.

I agree we want to capture the first-iteration bindings in any closures in those declarators' initializers.

This requires unrolling the loop once. Let's see how the desugaring from:

   for (let d1 = e1, ... dN = eN; cond; update) {
     body;
   }

looks. It doesn't seem terrible:

   $loopEnd: {
     let d1 = e1, ... dN = eN;
     if (cond) {
       body;
       update;
       const $loop = { |d1, ... dN|
         if (!cond) break $loopEnd;
         body;
         update;
         $loop(d1, ... dN);
       }
       $loop(d1, ... dN);
     }
   }


Notes:

* ... is meta-syntax, not rest/spread syntax.
* I've left out break and continue in the body.
* I'm using a block lambda for fun.

Mutations to the first-iteration d1, ... dN bindings in any closures in e1...N propagate to the second iteration.

Is this enough to restore the consensus we thought we had at the end of the meeting day on January 19th?

/be


> Jason Orendorff <mailto:jason.orendorff at gmail.com>
> February 4, 2012 8:01 AM
> On Sat, Feb 4, 2012 at 8:02 AM, Jason Orendorff
>
> I just realized—the loop variables have to be visible in the
> init-expressions if we want to support this:
>
> for (let a = getThings(), i = 0, n = a.length; i < n; i++)
>
> This is maybe not the best way to write "for (thing of getThings())",
> but people will write it, and so it probably ought to work. I think
> this is more important than escaping closures. This means that if such
> loops will have per-iteration bindings, they should have an additional
> set of bindings just for initialization-time—which seems ugly. Maybe
> it's not worth it.
>
> There is also this:
>
> for (let i = 0; i < n; ) {
> setTimeout(...closure using i...);
> if (shouldAdvance())
> i++;
> }
>
> This will not work no matter what semantics we choose. However,
> per-iteration bindings risk encouraging people to hit this problem.
>
> These two issues give me pause.


More information about the es-discuss mailing list