Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise was rejected with value ReferenceError: c2 has not been initialized #1385

Open
DSchroer opened this issue Dec 22, 2022 · 9 comments
Open

Comments

@DSchroer
Copy link

Hey, I am working on getting a popular library to work in Jint. At the moment I end up running into an issue related to variable initialization:

Promise was rejected with value ReferenceError: c2 has not been initialized

at Zo (e) <anonymous>:22341:149
   at fetchModule (?, n3, r2, a2) <anonymous>:22342:14
   at preloadModule (e) <anonymous>:22256:21

Following the stack trace, it points to this line:

const h2 = this.addModuleSource(e, n3, l2).then(() => [this.getResolveStaticDependencyPromises(l2), this.getResolveDynamicImportPromises(l2), c2]),
        c2 = Zo(h2).then(() => this.pluginDriver.hookParallel("moduleParsed", [l2.info]));

This code runs fine in NodeJS. I am trying to get a minimum reproducible example but it is proving difficult. I would really appreciate any guidance as to where the issue could be coming from and what I could do in order to create a minimal repro example.

@lahma
Copy link
Collaborator

lahma commented Dec 25, 2022

What might be this popular library? Without repro it's hard to say what's happening. You probably need to run Jint sources under debugger to gather some context about what's going wrong.

@Genteure
Copy link
Contributor

I searched getResolveStaticDependencyPromises on GitHub and the library is rollup, the snippet above is compiled from ModuleLoader.ts.

I made a "minimal"* repro:

*: might be possible to make it even shorter, but I think it's good enough.

if (typeof print === "undefined") {
    globalThis.print = console.log;
}

print("start of script");
const promise1 = new Promise((resolve, reject) => {
    print("promise1 before resolve");
    resolve();
    print("promise1 after resolve");
}).then(() => [promise2])
print("promise1 is defined");

const promise2 = waitForPromise(promise1).then(() => "This is the value of promise2");
print("promise2 is defined");

promise2.catch(e => {
    print("promise2 catch");
    print(e)
});

promise2.then((value) => {
    print("promise2 then");
    print(value)
});

print("end of script, print");
print(promise1)
print(promise2)
print("end of script, end of print")

async function waitForPromise(promise) {
    print("promise waiting");
    const value = await promise;
    print("promise waited");
    print(value);
    print("end of promise waited");
};
$ node -v
v18.10.0
$ node path/to/script.js
start of script
promise1 before resolve
promise1 after resolve
promise1 is defined
promise waiting
promise2 is defined
end of script, print
Promise { <pending> }
Promise { <pending> }
end of script, end of print
promise waited
[ Promise { <pending> } ]
end of promise waited
promise2 then
This is the value of promise2
jint> load("path/to/script.js")
start of script
promise1 before resolve
promise1 after resolve
promise1 is defined
promise waiting
promise2 is defined
end of script, print
System.Dynamic.ExpandoObject
System.Dynamic.ExpandoObject
end of script, end of print
promise2 catch
ReferenceError: promise2 has not been initialized
null

@Genteure
Copy link
Contributor

After transforming the script with babel so it uses regenerator-runtime it behaves the same in Jint as in node.

Probably related to async function and await and the timing/order of evaluations?

@lahma
Copy link
Collaborator

lahma commented Dec 25, 2022

I think the problem probably is that async function call result is not dispatched to event loop.

@DSchroer
Copy link
Author

Hey. To confirm it is Rollup that we are trying to get working. We tried Babel with regenerator-runtime but unfortunately it produced a script that would stack overflow for both node and jint.

We would like to remove babel entirely from our pipeline, for now we cant since there is a parser bug with the super keyword as well. Ill create a new ticket for that when I get a chance.

Out of curiosity, how much effort would it be to fix dispatching async results to the event loop?

@DSchroer
Copy link
Author

Thank you very much for looking into this and helping with the repro code.

@lahma
Copy link
Collaborator

lahma commented Dec 26, 2022

The problem is here as far as I can tell:

private static void AsyncBlockStart(EvaluationContext context, PromiseCapability promiseCapability, Func<EvaluationContext, Completion> asyncBody, in ExecutionContext asyncContext)
{
var runningContext = context.Engine.ExecutionContext;
// Set the code evaluation state of asyncContext such that when evaluation is resumed for that execution contxt the following steps will be performed:
Completion result;
try
{
result = asyncBody(context);
}
catch (JavaScriptException e)
{
promiseCapability.Reject.Call(JsValue.Undefined, new[] { e.Error });
return;
}
if (result.Type == CompletionType.Normal)
{
promiseCapability.Resolve.Call(JsValue.Undefined, new[] { JsValue.Undefined });
}
else if (result.Type == CompletionType.Return)
{
promiseCapability.Resolve.Call(JsValue.Undefined, new[] { result.Value });
}
else
{
promiseCapability.Reject.Call(JsValue.Undefined, new[] { result.Value });
}
/*
4. Push asyncContext onto the execution context stack; asyncContext is now the running execution context.
5. Resume the suspended evaluation of asyncContext. Let result be the value returned by the resumed computation.
6. Assert: When we return here, asyncContext has already been removed from the execution context stack and runningContext is the currently running execution context.
7. Assert: result is a normal completion with a value of unused. The possible sources of this value are Await or, if the async function doesn't await anything, step 3.g above.
8. Return unused.
*/
}

Basically it dispatches immediately the code in question (asyncBody) when it should actually wrap as lazy in event loop.

@DSchroer
Copy link
Author

DSchroer commented Jan 3, 2023

Hey. So I have shrunk down the repro to work as a unit test under Jint.Tests.CommonScripts/Scripts:

const promise1 = new Promise(resolve => {
    resolve();
}).then(() => [promise2])

const promise2 = waitForPromise(promise1);
promise2.catch(e => {
    assert(false, e)
});

async function waitForPromise(promise) {
    await promise;
}

Using this and digging deeper it seems that its an issue within the async evaluation. When the code gets to result = asyncBody(context); it has already been rejected.

@lofcz
Copy link

lofcz commented Feb 18, 2024

A minimal reproducible issue:

[Fact]
public void ShouldPromiseBeResolved()
{
    var log = new List<string>();
    
    Engine engine = new();
    engine.SetValue("log", (string str) =>
    {
        log.Add(str);
    });
    
    var result = engine.Execute("""
        async function main() {
            return new Promise(function (resolve) {
              log('Promise!')
              resolve(null)
            }).then(function () {
              log('Resolved!')
            })
        }
    """);
    
    JsValue val = result.GetValue("main");

    if (val is Function func)
    {
        func.Call().UnwrapIfPromise();
    }
    
    Assert.Equal(2, log.Count);
    Assert.Equal("Promise!", log[0]);
    Assert.Equal("Resolved!", log[1]);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants