For Loop Desugaring (was: return when desugaring to closures)

# Mark S. Miller (15 years ago)

On Mon, Oct 13, 2008 at 5:00 PM, Jon Zeppieri <jaz at bu.edu> wrote:

Mark said that there was a desugaring for 'for' to 'lambda,' without special cases, where this all works out, but I haven't been able to figure out what rewrite he had in mind.

That's not the point. I'm talking about a rewrite from 'for' to 'lambda' that satisfies the following properties:

  1. for (var i = 0; i < len; i++) ... continues to mean what it means in ES3.
  2. for (let i = 0; i < len; i++) ... has the proper scope for 'i' (which you reiterated above), and 'i' is rebound -- not mutated -- on each iteration.
  3. The rewrite rules are the same, regardless of whether it's a "for (var ...)" or a "for (let ...)" loop.

At least, that's what I took Mark to mean. He can correct me if I'm wrong.

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.

# Brendan Eich (15 years ago)

On Oct 13, 2008, at 7:48 PM, Mark S. Miller wrote:

On Mon, Oct 13, 2008 at 5:00 PM, Jon Zeppieri <jaz at bu.edu> wrote:

  1. for (var i = 0; i < len; i++) ... continues to mean what it
    means in ES3.
  2. for (let i = 0; i < len; i++) ... has the proper scope for 'i' (which you reiterated above), and 'i' is rebound -- not mutated -- on each iteration.
  3. The rewrite rules are the same, regardless of whether it's a
    "for (var ...)" or a "for (let ...)" loop.

At least, that's what I took Mark to mean. He can correct me if
I'm wrong.

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.

Requirement 3 is met because var hoists to the enclosing function.
This is the part I missed -- well done.

Note that continue; in body translates to break continueTarget; and
break; translates to break breakTarget; -- you knew that ;-).

However, in contradiction to my original claim, one couldn't usefully say "const" instead of "let" with a for(;;) loop.

Why not usefully? It's true the consts do not hoist to leave an effect
in the variable object, but they do ensure const-ness. The body and
update cannot mutate the nearest <varName> binding. This is sometimes

useful and existing const implementations allow for (const...). I do
not think it should be rejected just yet.

# Jon Zeppieri (15 years ago)

On Mon, Oct 13, 2008 at 10:48 PM, Mark S. Miller <erights at google.com> wrote:

On Mon, Oct 13, 2008 at 5:00 PM, Jon Zeppieri <jaz at bu.edu> wrote:

I'm talking about a rewrite from 'for' to 'lambda' that satisfies the following properties:

  1. for (var i = 0; i < len; i++) ... continues to mean what it means in ES3.
  2. for (let i = 0; i < len; i++) ... has the proper scope for 'i' (which you reiterated above), and 'i' is rebound -- not mutated -- on each iteration.
  3. The rewrite rules are the same, regardless of whether it's a "for (var ...)" or a "for (let ...)" loop.

At least, that's what I took Mark to mean. He can correct me if I'm wrong.

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.

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.

Then again, I'm not sure it matters. I think the real problem here is that the <updateExpr>, as written by the user, is (or, rather, usually

is) an assignment, and you should give the user what the user asked for.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ...

# David-Sarah Hopwood (15 years ago)

Brendan Eich wrote:

On Oct 13, 2008, at 7:48 PM, Mark S. Miller wrote:

On Mon, Oct 13, 2008 at 5:00 PM, Jon Zeppieri <jaz at bu.edu> wrote:

  1. for (var i = 0; i < len; i++) ... continues to mean what it
    means in ES3.
  2. for (let i = 0; i < len; i++) ... has the proper scope for 'i' (which you reiterated above), and 'i' is rebound -- not mutated -- on each iteration.
  3. The rewrite rules are the same, regardless of whether it's a
    "for (var ...)" or a "for (let ...)" loop.

At least, that's what I took Mark to mean. He can correct me if
I'm wrong.

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 think this is equivalent to my third attempt in www.mail-archive.com/[email protected]/msg00947.html,

except that the latter was not attempting to handle break/continue. To do that correctly, it would be:

label: for (let i = initExpr; condExpr; updateExpr) { body }

==> { const $loop = lambda(i) { (updateExpr); if (condExpr) { const ($continue$label = lambda {}) { body } $loop(i); } }; let i = initExpr; if (condExpr) { const ($continue$label = lambda {}) { body } $loop(i); } }

['continue label;' or 'continue;' within 'body' expands to '$continue$label()', assuming that escape continuations can be called. 'break label;' or 'break;' within 'body' expands to itself. An unlabelled 'for' is equivalent to one with a fresh label.]

Requirement 3 is met because var hoists to the enclosing function.

Assuming that var hoisting is done before this expansion, yes.

[...]

However, in contradiction to my original claim, one couldn't usefully say "const" instead of "let" with a for(;;) loop.

Why not usefully? It's true the consts do not hoist to leave an effect
in the variable object, but they do ensure const-ness.

The problem is the update expression -- it must be able to mutate a variable that is distinct from any of the variables captured by the body in any iteration. That's possible, but requires a different expansion. Something like:

label: for (const i = initExpr; condExpr; updateExpr) { body }

==> { let i = initExpr; const $loop = lambda { (updateExpr); const (i = i) { if (condExpr) { const ($continue$label = lambda {}) { body } $loop(); } } }; const (i = i) { if (condExpr) { const ($continue$label = lambda {}) { body } $loop(); } } }

(I'm assuming that 'const (i = i) {...}' is allowed and that the i on the RHS refers to the one in the surrounding scope. If this is not allowed, then use 'const ($t = i) {const (i = $t) {...}}'.)

This is subject to the criticism that the loop variable(s) are implicitly mutable in the update expression (only), when they were declared it to be const.

No such criticism would apply to 'for each (const ...', though.

# Jon Zeppieri (15 years ago)

On Tue, Oct 14, 2008 at 2:38 AM, Jon Zeppieri <jaz at bu.edu> wrote:

On Mon, Oct 13, 2008 at 10:48 PM, Mark S. Miller <erights at google.com> wrote:

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.

And then there are the batshit crazy cases. This was inspired by Dave Herman's latest blog post (calculist.blogspot.com/2008/10/todays-javascript-kookiness.html):

var arr = [];

for (eval("let x = 10;"); x >= 0; x--)
    arr.push(x);

alert(arr);

Works (for some value of "works") in JS1.7.

# Brendan Eich (15 years ago)

On Oct 14, 2008, at 7:17 AM, David-Sarah Hopwood wrote:

This is subject to the criticism that the loop variable(s) are implicitly mutable in the update expression (only), when they were declared it to be const.

My point was simpler: sometimes it is handy to write for (const x...)
and have no fresh binding for x, even though the loop iterates. It's a
corner case that falls out of parsing var x and const x using the same
top-down procedure, so not rejecting const where var is allowed. It
arguably simplifies both implementation and user modeling of the
grammar.

We could certainly reject for (const ...; ...; ...) loops if we saw
this as an anti-use-case.

No such criticism would apply to 'for each (const ...', though.

Right -- this is pure win.

# Brendan Eich (15 years ago)

On Oct 14, 2008, at 7:17 AM, David-Sarah Hopwood wrote:

Requirement 3 is met because var hoists to the enclosing function.

Assuming that var hoisting is done before this expansion, yes.

Absolutely -- var hoisting across lambdas to preserve TCP is prior
magic, assumed by the (revised) lambda proposal. In principle it could
be lambda coded but I don' t believe the result would be particularly
illuminating.

# Waldemar Horwat (15 years ago)

Jon Zeppieri wrote:

On Mon, Oct 13, 2008 at 10:48 PM, Mark S. Miller <erights at google.com> wrote:

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.

I believe it does. Very cool. It won't handle the fully general for(;;). E.g.,

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

Yeah, that's a problem with the current formulation.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ...

I do too.

Another issue with the rewrite is that it interacts badly with getters and setters on the iteration variable.

Waldemar

PS. What does lambda(x = y){...}() mean? Is it different from lambda(x){...}(y)? What does lambda(x = y){...}(z) do when z is undefined?

# Brendan Eich (15 years ago)

On Oct 14, 2008, at 12:39 PM, Waldemar Horwat wrote:

PS. What does lambda(x = y){...}() mean? Is it different from
lambda(x){...}(y)?

No.

What does lambda(x = y){...}(z) do when z is undefined?

Passes undefined bound to x. Undefined is not the same as missing.

These are my answers based on past default parameter proposals from
ES4 which met with approval at the Oslo meeting. Others will no doubt
shout if they seem wrong.

# Brendan Eich (15 years ago)

On Oct 14, 2008, at 1:36 PM, Brendan Eich wrote:

On Oct 14, 2008, at 12:39 PM, Waldemar Horwat wrote:

What does lambda(x = y){...}(z) do when z is undefined?

Passes undefined bound to x. Undefined is not the same as missing.

Lest anyone think otherwise, "missing" means actual argument count is
less than number of formal parameters. It's not yet another undefined- like value code.

The implementation knows that lambda has arity 1 with a default
parameter value. It knows the call passes an actual (that its value is
undefined does not matter). So the default parameter value is not used.

Default parameters are evaluated once when the lambda expression is
evaluated. That the lambda is immediately applied in your questions'
examples may make this point unclear. This is why my answer to your
question:

lambda(x = y){...}() mean? Is it different from lambda(x){...}(y)

was "No." But if you captured the lambda and applied it elsewhere to a
different y (y in a different scope chain), this equivalence would not
hold. Hope this is all non-controversial (modulo blunders on my part
explaining it).