How are block-level variable statements "hoisted" to the outer scope?

In web implementations and assuming non-strict mode, we see that both block-level variable statements and function declarations (FD) get “hoisted” to the dominant (global or outer function) scope:

(function foo() {
  
  console.log(x); // undefined  
  console.log(bar); // undefined
  
  {
    var x = 10;
    
    function bar() { 
      
      console.log("bar")
      
    };
    
  };
  
})();

However, I can’t seem to reconcile this with the GlobalDeclarationInstantiation (GDI) and FunctionDeclarationInstantiation (FDI) abstract operations. In both algorithms, only the TopLevelVarScopedDeclarations are initialised (see step 4 and 7 of GDI, step 9 and 10 of FDI and the SDT for FunctionStatementList).

I’m not entirely sure what “top-level” means here – does it exclude code inside blocks? If so, bar and x are not top-level, but we can still refer to them before they are declared in the code, which implies that they were “hoisted”, i.e. bindings for them were created inside foo's variable environment when FDI was performed for the foo call. But where does this happen in the FDI, if not at steps 9 and 10?

If we assume “top-level” includes the stuff inside blocks, then it explains the hoisting of bar and x, but it also means that block-level FDs are hoisted regardless of strict-mode. This violates step 29 of FDI though – which implies that block-level FDs are scoped to the outer function only if that function is not in strict-mode, and block-scoped otherwise – which is what we see in practice:

"use strict";

(function foo() {
  
  console.log(x); // undefined  
  console.log(bar); // ReferenceError: bar is not defined
  
  {
    var x = 10;
    
    function bar() { 
      
      console.log("bar")
      
    };
    
  };
  
})();

I'm relatively new to navigating the spec, so I apologise if this is a silly question and would be grateful for any guidance the community could provide.

Note that var is not block-level (that is, restricted to the innermost block scope); it is always top-level (that is, restricted to the innermost function/module/global scope). The block-level variable statement is let. Whether you’re in strict mode or not is irrelevant.

As for function declaration, the spec is (currently) not easy to understand at first reading, because the main text treats it as always block-level, then Annex B patches the main text in order to make it top-level in non-strict mode.

Thanks for following up @claudepache.

Yes, I understand that var statements are function-scoped. To be clear, by "block-level var statements", I do not mean block-scoped, but rather I am referring to var statements that appear inside of blocks.

What I'm unsure about is which step in the FunctionDeclarationInstantiation is responsible for hoisting the var x statement to foo's scope in my example. I originally thought it was step 9 of FunctionDeclarationInstantiation, but VarDeclaredNames only identifies TopLevelVarDeclaredNames and since var x appears inside a block (i.e. not at the top-level), it wouldn't be picked up.

Rather than looking at edition 11 (ES 2020), you might be better off with edition 12 (2021) or the current draft, as it 'consolidates' most syntax-directed operations (i.e. brings together all the definitions for a given SDO into a single clause). This makes it somewhat easier to grasp the general semantics of an SDO.

TopLevelVarDeclaredNames (TLVDN) doesn't have many definitions; it recurses a bit, but mostly delegates to VarDeclaredNames (VDN) or BoundNames as soon as it can. So one way to think of TLVDN is: it's mostly the same as VDN, but behaves differently when it reaches a FunctionDeclaration (or more generally, a HoistableDeclaration). TLVDN collects the BoundNames of the FunctionDeclaration, whereas VDN does not.

So I believe your intuition about the prefix "TopLevel" is isn't quite right (which is understandable, since I don't think the spec does much to give you the right intuition). TLVDN doesn't mean "a version of VDN that collects names only from the syntactic top level". Rather, I think it means something like "a version of VDN that you have to use when you're at the top level" or "a version of VDN that collects all the names that are relevant at the top level".

But to answer your question: if var x appears inside a Block, step 9 of FDI invokes VDN (on FunctionBody), which invokes TLVDN (on StatementList), which invokes VDN (on Statement), which recurses into Block : { StatementList }, which recurses into VariableStatement, which collects x.

1 Like

Thank you for walking me through the SDOs @jmdyck. The consolidated layout and anchor links in the 11th/12th edition is much easier to follow.