A bit confused by B.3.2 - Web Legacy Compatibility for Block-Level Function Declarations
On Jun 2, 2014, at 6:12 PM, John Lenz <concavelenz at gmail.com> wrote:
It seems to imply that existing "sloppy" code that does this is block scope:
for (let x = 1; x < 1; x++) { function f() { } store(f); }
The above can't be “legacy code” as it contains a “let”. But, I’ll assume you meant to use a “var” in place of the “let”.
But this is not (as there is a second definition of f):
for (let x = ; x < 10; x++) { for (let y = 1; x < 10; x++) { function f() { } store(f); } for (let y = 1; x < 10; x++) { function f() { } store(f); } }
Is this what is intended?
Yes, we are only defining a meaning of the code that falls into the interoperable semantic intersection of legacy browser Es implementations. Your example does not work interoperability across all legacy browsers. The reason is that some browsers apply the same inner function hoisting and initialization rules across an entire function body, include nested blocks. So, in your second example, the second f declaration would be the definition that is initialized on entry to the outer function and used for all references to f. Other browsers didn’t do this. So this code is not interoperable and ES6 does not try to preserve any legacy semantics for it.
On Mon, Jun 2, 2014 at 9:37 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
The above can't be “legacy code” as it contains a “let”. But, I’ll assume you meant to use a “var” in place of the “let”.
No I intentionally used "let". This is not legacy code (I shouldn't have use the word "existing") but new "sloppy" mode code that would like the code to be block scoped. Does a "let", "const", or "class" declaration in scope force the function to be block scoped? That would be great but I think I am missing how this is specified in the spec as that would prevent most of the ambiguity. My main concern is code written with the intent to be block scoped but is not due to being run in sloppy mode.
Or where the behavior changes due to a becoming block scoped (I have a harder time coming up with a scenario where this is the case).
OK, I see. No, the presence of “let” etc. doesn’t change anything. However, hopefully anybody using such new features will not depend upon anything other than pure block scoping and not write new code that depends upon the legacy compatibility semantics. Linters should flag code that depends upon the legacy semantics. Note that people.mozilla.org/~jorendorff/es6-draft.html#sec-web-legacy-compatibility-for-block-level-function-declarations says that ES implementations should log warnings when encountering such code.
Also, note that block scoping applies to all ES6 block level function declarations, in both strict and “sloppy” mode. The only difference is that in sloppy mode, in addition to the creation of the block level bindings of the function name, there is also a binding for the same name created (unless certain error conditions exist) at the top level of the function. That extra binding is only initialized (i.e., can be referenced without error) after a corresponding block level function declaration has been dynamically evaluated).
Code that uses uses block level functions and don’t depend upon the legacy semantics will produce the same results in either strict or sloppy mode.
On 4 June 2014 00:12, John Lenz <concavelenz at gmail.com> wrote:
No I intentionally used "let". This is not legacy code (I shouldn't have use the word "existing") but new "sloppy" mode code that would like the code to be block scoped.
Why would you want to write new sloppy mode code, though? Honest question.
Realistically, you should consider the larger question, why will new sloppy-mode code be written? The answer is obvious: it's the default mode.
But there's still a fair amount of hostility to "use strict", based on folklore about deoptimization (one reason), but also based on other arguments. One that came up on twitter recently:
On Wed, Jun 4, 2014 at 7:59 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
OK, I see. No, the presence of “let” etc. doesn’t change anything. However, hopefully anybody using such new features will not depend upon anything other than pure block scoping and not write new code that depends upon the legacy compatibility semantics. Linters should flag code that depends upon the legacy semantics. Note that people.mozilla.org/~jorendorff/es6-draft.html#sec-web-legacy-compatibility-for-block-level-function-declarations says that ES implementations should log warnings when encountering such code.
How does someone write code to make sure it doesn't fall into legacy semantics? Unless, I misunderstand these rules seem like a refactoring hazard. It would seem better to say: sloppy mode === no block scoped function declarations. strict mode === block scoped function declarations.
It seems like the most important case is a block scoped function used in a loop or otherwise capture block scoped values. Couldn't the sloppy mode rule be a function declaration is block scoped if it captures a block scoped value?
I don't personally want to write sloppy mode code, but there are places you need it (using eval to introduce new symbols into global scope). My interest is writing or support tools that do the "right" thing.
John Lenz wrote:
How does someone write code to make sure it doesn't fall into legacy semantics?
Use strict mode, or don't use the function outside of the block it was declared.
On Jun 4, 2014, at 2:43 PM, John Lenz <concavelenz at gmail.com> wrote:
How does someone write code to make sure it doesn't fall into legacy semantics? Unless, I misunderstand these rules seem like a refactoring hazard. It would seem better to say: sloppy mode === no block scoped function declarations. strict mode === block scoped function declarations.
We don’t want to leave the meaning of function declarations in blocks completely unspecified, as in ES1-5 as we; know there are legacy compatibility requirements for them. So, we need to have some semantics for them. The easiest way to provide any semantics is to simply use the same block scoping semantics that is used in strict mode (slight augmented to over the legacy intersection semantics). That actually limits the refactoring hazard because as long as you have no code that depends upon the legacy intersection there is no difference between the function in block strict and sloppy semantics.
On 4 June 2014 23:46, John Lenz <concavelenz at gmail.com> wrote:
I don't personally want to write sloppy mode code, but there are places you need it (using eval to introduce new symbols into global scope).
You don't necessarily need sloppy mode for that. In strict mode, you can still express it as assignment to the global object.
On Thu, Jun 5, 2014 at 2:06 AM, Andreas Rossberg <rossberg at google.com> wrote:
You don't necessarily need sloppy mode for that. In strict mode, you can still express it as assignment to the global object.
This excellent solution would be much more widely adopted if the global object had a standard identifier, like say "global".
Hm, I'm not sure why that would be a problem. What's wrong with using this
inside those evals?
Assigning to 'this' because it happens that 'this' is global is no better and perhaps worse than using undeclared variables for globals. Assignments to global have, well, non-local consequences: they should be explicit not context dependent.
Even if you don't agree, node has made "global" the de facto standard; lots
of code is now written with the (function(global) {...}(typeof global !== 'undefined' ? global : this));
goop. Modules avoid the smaller part of
that boilerplate but we still don't solve it all without a way to say
"global".
On Wed, Jun 4, 2014 at 8:52 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
We don’t want to leave the meaning of function declarations in blocks completely unspecified, as in ES1-5 as we; know there are legacy compatibility requirements for them. So, we need to have some semantics for them. The easiest way to provide any semantics is to simply use the same block scoping semantics that is used in strict mode (slight augmented to over the legacy intersection semantics). That actually limits the refactoring hazard because as long as you have no code that depends upon the legacy intersection there is no difference between the function in block strict and sloppy semantics.
It is the slightly augmented part that seems to be the problem here. It is (1) very confusing and (2) creating the refactoring hazard: introducing a function of the same name in a different block breaks the scoping. I would be happy to be misunderstanding that section.
That involves changing the code to use "this". But I don't want to derail the thread here. It is sufficient to say that "eval" "with" and "arguments.caller/callee" change behavior in strict mode and there is enough legacy code that need to be dealt with that you can't forcing things to always be strict mode.
We don't need an identifier for global. Much like with the :: operator in C++, a member expression starting with a . would always refer to the global object. So if you want to refer to member "foo" in the global object, ".foo" would be unambiguous. I feel like it's a simple, yet elegant solution.
On Jun 5, 2014, at 8:14 AM, John Lenz <concavelenz at gmail.com> wrote:
It is the slightly augmented part that seems to be the problem here. It is (1) very confusing and (2) creating the refactoring hazard: introducing a function of the same name in a different block breaks the scoping. I would be happy to be misunderstanding that section.
Nobody likes the augmented part, but it’s what is necessary for web legacy compatibility.
Is there any place that has some concrete examples of the different cases we are trying support with this section (and whether the function is block scoped or not in each case)?
Or a link to the discussion that led to the content of this section?
Or a link to the discussion that led to the content of this section?
There have been multiple discussions on this topic, on both es-discuss and during TC39 meetings, so it's hard to point to a single discussion. For example:
esdiscuss.org/topic/real-world-func-decl-in-block-scope-breakages, esdiscuss.org/topic/functions-as-blocks, rwaldron/tc39-notes/blob/master/es6/2014-01/jan-29.md#function
Great thanks for the links. I missed or had forgotten the Jan 2014 summary. The summary for that discussion is pretty clear that the functions have two bindings (a block local one and a function scope one, if it can) and I assume that is what the spec is trying to specify.
Specifically, within the block the function is locally bound so case like:
for (let x = 0;x<10;x++) {
function f() {return x};
storeF(function() {return f});
}
will work with block semantics, but references like:
if (x) {
function f() { return 1 }
} else {
function f() { return 2 }
}
f();
will still "work" (for some definition of "work"). I had previously thought it was "either or". There is still a sharp edge but not one that will break folks trying to use block scoped function declarations.
This seems like a pretty good compromise.
On Jun 6, 2014, at 6:30 PM, John Lenz <concavelenz at gmail.com> wrote:
Great thanks for the links. I missed or had forgotten the Jan 2014 summary. The summary for that discussion is pretty clear that the functions have two bindings (a block local one and a function scope one, if it can) and I assume that is what the spec is trying to specify.
Hopefully, it is exact what the spec. is saying. Do you see any bugs in that regard with the spec?
It seems to imply that existing "sloppy" code that does this is block scope:
for (let x = 1; x < 1; x++) { function f() { } store(f); }
But this is not (as there is a second definition of f):
for (let x = ; x < 10; x++) { for (let y = 1; x < 10; x++) { function f() { } store(f); } for (let y = 1; x < 10; x++) { function f() { } store(f); } }
Is this what is intended?