chakra-core / ChakraCore

ChakraCore is an open source Javascript engine with a C API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ES6 constructor returns class instead of object

eebbing opened this issue · comments

The code example below should log the numbers from 0 up to 500. Instead, the code breaks after about 100 iterations. The issue is that after about 100 iterations the constructor of class B returns the class itself instead of an instance of B.

The fact that this issue only occurs after multiple iterations suggests to me that the bug is somewhere in the JIT engine.

By the way, the error disappears if the method body of A.toB() is replaced by return new B(this);.

I tested the following code in Microsoft Edge 38.14393.0.0 (Microsoft EdgeHTML 14.14393)

var Test = {};

class A {
    constructor(foo) { this.foo = foo; }
    toB() { return new Test.B(this); }
}

class B {
    constructor(bar) { this.bar = bar; }
}

Test.B = B;

for (let i=0; i<500; i++)
{
    const a = new A(i);
    const b = a.toB();

    try
    {
        console.log(b.bar.foo);
    }
    catch (e)
    {
        console.log(e); // e.description -> "Unable to get property 'foo' of undefined or null reference"
        console.log(b); // b -> class B { ... }
        break;
    }
}

For what it's worth, I decided to look a little bit further into it, and was able to reproduce the error with the latest source code. I ran the script below via ch.exe (x64 debug build) with the following arguments:

ch.exe -forceNative -MaxinterpretCount:10 -MaxSimpleJITRunCount:10 E:\path-to\test.js

The script is:

var Test = {};

class A {
    constructor(foo) { this.foo = foo; }
    toB() { return new Test.B(this); }
}

class B {
    constructor(bar) { this.bar = bar; }
}

Test.B = B;

for (let i=0; i<20; i++)
{
    const a = new A(i);
    const b = a.toB();

    try
    {
    WScript.Echo(b.bar.foo);        
    }
    catch (e)
    {
        WScript.Echo(e);
        WScript.Echo(b);
        break;
    }
}

The output of the script is:

0
1
2
3
4
5
6
7
8
9
TypeError: Unable to get property 'foo' of undefined or null reference
class B {
    constructor(bar) { this.bar = bar; }
}

Awesome! Thanks @eebbing, having a simple repro that we can run with ch.exe makes it a lot easier to debug. We'll look into this soon.

@ianwjhalliday @suwc

not sure if this helps (hopefully, it might), but this testcase can be further simplified by running ../../Build/VcBuild/bin/x86_debug/ch.exe -trace:backend -off:backend:1,3 -dump:irbuilder -dump:lowerer -off:inline -off:simplejit -mic:3 tt.js

to only compile Function A.prototype.toB ( (#1.2), #3)

    s9.var          =  StartCall      2 (0x2).i32                             #000f
    arg2(s10)<4>.var = ArgOut_A       s2[LikelyCanBeTaggedValue_Object].var!, s9.var! #0012
    s11<s15>[UninitializedObject].var = NewScObjectNoCtor  0xXXXXXXXX (FunctionObject [B (#1.3), #4]).var #0015  Bailout: #0015 (BailOutFailedCtorGuardCheck)
    arg1(s12)<0>.var = ArgOut_A       s11<s15>[UninitializedObject].var, arg2(s10)<4>.var! #0015
    s0<s16>[UninitializedObject].var = CallIFixed  0xXXXXXXXX (FunctionObject [B (#1.3), #4]).var, arg1(s12)<0>.var! #0015

NewScObjectNoCtor (which relies on JavascriptOperators::NewScObjectCommon) might return the c-tor function back in the case below (this might be why func B is observed instead of an object)

       if (functionInfo->IsClassConstructor() && !isBaseClassConstructorNewScObject)
        {

Both Inliner and Lower use returnNewScObj flag to decide which object needs to be returned: the one returned by NewScObjectNoCtor or s0<s16>[UninitializedObject].var = CallIFixed 0xXXXXXXXX.

In this case, s0<s16>[UninitializedObject].var = CallIFixed 0xXXXXXXXX seems to construct the correct object (via NewScObjectNoCtorFull) but we still return s11?

IR after GlobOpt

-----------------------------------------------------------------------------
************   IR after GlobOpt (FullJit)  ************
-----------------------------------------------------------------------------
Function A.prototype.toB ( (#1.2), #3)            Instr Count:18

                       FunctionEntry                                          #

BLOCK 0: Out(1)

$L3:                                                                          #
    s1[Object].var  =  Ld_A           0xXXXXXXXX (GlobalObject)[Object].var   #
    s2.var          =  ArgIn_A        prm1<16>.var!                           #0000
    s2[LikelyCanBeTaggedValue_Object].var = StrictLdThis  s2.var!             #0002


  Line   5: return new Test.B(this); }
  Col   13: ^
                       StatementBoundary  #0                                  #0005
    s3<s13>[LikelyCanBeTaggedValue_Object].var = LdRootFld  s6(s1[Object]->Test)<1,m,~-,s?,s?>[LikelyCanBeTaggedValue_Object].var! #0005
                       CheckFixedFld  s7(s3<s13>[LikelyCanBeTaggedValue_Object]->B)<0,m=,++,s13!,s14+,{B(0)=}>.var! #000b  Bailout: #000b (BailOutFailedFixedFieldTypeCheck)
    s9.var          =  StartCall      2 (0x2).i32                             #000f
    arg2(s10)<4>.var = ArgOut_A       s2[LikelyCanBeTaggedValue_Object].var!, s9.var! #0012
    s11<s15>[UninitializedObject].var = NewScObjectNoCtor  0xXXXXXXXX (FunctionObject [B (#1.3), #4]).var #0015  Bailout: #0015 (BailOutFailedCtorGuardCheck)
    arg1(s12)<0>.var = ArgOut_A       s11<s15>[UninitializedObject].var, arg2(s10)<4>.var! #0015
    s0<s16>[UninitializedObject].var = CallIFixed  0xXXXXXXXX (FunctionObject [B (#1.3), #4]).var, arg1(s12)<0>.var! #0015


  Line   5: }
  Col   38: ^
                       StatementBoundary  #1                                  #0024
                       StatementBoundary  #-1                                 #0024
                       Ret            s11<s15>[UninitializedObject].var!      #0024

BLOCK 1: In(0)
commented

@eebbing @ianwjhalliday @Krovatkin thanks for participating - could you help review the PR pls?

commented

Fix is merged.

As of today with the current Fast Ring version of Edge (40.15025.1000), I am still seeing this issue, though the behavior changed slightly: In my test-case the problem reproduces as soon as I open the developer tools. It does not happen anymore when the tools stay closed. Once they are open, the constructor does not return the instance but the class. Is this expected? Is there a different version of Chakra running once the dev tools are open?

commented

Tried both (long and short) test cases above as JS code in HTML files, opened through Node http-server in Edge 40.15026.1000.0 with F12 opened. Couldn't repro. Do you have a repro you can share? Thanks!

I'm a colleague of @yGuy . We have prepared a repro that works against our public URLs: https://gist.github.com/michabaur/2a627a3e7a5703dbd682b6ef3274e5dc. Just extract the two files, open test.html, and then open the dev tools. For us, this breaks after a few seconds in Edge 40.15025.1000

commented

@michabaur @yGuy thanks for the test case.
Fix is merged in release/1.4

What Edge version/release will contain this fix?