Generator issue: exceptions while initializing arguments

# Allen Wirfs-Brock (13 years ago)

This is the first of probably several semantic issues WRT generator that will need to be resolved in the spec.

Here is the user level issue:

let thrower = {throw () {throw "exception"}}; function * G(arg = thrower.throw()) { //possibly other explicit initialization code which could also throw yield arg; } let g = G(); //do we see "exception" here? (loc 1) ... //possibly many lines of code g.next(); //or do we see it here (loc 2)

The generator proposal 1 doesn't really address this issue. It assumes 2 that an environment record with the parameter bindings can be created and captured at loc 1 without triggering any exceptions and it defers instantiation and execution of all of the generator's code body until the first "next" call. However, the semantics of parameter default value initialization can result in exceptions and more closely links argument instantiations with the actual activation of the generator function. You can't really build the parameter bindings without performing declaration instantiation for the function.

Arguably, exceptions involving initialization of the the parameter would be more meaningful at loc 1 (loc 2 could be very remote and in somebody else's code). However, the body of the generator may contain other explicit initialization code and there really is no way to identify and move evaluation of that code (and its possible exceptions) to loc 1. So, even if parameter initialization exceptions are made observable at loc 1 there may still be other logical initialization-related exceptions that will onlybe observable at loc 2.

Which semantics do we want? Deferring parameter initialization to loc 2 may be somewhat simpler from a specification perspective. It's hard to say which would have bigger implementation impact.

Allen

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

This is the first of probably several semantic issues WRT generator that will need to be resolved in the spec.

Here is the user level issue:

let thrower = {throw () {throw "exception"}}; function * G(arg = thrower.throw()) { //possibly other explicit initialization code which could also throw yield arg; } let g = G(); //do we see "exception" here? (loc 1) ... //possibly many lines of code g.next(); //or do we see it here (loc 2)

Python is no help since its parameter default values are evaluated when the definition is evaluated, not on each call:

Python 3.2 (r32:88452, Feb 20 2011, 11:12:31) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

def thrower(): ... raise BaseException('exception') ...

def G(arg = thrower()): ... yield arg ... Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in thrower

BaseException: exception

The generator proposal [1] doesn't really address this issue. It assumes [2] that an environment record with the parameter bindings can be created and captured at loc 1 without triggering any exceptions and it defers instantiation and execution of all of the generator's code body until the first "next" call.

Sure, the proposal predates parameter default values and has not been updated to take them into account. Thanks for raising this issue.

However, the semantics of parameter default value initialization can result in exceptions and more closely links argument instantiations with the actual activation of the generator function. You can't really build the parameter bindings without performing declaration instantiation for the function.

Agreed, that's absolutely true (no "really" about it -- or rather, "for real" ;-).

Arguably, exceptions involving initialization of the the parameter would be more meaningful at loc 1 (loc 2 could be very remote and in somebody else's code). However, the body of the generator may contain other explicit initialization code and there really is no way to identify and move evaluation of that code (and its possible exceptions) to loc 1. So, even if parameter initialization exceptions are made observable at loc 1 there may still be other logical initialization-related exceptions that will onlybe observable at loc 2.

That's fine because those loc2 exceptions originate from code explicitly in the generator function's body -- after the opening brace.

Which semantics do we want? Deferring parameter initialization to loc 2 may be somewhat simpler from a specification perspective. It's hard to say which would have bigger implementation impact.

The implementation is not hard in any event, as it involves an implicit (yield); statement inserted before the first evaluation of anything explicitly in the body.

Possible side effects from "binding arguments to parameters" should be observed early, at loc 1, for the reason you give: late means something will be missed, or happen out of order. For me this is the clincher.

Cc'ing Jason for his thoughts.

# Brendan Eich (13 years ago)

Brendan Eich wrote:

However, the semantics of parameter default value initialization can result in exceptions and more closely links argument instantiations with the actual activation of the generator function. You can't really build the parameter bindings without performing declaration instantiation for the function.

Agreed, that's absolutely true (no "really" about it -- or rather, "for real" ;-).

And (I should have written), if your point was that we must bind all var and top level function/let/const declarations before the implicit yield; inserted "after the opening brace", I don't see a problem. Hoisted functions with usable initial values, hoisted vars with undefined initial values, and temporal dead zones for the let and const declarations that are at top (generator body) level. Right?

# Rick Waldron (13 years ago)

On Saturday, September 8, 2012 at 4:01 PM, Allen Wirfs-Brock wrote:

This is the first of probably several semantic issues WRT generator that will need to be resolved in the spec.

Here is the user level issue:

let thrower = {throw () {throw "exception"}}; function * G(arg = thrower.throw()) { //possibly other explicit initialization code which could also throw yield arg; } let g = G(); //do we see "exception" here? (loc 1) ... //possibly many lines of code g.next(); //or do we see it here (loc 2)

If this weren't a generator, we'd see the exception at loc1, correct? Looking at your example, before I read further, I would assume loc 1

# Brendan Eich (13 years ago)

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; }

js> f()

function g(){}

So function g is hoisted and a defaults to it, as expected.

The trivial generator variant, i.e., what you get by putting a * after function and before f, above:

js> function* f(a = g) { function g() {}; return a; }

js> var it = f();

js> it.next();

function g(){}

(note well, not yet implemented in SpiderMonkey) should behave the same, or crazypants.

# Allen Wirfs-Brock (13 years ago)

On Sep 8, 2012, at 3:20 PM, Brendan Eich wrote:

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; } js> f() function g(){}

So function g is hoisted and a defaults to it, as expected.

While I agree that the above is reasonable behavior. It wasn't the consensus that was reach earlier this year at the Jan. (or may March??) meeting. What we agreed upon is that default value expressions in parameter lists have visibility to the left and upward in scope but do not have visibility of anything declared within the curlies that surround the body. So, in the above example, g should be a reference error when evaluated as a default value initializer.

As the NOTE in step 9 of 10-5-3 says:

NOTE	Binding Initialisation for formals is performed prior to instantiating any non-parameter declarations in order to ensure that any such local declarations are not visible to any parameter Initialisation code that may be evaluated.
# Brendan Eich (13 years ago)

Sorry to over-reply. This does work in SpiderMonkey:

js> function f(a = g) { function g(){}; yield a; }

js> var it = f()

js> it.next()

function g(){}

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:20 PM, Brendan Eich wrote:

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; } js> f() function g(){}

So function g is hoisted and a defaults to it, as expected.

While I agree that the above is reasonable behavior. It wasn't the consensus that was reach earlier this year at the Jan. (or may March??) meeting. What we agreed upon is that default value expressions in parameter lists have visibility to the left and upward in scope but do not have visibility of anything declared within the curlies that surround the body. So, in the above example, g should be a reference error when evaluated as a default value initializer.

As the NOTE in step 9 of 10-5-3 says:

NOTE Binding Initialisation for formals is performed prior to instantiating any non-parameter declarations in order to ensure that any such local declarations are not visible to any parameter Initialisation code that may be evaluated.

You're right, I had forgotten that.

Is it well-motivated other than in the naive left-to-right sense, which function hoisting already violates? Perhaps, because parameters to the right are not yet bound.

If so, then I can live with it, and g in the f(a = g) bit above would use an outer g, if any (or throw on undefined g).

But this still does not mean the implicit |yield;| at entry to generator function should be other than observably "after" parameter defaulting, so that we get the throw at loc 1. Right?

IOW, this is a tangent, good to nail down (sorry for prying it up), but not a crucial experiment for generator default parameter semantics.

# Brendan Eich (13 years ago)

Here's an attempt at a crucial experiment:

js> var g = 'now'

js> function f(a = g) { yield a; }

js> var it = f()

js> g = 'later' "later" js> it.next() "later"

So I'm wrong, SpiderMonkey inserts the impliciti |yield;| before parameter defaulting. Rats.

It may not matter, but I still think early beats late. That Python binds in the definition context may still be relevant, even though Python evaluates parameter default values at function definition evaluation time (way early), insofar as translating the above to Python:

Python 2.6.6 (r266:84292, May 28 2011, 19:08:00) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

g = 'now' def f(a = g): ... yield a ...

it = f() g = 'later' it.next() 'now'

does indeed result in 'now'.

# Allen Wirfs-Brock (13 years ago)

On Sep 8, 2012, at 3:51 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:20 PM, Brendan Eich wrote:

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; } js> f() function g(){}

So function g is hoisted and a defaults to it, as expected.

While I agree that the above is reasonable behavior. It wasn't the consensus that was reach earlier this year at the Jan. (or may March??) meeting. What we agreed upon is that default value expressions in parameter lists have visibility to the left and upward in scope but do not have visibility of anything declared within the curlies that surround the body. So, in the above example, g should be a reference error when evaluated as a default value initializer.

As the NOTE in step 9 of 10-5-3 says:

NOTE Binding Initialisation for formals is performed prior to instantiating any non-parameter declarations in order to ensure that any such local declarations are not visible to any parameter Initialisation code that may be evaluated.

You're right, I had forgotten that.

Is it well-motivated other than in the naive left-to-right sense, which function hoisting already violates? Perhaps, because parameters to the right are not yet bound.

Personally, I think the consensus rules create special case anomaly in the scoping rules that it would be good to avoid. However, some of the TC39 member had a hard time accepting that the parameter list had visibility into what they perceived as a deeper level of curlies (the function body). Personally, I would prefer to just think of the parameter list declaration as being logically part of the function body and treat all declaration in that scope contour consistently.

If so, then I can live with it, and g in the f(a = g) bit above would use an outer g, if any (or throw on undefined g).

But this still does not mean the implicit |yield;| at entry to generator function should be other than observably "after" parameter defaulting, so that we get the throw at loc 1. Right?

Right. It just means have to place the initial implicit (almost) yield right at the end of generator declaration instantiation and before any user code executes.

(I have more to say about that in a separate message)

IOW, this is a tangent, good to nail down (sorry for prying it up), but not a crucial experiment for generator default parameter semantics.

Right, it's a separate issue and it's good that you identified that the FF implementation doesn't match the spec.

# Allen Wirfs-Brock (13 years ago)

On Sep 8, 2012, at 3:59 PM, Brendan Eich wrote:

Here's an attempt at a crucial experiment:

js> var g = 'now' js> function f(a = g) { yield a; } js> var it = f() js> g = 'later' "later" js> it.next() "later"

So I'm wrong, SpiderMonkey inserts the impliciti |yield;| before parameter defaulting. Rats.

It may not matter, but I still think early beats late. That Python binds in the definition context may still be relevant, even though Python evaluates parameter default values at function definition evaluation time (way early), insofar as translating the above to Python:

Python 2.6.6 (r266:84292, May 28 2011, 19:08:00) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

g = 'now' def f(a = g): ... yield a ...

it = f() g = 'later' it.next() 'now'

does indeed result in 'now'.

I think early beats late and that this concept extends to include any code within the generator function that is involved in setting up the initial state of the generator. For example, it seems quite reasonable for somebody to try to write something like:

function *dataSnapshot(aCollection) { snapshot = aCollection.captureCurrentState(); //initialization complete for (let i=0; i<snapshot.length; i++) yield snapshot[i] throw StopIteration; } ...

lets snappedItr= dataSnapshot(myData); ... ... myData.doABunchOfUpdates(); ... ... for (let d of snappedItr) {/* look at old data */} ...

Unfortunately, they will see the new updated data not the old data when they iterate. What they need is the ability to move the implicit "initialization is complete" yield from the beginning of the body to the point of the comment I provided. We don't have a way to do that and , of course, if we did it would complicate things and probably create other ways to mess things up. The only reason approach that I've come up would be to treat a yield without an associated expression as the initialization complete marker:

function *dataSnapshot(aCollection) { snapshot = aCollection.snapState(); yield; //initialization complete for (let i=0; i<snapshot.length; i++) yield snapshot[i] throw StopIteration; }

The semantics would probably be if the body does not contain such a yield, then one is auto generated at the top of the function body. So the default would be "late" (possibly include the arguments) but it could be explicitly make "early" to an arbitrary point in the body.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:51 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:20 PM, Brendan Eich wrote:

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; } js> f() function g(){}

So function g is hoisted and a defaults to it, as expected. While I agree that the above is reasonable behavior. It wasn't the consensus that was reach earlier this year at the Jan. (or may March??) meeting. What we agreed upon is that default value expressions in parameter lists have visibility to the left and upward in scope but do not have visibility of anything declared within the curlies that surround the body. So, in the above example, g should be a reference error when evaluated as a default value initializer.

As the NOTE in step 9 of 10-5-3 says:

NOTE Binding Initialisation for formals is performed prior to instantiating any non-parameter declarations in order to ensure that any such local declarations are not visible to any parameter Initialisation code that may be evaluated. You're right, I had forgotten that.

Is it well-motivated other than in the naive left-to-right sense, which function hoisting already violates? Perhaps, because parameters to the right are not yet bound.

Personally, I think the consensus rules create special case anomaly in the scoping rules that it would be good to avoid. However, some of the TC39 member had a hard time accepting that the parameter list had visibility into what they perceived as a deeper level of curlies (the function body). Personally, I would prefer to just think of the parameter list declaration as being logically part of the function body and treat all declaration in that scope contour consistently.

I agree. ES1-5 had no observable scope or hoisting boundaries among parameters, or between parameters and body top-level bindings.

I say we re-raise this at the TC39 meeting week after next. What say you?

If so, then I can live with it, and g in the f(a = g) bit above would use an outer g, if any (or throw on undefined g).

But this still does not mean the implicit |yield;| at entry to generator function should be other than observably "after" parameter defaulting, so that we get the throw at loc 1. Right?

Right. It just means have to place the initial implicit (almost) yield right at the end of generator declaration

s/generator/parameter/

instantiation and before any user code executes.

(I have more to say about that in a separate message)

IOW, this is a tangent, good to nail down (sorry for prying it up), but not a crucial experiment for generator default parameter semantics.

Right, it's a separate issue and it's good that you identified that the FF implementation doesn't match the spec.

Indeed the prototype default parameter implementation in Firefox (SpiderMonkey) has more of what you and I prefer:

js> function f(a, b=a, c=d, d=d) {return [a,b,c,d]}

js> var d='global'

js> f(1) [1, 1, (void 0), (void 0)] js> f(1,2) [1, 2, (void 0), (void 0)] js> f(1,2,3) [1, 2, 3, (void 0)] js> f(1,2,3,4) [1, 2, 3, 4]

Note no outer 'd' shows up in the result of f(1,2). When SpiderMonkey implements support for undefined as the default-triggering sentinel, then we'd have

js> f(1, 2, void 3) [1, 2, (void 0), (void 0)] js> f(1, 2, void 3, 4) [1, 2, 4, 4]

which I prefer on the simpler-to-have-one-contour basis. What do you think?

The TC39 meeting that favored let* (Scheme let*, I mean) binding of parameters seems unnecessarily complicated, without a motivating use-case, and alient to the rest of the language.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:59 PM, Brendan Eich wrote:

Here's an attempt at a crucial experiment:

js> var g = 'now' js> function f(a = g) { yield a; } js> var it = f() js> g = 'later' "later" js> it.next() "later"

So I'm wrong, SpiderMonkey inserts the impliciti |yield;| before parameter defaulting. Rats.

It may not matter, but I still think early beats late. That Python binds in the definition context may still be relevant, even though Python evaluates parameter default values at function definition evaluation time (way early), insofar as translating the above to Python:

Python 2.6.6 (r266:84292, May 28 2011, 19:08:00) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

g = 'now' def f(a = g): ... yield a ...

it = f() g = 'later' it.next() 'now'

does indeed result in 'now'.

I think early beats late and that this concept extends to include any code within the generator function that is involved in setting up the initial state of the generator.

So far, so good, but I think the rest of your message mixes shared mutable objects in the heap (a JS feature, not a bug) with generators and then tries to patch generators to avoid the consequences of the shared-mutable-objects feature.

For example, it seems quite reasonable for somebody to try to write something like:

function *dataSnapshot(aCollection) { snapshot = aCollection.captureCurrentState();

(missing var or let -- implicit global...)

//initialization complete
for (let i=0; i<snapshot.length; i++) yield snapshot[i]
throw StopIteration;

This last throw statement is unnecessary and no one should write it.

} ...

lets snappedItr= dataSnapshot(myData); ... ... myData.doABunchOfUpdates();

The contract of dataSnapshot wants a deep copy of aCollection, so it must make one. Fortunately if we bind parameters before the implicit yield, it can:

function dataSnapshot(aCollection) { var snapshot = aColllection.clone(); return (function* () { for (let i = 0; i < snapshot.length; i++) yield snapshot[i]; })(); }

There's no need to add more magic to generators. Just use a function and a generator together. Compositionality.

... ... for (let d of snappedItr) {/* look at old data */} ...

Unfortunately, they will see the new updated data not the old data when they iterate. What they need is the ability to move the implicit "initialization is complete" yield from the beginning of the body to the point of the comment I provided. We don't have a way to do that and , of course, if we did it would complicate things and probably create other ways to mess things up. The only reason approach that I've come up would be to treat a yield without an associated expression as the initialization complete marker:

function *dataSnapshot(aCollection) { snapshot = aCollection.snapState(); yield;

No, this yield; is well-defined and must mean (yield void 0); -- it should not be reinterpreted just because it is first.

//initialization complete
for (let i=0; i<snapshot.length; i++) yield snapshot[i]
throw StopIteration;

}

The semantics would probably be if the body does not contain such a yield, then one is auto generated at the top of the function body. So the default would be "late" (possibly include the arguments) but it could be explicitly make "early" to an arbitrary point in the body.

We still have the choice of the implicit yield going after argument defaulting, not before. SpiderMonkey implements before, though, and my own argument from compositionality, above, makes it more clear to me that we should go with implicit-yield-first. This makes contrived throwing parameter default expressions throw "late" but if that matters, use a function combined with a generator.

In this thread, you and I have agreed on simpler scope/hoisting/defaulting/initialisting a la ES1-5. I think on reflection that this argues for the implicit yield in a generator function going before anything with observable effects, including argument defaulting.

# Allen Wirfs-Brock (13 years ago)

On Sep 8, 2012, at 5:11 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:51 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:20 PM, Brendan Eich wrote:

SpiderMonkey (Firefox 15 and newer has default parameters):

js> function f(a = g) { function g(){}; return a; } js> f() function g(){}

So function g is hoisted and a defaults to it, as expected. While I agree that the above is reasonable behavior. It wasn't the consensus that was reach earlier this year at the Jan. (or may March??) meeting. What we agreed upon is that default value expressions in parameter lists have visibility to the left and upward in scope but do not have visibility of anything declared within the curlies that surround the body. So, in the above example, g should be a reference error when evaluated as a default value initializer.

As the NOTE in step 9 of 10-5-3 says:

NOTE Binding Initialisation for formals is performed prior to instantiating any non-parameter declarations in order to ensure that any such local declarations are not visible to any parameter Initialisation code that may be evaluated. You're right, I had forgotten that.

Is it well-motivated other than in the naive left-to-right sense, which function hoisting already violates? Perhaps, because parameters to the right are not yet bound.

Personally, I think the consensus rules create special case anomaly in the scoping rules that it would be good to avoid. However, some of the TC39 member had a hard time accepting that the parameter list had visibility into what they perceived as a deeper level of curlies (the function body). Personally, I would prefer to just think of the parameter list declaration as being logically part of the function body and treat all declaration in that scope contour consistently.

I agree. ES1-5 had no observable scope or hoisting boundaries among parameters, or between parameters and body top-level bindings.

I say we re-raise this at the TC39 meeting week after next. What say you?

I'm in...

... js> function f(a, b=a, c=d, d=d) {return [a,b,c,d]} js> var d='global' ... Note no outer 'd' shows up in the result of f(1,2). When SpiderMonkey implements support for undefined as the default-triggering sentinel, then we'd have

js> f(1, 2, void 3)

[1, 2, (void 0), (void 0)] js> f(1, 2, void 3, 4) [1, 2, 4, 4]

which I prefer on the simpler-to-have-one-contour basis. What do you think?

I'd prefer to consider the RHS references to d in the third and fourth parameters to be within d's TDZ. Of course, that means that parameters (at least ones with initializers) are treated more like lets than vars. That might be considers a departure from ES<=5.1 but I think it is more consistent with the new declaration semantics.

# Allen Wirfs-Brock (13 years ago)

On Sep 8, 2012, at 5:20 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 3:59 PM, Brendan Eich wrote:

Here's an attempt at a crucial experiment:

js> var g = 'now' js> function f(a = g) { yield a; } js> var it = f() js> g = 'later' "later" js> it.next() "later"

So I'm wrong, SpiderMonkey inserts the impliciti |yield;| before parameter defaulting. Rats.

It may not matter, but I still think early beats late. That Python binds in the definition context may still be relevant, even though Python evaluates parameter default values at function definition evaluation time (way early), insofar as translating the above to Python:

Python 2.6.6 (r266:84292, May 28 2011, 19:08:00) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

g = 'now' def f(a = g): ... yield a ...

it = f() g = 'later' it.next() 'now'

does indeed result in 'now'.

I think early beats late and that this concept extends to include any code within the generator function that is involved in setting up the initial state of the generator.

So far, so good, but I think the rest of your message mixes shared mutable objects in the heap (a JS feature, not a bug) with generators and then tries to patch generators to avoid the consequences of the shared-mutable-objects feature.

sounds like real-life JS programming...

For example, it seems quite reasonable for somebody to try to write something like:

function *dataSnapshot(aCollection) { snapshot = aCollection.captureCurrentState();

(missing var or let -- implicit global...)

oops

//initialization complete for (let i=0; i<snapshot.length; i++) yield snapshot[i] throw StopIteration;

This last throw statement is unnecessary and no one should write it.

yes, I blurred over that when rereading the generator proposal

} ...

lets snappedItr= dataSnapshot(myData); ... ... myData.doABunchOfUpdates();

The contract of dataSnapshot wants a deep copy of aCollection, so it must make one. Fortunately if we bind parameters before the implicit yield, it can:

function dataSnapshot(aCollection) { var snapshot = aColllection.clone(); return (function* () { for (let i = 0; i < snapshot.length; i++) yield snapshot[i]; })(); }

There's no need to add more magic to generators. Just use a function and a generator together. Compositionality.

Yes, that does it. But it's a pattern will will have to learn or discover. It may be obvious if you are thinking functionally but, it may be less obvious if you are thinking in terms of objects and method. Particularly if you expect that methods that return generators (I just spec'ed a bunch of those this morning) will have their names prefixed with * when you declare them. (I may be starting to double that allowing concise method syntax for generator is a good idea).

... ... for (let d of snappedItr) {/* look at old data */} ...

Unfortunately, they will see the new updated data not the old data when they iterate. What they need is the ability to move the implicit "initialization is complete" yield from the beginning of the body to the point of the comment I provided. We don't have a way to do that and , of course, if we did it would complicate things and probably create other ways to mess things up. The only reason approach that I've come up would be to treat a yield without an associated expression as the initialization complete marker:

function *dataSnapshot(aCollection) { snapshot = aCollection.snapState(); yield;

No, this yield; is well-defined and must mean (yield void 0); -- it should not be reinterpreted just because it is first.

The wiki proposal doesn't appear to say that. That's why I thought the yield; form was available.

//initialization complete for (let i=0; i<snapshot.length; i++) yield snapshot[i] throw StopIteration; }

The semantics would probably be if the body does not contain such a yield, then one is auto generated at the top of the function body. So the default would be "late" (possibly include the arguments) but it could be explicitly make "early" to an arbitrary point in the body.

We still have the choice of the implicit yield going after argument defaulting, not before. SpiderMonkey implements before, though, and my own argument from compositionality, above, makes it more clear to me that we should go with implicit-yield-first. This makes contrived throwing parameter default expressions throw "late" but if that matters, use a function combined with a generator.

In this thread, you and I have agreed on simpler scope/hoisting/defaulting/initialisting a la ES1-5. I think on reflection that this argues for the implicit yield in a generator function going before anything with observable effects, including argument defaulting.

I agree, once you start doing some of the initialization early it leads you to want to do more of it within the generator. In practice, make parameter initialization occur late it means that the raw argument list needs to be capture as part of the creation of the generator instance.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

On Sep 8, 2012, at 5:20 PM, Brendan Eich wrote:

The contract of dataSnapshot wants a deep copy of aCollection, so it must make one. Fortunately if we bind parameters before the implicit yield, it can:

function dataSnapshot(aCollection) { var snapshot = aCollection.clone(); return (function* () { for (let i = 0; i< snapshot.length; i++) yield snapshot[i]; })(); }

There's no need to add more magic to generators. Just use a function and a generator together. Compositionality.

Yes, that does it. But it's a pattern will will have to learn or discover. It may be obvious if you are thinking functionally but, it may be less obvious if you are thinking in terms of objects and method.

The same situation arises with closures, no generators needed:

function dataSnapshot(aCollection) { var snapshot = aCollection.clone(); let i = 0; return { next: function () { if (++i == snapshot.length) throw StopIteration; return snapshot[i]; } }; }

Anyone trying to avoid the .clone() call would be dispapointed. Anyone trying to avoid the extra level of function nesting would be disappointed. There is irreducible complexity here

Particularly if you expect that methods that return generators (I just spec'ed a bunch of those this morning) will have their names prefixed with * when you declare them.

When you write "method" here, I think you mean something different from what most people who use "method" when writing JS mean. It's functions all the way down :-P.

(I may be starting to double that allowing concise method syntax for generator is a good idea).

Could be. Not a big deal, but I don't see this case as relevant. Perhaps you should fully define your meaning of "method".

No, this yield; is well-defined and must mean (yield void 0); -- it should not be reinterpreted just because it is first.

The wiki proposal doesn't appear to say that. That's why I thought the yield; form was available.

That's a gap in the wiki'ed page. As in Python, and in SpiderMonkey for almost six years, yield like return has an operation operand. Unlike return, and after Python 2.5, yield is an operator (low-precedence, at assignment level).

I'd better review your draft specs -- shoot me some copy? Thanks.

In this thread, you and I have agreed on simpler scope/hoisting/defaulting/initialisting a la ES1-5. I think on reflection that this argues for the implicit yield in a generator function going before anything with observable effects, including argument defaulting.

I agree, once you start doing some of the initialization early it leads you to want to do more of it within the generator. In practice, make parameter initialization occur late it means that the raw argument list needs to be capture as part of the creation of the generator instance.

Yes. This is where we already parted company with Python, over parameter default values being computed on entry to the function, not just assigned then ("bound" in Python's one of many uses of "bind") from values evaluated at the function definition site.

It seems strictly simply to have the implicit |yield;| first, and to do defaulting after.

Temporal dead zone would follow if parameters were let-like, but I don't believe they can be. They're var-like, for at least these reasons:

  1. In non-strict functions (1JS for-evah! ;-) arguments[0] aliases x in funtion f(x) { arguments[i] = 42; return x; }. We do not want let bindings to be aliased!

  2. In non-strict functions, function f(x) { var x; ... } is legal. But in ES6, function f() {let x; var x;} is not legal.

# Brendan Eich (13 years ago)

Brendan Eich wrote:

That's a gap in the wiki'ed page. As in Python, and in SpiderMonkey for almost six years, yield like return has an operation

optional

operand. Unlike return, and after Python 2.5, yield is an operator (low-precedence, at assignment level).

"Send from my iPad", LOL

# Brendan Eich (13 years ago)

Brendan Eich wrote:

function dataSnapshot(aCollection) { var snapshot = aCollection.clone(); let i = 0;

let i = -1;

rather,

return { next: function () { if (++i == snapshot.length) throw StopIteration; return snapshot[i]; } }; }

or this might run a while!

# Brendan Eich (13 years ago)

I wanted to leave a clean example (no var and let mixing, either):

function dataSnapshot(aCollection) { let snapshot = aCollection.clone(); let i = 0; return { next: function () { if (i == snapshot.length) throw StopIteration; return snapshot[i++]; } }; }

(I usually prefer post-increment for loop-ish constructs. Old C hacker here.)

Again, anyone trying to avoid the .clone() call would be disappointed. Anyone trying to avoid the extra level of function nesting would be disappointed. There is irreducible complexity here.

But the generator form is still winning:

function dataSnapshot(aCollection) { let snapshot = aCollection.clone(); return function*() { for (let i = 0; i < snapshot.length; i++){ yield snapshot[i]; } }(); }

IMHO.

# Allen Wirfs-Brock (13 years ago)

On Sep 9, 2012, at 3:32 PM, Brendan Eich wrote:

I wanted to leave a clean example (no var and let mixing, either):

function dataSnapshot(aCollection) { let snapshot = aCollection.clone(); let i = 0; return { next: function () { if (i == snapshot.length) throw StopIteration; return snapshot[i++]; } }; }

(I usually prefer post-increment for loop-ish constructs. Old C hacker here.)

Again, anyone trying to avoid the .clone() call would be disappointed. Anyone trying to avoid the extra level of function nesting would be disappointed. There is irreducible complexity here.

But the generator form is still winning:

function dataSnapshot(aCollection) { let snapshot = aCollection.clone(); return function*() { for (let i = 0; i < snapshot.length; i++){ yield snapshot[i]; } }(); }

What's going on here seems clearer to me, if I think about a generator as a unusual kind of constructor rather than an unusual kind of function that can be suspended and resumed. From that perspective a call to the generator is really a "constructor called as a function" that implicitly does a new, much like several other built-in constructors. Thinking about it that way, you might alternatively write your dataSnapshot function as:

function dataSnapshot(aCollection) { let snapshot = aCollection.clone(); return new function*() { for (let i = 0; i < snapshot.length; i++){ yield snapshot[i]; } }; }

Is new'ing generators intended to be legal?

Instances of generators are iterator objects that implement a state machine based upon the code provided as the body of the constructor. I shouldn't be thinking about the call to the generator as returning a suspended function activation, instead I should be think of it as simply return an object that implements the iterator interface.

My concern about concise generator method syntax is that it makes the generator-ness of the method appear to be an important part of the class instance interface when it should really be an implementation detail. Consider as class such as:

class DataCollection extends Collection { *@iterator() { ... //implementation details } }

The interface of this class should be described as having an @iterator method that returns an object that implements the abstract iterator interface. Whether that object is an instance of a generator or a non-generator based iterator object shouldn't be relevant to clients of this class. It's an implementation detail that can be subject to change within impacting clients. However, the appearance of * as the first character of the method definition gives it an unjustified importance. I might actually prefer the above written as:

class DataCollection extends Collection { @iterator() { return new function*() { ... //implementation details } } }

# Jason Orendorff (13 years ago)

On Sat, Sep 8, 2012 at 3:01 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

This is the first of probably several semantic issues WRT generator that will need to be resolved in the spec.

I mentioned this to David Herman when we started implementing default parameters.

What I think is that default parameters should be specified to be as much like the following desugaring as possible:

function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}

Here is my rationale:

  • The question is really about the semantics of default params, not generators.
  • Default parameters are a convenience feature: certainly welcome and useful, but not powerful, and thus by the principle of least astonishment they should in fact be skin-deep.

I think this is what Brendan's saying as well.

SpiderMonkey's current implementation of default params isn't quite right in a few ways. I'll file some bugs and we'll get after it.

# Jason Orendorff (13 years ago)

On Sun, Sep 9, 2012 at 8:50 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

What I think is that default parameters should be specified to be as much like the following desugaring as possible:

function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}

In case it isn't clear what I'm saying here, I think the same desugaring should hold for generators. Exceptions would then be deferred to "loc 2" in Allen's example.

# Brendan Eich (13 years ago)

Jason Orendorff wrote:

On Sun, Sep 9, 2012 at 8:50 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

What I think is that default parameters should be specified to be as much like the following desugaring as possible:

 function f(x=EXPR1, y=EXPR2) { BODY }
 ===>
 function f(x, y) {
     if (x === void 0) x = EXPR1;
     if (y === void 0) y = EXPR2;
     BODY
 }

In case it isn't clear what I'm saying here, I think the same desugaring should hold for generators. Exceptions would then be deferred to "loc 2" in Allen's example.

Agreed. My loc 1 initial reaction was based on the shallow preference for early vs. late errors in the contrived "thrower" case.

# Rick Waldron (13 years ago)

On Mon, Sep 10, 2012 at 1:08 PM, Brendan Eich <brendan at mozilla.com> wrote:

Jason Orendorff wrote:

On Sun, Sep 9, 2012 at 8:50 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

What I think is that default parameters should be specified to be as much like the following desugaring as possible:

 function f(x=EXPR1, y=EXPR2) { BODY }
 ===>
 function f(x, y) {
     if (x === void 0) x = EXPR1;
     if (y === void 0) y = EXPR2;
     BODY
 }

In case it isn't clear what I'm saying here, I think the same desugaring should hold for generators. Exceptions would then be deferred to "loc 2" in Allen's example.

Agreed. My loc 1 initial reaction was based on the shallow preference for early vs. late errors in the contrived "thrower"

When you present the question without the generator + thrower, it makes perfect sense that the exception would occur at loc 2 and I agree with this behaviour as well.

Now for the conjecture, feel free to skip the following two paragraphs...

While I don't think "people won't get it" is an argument for anything, as a representative of all web developers, I'm inclined to hold my position that the exception will be expected at the call site, ie. loc 1. I say this because I suspect that most developers will view the call site as "Part 1" of the generator and the yields as "Part 2".

Now that I've made that case, I'd like to also state that it's perfectly ok for us to say that generators are allowed to have different behaviour, because they are a different thing.

# Kevin Smith (13 years ago)
function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}

In case it isn't clear what I'm saying here, I think the same desugaring should hold for generators. Exceptions would then be deferred to "loc 2" in Allen's example.

I'm not so sure - the desugaring above would mean that default expressions would have visibility across curly-brace boundaries, which I find to be quite surprising. That's what makes "var" so weird, after all. The conceptual link between curlies and visibility is inherent in C-like languages.

Moreover, curlies surrounding a function body have an even stronger connotation of isolation than statement block curlies.

IMO the expectation will be that default parameter values are evaluated when the generator function is called. Any exceptions that occur in the generator body prior to the first yield will be expected at the first call to next().

# Brendan Eich (13 years ago)

Kevin Smith wrote:

function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}
In case it isn't clear what I'm saying here, I think the same
desugaring should hold for generators. Exceptions would then be
deferred to "loc 2" in Allen's example.

I'm not so sure - the desugaring above would mean that default expressions would have visibility across curly-brace boundaries, which I find to be quite surprising. That's what makes "var" so weird, after all. The conceptual link between curlies and visibility is inherent in C-like languages.

But as I pointed out, we have problems already in JS:

function f(x) {var x; ...}

the var x restates the formal parameter. It does not shadow it.

IMO the expectation will be that default parameter values are evaluated when the generator function is called.

Python "binds" then, but evaluates the expressions at generation definition evaluation time.

Expectations may vary, and I still concur with Jason that the simplest and thinnest "skin" is best here.

Any exceptions that occur in the generator body prior to the first yield will be expected at the first call to next().

That would be loc 2, though -- seems to go against what you wrote earlier.

It also is not how generators work in Python or JS1.7+:

Python 2.6.6 (r266:84292, May 28 2011, 19:08:00) [GCC 4.2.1 (Apple Inc. build 5664)] on darwin Type "help", "copyright", "credits" or "license" for more information.

def g(): ... raise BaseException('foo') ... yield ...

i = g() i.next()

Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in g

BaseException: foo

js> function g(){throw 42; yield}

js> i = g() ({}) js> i.next()

uncaught exception: 42

The exception comes after the first .next() that starts from the implicit yield point.

The design doesn't work if there's no implicit yield first. The rest of what's shown above follows from that.

# Brendan Eich (13 years ago)

Brendan Eich wrote:

Python "binds" then, but evaluates the expressions at generation definition evaluation time.

"generator definition evaluation time" of course -- -ion on the brain (IonMonkey!).

# Brendan Eich (13 years ago)

Brendan Eich wrote:

Any exceptions that occur in the generator body prior to the first yield will be expected at the first call to next().

That would be loc 2, though -- seems to go against what you wrote earlier.

I stick by this! ;-)

It also is not how generators work in Python or JS1.7+:

Sorry for rehashing something else here, I see your point. If we want loc 1 (if you do ;-), then there could be a pending exception from a thrower() default parameter. That's where I started, up-thread. But this is strictly more complicated.

Morever, the opening left brace does not make a scope boundary between formals and local vars. It doesn't even make a hoisting boundary for functions:

js> function f(x){ function x(){}; return x; }

js> f(42)

function x(){}

Sorry, C fans (sorry, me!).

# Jason Orendorff (13 years ago)

On Mon, Sep 10, 2012 at 12:50 PM, Kevin Smith <khs4473 at gmail.com> wrote:

function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}

I'm not so sure - the desugaring above would mean that default expressions would have visibility across curly-brace boundaries, which I find to be quite surprising.

It is surprising. It could even bite unwary programmers. But in what scope do you propose to evaluate default-param expressions?

# Tobie Langel (13 years ago)

On Sep 10, 2012, at 9:48 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

On Mon, Sep 10, 2012 at 12:50 PM, Kevin Smith <khs4473 at gmail.com> wrote:

function f(x=EXPR1, y=EXPR2) { BODY } ===> function f(x, y) { if (x === void 0) x = EXPR1; if (y === void 0) y = EXPR2; BODY }

I'm not so sure - the desugaring above would mean that default expressions would have visibility across curly-brace boundaries, which I find to be quite surprising.

It is surprising. It could even bite unwary programmers. But in what scope do you propose to evaluate default-param expressions?

In their lexical scope.

# Kevin Smith (13 years ago)

But as I pointed out, we have problems already in JS:

function f(x) {var x; ...}

the var x restates the formal parameter. It does not shadow it.

Ah - observable here:

> function crazypants(x) { var x; return x; }
undefined
> crazypants(123);
123

If it shadowed, we would expect undefined. Even so, information can only flow from left to right across the curly boundary. That's expected. But if we allow parameter default expressions to have visibility into the function body, then we have bidirectional flow across curlies.

# Brendan Eich (13 years ago)

Brendan Eich wrote:

Morever, the opening left brace does not make a scope boundary between formals and local vars. It doesn't even make a hoisting boundary for functions:

js> function f(x){ function x(){}; return x; } js> f(42) function x(){}

I plead jetlag. Still not quite what I wanted. Try this:

js> function f(x){ function x(){}; return [arguments[0], x]; }

js> f(42) [function x(){}, function x(){}]

If hoisting stopped at the { then one might expect, even with one scope contour in which to bind 'x', that arguments[0] would be 42. Not so, but of course with arguments a magic object whose element accessors alias formal parameters, this still doesn't prove enough.

But I claim it does show backward compatibility constraints that we must satisfy, while also minimizing the complexity of the user's cognitive model, and of the spec. The sweet spot for me is to fold formal parameters into the body, as far as default parameters and anything else involving expression evaluation go.

Yes, we could complicate the spec with temporal dead zones for parameters to the right, so that

function f(a, b=a, c=d, d=42) {...}

would either find an outer 'd' for the c parameter default value, or throw on read from a temporally dead 'd' parameter. But given the var-like nature of args in ES1-5:

function f(x) { var x; return x; } // identity function function f(x) { arguments[i]=42; return x; } // 42 if i=0

the simplest, or least mentally burdensome in full (avoiding splitting cases into compatible and new), way forward is for parameters to be var-like bindings in the same scope as top level var and function (but not let), and for default parameters to be evaluated as if in the body, first thing after any implicit yield (if in a generator function), as Jason suggested.

# Brendan Eich (13 years ago)

Tobie Langel wrote:

On Sep 10, 2012, at 9:48 PM, Jason Orendorff<jason.orendorff at gmail.com> wrote:

On Mon, Sep 10, 2012 at 12:50 PM, Kevin Smith<khs4473 at gmail.com> wrote:

function f(x=EXPR1, y=EXPR2) { BODY }
===>
function f(x, y) {
    if (x === void 0) x = EXPR1;
    if (y === void 0) y = EXPR2;
    BODY
}

I'm not so sure - the desugaring above would mean that default expressions

would have visibility across curly-brace boundaries, which I find to be quite surprising. It is surprising. It could even bite unwary programmers. But in what scope do you propose to evaluate default-param expressions?

In their lexical scope.

Which one? If you mean let*, so that

function f(a = a, b = b*a) { return [a, b]; } var a = 42; console.log(f()) // [42, NaN] console.log(f(1)) // [1, NaN] console.log(f(1,2)) // [1, 2]

works, that's one "lexical scope" approach. But it is novel to JS and seen nowhere else in the language (in particular, there's no orthogonal analogue of let* in Scheme).

If you mean let with temporal dead zone (a kind of let rec, or let with hoisting and no read before initialization), then the reads of a and b above before they are initialized for f() and f(1) would throw.

What's more, as Allen reminded me tonight, "lexical scope" can be made to work with magic arguments objects, which alias the lexical bindings through spec and implementation magical back doors. But this violates the spirit of lexical scope. Do you really want to claim formal parameters have lexical scope given such junk as:

function f(a, b = arguments[0]) { arguments[0] = 99; return [a, b]; } console.log(f(1)) // [99, 1] console.log(f(1, 2)) // [99, 2]

?

We could forbid arguments from being used to mean f's activation's arguments object in parameter default values. But that still leaves arguments[0] = 99 setting "lexically scoped" x to 99. This is required by backward compatibility, and I argue it makes x var-like, not let-like or lexically scoped -- even though one could work magic via arguments object getters and setters, in the spec.

Implementations want let and anything let-like to be optimized assuming no such aliasing. I think users want that too, for bug reduction and readability.

# Brendan Eich (13 years ago)

Kevin Smith wrote:

But as I pointed out, we have problems already in JS:

  function f(x) {var x; ...}

the var x restates the formal parameter. It does not shadow it.

Ah - observable here:

function crazypants(x) { var x; return x; } undefined crazypants(123); 123

If it shadowed, we would expect undefined. Even so, information can only flow from left to right across the curly boundary.

Not true of curlies in general in JS as we know and love it:

function f(x) { console.log(x); if (truthy()) { with ({x: 'ha ha'}) { var x = 'ho ho'; console.log(x); } } function x(){} console.log(x); }

That's expected. But if we allow parameter default expressions to have visibility into the function body, then we have bidirectional flow across curlies.

In the above function f, which is pure ES3 (assume truthy() returns only truthy values and console.log works as expected), the hoisting of function x means f logs 'function x(){}' then 'ho ho' then 'function x(){}'.

If you change the with head's object to have property y not x, then f logs 'function x(){}', then 'ho ho', then 'ho ho'.

In these cases there is information flowing right to left, even across unbalanced left brace!

This is JS. We can't change it. Are default parameters really the place to make a new last stand for something different?

# Jason Orendorff (13 years ago)

On Mon, Sep 10, 2012 at 2:59 PM, Tobie Langel <tobie.langel at gmail.com> wrote:

On Sep 10, 2012, at 9:48 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

On Mon, Sep 10, 2012 at 12:50 PM, Kevin Smith <khs4473 at gmail.com> wrote:

function f(x=EXPR1, y=EXPR2) { BODY } ===> function f(x, y) { if (x === void 0) x = EXPR1; if (y === void 0) y = EXPR2; BODY }

I'm not so sure - the desugaring above would mean that default expressions would have visibility across curly-brace boundaries, which I find to be quite surprising.

It is surprising. It could even bite unwary programmers. But in what scope do you propose to evaluate default-param expressions?

In their lexical scope.

This isn't a complete answer. As it stands, the lexical scope of arguments is the same as the scope of everything else in the function body. Which leaves us with exactly what I was saying earlier.

# Brendan Eich (13 years ago)

Oops, missing one line below:

Brendan Eich wrote:

Which one? If you mean let*, so that

function f(a = a, b = b*a) { return [a, b]; } var a = 42;

var b;

console.log(f()) // [42, NaN] console.log(f(1)) // [1, NaN] console.log(f(1,2)) // [1, 2]

Otherwise b*a will throw on unbound b.

# Jason Orendorff (13 years ago)

(re-adding the list)

On Mon, Sep 10, 2012 at 5:20 PM, Tobie Langel <tobie.langel at gmail.com> wrote:

Yes, sorry for being unclear. What I meant to say was that I would expect EXPR1 and EXPR2 to have been evaluated within the lexical scope of f() at the time of function declaration, in source order, so:

That is how it works in Python, and it's astonishing when the default-expression produces something mutable:

def getNames(target=[]):
    target.append('kaitlin')
    return target

getNames()    # returns ['kaitlin']
getNames()    # returns the same list, which now contains

['kaitlin', 'kaitlin']

It also means that later arguments' default-expressions can't use earlier arguments' values, because at the time the default-expression is evaluated, those arguments don't have values yet.

C++ evaluates the default-expression each time the value is needed. This seems more sensible to me and it's what has been specified (and implemented in SM) for ES6.

var FOO = 2;

function f(y = FOO + 3) { return y; }

f();

5 FOO = 45;

45 f();

5

This example doesn't bring up any scoping issues, though. It only brings up timing issues. I think those are pretty well settled.

# Dmitry Soshnikov (13 years ago)

On Mon, Sep 10, 2012 at 2:47 PM, Jason Orendorff <jason.orendorff at gmail.com>wrote:

On Mon, Sep 10, 2012 at 2:59 PM, Tobie Langel <tobie.langel at gmail.com> wrote:

On Sep 10, 2012, at 9:48 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

On Mon, Sep 10, 2012 at 12:50 PM, Kevin Smith <khs4473 at gmail.com> wrote:

function f(x=EXPR1, y=EXPR2) { BODY } ===> function f(x, y) { if (x === void 0) x = EXPR1; if (y === void 0) y = EXPR2; BODY }

I'm not so sure - the desugaring above would mean that default expressions

would have visibility across curly-brace boundaries, which I find to be quite surprising.

It is surprising. It could even bite unwary programmers. But in what scope do you propose to evaluate default-param expressions?

In their lexical scope.

This isn't a complete answer. As it stands, the lexical scope of arguments is the same as the scope of everything else in the function body. Which leaves us with exactly what I was saying earlier.

I guess "lexical" here was meant the function definition scope, not the parameters.

So in your later example:

var FOO = 2;

function bar(y = FOO + 3) { return y; }

The lexical scope of "y" is obviously the function body. However, the lexical scope of "FOO" (from top-down viewing of the code) is the outer scope, where "bar" function is defined.

So this should resolve to:

bar(); // 5

However, to avoid Python's issues with definition type bindings for defaults, ES can eval every time at activation (in the bar.[[Scope]] object, i.e. directly parent, but not the activation one).

FOO = 45;

bar(); // 48

Use cases like:

function bar(y = FOO + 3) { var FOO = 10; return y; }

My look subtle, however, if again to accept lexical scope as the function definition, then FOO again should be resolved in the bar.[[Scope]], not in the bar's activation:

bar(); // still 48

P.S.: sorry, didn't have a change to read the whole discussion, apologies if it was noted already.

Dmitry

# Brendan Eich (13 years ago)

Dmitry Soshnikov wrote:

FOO = 45;

bar(); // 48

Use cases like:

function bar(y = FOO + 3) { var FOO = 10; return y; }

May look subtle, however, if again to accept lexical scope as the function definition, then FOO again should be resolved in the bar.[[Scope]], not in the bar's activation:

bar(); // still 48

It's a good point. The ES6 proposal is more expressive than Python and subsumes it. You can compose:

var FOO = 42; function bar(x, y = FOO + x) {...}

and use both activation-scope parameters and outer-scope closed-over variables as usual.

# Dmitry Soshnikov (13 years ago)

On Mon, Sep 10, 2012 at 5:38 PM, Brendan Eich <brendan at mozilla.com> wrote:

Dmitry Soshnikov wrote:

FOO = 45;

bar(); // 48

Use cases like:

function bar(y = FOO + 3) { var FOO = 10; return y; }

May look subtle, however, if again to accept lexical scope as the function definition, then FOO again should be resolved in the bar.[[Scope]], not in the bar's activation:

bar(); // still 48

It's a good point. The ES6 proposal is more expressive than Python and subsumes it. You can compose:

var FOO = 42; function bar(x, y = FOO + x) {...}

and use both activation-scope parameters and outer-scope closed-over variables as usual.

Yes, and it sounds interesting. However, then we may end up with complicated rules for default expression resolutions.

So, logically, "FOO" should be taken from the outer scope:

var FOO = 42; function bar(x, y = FOO) { ... }

However, then if we take the case with accessing the activation frame as well:

var FOO = 42; function bar(x, y = FOO + x) { ... }

then we need to described more complicated algorithms of defaults resolutions to cover these use cases:

var FOO = 42; function bar(x, y = FOO) { var FOO = 10; }

bar(); // 42

aka "try to resolve in bar.[[Scope]], if not found, then go the activation frame". So in this case:

function bar(x, y = FOO) { var FOO = 10; }

bar(); // it should be 10 then, since isn't found in the bar.[[Scope]]

Or even more confusing:

var FOO = 42; function bar(x, y = FOO + FOO) { var FOO = 10; } // wait ..wha..?

by the rules from above it should be 84, since found in the bar.[[Scope]], or 20 if FOO doesn't exists above.

So to avoid such complex rules, I'd stick with only parent scope.

Or, if it's desirable nevertheless to have the ability to refer args, then to reduce only to them, not to the inner variables:

var FOO = 42; function bar(x, y = FOO + x) { ... } // OK

But not this one:

var FOO = 42; function bar(x, y = FOO + y) { var y = 10; } // Error .. ?

This will make semantic difference b/w vars and locals though.

So, the simplest and straightforward resolution seems to me as the parent scope only, w/o referring to the locals/args.

Dmitry

# Dmitry Soshnikov (13 years ago)

On Mon, Sep 10, 2012 at 5:55 PM, Dmitry Soshnikov < dmitry.soshnikov at gmail.com> wrote:

But not this one:

var FOO = 42; function bar(x, y = FOO + y) { var y = 10; } // Error .. ?

Typo, mean if there is no such parameter, but only the local var:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ?

P.S.: Though, it still seems that the parent-only scope fits better since avoids complexities. Otherwise, we'll get yet another interesting quiz questions like this one:

var x = 10;

(function foo() { console.log(x); // undefined? var x = 20; })();

(which is funny, though). Similar will come for this outer FOO and inner FOO in different run-time circumstances, i.e. unpredictable behavior -- first the outer FOO exists, then it's removed, and at the second call it resolves already to the inner frame. This will definitely be banned as a bad practice (having FOO inside and outside), so to avoid this -- outer only scope is more preferable.

Dmitry

# Brendan Eich (13 years ago)

Nothing is new here, under the straightforward semantics Jason described. JS today:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + x; ... }

ES6 as proposed:

var FOO = 42; function bar(x, y = FOO + x) { ... }

What's the problem?

The other cases you show are all required to work today, including those with local var FOO and var y. We can't break compatibility.

Default parameters only enter into the picture if you think default parameter syntax should trigger new semantics (new scope contour(s) for parameter binding, new hoisting boundary in between parameters and body bindings, etc.).

New semantics for new syntax are certainly possible, but anything more complicated needs strong justification.

If you really want the Pythonic default parameter rules (evaluation in definition context) then you need to address the bad case Jason showed:

function foo(arg = []) { arg.push('strange'); return arg; } foo(); // ['strange'] foo(); // ['strange', 'strange']

This is a side channel and a big footgun. ES6 default parameters as proposed avoid it.

# Brendan Eich (13 years ago)

Dmitry Soshnikov wrote:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ?

Translating to JS as it is:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; var z = 10; }

No error expected, undefined hoisted z used in FOO + z resulting in y defaulting to NaN if y is not passed or given actual undefined value in a call.

P.S.: Though, it still seems that the parent-only scope fits better since avoids complexities. Otherwise, we'll get yet another interesting quiz questions like this one:

var x = 10;

(function foo() { console.log(x); // undefined? var x = 20; })();

This case has nothing to do with default parameters. The answer is of course undefined and we can't change that. I'm not sure why you bring it up.

# Tobie Langel (13 years ago)

On Tue, Sep 11, 2012 at 10:33 AM, Brendan Eich <brendan at mozilla.com> wrote:

If you really want the Pythonic default parameter rules (evaluation in definition context) then you need to address the bad case Jason showed:

function foo(arg = []) { arg.push('strange'); return arg; } foo(); // ['strange'] foo(); // ['strange', 'strange']

This is a side channel and a big footgun. ES6 default parameters as proposed avoid it.

I agree that's terrible and should absolutely be avoided. Sorry for bringing it up.

# Brendan Eich (13 years ago)

Brendan Eich wrote:

Dmitry Soshnikov wrote:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ?

Translating to JS as it is:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; var z = 10; }

Another benefit of the simple semantics: existing JITs can optimize easily, no new semantics to special-case. This is not a dominant concern but it is a selling point from my experience dealing with (and playing one on TV ;-)) implementors.

# Domenic Denicola (13 years ago)

On Sep 11, 2012, at 4:39, "Brendan Eich" <brendan at mozilla.com> wrote:

Brendan Eich wrote:

Dmitry Soshnikov wrote:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ?

Translating to JS as it is:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; var z = 10; }

Another benefit of the simple semantics: existing JITs can optimize easily, no new semantics to special-case. This is not a dominant concern but it is a selling point from my experience dealing with (and playing one on TV ;-)) implementors.

/be

And, if one replaced all vars in the above example with lets, one would get the desired error, right? To me the most important use cases to consider are those in an "idiomatic ES6" let-is-the-new-var world.

# Brendan Eich (13 years ago)

Domenic Denicola wrote:

On Sep 11, 2012, at 4:39, "Brendan Eich"<brendan at mozilla.com> wrote:

Brendan Eich wrote:

Dmitry Soshnikov wrote:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ? Translating to JS as it is:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; var z = 10; } Another benefit of the simple semantics: existing JITs can optimize easily, no new semantics to special-case. This is not a dominant concern but it is a selling point from my experience dealing with (and playing one on TV ;-)) implementors.

/be

And, if one replaced all vars in the above example with lets, one would get the desired error, right?

Let's see:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; let z = 10; }

Yup, in the case of y missing or passed explicit undefined value, z is used before initialized, so in its temporal dead zone => error.

To me the most important use cases to consider are those in an "idiomatic ES6" let-is-the-new-var world.

Sure, although as I keep arguing, formal parameters are var-like at least because compatibility requires us to keep this working:

function f(x) { var x; return x }

as the identity function. Whereas

function f() { let x; var x; }

is an early error.

I've also argued that arguments aliasing in non-strict functions makes parameters var not let, but Allen countered that arguments can be magical no matter how we model parameter binding. To be explicit

js> function f() { let x = 0; let magic = {get x() { return x; }, set x(nx) { x = nx; }}; magic.x = 1; return x; } js> f()

1 js>

Obviously, x is let-bound but still aliased by magic.x, just as for a first formal parameter x aliased by arguments[0].

So Allen's right, and my point becomes aesthetic or pedagogical: I don't think we should say formal parameters are let-like given arguments aliasing in non-strict functions, but this is a weaker objection, and secondary to the main one:

function f(y) { let x = y; var x; return x; }

in ES6 gets an early error on the var x; line, so

function f(x) { var x; return x; }

should too if formal parameters are truly let-based. But we cannot give an early error for this last function f, because of backward compatibility.

All of this is probably too subtle for most folks, but it matters at least for spec aesthetics/philosophy/coherence.

And it again makes me prefer the simpler, flatter semantics for parameter defaults that we've been discussing, whicih (modulo a few edge cases) has been prototyped in SpiderMonkey (in Firefox 15).

# Kevin Smith (13 years ago)

function f(x) { console.log(x); if (truthy()) { with ({x: 'ha ha'}) { var x = 'ho ho'; console.log(x); } } function x(){} console.log(x);

}

In these cases there is information flowing right to left, even across unbalanced left brace!

You're not seriously arguing for how new things should work based on "with" and "arguments" are you? : )

This is JS. We can't change it. Are default parameters really the place to

make a new last stand for something different?

Maybe not. It may not be worthwhile to try and maintain the "curly law" here, but just as a thought experiment:

function g() { return "outer"; }
function f(a = g()) { return a; function g() { return "inner"; } }
console.log(f());

What should we see logged? If we're inclined to think that curlies should define scope boundaries, then we might answer "outer". In order to make that happen, we'd need to introduce a new scope in between f's declaration and f's body:

{0: g, f {1: a } {2: a, g } }

where 0 is the outer scope, 1 is the scope of parameter default expressions, and 2 is the function body scope. This model isn't conceptually clean either though, because we have weird "copy-like" semantics as parameter values "teleport" from scope 1 to scope 2. Meh.

If both scoping semantics (Jason's "top of function body" and the one above) are awkward for different reasons, then maybe there's a better way to approach default values.

A while ago existential and default operators were discussed, but I didn't pay much attention. If we have a default operator, then we could cut default parameter values altogether:

function f(a = g()) {
  return a;
  function g() {}
}

// is the same as:

function f(a) {
  a SOME_DEFAULT_OPERATOR g;
  return a;
  function g() {}
}

It's not quite as pretty as parameter defaults, but it doesn't suffer from the scope awkwardness, either. As an added bonus the loc1 vs. loc2 issue that started the thread basically goes away.

# Dmitry Soshnikov (13 years ago)

On Tue, Sep 11, 2012 at 1:33 AM, Brendan Eich <brendan at mozilla.com> wrote:

If you really want the Pythonic default parameter rules (evaluation in definition context) then you need to address the bad case Jason showed:

function foo(arg = []) { arg.push('strange'); return arg; } foo(); // ['strange'] foo(); // ['strange', 'strange']

This is a side channel and a big footgun. ES6 default parameters as proposed avoid it.

Sure, and as I mentioned in my first message -- ES may eval defaults every time at activation, not only once as Python does. But -- still in the outer scope (in the foo.[[Scope]] in your example).

function foo(arg = <expression>) { ... }

evaluates the <expression> every time at activation as:

  1. If expression is a literal value, create a new value corresponding to literal. Assign to the parameter. Return. 2 Else, eval the <expression> in foo.[[Scope]]

The first step covers and fixes Python's issue. The second allows not going to the activation frame and avoid complexities I noted.

That's said, we should stick either with outer, or with the inner scope for evaluation. The outer seems more logical. Not with both, otherwise it will be too complicated and error prone use cases (which as I note will anyway be banned as a bad practice, so to support it?).

Dmitry

# Dmitry Soshnikov (13 years ago)

On Tue, Sep 11, 2012 at 1:36 AM, Brendan Eich <brendan at mozilla.com> wrote:

Dmitry Soshnikov wrote:

var FOO = 42; function bar(x, y = FOO + z) { var z = 10; } // Error .. ?

Translating to JS as it is:

var FOO = 42; function bar(x, y) { if (y === void 0) y = FOO + z; var z = 10; }

No error expected, undefined hoisted z used in FOO + z resulting in y defaulting to NaN if y is not passed or given actual undefined value in a call.

Ah, OK, so then it's just evals as a casual local var/parameter (in activation + bar.[[Scope]]) and that's it. And there probably will not be a confusion.

Yes, this one may be better than just outer scope. Sorry, I thought about some more complicated stuff (first outer, then inner). If this is the same as a casual local var resolutions, then it's OK.

Dmitry

# Rick Waldron (13 years ago)

On Tue, Sep 11, 2012 at 4:53 AM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

...

And, if one replaced all vars in the above example with lets, one would get the desired error, right? To me the most important use cases to consider are those in an "idiomatic ES6" let-is-the-new-var world.

This is good opportunity to dispel the mythological "let is the the new var" mantra. In the 1JS World (which trumps the "idiomatic ES6" world), "var" is still very much in the game, as is function scope. "let is the new let" correctly describes "let" presence the 1JS world, where "let" exists to add a new block scope contours.

# Dmitry Soshnikov (13 years ago)

On Tue, Sep 11, 2012 at 11:37 AM, Dmitry Soshnikov < dmitry.soshnikov at gmail.com> wrote:

On Tue, Sep 11, 2012 at 1:33 AM, Brendan Eich <brendan at mozilla.com> wrote:

If you really want the Pythonic default parameter rules (evaluation in definition context) then you need to address the bad case Jason showed:

function foo(arg = []) { arg.push('strange'); return arg; } foo(); // ['strange'] foo(); // ['strange', 'strange']

This is a side channel and a big footgun. ES6 default parameters as proposed avoid it.

Sure, and as I mentioned in my first message -- ES may eval defaults every time at activation, not only once as Python does. But -- still in the outer scope (in the foo.[[Scope]] in your example).

function foo(arg = <expression>) { ... }

evaluates the <expression> every time at activation as:

  1. If expression is a literal value, create a new value corresponding to literal. Assign to the parameter. Return.

OK, now I see that this defers to the activation time only, which probably doesn't make much sense in evaluation in only parent scope. Sorry for confusion, yes, in this case evaluation including activation frame fits better.

And I also see now Allen's example with generators, which, if to accept desugaring you noted (with if (== void 0) then assign) may be an issue if creation of the generator should not trigger the activation until the "next" call.

Dmitry

# Brendan Eich (13 years ago)

Kevin Smith wrote:

function f(x) {
    console.log(x);
    if (truthy()) {
      with ({x: 'ha ha'}) {
        var x = 'ho ho';
        console.log(x);
      }
    }
    function x(){}
        console.log(x);

  }

In these cases there is information flowing right to left, even
across unbalanced left brace!

You're not seriously arguing for how new things should work based on "with" and "arguments" are you? : )

No arguments object usage here, and takea way the 'with' and you still have hoisting to contend with, even across a (non-body) left brace if the var is nested.

My point is serious, in that we are not bound to follow C, C++, or Java and try to separate formal parameter scope, default parameter initialization, or other observables from those languages, just because of that left curly brace at the start of a function body.

This is JS. We can't change it. Are default parameters really the
place to make a new last stand for something different?

Maybe not. It may not be worthwhile to try and maintain the "curly law" here, but just as a thought experiment:

function g() { return "outer"; }
function f(a = g()) { return a; function g() { return "inner"; } }

Function hoisting is what it is, so putting nested function g's declaration after the return statement shouldn't be relevant to us in this discussion. You could as well have put it before the return. Just sayin' ;-).

console.log(f());

What should we see logged? If we're inclined to think that curlies should define scope boundaries, then we might answer "outer". In order to make that happen, we'd need to introduce a new scope in between f's declaration and f's body:

{0: g, f {1: a } {2: a, g } }

where 0 is the outer scope, 1 is the scope of parameter default expressions, and 2 is the function body scope. This model isn't conceptually clean either though, because we have weird "copy-like" semantics as parameter values "teleport" from scope 1 to scope 2. Meh.

Yup.

If both scoping semantics (Jason's "top of function body" and the one above) are awkward for different reasons, then maybe there's a better way to approach default values.

A while ago existential and default operators were discussed, but I didn't pay much attention. If we have a default operator, then we could cut default parameter values altogether:

function f(a = g()) {
  return a;
  function g() {}
}

// is the same as:

function f(a) {
  a SOME_DEFAULT_OPERATOR g;
  return a;
  function g() {}
}

It's not quite as pretty as parameter defaults, but it doesn't suffer from the scope awkwardness, either.

We don't have default operators in ES6 but we do have default parameters, which address the most frequently seen use-case. Flipping it around at this point is bad for two reasons:

  • we have to escalate default operator out of strawman and undo the default parameter work;

  • more significant: we have less user testing and frequent-use-case evidence in favor of the default operator approach;

  • the default operator approach is verbose, restating the parameter name on the LHS.

As an added bonus the loc1 vs. loc2 issue that started the thread basically goes away.

No, I think not -- that was about the implicit yield in generators, and a red herring, I think. The real issue is the scope of default parameters, as you say.

# Brendan Eich (13 years ago)

Dmitry Soshnikov wrote:

On Tue, Sep 11, 2012 at 1:33 AM, Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>> wrote:

If you really want the Pythonic default parameter rules
(evaluation in definition context) then you need to address the
bad case Jason showed:

  function foo(arg = []) {
    arg.push('strange');
    return arg;
  }
  foo(); // ['strange']
  foo(); // ['strange', 'strange']

This is a side channel and a big footgun. ES6 default parameters
as proposed avoid it.

Sure, and as I mentioned in my first message -- ES may eval defaults every time at activation, not only once as Python does. But -- still in the outer scope (in the foo.[[Scope]] in your example).

No, you've solved the literal problem but not the problem of supporting later parameters' values depending on earlier parameters.

The scope has to be unique to the activation at hand.

function foo(arg = <expression>) { ... }

evaluates the <expression> every time at activation as:

  1. If expression is a literal value, create a new value corresponding to literal. Assign to the parameter. Return.

Sorry, special casing like this does not work, the expression could contain mutable (object or array) literals but not be a single literal.

2 Else, eval the <expression> in foo.[[Scope]]

The first step covers and fixes Python's issue. The second allows not going to the activation frame and avoid complexities I noted.

That's said, we should stick either with outer, or with the inner scope for evaluation. The outer seems more logical. Not with both, otherwise it will be too complicated and error prone use cases (which as I note will anyway be banned as a bad practice, so to support it?).

You say it's too error prone but you don't adduce any evidence.

Meanwhile we have lots of extant code that does things like

function foo(bar, baz) { baz = baz || default_baz; ... }

That's the cowpath we are paving.

# Kevin Smith (13 years ago)

My point is serious, in that we are not bound to follow C, C++, or Java and try to separate formal parameter scope, default parameter initialization, or other observables from those languages, just because of that left curly brace at the start of a function body.

Yeah - but the argument against doesn't lean on those other languages. It leans on the fact that curly braces signal scope boundaries (in a symbolic-human-conceptual way), except when they don't. Minimizing the number of exceptions to that rule minimizes confusion.

On the other hand, thinking of default expressions as "belonging" to the function body ends up being cleaner overall and wins, I think, for me. But any documentation we create on this language feature will need to be painfully explicit about the scope rules.

# Dmitry Soshnikov (13 years ago)

On Wed, Sep 12, 2012 at 5:13 AM, Brendan Eich <brendan at mozilla.com> wrote:

You say it's too error prone but you don't adduce any evidence.

Meanwhile we have lots of extant code that does things like

function foo(bar, baz) { baz = baz || default_baz; ... }

That's the cowpath we are paving.

I see. Although, I cannot recall any other language which uses complex expressions (such as function calls, or even referencing to the neighbor parameters) for defaults. Usually they are just literals (no matter which, primitive or objects -- they are recreated every time at activation).

But, I agree, they way you describe brings more power, and maybe JS will be the first language with such semantics for defaults.

If so, then it's just the matter of the specification to notice all the subtle cases.

function getDefaul() {}

function foo(bar = [], baz = bar, quz = getDefault()) { function getDefault() {} }

The first thing which will confuse the devs is that getDefault() expression is not the immediate function call to only initialize the default value of "quz", but is the syntactic form of calling this function every time to calculate the default value. This should be mentioned very explicitly, because from the top-down code viewing it really looks like getDefault() is just an initialize of the default, and in this view, its scope is even looks like the outer scope.

Second, of course the scope of all the defaults expressions, with the bold font: "Notice, that the scope of defaults evaluation is the body of the function. This is done to support referencing to the neighbor arguments".

As for generators,

function createGen(foo = thrower()) { yield 1; }

var gen = createGen(); // loc1 gen.next(); // loc2

Of course the devs (again, w/o referencing to spec or detailed docs), would expect that the "thrower()" is called right away when the "createGen" function is defined. After they have read the docs, and now know that such function calls are executed at function activation instead, they would expect that the "thrower()" is called at "loc1", when the function is activated to create the generator object.

OTOH, we understand the technical implementation, that the call to "createGen" may not execute the function body, and therefore, not to call "thrower()". And this is again will be too confusing for novices (?) that it (by the technical details/implementation?) should/may be an "loc2".

So, if all these things described above are worth doing/supporting/explaining/etc., then we can build defaults based on "foo = foo || bar()" pattern which is used today. Then it's completely OK, I agree, it's powerful.

However, if to take new programmers which will join JS from now, potentially they should not know about old patterns for defaults as "foo = foo || default", and they can use simple defaults as literals only.

In some/many languages the defaults are used with nulls only to be able to call a function with lesser parameters number. E.g. (PHP):

function foo($foo, $bar = null) { if (!$bar) { $bar = $this->getDefault(); } }

And now they can use foo(10), or foo(10, "bar"); But still, the expression which calculates the default $bar is described explicitly inside (in the scope of) the function.

JS has no such problems, we may call foo(), or foo(10), or foo(10, "bar") regardless default values, even today.

Also, the fact, that the expressions should be calculated at activation, not creation, comes from legacy, when function declarations should be created on entering the context. Then, even this code:

var foo = function () {};

function bar(baz = foo()) { ... }

Should not logically work, since "bar", being a FD, is created on entering the context, when "foo" is not function yet. This can satisfy the activation frame + activation time rules for such expressions.

Dmitry

# Kevin Smith (13 years ago)

We still have the choice of the implicit yield going after argument defaulting, not before. SpiderMonkey implements before, though, and my own argument from compositionality, above, makes it more clear to me that we should go with implicit-yield-first. This makes contrived throwing parameter default expressions throw "late" but if that matters, use a function combined with a generator.

In this thread, you and I have agreed on simpler scope/hoisting/defaulting/ **initialisting a la ES1-5. I think on reflection that this argues for the implicit yield in a generator function going before anything with observable effects, including argument defaulting.

Sorry to backtrack the thread, but this is going to cause riots.

function* g(a, b = makeTheWorldABetterPlace()) { ... }

let iter = g(123);
// Is the world a better place yet?  I hope so.
iter.next();

No amount of argument is going to convince a user that defaults should not be executed at the location of the call. That's just not how it reads. No?

# Jason Orendorff (13 years ago)

On Thu, Sep 13, 2012 at 8:23 PM, Kevin Smith <khs4473 at gmail.com> wrote:

No amount of argument is going to convince a user that defaults should not be executed at the location of the call. That's just not how it reads. No?

I kind of agree, but at this point we could do with a little less argument and a little more in the way of concrete proposals.

So here is a concrete proposal. Please improve on it; it’s an honest attempt, not a straw man in the rhetorical sense...

I think Allen is proposing a scheme where in code like

function f(a1=EXPR1, a2=EXPR2, a3=EXPR3) { ... }
f();

first EXPR1 is evaluated in a scope including only a1, then EXPR2 is evaluated in a scope that contains a1 and a2, then EXPR3 is evaluated in a scope that contains a1, a2, and a3, then a new environment is created for the function body, its vars and local functions.

If EXPR1 contains a closure that uses the name a2, that means the global a2, not the argument a2. Even if it gets called after a2 is bound.

In the spec language this would have to be modeled with four nested environments; a single mutable environment wouldn't work. In SpiderMonkey, if any of the EXPRs contained a closure that uses with or direct eval, we would have to actually reify one of the nested environments. Ugly but doable; any other implementors want to comment?

There would have to be some specification hackery to make this work just like ES3 in cases where f also contains a var or function with the same name as an argument. But Allen can do it. :)

I don’t have a strong preference. It seems to be is a classic Right Thing vs. Worse Is Better situation. The advantage of the Worse Is Better approach is that you can correctly and completely explain what’s going on in one sentence: “It’s just like an if statement at the top of the function body.” It may not be reasonable, but ordinary developers can reason about it. When it comes to corner cases, which is more important?

# Kevin Smith (13 years ago)

I don’t have a strong preference. It seems to be is a classic Right Thing vs. Worse Is Better situation. The advantage of the Worse Is Better approach is that you can correctly and completely explain what’s going on in one sentence: “It’s just like an if statement at the top of the function body.” It may not be reasonable, but ordinary developers can reason about it. When it comes to corner cases, which is more important?

Apologies for not making this clear earlier, but I agree with the "Worse is Better" approach. That is, execute defaults in the scope of the function body. When I tried to formulate the scope boundaries (as you have) it became clear to me that it would be convoluted any other way.

What I'm questioning now is the idea that, in generators, the implicit first yield should occur before the defaults are evaluated. I don't think that's a tenable position. Is there an implementation problem with inserting the implicit first yield after the defaults are evaluated but before the rest of the function body?

Thanks for your time on this, BTW.

# Brendan Eich (13 years ago)

Kevin Smith wrote:

What I'm questioning now is the idea that, in generators, the implicit first yield should occur before the defaults are evaluated. I don't think that's a tenable position. Is there an implementation problem with inserting the implicit first yield after the defaults are evaluated but before the rest of the function body?

Not particularly, and this was where I landed at first.

In the SpiderMonkey implementation, at first, the bytecode emitter emits the implicit yield opcode (JSOP_GENERATOR) before all other generated code. But since then some conditional opcodes are emitted even before the implicit yield:

dxr.mozilla.org/mozilla-central/js/src/frontend/BytecodeEmitter.cpp.html#l2620

Other implementations may generate different intermediate or target forms from source, but I think the choice of where to put the implicit yield can be made either way without hardship.

Other implementors should weigh in.

# Brendan Eich (13 years ago)

Jason Orendorff wrote:

I think Allen is proposing a scheme where in code like

 function f(a1=EXPR1, a2=EXPR2, a3=EXPR3) { ... }
 f();

first EXPR1 is evaluated in a scope including only a1, then EXPR2 is evaluated in a scope that contains a1 and a2, then EXPR3 is evaluated in a scope that contains a1, a2, and a3, then a new environment is created for the function body, its vars and local functions.

Actually Allen was just reminding me of a TC39 meeting's agreement to bind formals from the left, as if by let* (from Scheme) -- and a1 would not be bound in EXPR1's scope, so any a1 used in EXPR1 would be from an outer scope, etc.

This is without precedent in JS.

Since Kevin was really getting at where the implicit yield goes in

function* f(a1 = E1, ~~~ aN = EN) { ~~~ }

where ~~~ is meta-ellipsis, and he agreed the activation scope for all parameter default expressions is simpler (and thanks to both of you for clarifying and separating issues here), we're left with the implicit yield placement issue.

I wonder whether we can't shed light on this issue by looking at the thinnest basis case of a generator with an observable default parameter:

let n = 0; function *gen(a = ++n) {} for (let x of gen()) {} assert(n === 1);

Does this help?

# Brendan Eich (13 years ago)

So to sum up:

  1. Default parameter expressions are evaluated in the activation scope, with all formal parameters bound, no use-before-set dead zone (so formals are var-like), functions hoisted and in scope -- because in this case, simpler and worse are better.

  2. Generator function with default parameters has implicit yield after defaulting (which per 1 comes after function hoisting) -- because:

let n = 0; function *gen(a = ++n) {} for (let x of gen()) {} assert(n === 1);

This is where I am currently, FWIW.

# Rick Waldron (13 years ago)

On Sat, Sep 15, 2012 at 2:14 AM, Brendan Eich <brendan at mozilla.org> wrote:

So to sum up:

  1. Default parameter expressions are evaluated in the activation scope, with all formal parameters bound, no use-before-set dead zone (so formals are var-like), functions hoisted and in scope -- because in this case, simpler and worse are better.

  2. Generator function with default parameters has implicit yield after defaulting (which per 1 comes after function hoisting) -- because:

    let n = 0;

function *gen(a = ++n) {} for (let x of gen()) {} assert(n === 1);

This is where I am currently, FWIW.

Apologies in advance for making you repeat yourself, but I just want to circle back to the original question :)

The implicit yield means the thrower example will throw an exception at loc 1, correct?

function thrower() { throw "exception"; }

function * G(arg = thrower()) { yield arg; } let g = G(); // Exception seen here?

# Brendan Eich (13 years ago)

Rick Waldron wrote:

On Sat, Sep 15, 2012 at 2:14 AM, Brendan Eich <brendan at mozilla.org <mailto:brendan at mozilla.org>> wrote:

So to sum up:

1. Default parameter expressions are evaluated in the activation
scope, with all formal parameters bound, no use-before-set dead
zone (so formals are var-like), functions hoisted and in scope --
because in this case, simpler and worse are better.

2. Generator function with default parameters has implicit yield
after defaulting (which per 1 comes after function hoisting) --
because:

      let n = 0;
      function *gen(a = ++n) {}
      for (let x of gen()) {}
      assert(n === 1);


This is where I am currently, FWIW.

Apologies in advance for making you repeat yourself, but I just want to circle back to the original question :)

The implicit yield means the thrower example will throw an exception at loc 1, correct?

function thrower() { throw "exception"; }

function * G(arg = thrower()) { yield arg; } let g = G(); // Exception seen here?

Yes, back to loc 1. Kevin Smith made the

 function* g(a, b = makeTheWorldABetterPlace()) { ... }

 let iter = g(123);
 // Is the world a better place yet?  I hope so.

case and I tried beefing it up with the empty generator and single parameter with default value example (assert(n === 1) after, above).