4

The following code prints 1 in Safari 13.0.4 on OSX.

let set = new Set

for(let x = 0; x < 2; x++) {
    function f() {}
    set.add(f)
}

console.log(set.size) // 1 in Safari non-strict mode

Also:

let set = new Set

for(let x = 0; x < 2; x++) {
    function f() {}
    f.test = x
    set.add(f)
}

console.log(set.size); // 1 in Safari
for(let x of set) console.log(x.test) // 1 in Safari non-strict mode

And:

let set = new Set;

for(let x = 0; x < 2; x++) {
    var v = (function () {})
    set.add(v);
}

console.log(set.size); // 2 in Safari non-strict mode

Is this behavior compatible with section 13.7.4.8 (see below) of the specification?

Note that: Node 13.9.0, Chrome 80.0.3987.122, and Brave 1.3.118 print 2.

13.7.4.8 of the spec:

(4.b seems pertinent)

The abstract operation ForBodyEvaluation with arguments test, 
increment, stmt, perIterationBindings, and labelSet is 
performed as follows:

1. Let V = undefined.

2. Let status be CreatePerIterationEnvironment(perIterationBindings).

3. ReturnIfAbrupt(status).

4. Repeat

  a. If test is not [empty], then

    i. Let testRef be the result of evaluating test.

    ii. Let testValue be GetValue(testRef).

    iii. ReturnIfAbrupt(testValue).

    iv. If ToBoolean(testValue) is false, return NormalCompletion(V).

  b. Let result be the result of evaluating stmt.

  c. If LoopContinues(result, labelSet) is false, return d.
     Completion(UpdateEmpty(result, V)).

  d. If result.[[value]] is not empty, let V = result.[[value]].

  e. Let status be CreatePerIterationEnvironment(perIterationBindings).

  f. ReturnIfAbrupt(status).

  g. If increment is not [empty], then

    i. Let incRef be the result of evaluating increment.

    ii. Let incValue be GetValue(incRef).

    iii. ReturnIfAbrupt(incValue).
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • I think a more germane (or equally germane) section would be one that handles the behavior of function statements, which [as of ES6 were given block-level scope](https://stackoverflow.com/q/35909072/710446). It's possible that in-block function statements are given special handling in the ES spec such that only one function is created per block, rather than per iteration of that block's code. In ES5, block-level functions were undefined behavior and browsers supported them as an extension beyond the ES5 spec. See also https://stackoverflow.com/q/31419897/710446 – apsillers Feb 26 '20 at 15:37
  • Did you use strict mode? – Bergi Feb 26 '20 at 15:39
  • Ah good question. No. I did not specify strict mode for any of the tests in any browser, or in Node. – Ben Aston Feb 26 '20 at 15:40
  • It prints 2 in strict mode... – Ben Aston Feb 26 '20 at 15:41
  • 1
    @Ben Ah good to know. Then this is probably not a violation of §13.7.4.8, but rather a violation of Annex B3.3 [which specifies the legacy semantics for block-level function declarations](https://stackoverflow.com/a/31461615/1048572). – Bergi Feb 26 '20 at 15:54
  • Further tests to confirm this: Please try calling `f()` before it was declared, i.e. above the loop. And try to replace the loop with two block statements, i.e. `f(); { function f() {} f.test = 1; } { function f() {} console.log(f.test); f.test = 2; } console.log(f)` – Bergi Feb 26 '20 at 15:56
  • 1
    When you wrap the whole code with a normal function (immediately invoked), do you get the same behaviour in Safari, or is it specific to having this code at the top level of the script? – trincot Feb 26 '20 at 16:27
  • 1
    @Bergi `f` appears to be hoisted outside of the block in Safari non-strict. – Ben Aston Feb 26 '20 at 16:28
  • `f(); { function f() {} f.test = 1; } { function f() {} console.log(f.test); f.test = 2; } console.log(f)` prints `1` in Safari non-strict. `f.test === 2` is `true`. – Ben Aston Feb 26 '20 at 16:31
  • @trincot In a non-strict IIFE it prints `2`. `(function() { let set = new Set; for(let x = 0; x < 2; x++) { function f() {} set.add(f); } console.log(set.size)})()` – Ben Aston Feb 26 '20 at 16:34
  • 1
    Ah, so this is only happening in global scope? Good call @trincot – Bergi Feb 26 '20 at 16:36
  • To test this in Safari, I am running the code in the dev console in Safari, which I presume is backed by `eval`. – Ben Aston Feb 26 '20 at 16:40

2 Answers2

1

To my understanding, code that has a function declaration placed within a block, should follow the specification of 13.2.14 (I put in bold):

When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record.

One of the steps deals with function declarations explicitly, which depends on InstantiateFunctionObject, which in turn depends on OrdinaryFunctionCreate, OrdinaryObjectCreate, MakeBasicObject ... which creates a new object.

All this happens at the evaluation. Your quote from the specifications dictate that the evaluation happens for each iteration, and so the function object should be newly created in each iteration.

Differences in implementation

The specification has a section on implementation differences related to block-level function declarations. It says:

Prior to ECMAScript 2015, the ECMAScript specification did not define the occurrence of a FunctionDeclaration as an element of a Block statement's StatementList. However, support for that form of FunctionDeclaration was an allowable extension and most browser-hosted ECMAScript implementations permitted them. Unfortunately, the semantics of such declarations differ among those implementations. Because of these semantic differences, existing web ECMAScript code that uses Block level function declarations is only portable among browser implementation if the usage only depends upon the semantic intersection of all of the browser implementations for such declarations. The following are the use cases that fall within that intersection semantics:

  1. A function is declared and only referenced within a single block

    • One or more FunctionDeclarations whose BindingIdentifier is the name f occur within the function code of an enclosing function g and that declaration is nested within a Block.
    • No other declaration of f that is not a var declaration occurs within the function code of g
    • All occurrences of f as an IdentifierReference are within the StatementList of the Block containing the declaration of f.

Now the case in your question behaves according to specification (print 2) when the code is not a top-level script, but placed in a function body. In that case we are in situation 1 (in the above quote). But this point is not applicable when the script is global. And so, we see indeed deviating behaviour...

trincot
  • 317,000
  • 35
  • 244
  • 286
1

Yes, this is a bug in Safari[1]. However, as you noticed, it only occurs in global (or eval) scope and only in sloppy mode.

In general, these should definitely be distinct function instances, not getting hoisted outside of the block. However, Safari - being a browser - does implement the Block-Level Function Declarations Web Legacy Compatibility Semantics from Annex B3.3 of the specification (see here for details). In ES6 and ES7, these did apply only to block statements inside functions though. Only since ES8, they are also specified for declaration instantiations in global and eval scopes.

It seems that Safari did not adopt that change from ES8 yet, and has kept their own (noncompliant) pre-ES6 semantics for block-scoped declarations in the global scope, where they hoist the declaration completely.

1: Probably #201695 or #179698. "We don't support this in global scope. We do support it inside functions and I believe eval. We still need to implement it for the global scope."

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Does the spec basically encode in the Web Legacy Compatibility Semantics, the intersection of all the known implementation specific behaviors, and any deviation from these in non-strict mode is non-compliant? – Ben Aston Feb 26 '20 at 17:11
  • 1
    @Ben The annex standardises the most useful behaviour, with highest web compatibility (i.e. keeping existing code working) and still being reasonable to implement and not deviating from the correct behaviour too far. It's not exactly an intersection of the known implementations. But yes, I'd call any deviation from these semantics non-compliant: the goal is either to have an engine not implement these at all (because it doesn't execute scripts from the web), or if they need to implement compatibility features, they should do it exactly as standardised in Annex B. – Bergi Feb 26 '20 at 17:16