LibJS: Perform TLA async function construction in the module context

Previously it was only pushing the module context for the call to
capture the module execution context. This is incorrect, as the capture
occurs upon function construction. This resulted in it capturing the
execution context that execute_module was called from, instead of the
newly created module_context.
f87041bf3a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp (L92)

This can be demonstrated with the following setup:
index.html:
```html
<script>
    var foo = 1;
</script>
<script type="module">
    import {test} from "./scriptA.mjs";
</script>
```

scriptA.mjs:
```js
function foo() {
	return {a: "b"};
}
export let test = await foo();
```

Before this fix, this would throw:
```
[TypeError] 1 is not a function (evaluated from 'foo')
    at module code with top-level await
    at module code with top-level await
    at <unknown>
    at <unknown>
```

Fixes #2245.
This commit is contained in:
Luke Wilde 2024-11-15 15:11:48 +00:00 committed by Andreas Kling
parent 1383d03c02
commit 6319dedbcd
Notes: github-actions[bot] 2024-11-15 18:36:18 +00:00
3 changed files with 39 additions and 4 deletions

View file

@ -760,6 +760,10 @@ ThrowCompletionOr<void> SourceTextModule::execute_module(VM& vm, GC::Ptr<Promise
// the top-level module code.
// FIXME: Improve this situation, so we can match the spec better.
// AD-HOC: We push/pop the moduleContext around the function construction to ensure that the async execution context
// captures the module execution context.
vm.push_execution_context(*module_context);
FunctionParsingInsights parsing_insights;
parsing_insights.uses_this_from_environment = true;
parsing_insights.uses_this = true;
@ -768,12 +772,10 @@ ThrowCompletionOr<void> SourceTextModule::execute_module(VM& vm, GC::Ptr<Promise
{}, 0, {}, environment(), nullptr, FunctionKind::Async, true, parsing_insights);
module_wrapper_function->set_is_module_wrapper(true);
// AD-HOC: We push/pop the moduleContext around the call to ensure that the async execution context
// captures the module execution context.
vm.push_execution_context(*module_context);
auto result = call(vm, Value { module_wrapper_function }, js_undefined(), ReadonlySpan<Value> {});
vm.pop_execution_context();
auto result = call(vm, Value { module_wrapper_function }, js_undefined(), ReadonlySpan<Value> {});
// AD-HOC: This is basically analogous to what AsyncBlockStart would do.
if (result.is_throw_completion()) {
MUST(call(vm, *capability->reject(), js_undefined(), result.throw_completion().value().value()));

View file

@ -53,6 +53,33 @@ describe("normal behavior", () => {
expect(value).not.toHaveProperty("default", null);
expect(value).not.toHaveProperty("bar", null);
expect(value).not.toHaveProperty("baz", null);
expect(value).not.toHaveProperty("qux", null);
passed = true;
})
.catch(value => {
error = value;
});
runQueuedPromiseJobs();
expect(error).toBeNull();
expect(passed).toBeTrue();
});
test("value from async module from top-level awaited function", () => {
const shadowRealm = new ShadowRealm();
const promise = shadowRealm.importValue("./async-module.mjs", "qux");
expect(promise).toBeInstanceOf(Promise);
let error = null;
let passed = false;
promise
.then(value => {
expect(value).toBe("'qux' export");
expect(typeof value).toBe("string");
expect(value).not.toHaveProperty("default", null);
expect(value).not.toHaveProperty("foo", null);
expect(value).not.toHaveProperty("bar", null);
expect(value).not.toHaveProperty("baz", null);
passed = true;
})
.catch(value => {

View file

@ -9,3 +9,9 @@ export default "Default export";
await Promise.resolve(2);
export const bar = "'bar' export";
async function baz() {
return "'qux' export";
}
export const qux = await baz();