Hoisting behaviour of 'const' and 'let'

# David-Sarah Hopwood (17 years ago)

Mark S. Miller wrote:

On Sat, Oct 11, 2008 at 4:01 PM, Brendan Eich <brendan at mozilla.com> wrote:

If we want to avoid the read-barrier, we should not hoist either const or let. If we are to consider not hoisting const, WE NEED TO DECIDE THIS NOW, before ES3.1 mandates a hoisting const.

A few messages back you nicely repeated the least-concepts/least-astonishment/most-symmetric case for hoisting to block top that has carried so far. I don't see how we can backtrack here. We'll flail hard. [...other good stuff snipped...]

I'm convinced; thanks. I agree it's too late to consider non-hoisting const even if we come to regret it. Given that const hoists, let declarations must as well, and we can argue about let-read-barriers and repeated declarations later.

I disagree with this reasoning.

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

A function initialization -- that is, initializing the variable with its function value -- never has side effects. A 'const' or 'let' initializer, OTOH, can have side effects. Therefore, it is reasonable to hoist a function declaration together with the corresponding initialization, but it would be far too confusing to hoist a 'const' or 'let' initializer (and no-one is proposing the latter).

What is proposed to be hoisted in the case of 'const' and 'let' is just the point at which the variable's scope begins. But, unlike the case of function definitions, this in general extends the scope to a point where the variable has not been initialized. If 'const' and 'let' (with an initializer) were not hoisted, then there could be a static guarantee that the variable will have been initialized at every point at which it is in scope. By hoisting the scope, we're effectively throwing away the possibility of such a guarantee -- at least if we want to avoid a much more complicated static analysis.

Hoisting to the top of the enclosing block is more useful for function declarations than it is for 'const' and 'let'. A function declaration may be quite long (in comparison to typical 'const' and 'let' initializers), and so it is more important to have some flexibility in where to put it in order to make the code read well, which may require that it be forward-referenced. Forward references are also useful for mutually recursive functions.

In the case of 'function' it is desirable that the hoisting behaviour of a function declaration within a block, be consistent with the ES3-specified hoisting behaviour of a function declaration (using the same keyword) at the top-level of the enclosing function or global scope. For 'const' and 'let', there is no existing behaviour specified by ES3 that we need to be consistent with.

I agree that 'const' and 'let' should be consistent with each other, but neither should hoist, IMHO -- their scope should only be the section of the block after the declaration. In that case, we would have the equivalence

{ ...; const foo = expr; ... } <=> { ...; { const foo = expr; ... } }

and similarly for 'let'. This makes the meaning of multiple declarations of the same variable identifier within a block perfectly straightforward, even if they have different types.

# Yuh-Ruey Chen (17 years ago)

David-Sarah Hopwood wrote:

Mark S. Miller wrote:

On Sat, Oct 11, 2008 at 4:01 PM, Brendan Eich <brendan at mozilla.com> wrote:

If we want to avoid the read-barrier, we should not hoist either const or let. If we are to consider not hoisting const, WE NEED TO DECIDE THIS NOW, before ES3.1 mandates a hoisting const.

A few messages back you nicely repeated the least-concepts/least-astonishment/most-symmetric case for hoisting to block top that has carried so far. I don't see how we can backtrack here. We'll flail hard. [...other good stuff snipped...]

I'm convinced; thanks. I agree it's too late to consider non-hoisting const even if we come to regret it. Given that const hoists, let declarations must as well, and we can argue about let-read-barriers and repeated declarations later.

I disagree with this reasoning.

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

<snip>

I think you're forgetting about |var| hoisting. |const| and |let| should be compared with |var|, not just function declarations. And if |var| hoists, yet |const| and |let| do not, it's inconsistent.

Honestly though, this is an inconsistency I could live with.

# David-Sarah Hopwood (17 years ago)

David-Sarah Hopwood wrote: [...]

What is proposed to be hoisted in the case of 'const' and 'let' is just the point at which the variable's scope begins. But, unlike the case of function definitions, this in general extends the scope to a point where the variable has not been initialized. If 'const' and 'let' (with an initializer) were not hoisted, then there could be a static guarantee that the variable will have been initialized at every point at which it is in scope. By hoisting the scope, we're effectively throwing away the possibility of such a guarantee -- at least if we want to avoid a much more complicated static analysis.

This would also mean that we cannot have any types that do not include the value 'undefined' (either primitive types, or "non-nullable" reference types).

# Mark S. Miller (17 years ago)

On Sat, Oct 11, 2008 at 8:35 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

The following issue invalidates one of your reasons.

{ .... f(); ...

const x = 3;

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

}

Since functions hoist, if const do not, then in the example above, either

  1. we must not allow f to refer to x, or
  2. we must still have a read barrier, since f may still be invoked and access x before x is initialized.

I hope we can agree that #1 is unacceptable. Thus, whether const or let hoists or not, we must still deal with the same use-before-init issues.

# David-Sarah Hopwood (17 years ago)

Yuh-Ruey Chen wrote:

David-Sarah Hopwood wrote:

Mark S. Miller wrote:

On Sat, Oct 11, 2008 at 4:01 PM, Brendan Eich <brendan at mozilla.com> wrote:

If we want to avoid the read-barrier, we should not hoist either const or let. If we are to consider not hoisting const, WE NEED TO DECIDE THIS NOW, before ES3.1 mandates a hoisting const. A few messages back you nicely repeated the least-concepts/least-astonishment/most-symmetric case for hoisting to block top that has carried so far. I don't see how we can backtrack here. We'll flail hard. [...other good stuff snipped...] I'm convinced; thanks. I agree it's too late to consider non-hoisting const even if we come to regret it. Given that const hoists, let declarations must as well, and we can argue about let-read-barriers and repeated declarations later. I disagree with this reasoning.

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

<snip>

I think you're forgetting about |var| hoisting.

I'm not forgetting, but I don't think it's relevant, because hoisting was exactly the problem with 'var' that we're trying to fix. That is,

  • the hoisting behaviour of 'let' must differ from 'var', otherwise what is the point of 'let'?
  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike 'var';
    • the argument about guaranteed initialization is even more important for 'const' than it is for 'let';
    • obviously we do not want four different hoisting behaviours (for 'var', 'function', 'const' and 'let'). [Note that ES3 already has two hoisting behaviours, for 'var' and 'function', so what I've proposed would only be adding one more.]
# David-Sarah Hopwood (17 years ago)

Mark S. Miller wrote:

On Sat, Oct 11, 2008 at 8:35 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

The following issue invalidates one of your reasons.

{ .... f(); ...

const x = 3;

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

}

Why does this need to be allowed? This example will always fail whenever f() actually accesses x. A static error is perfectly reasonable.

It is possible that f() might not access x in the particular call(s) to it that run before 'x = 3', but in that case the declaration of x should be manually hoisted above the call to f:

{ let x; // equivalent to let x = undefined; ... f(); ... x = 3; function f() { ... x ... } }

which makes it clear that f() can refer to x, that x is actually mutable (because both 'x === undefined' and 'x === 3' are observable states), and that x = 3 is an assignment, not an initializer.

Since functions hoist, if const do not, then in the example above, either

  1. we must not allow f to refer to x, or
  2. we must still have a read barrier, since f may still be invoked and access x before x is initialized.
  1. we allow f to refer to x, but disallow the reference to f outside the scope of x.

More generally,

  • if a function refers to a 'let' or 'const' variable, its effective scope is intersected with the scope of that variable.

This rule is simple, easy to explain and motivate, does not unnecessarily reject "good" programs, and is straightforward to work around in cases where the program would be dynamically safe. If it is used then no read or write barriers are required.

# David-Sarah Hopwood (17 years ago)

David-Sarah Hopwood wrote:

Yuh-Ruey Chen wrote:

David-Sarah Hopwood wrote:

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

<snip> I think you're forgetting about |var| hoisting.

I'm not forgetting, but I don't think it's relevant, because hoisting was exactly the problem with 'var' that we're trying to fix. That is,

  • the hoisting behaviour of 'let' must differ from 'var', otherwise what is the point of 'let'?
  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike 'var';
    • the argument about guaranteed initialization is even more important for 'const' than it is for 'let';
    • obviously we do not want four different hoisting behaviours (for 'var', 'function', 'const' and 'let'). [Note that ES3 already has two hoisting behaviours, for 'var' and 'function', so what I've proposed would only be adding one more.]
  * if 'const' has the same hoisting behaviour as 'let', then we
    don't need a separate 'let const'.
# Mike Shaver (17 years ago)

On Sun, Oct 12, 2008 at 9:29 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike 'var';

Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe.

Mike

# Mark S. Miller (17 years ago)

On Sun, Oct 12, 2008 at 8:52 AM, Mike Shaver <mike.shaver at gmail.com> wrote:

On Sun, Oct 12, 2008 at 9:29 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike 'var';

Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe.

Since IE rejects const, cross-browser web pages don't currently use it. Both the ES3.1 effort and the Harmony effort have not taken compatibility with existing browser const behavior as constraining on the standard. ES3.1 does take on the "parse on 3/4 browsers" constraint, but this constrains only syntax, not scoping or semantics. Both the "const hoists to block start" currently in draft ES3.1 and the "const doesn't hoist" that David-Sarah proposes would be equally incompatible with existing browser semantics and equally compatible with existing browser syntax.

# Mark S. Miller (17 years ago)

On Sun, Oct 12, 2008 at 7:07 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

 * if 'const' has the same hoisting behaviour as 'let', then we
   don't need a separate 'let const'.

Indeed. "let const" is long dead. Someone needs to clean up the wiki.

# Brendan Eich (17 years ago)

On Oct 12, 2008, at 10:38 AM, Mark S. Miller wrote:

On Sun, Oct 12, 2008 at 7:07 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

* if 'const' has the same hoisting behaviour as 'let', then we
  don't need a separate 'let const'.

Indeed. "let const" is long dead. Someone needs to clean up the wiki.

There's no strawman:* page talking about "let const", and several
recent posts reiterated its demise. But you're right that
proposals:block_expressions still talks about "let const".

Rather than edit the proposals:* section of the wiki I propose we
ignore it for now. Instead of breaking links into the wiki, I will edit

doku.php

to move the link to the ES4 proposals:proposals page down to "Known
obsolete", or just remove it (and the "Known obsolete" section). I
will remove the feature_specs, discussion, spec, and ri lines too.
That will leave

The 'strawman' namespace is intended to hold Harmony proposals prior
to approval. The 'harmony' namespace is intended to hold proposals on the “ES- Harmony” language. The 'meetings:' namespace is for posting meeting agendas, minutes,
schedules, etc. The 'clarification:' namespace is intended to capture high-level
issues that need clarification. The 'resources:' namespace is for uploading papers or listing pointers
to external sites of interest to the work of this group. The 'es3.1:' namespace is for the “maintenance of es3” work The 'ses:' namespace is for the Secure ECMAScript work

Not perfect, just a good start. I doubt anyone will object, but here's
a quick proposal before I dispose (tomorrow).

# David-Sarah Hopwood (17 years ago)

Mike Shaver wrote:

On Sun, Oct 12, 2008 at 9:29 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike 'var';

Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe.

You mean code like this?

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

What is the point of compatibility with such code?

# David-Sarah Hopwood (17 years ago)

Mark S. Miller wrote:

On Sun, Oct 12, 2008 at 7:07 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

 * if 'const' has the same hoisting behaviour as 'let', then we
   don't need a separate 'let const'.

Indeed. "let const" is long dead. Someone needs to clean up the wiki.

Someone needs to enable the wiki to be cleaned up, by making it editable by us ordinary mortals.

# David-Sarah Hopwood (17 years ago)

Peter Michaux wrote:

Hi David,

offlist as I haven't been following this closely enough...

I prefer to discuss on the list, for future reference.

  • it doesn't need to hoist for backward compatibility, unlike 'var'; Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe. You mean code like this?

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

The expected output should be "undefined", shouldn't it?

For some definition of "expected". That's what it does in FF3 (I don't have Safari or Opera installed, and IE7 throws a SyntaxError), but I don't think it is expected, or useful, that a const variable can effectively be mutable. For 'var' there are potential uses of hoisting (even if they would be more clearly expressed in other ways), but not really for 'const'.

# David-Sarah Hopwood (17 years ago)

David-Sarah Hopwood wrote:

Peter Michaux wrote:

Hi David,

offlist as I haven't been following this closely enough...

I prefer to discuss on the list, for future reference.

Sorry, Peter, I didn't intend to redirect your mail to the list without asking. It's just automatic for me to edit the To: field for lists that don't edit it for me.

# Peter Michaux (17 years ago)

On Sun, Oct 12, 2008 at 12:11 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

Peter Michaux wrote:

Hi David,

offlist as I haven't been following this closely enough...

I prefer to discuss on the list, for future reference.

  • it doesn't need to hoist for backward compatibility, unlike 'var'; Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe. You mean code like this?

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

The expected output should be "undefined", shouldn't it?

For some definition of "expected". That's what it does in FF3 (I don't have Safari or Opera installed, and IE7 throws a SyntaxError), but I don't think it is expected, or useful, that a const variable can effectively be mutable. For 'var' there are potential uses of hoisting (even if they would be more clearly expressed in other ways), but not really for 'const'.

Either const should not hoist, the print above should show "undefined", or an error is thrown "using a const not yet assigned."

It would be completely unintelligible to read code where the print above shows "42".

Peter

# Yuh-Ruey Chen (17 years ago)

David-Sarah Hopwood wrote:

Mark S. Miller wrote:

On Sat, Oct 11, 2008 at 8:35 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

The reason for making 'const' hoist to the top of the enclosing block, AFAIR, was consistency with function declarations. However, there are good reasons why 'const'/'let' and function declarations should be treated differently:

The following issue invalidates one of your reasons.

{ .... f(); ...

const x = 3;

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

}

<snip>

Since functions hoist, if const do not, then in the example above, either

  1. we must not allow f to refer to x, or
  2. we must still have a read barrier, since f may still be invoked and access x before x is initialized.
  1. we allow f to refer to x, but disallow the reference to f outside the scope of x.

More generally,

  • if a function refers to a 'let' or 'const' variable, its effective scope is intersected with the scope of that variable.

This rule is simple, easy to explain and motivate, does not unnecessarily reject "good" programs, and is straightforward to work around in cases where the program would be dynamically safe. If it is used then no read or write barriers are required.

To clarify, would this proposed rule also invalidate the following example?

{ var g = f; // is this what you mean by "reference to f"? g(); const x = 3; function f() { ... x ... } }

Then what about this example?

// global scope if (cond) // suppose it's true y = 'f'; thisy; const x = 3; function f() { ... x ... }

This is a case where static analysis would not find the premature call to f().

# David-Sarah Hopwood (17 years ago)

Peter Michaux wrote:

On Sun, Oct 12, 2008 at 12:11 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

Peter Michaux wrote:

David-Sarah wrote:

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

The expected output should be "undefined", shouldn't it?

For some definition of "expected". That's what it does in FF3 (I don't have Safari or Opera installed, and IE7 throws a SyntaxError), but I don't think it is expected, or useful, that a const variable can effectively be mutable. For 'var' there are potential uses of hoisting (even if they would be more clearly expressed in other ways), but not really for 'const'.

Either const should not hoist, the print above should show "undefined", or an error is thrown "using a const not yet assigned."

It would be completely unintelligible to read code where the print above shows "42".

Of course. In both my proposal and in the current draft of ES3.1, this is a static error, because x isn't in scope at the 'print' -- except that if there is an x declared in some surrounding scope, that x is used.

(Static errors might still be reported at run-time, depending on what kind of environment the code is running in.)

# David-Sarah Hopwood (17 years ago)

Yuh-Ruey Chen wrote:

David-Sarah Hopwood wrote:

Mark S. Miller wrote:

{ .... f(); ...

const x = 3;

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

} <snip>

  1. we allow f to refer to x, but disallow the reference to f outside the scope of x.

More generally,

  • if a function refers to a 'let' or 'const' variable, its effective scope is intersected with the scope of that variable.

This rule is simple, easy to explain and motivate, does not unnecessarily reject "good" programs, and is straightforward to work around in cases where the program would be dynamically safe. If it is used then no read or write barriers are required.

To clarify, would this proposed rule also invalidate the following example?

{ var g = f; // is this what you mean by "reference to f"? g(); const x = 3; function f() { ... x ... } }

Yes. Since 'x' is a free identifier of 'f' and there exists a (single) 'const' or 'var' declaration of 'x' in the same block, 'f' is not in scope until after that declaration of x.

(To fill in some details: multiple initializers are treated as if they were separate statements, e.g. "var a = x, b = y;" is equivalent to "var a = x; var b = y;". A variable is not in scope in its own initializer. If there are multiple declarations of a 'const' or 'let' variable x in a block, then no function in that block can refer to x, since it is not clear which x is meant.)

Then what about this example?

// global scope if (cond) // suppose it's true y = 'f';

I think you meant y = f;

thisy; const x = 3; function f() { ... x ... }

This is a case where static analysis would not find the premature call to f().

Yes it would.

If the program is not fully known in advance (e.g. the statements are being entered in a REPL, or the declaration of 'f' is in a later <script>

block than the 'y = f;'), then it cannot have been fully "processed for function declarations" before the assignment to y, so y will have the value undefined (assuming there was no previous definition of 'f'), and "thisy;" will throw without referencing x.

Alternatively, if the program is completely known in advance, then it can be fully processed for function declarations, but in that case it is statically invalid because f is referenced outside its scope.

# David-Sarah Hopwood (17 years ago)

David-Sarah Hopwood wrote:

Yuh-Ruey Chen wrote:

David-Sarah Hopwood wrote:

Mark S. Miller wrote:

{ .... f(); ...

const x = 3;

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

} <snip>

  1. we allow f to refer to x, but disallow the reference to f outside the scope of x.

More generally,

  • if a function refers to a 'let' or 'const' variable, its effective scope is intersected with the scope of that variable.

This rule is simple, easy to explain and motivate, does not unnecessarily reject "good" programs, and is straightforward to work around in cases where the program would be dynamically safe. If it is used then no read or write barriers are required. To clarify, would this proposed rule also invalidate the following example?

{ var g = f; // is this what you mean by "reference to f"? g(); const x = 3; function f() { ... x ... } }

Yes. Since 'x' is a free identifier of 'f' and there exists a (single) 'const' or 'var' declaration of 'x' in the same block, 'f' is not in scope until after that declaration of x.

(To fill in some details: multiple initializers are treated as if they were separate statements, e.g. "var a = x, b = y;" is equivalent to "var a = x; var b = y;". A variable is not in scope in its own initializer. If there are multiple declarations of a 'const' or 'let' variable x in a block, then no function in that block can refer to x, since it is not clear which x is meant.

Actually that restriction about multiple declarations isn't necessary: the variable reference is resolved as normal depending on where the function is written, and the scope of the function identifier is intersected with the scope of that particular variable instance.

# Yuh-Ruey Chen (17 years ago)

David-Sarah Hopwood wrote:

Yuh-Ruey Chen wrote:

Then what about this example?

// global scope if (cond) // suppose it's true y = 'f';

I think you meant y = f;

No, I mean what is written: y is assigned the string "f". The example would not make any sense if it were not a string. |this[y]| really implies that y has a string.

thisy; const x = 3; function f() { ... x ... }

This is a case where static analysis would not find the premature call to f().

Yes it would.

If the program is not fully known in advance (e.g. the statements are being entered in a REPL, or the declaration of 'f' is in a later <script> block than the 'y = f;'), then it cannot have been fully "processed for function declarations" before the assignment to y, so y will have the value undefined (assuming there was no previous definition of 'f'), and "thisy;" will throw without referencing x.

Alternatively, if the program is completely known in advance, then it can be fully processed for function declarations, but in that case it is statically invalid because f is referenced outside its scope.

I'm sure you've misread the example, considering your confusion with |y = 'f'|. |thisy| effectively calls f().

# David-Sarah Hopwood (17 years ago)

Yuh-Ruey Chen wrote:

David-Sarah Hopwood wrote:

Yuh-Ruey Chen wrote:

Then what about this example?

// global scope if (cond) // suppose it's true y = 'f'; I think you meant y = f;

No, I mean what is written: y is assigned the string "f". The example would not make any sense if it were not a string. |this[y]| really implies that y has a string.

Oh, right. But it doesn't matter at all, the argument I gave still applies with minor corrections. I'll write it out again below with those corrections.

thisy; const x = 3; function f() { ... x ... }

This is a case where static analysis would not find the premature call to f().

Yes it would.

If the program is not fully known in advance (e.g. the statements are being entered in a REPL, or the declaration of 'f' is in a later <script> block than the "thisy;"), then it cannot have been fully "processed for function declarations" before the access of this['f'], so this['f'] will have the value undefined (assuming there was no previous definition of 'f' in the global scope), and "thisy;" will throw without referencing x.

Alternatively, if the program is completely known in advance, then it can be fully processed for function declarations, but in that case it is statically invalid because f is referenced outside its scope.

# YR Chen (17 years ago)

On Sun, Oct 12, 2008 at 5:27 PM, David-Sarah Hopwood < david.hopwood at industrial-designers.co.uk> wrote:

Alternatively, if the program is completely known in advance, then it can be fully processed for function declarations, but in that case it is statically invalid because f is referenced outside its scope.

My example assumes that this is in a single script, so the function definition is hoisted to the top. But you're not still not following the example properly. How can a syntax analyzer determine that f is being referenced in that example? f is being recalled as a property of the global object. Given the following:

// y is dynamically set to 'f' g = this[y]; // g is now f g(); const x = 10; function f() { ... x ... }

how can you determine that g refers to f via static analysis?

# David-Sarah Hopwood (17 years ago)

YR Chen wrote:

On Sun, Oct 12, 2008 at 5:27 PM, David-Sarah Hopwood < david.hopwood at industrial-designers.co.uk> wrote:

Alternatively, if the program is completely known in advance, then it can be fully processed for function declarations, but in that case it is statically invalid because f is referenced outside its scope.

My example assumes that this is in a single script, so the function definition is hoisted to the top. But you're not still not following the example properly. How can a syntax analyzer determine that f is being referenced in that example? That is f is being recalled as a property of the global object.

Ah, I see your point now (it was obviously too early in the morning for me when I answered before).

It is possible to fix this by only setting the 'f' property at the start of f's effective scope.

That is, for each function:

  • find the point just after the textually last declaration of a 'const' or 'let' variable that the function refers to and that is declared in the same block.
  • if not all of the other 'const' and 'let' variables that the function refers to are in scope at that point, then the function's effective scope is empty (and it can be optimized out);
  • otherwise, generate an assignment of the function value to the function's identifier [*1] at that point.

(It is not necessary to delete the property after the scope of f finishes, since if the start of its scope is reached, we know that all 'const' and 'let' variables it refers to have already been assigned.)

This is slightly more complex than I'd hoped, but I think the guarantee that it is never possible to see uninitialized values of 'const' or 'let' variables is worth it.

To anticipate a possible objection: yes, you can now see an undefined value for the function identifier in a case where that would not have happened under the old semantics [*2] -- but that can only happen for functions defined in the global scope that use 'const' or 'let' and are accessed dynamically; and no read or write barriers are required even in that case.

[*1] The scoping issue only arises for function declarations since only those are hoisted; it does not arise for anonymous function expressions. [*2] But there is no backward incompatibility with ES3, because only functions that use 'const' or 'let' are affected.

That is, Given the following:

// y is dynamically set to 'f' g = this[y]; // g is now f g(); const x = 10; function f() { ... x ... }

how can you determine that g refers to f via static analysis?

You can't, but you don't need to. At "this[y]", f is not in scope so it would not have been assigned to "this['f']" yet.

I.e. the code generated is something equivalent to:

// y is dynamically set to 'f' g = this[y]; g(); const x = 10; f = $f; // <- inserted at start of f's effective scope function $f() { ... x ... }

where '$f' is not accessible to the program.

# Brendan Eich (17 years ago)

On Oct 11, 2008, at 9:02 PM, David-Sarah Hopwood wrote:

David-Sarah Hopwood wrote: [...]

What is proposed to be hoisted in the case of 'const' and 'let' is
just the point at which the variable's scope begins. But, unlike the
case of function definitions, this in general extends the scope to a point where the variable has not been initialized. If 'const' and
'let' (with an initializer) were not hoisted, then there could be a static
guarantee that the variable will have been initialized at every point at
which it is in scope. By hoisting the scope, we're effectively throwing away
the possibility of such a guarantee -- at least if we want to avoid a
much more complicated static analysis.

This would also mean that we cannot have any types that do not include the value 'undefined' (either primitive types, or "non-nullable"
reference types).

An alternative implemented in some JS variants has a default value
other than undefined, specific to the annotate type of the hoisted
binding. This works well for scalar types. It leads to a value vs.
reference type (struct vs. class in C#) distinction. We considered
taking ES4 that far, but in the end decided not to go all the way,
instead hardcoding the default value for each built-in primitive or
"value type".

If we don't require let initializers then users will sometimes forget
them, and use-before-set errors will happen -- unless we require
analysis or a read barrier to make use-before-set an error. If we do
not mandate analysis, then test coverage gaps will leave some such
errors to be found in the field.

Meanwhile, real code on the web counts on var being default- initialized to undefined. Swimming upstream against this current will
be hard. Because let (even with a use-before-set hazard) has other
virtues over var, in order to make let the new var and encourage its
widespread use, I do not think we should require let declarations to
have initializers.

[Another point, mostly historical about ES4: changing let and const
not to hoist (when we considered that choice) was not enough to avoid
the "default value" problem in ES4. The ES4 class syntax left instance
variables with non-nullable type annotations in need of default
values, or else error on use-before-set (with attendant read barrier
or static analysis costs). But the harmonized class syntax I proposed
in Oslo avoids at least this case:

class Point(x, y) { let r = Math.sqrt(xx + yy); let theta = Math.atan2(y, x); . . . }

by making the constructor head part of the class head, so the
constructor parameters are in scope for evaluation of the constructor
body, which is the class body.

There are other cases remaining where it's anywhere from awkward to
painful to mandate initialization of every binding. Such hard cases
can all be worked around at some level of discomfort, but var has no
such mandatory initialization burden, so to keep let competitive as
the new var, we passed on requiring initializers.]

My two cents: the argument for let and const not hoisting is strong
enough on its merits, ignoring default value issues. The question of
initial values for uninitialized type-annotated bindings does not
weigh heavily in its favor, as it calls for type-specialized default
value support.

# Brendan Eich (17 years ago)

On Oct 12, 2008, at 1:45 PM, David-Sarah Hopwood wrote:

Peter Michaux wrote:

On Sun, Oct 12, 2008 at 12:11 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

Peter Michaux wrote:

David-Sarah wrote:

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

. . .

It would be completely unintelligible to read code where the print above shows "42".

Of course. In both my proposal and in the current draft of ES3.1, this is a static error, because x isn't in scope at the 'print' -- except that if there is an x declared in some surrounding scope, that x is
used.

No, not in ES3.1 -- quoting from the latest (draft13oct08), section
12.15:

If the constant statement occurs directly inside a
FunctionDeclaration, the constants are defined with function-local scope in that function, as described in s10.1.3. If a
constant statement occurs inside a Block, the constants are defined with block-local scope. Otherwise, they are
defined with global scope (that is, they are created as members of the global object, as described in
10.1.3) using property attributes { [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false }.
Constants are created when the execution scope is entered. Constants have no value when created. A
constant is assigned the value of the AssignmentExpression of it‘s Initialiser when the ConstantStatement is
executed, not when the constant is created. Any attempts to access the value of a constant before it is
assigned a value throws a ReferenceError exception.

(Static errors might still be reported at run-time, depending on what kind of environment the code is running in.)

Then they are not static errors and the spec should not call them
that. Static == compile time. The spec should not require static
analysis without a lot more discussion of the trade-offs for small
implementations.

# Brendan Eich (17 years ago)

On Oct 13, 2008, at 2:40 PM, Brendan Eich wrote:

If we don't require let initializers then users will sometimes forget them, and use-before-set errors

s/errors/mistakes/

will happen -- unless we require analysis or a read barrier to make use-before-set an error. If we do not mandate analysis, then test coverage gaps will leave some such errors to be found in the field.

I hope this was clear enough the first time, but in case not, my
argument is that let should not require an initializer because var
does not, and we wish to migrate code using var to use let, and
encourage let instead of var in new code. Requiring an initializer is
unnecessary, and it taxes all programers whether trying to migrate old
code they didn't write, use let as they're accustomed to using var in
new code, or something in between.

Anyway, that's all about let. For const, which requires an
initializer, everyone agrees that use before initialization is an
error. Whether it must be a static error is not yet decided by the
committee, and the latest ES3.1 draft does not say more than "Any
attempts to access the value of a constant before it is assigned a
value throws a ReferenceError exception." (Ugly wording there, I'll
help get it fixed.)

And ES3.1 specifies const hoisting to top of block, so you cannot use
an outer x in the first ... in { ... const x = 42; ... }.

# David-Sarah Hopwood (17 years ago)

Brendan Eich wrote:

On Oct 12, 2008, at 1:45 PM, David-Sarah Hopwood wrote:

Peter Michaux wrote:

On Sun, Oct 12, 2008 at 12:11 PM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

Peter Michaux wrote:

David-Sarah wrote:

(function () { print(x); { const x = 42; } })();

(calls 'print(undefined)').

. . .

It would be completely unintelligible to read code where the print above shows "42".

Of course. In both my proposal and in the current draft of ES3.1, this is a static error, because x isn't in scope at the 'print' -- except that if there is an x declared in some surrounding scope, that x is used.

No, not in ES3.1 -- quoting from the latest (draft13oct08), section 12.15:

[...] Any attempts to access the value of a constant before it is assigned a value throws a ReferenceError exception.

OK, "statically detectable error", then.

# Maciej Stachowiak (17 years ago)

On Oct 12, 2008, at 8:52 AM, Mike Shaver wrote:

On Sun, Oct 12, 2008 at 9:29 AM, David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk> wrote:

  • the hoisting behaviour of 'const' should be the same as 'let', because:
    • it doesn't need to hoist for backward compatibility, unlike
      'var';

Not for compatibility with the standard, but const as implemented in at least 2/4 (not sure about JSCore) hoists, I believe.

JavaScriptCore does hoist const declarations (but not initializations)
to the top of the function.

# Igor Bukanov (17 years ago)

2008/10/13 David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk>:

This is slightly more complex than I'd hoped, but I think the guarantee that it is never possible to see uninitialized values of 'const' or 'let' variables is worth it.

What about the functions introduced in the let or const initializers? Like the following 2 examples, where the first does access the unitilized value but the second does not.

const x = (function() { return x; })()

versus

var f = null; const x = (f = function() { return f ? x : null;})()

, Igor

# David-Sarah Hopwood (17 years ago)

Igor Bukanov wrote:

2008/10/13 David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk>:

This is slightly more complex than I'd hoped, but I think the guarantee that it is never possible to see uninitialized values of 'const' or 'let' variables is worth it.

What about the functions introduced in the let or const initializers? Like the following 2 examples, where the first does access the unitilized value but the second does not.

const x = (function() { return x; })()

Invalid because x is not in scope in its own initializer.

versus

var f = null; const x = (f = function() { return f ? x : null;})()

Same here. The same effect, apart from x not being const, can be achieved by:

var f = null; let x; x = (f = function() { return f ? x : null;})();

In this case the compiler's analysis is not powerful enough to guarantee that x is observationally immutable (unsurprisingly, at least because the condition of the ?: could have been an arbitrary expression), so it won't let you declare it as const under my proposal. I don't think this is a problem.

# David-Sarah Hopwood (17 years ago)

David-Sarah Hopwood wrote:

Igor Bukanov wrote:

2008/10/13 David-Sarah Hopwood <david.hopwood at industrial-designers.co.uk>:

This is slightly more complex than I'd hoped, but I think the guarantee that it is never possible to see uninitialized values of 'const' or 'let' variables is worth it. What about the functions introduced in the let or const initializers? Like the following 2 examples, where the first does access the unitilized value but the second does not.

const x = (function() { return x; })()

Invalid because x is not in scope in its own initializer.

versus

var f = null; const x = (f = function() { return f ? x : null;})()

Same here. The same effect, apart from x not being const, can be achieved by:

var f = null; let x; x = (f = function() { return f ? x : null;})();

In this case the compiler's analysis is not powerful enough to guarantee that x is observationally immutable (unsurprisingly, at least because the condition of the ?: could have been an arbitrary expression),

Oh, and because there's a bug in your example: it does access x before it is assigned (first f is assigned, then the function is called, then x is assigned). So my proposed scoping rule found a bug that both you and I initially missed.