24

I'm trying to figure out whether some behavior I'm seeing in Node v4.1.1 (V8 v4.5.103.33) regarding super and arrow functions is specified behavior, and if so (or indeed, if not), where it is in the specification that it says it should (or should not) work in the various cases I have.

In brief: Using super in an arrow function (inner) inside another arrow function (outer) inside a method works unless outer has arguments or variables inner references, even if inner references arguments or variables of method. I want to know what the spec says about that: Should it work all the time, even where V8 is failing? None of the time? Only in the specific cases where V8 is currently letting it work, and not where it isn't?

Here's an MCVE:

"use strict";

class Parent {
    show(msg) {
        console.log(`Parent#show: ${msg}`);
    }
}

class Child extends Parent {
    method(arg) {
        let outer = (x) => {
            console.log(`outer: x = ${x}`);
            let inner = () => {
                super.show(`arg = ${arg}, x = ${x}`);
            };
            inner();
        };
        outer(42);
    }
}

new Child().method("arg");

That fails with:

$ node test.js
/path/test.js:13
                super.show(`arg = ${arg}, x = ${x}`);
                ^^^^^

SyntaxError: 'super' keyword unexpected here
    at outer (/path/test.js:16:13)
    at Child.method (/path/test.js:18:9)
    at Object. (/path/test.js:22:13)
    at Module._compile (module.js:434:26)
    at Object.Module._extensions..js (module.js:452:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:475:10)
    at startup (node.js:117:18)
    at node.js:951:3

If you remove the reference to x that's in inner:

            let inner = () => {
                super.show(`arg = ${arg}`); // <== removed x from this
            };

it works and outputs:

outer: x = 42
Parent#show: arg = arg

To prove to myself that the "works" case wasn't that the functions were being optimized away, I returned them out of the method and called them. Here's that slightly-more complex case (note the comments); this version works:

"use strict";

class Parent2 {
    show(msg) {
        console.log(`Parent2#show: ${msg}`);
    }
}

class Child2 extends Parent2 {
    method(arg) {
        let flag = Math.random() < 0.5;
        console.log(`method called with ${arg}, flag is ${flag}`);
        let x = "A";                 // **A**
        let outer2 = (/*x*/) => {    // **B**
            //let x = "C";           // **C**
            let inner2 = () => {
                super.show(`${x}: ${arg} (${flag})`);
            };
            return inner2;
        };
        return outer2;
    }
}

let o = new Child2().method("arg");
console.log(`type of outer2: ${typeof o}`);
let i = o();
console.log(`type of inner2: ${typeof i}`);
i("B");

Output:

method called with arg, flag is false
type of outer2: function
type of inner2: function
Parent2#show: A: arg (false)

But if we comment out the line labelled A and uncomment either B or C, it fails like the MCVE does.

More notes:

  • I should emphasize that you need to have the arrow functions nested. outer has no trouble accessing super. I don't want to clutter up the question with another big code block, but if you add super.show(`outer: arg = ${arg}, x = ${x}`); at the top of outer, it works just fine.

  • As you can see, inner uses both an argument and a variable from method (well, the MCVE just uses an arg), and that's fine, but as soon as inner tries to use an argument or variable from outer, things blow up.

  • Babel and Traceur are both perfectly happy to transpile the case that V8 won't run (here and here), but that could just be them getting something wrong that V8 gets right (or, of course, vice-versa).

  • It doesn't relate to template strings; the pre-MCVE version of this didn't use them (and did use promises, which is how we ended up with arrows inside arrows).

Just to emphasize, the question is what's the specified behavior here, and where in the spec is it specified.

My gut tells me this is just a V8 bug — it's early days for this stuff, after all, fair 'nuff. But either way, I'm just trying to figure out what the behavior should be, what the spec says. I've tried to follow its various and sundry sections talking about super and "base objects" and such, and frankly I'm just not getting it.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875

1 Answers1

12

It appears that this is indeed a bug in V8 (it has now been fixed). Note that if there isn't the nested arrow function, it works fine.

So if we're going to take a look through the literal specification text to see whether this is a bug, let's start with the super keyword itself:

12.3.5.3 Runtime Semantics: MakeSuperPropertyReference(propertyKey, strict)

The abstract operation MakeSuperPropertyReference with arguments propertyKey and strict performs the following steps:

  1. Let env be GetThisEnvironment( ).
  2. If env.HasSuperBinding() is false, throw a ReferenceError exception.
  3. Let actualThis be env.GetThisBinding().
  4. ReturnIfAbrupt(actualThis).
  5. Let baseValue be env.GetSuperBase().
  6. Let bv be RequireObjectCoercible(baseValue).
  7. ReturnIfAbrupt(bv).
  8. Return a value of type Reference that is a Super Reference whose base value is bv, whose referenced name is propertyKey, whose thisValue is actualThis, and whose strict reference flag is strict.

Let's ignore most of the wordy stuff and worry about GetThisEnvironment():

8.3.2 GetThisEnvironment ( )

The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword this. GetThisEnvironment performs the following steps:

  1. Let lex be the running execution context’s LexicalEnvironment.
  2. Repeat
    a. Let envRec be lex’s EnvironmentRecord.
    b. Let exists be envRec.HasThisBinding().
    c. If exists is true, return envRec.
    d. Let outer be the value of lex’s outer environment reference.
    e. Let lex be outer.

NOTE The loop in step 2 will always terminate because the list of environments always ends with the global environment which has a this binding.

Now as we know that arrow functions don't have bindings to this, it should skip the environment record of the current function and the function immediately enclosing it.

This will stop once reaching the "regular" functions and go on to retrieve the reference to the super object as expected, according to the specification.

Allen Wirfs-Brock, project editor of the ECMAScript specification, seems to confirm this was intended in a reply on the es-discuss mailing list a few years back:

super is lexically scoped, just like this to the closest enclosing function that defines it. All function definition forms except for arrow functions introduce new this/super bindings so we can just [say] that this/super binds according to the closest enclosing non-arrow function definition.

Community
  • 1
  • 1
Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
  • 1
    So just to be clear, you're saying: It's a bug, V8 isn't handling the loop in `GetThisEnvironment` correctly. Right? (I'm embarrassed not to have found that loop myself, I was certainly looking for it.) – T.J. Crowder Oct 05 '15 at 08:41
  • Well yes, only if I'm understanding the specification correctly... ;) – Qantas 94 Heavy Oct 05 '15 at 08:42
  • Heh. You and me both. :-) And I thought the *last* spec was unclear. *This* one makes it look like a clear blue day... – T.J. Crowder Oct 05 '15 at 08:43
  • 1
    On the basis we're both reading it the same way, I've [filed a bug report](https://code.google.com/p/v8/issues/detail?id=4466). I'll give it a while to see if someone has a different interpretation, but I'm pretty sure you're spot on with this. – T.J. Crowder Oct 05 '15 at 09:00
  • 1
    ...and the bug report has been triaged as Type-Bug and Priority-Medium, so apparently they agree (at least on first glance). – T.J. Crowder Oct 05 '15 at 10:37