issue: function hoisting and parameter default value initialization

# Allen Wirfs-Brock (5 years ago)

The Sept 20 TC39 meeting notes says the following was (almost) agreed regarding function parameter scoping, pending some further investigation.

Temporarily, this happened:

Conclusion/Resolution

  • var bindings and are in scope within the function
  • cannot use let to shadow a parameter
  • defaults can refer to any top level binding

Conclusion/Resolution Revisit when data is gathered, re: perf or unexpected behaviours

We also agreed, that repeat use of an identifier as a parameter, for example:

function f(x,x,x,x) {}

is only allowed for legacy compatibility and that it will be an early error if repeated parameter names are used in a parameter list that contains any of the new ES6 parameter list syntactic affordances. (Call this Agreement 0)For example, these would all be errors:

function f(x,x,x,x, ...x) {} function f(x,x=1) {} function f( {x,x} ){ };

An implication of the "defaults can rely on any top level binding" point is that parameter default value initializers can refer to inner functions defined using function top-level function declarations. For example:

function f(p=computeDefaultValue()) { function computeDefaultValue() {return Math.random()} return p; }

The notes don't literally say this, but it was one of the intentions of that item that we discussed at the meeting.

This implies that inner function declaration initialization is hoisted ahead of formal parameter initialization.

This however exposes some strange interactions between ES<=5.1 rules regarding duplicate parameter names that are the same as declared inner function.

ES5.1 requires that

(function(p) { return typeof p; function p() {}; })(1)

returns "function" rather than "number".

To maintain compatibility, this means that if function initialization is hoisted before parameter initialization, that in cases like the above either

  1. initialization of parameters that have the same name as a function declaration are skipped
  2. declared functions that share names with formal parameters must be reinitialized to their original function value after parameter initialization

The choice of one of the above specification alternatives really doesn't make any observable difference, as long as default value initializer aren't used.

However, alternative 1 introduces a specification (and perhaps implementation) complication. Consider:

(function ({a:{b: {p}}}) { function p() {}; return typeof p; }) ({a: {b: {p: 1}}});

The spec. complication is that deep within the destructuring algorithm we need to be able to skip any binding name that is the same as a declared function name.

Also, it seems highly unlikely that anyone would actually want to give an inner function the same name as such a interior destructured parameter name. It almost surely is a bug. There are no backwards compatibility requires on such destructured parameter names, so:

Proposal #1: Local bindings introduced via formal parameter destructing are treated as let bindings rather than var binding. They cannot be the same as top inner function or var declared name. There similarly are no compatibility issue with the name of the rest parameter, so it also should be treated as a let binding.

When default value initializers, the difference between approach 1 and 2 above becomes observable. Consider,

function (p=[ ], q=p.length) { function p(a,b,c) {}; return q; }) ();

If approach 1 above is used, the function call will return 3 -- the number of parameters of the inner function p. If approach 2 is used, the function will return 0 -- the length of the empty array that provided the default value of the p parameter.

Another odd situation where the approaches would differ is:

function (a=p, p=1, c=p) { function p() {}; return a===c; }) ();

If approach 1 is used, the function will return true. If approach 2 is used the function will return false.

These are crazy things for anybody to code and it seems a waste to try to specify rationale runtime behavior for such irrational code. Instead I suggest:

Proposal #2: It is an early error if both any formal parameter is the same name as an top level inner function declaration and the formal parameter lists includes a destructuring, default value initializer, or a rest parameter.

Essential the net of Agreement 0 plus Proposals 1 & 2 would be that only ES<=5.1 style simple formal parameter lists may have repeated parameter names or parameter names that are the same as top level inner function declarations.

This could be even more cleanly expressed as: Proposal #3: a) ES<=5.1 style formal parameters lists introduce var bindings and follow ES5.1 initialization rules. This permits top level inner functinon and var declarations that use the same name as a parameter. b) parameter lists containing any new ES6 syntax introduce let bindings for the parameters. This prohibits multiple declaration of a formal parameter name and inner var/function redeclaration of a parameter name.

Bottom line, I suggest we implement proposal 3, rather than the temporary conclusions that were discussed at the Sept. meeting.

# Aron Homberg (5 years ago)

Idea:

Additionally disallow redeclaration (I mean redeclaration not reassignment; eg. by parameter name in parameter list or function identified by a name already in in use) with different type in same function scope when code is executed in ES6 strict mode. (New strict mode rule.)

This should not break existing code (okay, it could break crazy es5 code which already runs in strict mode and makes use of redeclaring with different type - but I guess this would just point out existing bugs) and helps developers to prevent writing crazy, buggy code even when they aren't aware of this.

Redeclaring variables with different type in same function scope often leads to hard to find bugs in practice.

  • Aron

Am 06.10.2012 um 02:35 schrieb Allen Wirfs-Brock <allen at wirfs-brock.com>:

# Andreas Rossberg (5 years ago)

On 6 October 2012 02:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Bottom line, I suggest we implement proposal 3, rather than the temporary conclusions that were discussed at the Sept. meeting.

Of the options you suggest, I also think that #3 is preferable.

However, your examples have reconfirmed my suspicion that having defaults live within the scope of local declarations is the path to insanity. I suggest that we reconsider. There should be a clean, simple, and sane nesting of scopes here.

Let me try again. How about the following desugaring?

function f(x1 = e1, ~~~, xN = eN) { body }

means

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~, xN) { body })(x1, ~~~, xN); }

That is:

  • no local declaration is visible in any of the defaults
  • defaults can see other parameters, but they are initialized left to right
  • local declarations behave as always, no extra rules needed

There is an additional advantage to this scheme: to understand the definition of a default a caller never needs to look at the implementation details of f's body -- which I think is a very useful property. In particular, if a default expression is itself a function, and closes over one of the other arguments, then you actually get what you'd expect even if f, way down in its body, messes with some of those arguments (e.g. abusing them as a loop variable):

function f(i, log = (s) => print("f(" + i + "): " + msg)) { log("starting..."); while (i > 0) { log("processing " + i); // ... whatever --i; } log("done..."); }

f(6); // no surprises here

# Allen Wirfs-Brock (5 years ago)

On Oct 8, 2012, at 8:29 AM, Andreas Rossberg wrote:

On 6 October 2012 02:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Bottom line, I suggest we implement proposal 3, rather than the temporary conclusions that were discussed at the Sept. meeting.

Of the options you suggest, I also think that #3 is preferable.

However, your examples have reconfirmed my suspicion that having defaults live within the scope of local declarations is the path to insanity. I suggest that we reconsider. There should be a clean, simple, and sane nesting of scopes here.

You desugaring would break ES<=5.1 compatibility for:

(unction (p) { var p; return typeof p })("test")

ES5.1 returns "string", your desugaring returns "undefined"

I'm actually favorably included towards the intent of your proposal: parameter default value expressions can't reference bindings introduced in the body. However, it needs to be specified in a way that preserves ES5.1 semantics. I also think that, for new parameter constructs, that names defined in the parameter list should not be allowed to be the same as a function top-level declaration. EG,

We have to allow this:

function f(p) { var p; }

but we don't have to allow:

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

So, my alternative proposal #4 is:

   a) ES<=5.1 style formal parameters lists introduce var bindings and follow ES5.1 initialization rules. This permits top level inner functinon and var declarations that use the same name as a parameter.
   b) parameter lists containing any new ES6 syntax introduce let bindings for the parameters.  This prohibits multiple declaration of a formal parameter name and inner var/function redeclaration of a parameter name.
   c) parameter default value expressions do not have visibility of bindings created within the function body.
# Andreas Rossberg (5 years ago)

On 8 October 2012 18:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

You desugaring would break ES<=5.1 compatibility for:

(unction (p) { var p; return typeof p })("test")

ES5.1 returns "string", your desugaring returns "undefined"

I'm probably just being blind, but I'm not seeing it. Why?

# Allen Wirfs-Brock (5 years ago)

On Oct 8, 2012, at 10:04 AM, Andreas Rossberg wrote:

On 8 October 2012 18:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

You desugaring would break ES<=5.1 compatibility for:

(function (p) { var p; return typeof p })("test")

ES5.1 returns "string", your desugaring returns "undefined"

I'm probably just being blind, but I'm not seeing it. Why?

/Andreas

In ES <= 5.1, function declarations over-write formal parameters. But var declarations, without initializer do not assign undefined to already local bindings (including formal parameters) that already exist.

You desugaring would create a new var declaration in a new scope contours that would be initialized to undefined and shadow the like-named formal parameter.

# Andreas Rossberg (5 years ago)

On 8 October 2012 19:18, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On Oct 8, 2012, at 10:04 AM, Andreas Rossberg wrote:

On 8 October 2012 18:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

You desugaring would break ES<=5.1 compatibility for:

(function (p) { var p; return typeof p })("test")

ES5.1 returns "string", your desugaring returns "undefined"

I'm probably just being blind, but I'm not seeing it. Why?

In ES <= 5.1, function declarations over-write formal parameters. But var declarations, without initializer do not assign undefined to already local bindings (including formal parameters) that already exist.

Yes.

You desugaring would create a new var declaration in a new scope contours that would be initialized to undefined and shadow the like-named formal parameter.

But it creates no new var declarations. Your function would presumably become

function(p) { return (function(p) { var p; return typeof p; })(p) }

That is, the desugaring degenerates into a simple (and redundant) eta-expansion in all cases where no actual defaults occur. As far as I can see that shouldn't change the meaning of any local var declarations.

# Allen Wirfs-Brock (5 years ago)

On Oct 8, 2012, at 10:35 AM, Andreas Rossberg wrote:

On 8 October 2012 19:18, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On Oct 8, 2012, at 10:04 AM, Andreas Rossberg wrote:

On 8 October 2012 18:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

You desugaring would break ES<=5.1 compatibility for:

(function (p) { var p; return typeof p })("test")

ES5.1 returns "string", your desugaring returns "undefined"

I'm probably just being blind, but I'm not seeing it. Why?

In ES <= 5.1, function declarations over-write formal parameters. But var declarations, without initializer do not assign undefined to already local bindings (including formal parameters) that already exist.

Yes.

You desugaring would create a new var declaration in a new scope contours that would be initialized to undefined and shadow the like-named formal parameter.

But it creates no new var declarations. Your function would presumably become

Right, I glossed over the fact that both the inner and outer function declaration introduce bindings for the formals

# Brendan Eich (5 years ago)

Andreas Rossberg wrote:

On 6 October 2012 02:35, Allen Wirfs-Brock<allen at wirfs-brock.com> wrote:

Bottom line, I suggest we implement proposal 3, rather than the temporary conclusions that were discussed at the Sept. meeting.

Of the options you suggest, I also think that #3 is preferable.

I do too.

However, your examples have reconfirmed my suspicion that having defaults live within the scope of local declarations is the path to insanity. I suggest that we reconsider. There should be a clean, simple, and sane nesting of scopes here.

Let me try again. How about the following desugaring?

function f(x1 = e1, ~~~, xN = eN) { body }

means

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~, xN) { body })(x1, ~~~, xN);

Nit: need (function(x1, ~~~ xN) { body }).call(this, x1, ~~~ xN) and not a direct this-free call.

}

That is:

  • no local declaration is visible in any of the defaults
  • defaults can see other parameters, but they are initialized left to right
  • local declarations behave as always, no extra rules needed

This still is seems observable, because arguments.length will always be N, even when f is called with < N actual parameters.

The fix that uses apply:

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~ xN) { body }).apply(this, arguments); }

seems to suffice (check me on this!).

There is an additional advantage to this scheme: to understand the definition of a default a caller never needs to look at the implementation details of f's body -- which I think is a very useful property.

I agree.

In particular, if a default expression is itself a function, and closes over one of the other arguments, then you actually get what you'd expect even if f, way down in its body, messes with some of those arguments (e.g. abusing them as a loop variable):

function f(i, log = (s) => print("f(" + i + "): " + msg)) { log("starting..."); while (i> 0) { log("processing " + i); // ... whatever --i; } log("done..."); }

f(6); // no surprises here

JS has such surprises already, of course, but appreciate trying to draw a line here.

# Kevin Smith (5 years ago)

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~ xN) { body }).apply(this, arguments); }

Effectively sibling scopes, with a copy mechanism from the "defaults" scope to the "body" scope. That's what I was going for originally but I wasn't clever enough to see a natural desugaring implementation. Neat!

# Andreas Rossberg (5 years ago)

On 9 October 2012 01:55, Brendan Eich <brendan at mozilla.org> wrote:

Andreas Rossberg wrote:

Let me try again. How about the following desugaring?

function f(x1 = e1, ~~~, xN = eN) { body }

means

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~, xN) { body })(x1, ~~~, xN);

Nit: need (function(x1, ~~~ xN) { body }).call(this, x1, ~~~ xN) and not a direct this-free call.

Yes, indeed. Also, destructuring parameters of course require more work.

This still is seems observable, because arguments.length will always be N, even when f is called with < N actual parameters.

The fix that uses apply:

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~ xN) { body }).apply(this, arguments); }

seems to suffice (check me on this!).

OK, tell me how this is supposed to work. :) If arguments.length = M < N, then how are xM...XN-1 possibly bound to eM...eN-1 in the body with this version?

As an aside, did we actually ever talk about the interference of defaults with 'arguments'? I'm not even sure what makes sense here. If it wasn't for 1JS, I'd say don't provide 'arguments' at all in a function with defaults.

# Allen Wirfs-Brock (5 years ago)

On Oct 10, 2012, at 6:33 AM, Andreas Rossberg wrote:

On 9 October 2012 01:55, Brendan Eich <brendan at mozilla.org> wrote:

Andreas Rossberg wrote:

Let me try again. How about the following desugaring?

function f(x1 = e1, ~~~, xN = eN) { body }

means

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1; ~~~ if (xN === undefined) xN = eN; return (function(x1, ~~~, xN) { body })(x1, ~~~, xN);

Nit: need (function(x1, ~~~ xN) { body }).call(this, x1, ~~~ xN) and not a direct this-free call.

Yes, indeed. Also, destructuring parameters of course require more work.

This still is seems observable, because arguments.length will always be N, even when f is called with < N actual parameters.

The fix that uses apply:

function f(x1, ~~~, xN) { if (x1 === undefined) x1 = e1;

if (xN === undefined) xN = eN;
return (function(x1, ~~~ xN) { body }).apply(this, arguments);
}

seems to suffice (check me on this!).

OK, tell me how this is supposed to work. :) If arguments.length = M < N, then how are xM...XN-1 possibly bound to eM...eN-1 in the body with this version?

As an aside, did we actually ever talk about the interference of defaults with 'arguments'? I'm not even sure what makes sense here. If it wasn't for 1JS, I'd say don't provide 'arguments' at all in a function with defaults.

You are getting hung up on your desugaring, and loosing sight of the actual semantics we need to specify. In fact I won't use a desugaring to specify any of this.

length.arguments is the number of actual arguments provided at the call site. It has nothing to do with the form of the formal parameter list.

All of the above (body declarations not visible to default value expressions, handling of destructuring, correctly setting up arguments) are all things that I can handle using current specification techniques.

However, the above does point out that if you try to use desugaring as an implementation technique there are a number of issue will have to carefully address.

# Brendan Eich (5 years ago)

Agreed, desugaring is helpful for checking intuition or a design idea but it can be the wrong tool for the job.

Main thing we should discuss is Andreas's idea of a separate scope for parameter default expressions, shadowed by the function's body scope.