Uniform block scoping

Andreas Rossberg rossberg at google.com
Thu Jul 17 04:13:54 PDT 2014


On 16 July 2014 18:38, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
> (Note that this topic is on the agenda for this month's TC39 meeting)

Unfortunately, I won't be at the meeting. Do you think we can afford
to defer this until September?

> (Also, note that as far as I can tell, the disagreement is only about the early errors described below. The use of a separate parameter scope that limits what closures in parameter expressions can capture  has been agreed upon and is already in the ES6 draft spec.)

Yes, though the scope separation that this already implies is what
makes my suggestion natural and easy (I believe), see below.


> Everything would be so much cleaner if all we had were strictly lexically scoped declarations and no legacy to deal with...

Yes indeed, but var scoping being what it is, we can still try to make
lexically scoping itself as clean as possible. Which is what I am
proposing. :)


> Consider,
>
> We've decided that that we don't want to allow multiple declarations for the same name in a scope:
>
> function f() {
>     let x;
>     const x;  //early redeclaration error
> }
>
> function f() {
>   let x;
>   var x;  //early redeclaration error
> }
>
> function f() {
>     let x;
>     function x() {};  //early redeclaration error
> }
>
> We don't even have a runtime semantics for any of the above duplicate declarations.

Agreed.


> Except that for legacy reasons we have to allow:
> function f() {
>     var x;
>     function x() {};
>     function x() {};
>     var x;
> }
> using the legacy ES semantics.
>
> The legacy ES semantics also requires that:
>
> function f(x) {
>    var x;   //not an error
>    console.log(x);
> }
>  f(1);  //logs 1, not undefined
>
> just like:
>
> function g(x) {
>    console.log(x);
> }
> g(1);  //logs 1
>
> In other words, from the perspective of  the body of the function, the following two declarations appear to be equivalent:
> function f(x) {
> }
> function f(x) {
>    var x;
> }

Agreed as well.


> But we've already established this is an error:
> function f(x) {
>    var x;
>    let x;  //early error, duplicate definition
> }
>
> and if that is an error, then its equivalent alternative form should also be an error:
> function f(x) {
>    let x;  //early error, duplicate definition
> }
>
> And that is the crux of the disagreement.

Yes, that is were I disagree. My suggestion simply amounts to these
examples behaving as if you had written, under the current draft:

  function f(x) {{
    var x;  // fine, still same x
  }}

  function f (x) {{
    var x;
    let x;  // still a redeclaration error
  }}

  function f(x) {{
    let x;  // shadowing
  }}

In particular, we already have a separate scope for the function body
(due to default parameters). The special rules for var names in this
scope aliasing parameter names are merely necessary for backwards
compatibility (and moreover, only apply to ES5 cases, i.e., simple
parameter lists).

I don't see a good reason to extend this special-case mechanism to
lexical declarations. It is not consistent with either lexical scoping
nor with default parameter semantics, which is why we already have to
limit it to legacy cases. We can perfectly well choose to treat it as
the legacy compatibility special-case it already is, and not infect
other future design choices with it.


> Andreas would like to reason about ES scoping as simple nested block contours with shadowing.  But the reality  is more complex than that.  We have vars declarations the hoist to the top level.  We have interactions between formal parameters and var declarations that interact with outer scopes (that are inner relative to the parameters if you think of the parameter list is a distinct scope contour).  In fact, in my current working draft, FunctionDeclarationInstantiation is almost 3 pages of detailed algorithmic specification. (and adding a parameter scope didn't simplify things...).
>
> We really don't want the average (and certainly not the novice) ES programmer to have to understand all the technical subtleties of these interactions. It's much easer to have a few simple rules that states which declarations are legal and which declarations are not.  The rules are:
>
>    It is illegal  for let/const/class declarations at the top level of a function to multiply define the same name.
>    It is illegal  for a let/const/class declaration at the top level of a function to define the same name as a top level function declaration.
>    It is illegal  for a let/const/class declaration at the top level of a function to define the same name as a var declaration that occurs anywhere within the function body.
>    It is illegal  for a let/const/class declaration at the top level of a function to define the same name as a formal parameter of the function.
>
> This is what the ES6 spec. currently says (using slightly different words). Andreas would like to eliminate that lat rule. I think it should remain, both for the specific equivalence discussed above and for overall simplicity.

I see, but don't think this consistency argument trumps the ones I
gave. It's probably true that you have to choose between one or the
other, but shouldn't forward-facing consistency be preferred over
backward-facing consistency?

You haven't convinced me yet of the simplicity argument, which in fact
goes the opposite way, I believe, see below.


>> In terms of spec, this simply amounts to dropping several bullet
>> points imposing syntactic restrictions about LexicallyDeclaredNames
>> [2] -- i.e., simplification :). (Note: The legacy rule that legal ES5
>> function declarations are var-like, not lexical, would of course be
>> unaffected.)
>
> It's actually not quite that simple.  There are a number of places where the specified runtime semantics are simplified because it is known that early error rules have eliminated various possibilities, such as parameter/let naming conflicts.  So, if we made this changeI will need to review all the relevant runtime semantics and look for newly exposed cases that need to be handled.

Yes, you are right, a change is necessary to the algorithm in 9.2.13.
But AFAICS it is a net simplification as well: isn't all you need to
do getting rid of the needsParameterEnvironment special casing and
always create the separate environment for the body? (*) Everything
else is already perfectly taken care of by the existing algorithm,
namely by the instantiatedVarNames list.

(*) Concretely, you get rid of steps 6 and the condition 28.0 in the
current algorithm.

Am I missing something? Is there anything else you have in mind?

(And before anybody gets concerned: always having this extra
environment is purely a spec device, not a runtime cost. An
implementation will of course be able to merge the scopes in almost
all practical cases, as is the case in the presence of default
parameters already. The current draft effectively tries to implement
this very optimisation in the spec itself, but I don't think it
belongs there.)

/Andreas


More information about the es-discuss mailing list