Skip to content

Commit

Permalink
[GR-61889] Error message implies Promise rejected in sync modules.
Browse files Browse the repository at this point in the history
PullRequest: js/3409
  • Loading branch information
woess committed Feb 8, 2025
2 parents 5f74353 + 2e6fcdd commit a4375db
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -304,19 +304,19 @@ private Object evalModule(JSRealm realm) {
// Note: If loading failed, we must not perform module linking.

moduleRecord.link(realm);
Object promise = moduleRecord.evaluate(realm);
boolean isAsync = context.isOptionTopLevelAwait() && moduleRecord.isAsyncEvaluation();
if (isAsync) {
JSFunctionObject onRejected = createTopLevelAwaitReject(context, realm);
JSFunctionObject onAccepted = createTopLevelAwaitResolve(context, realm);
// Non-standard: throw error from onRejected handler.
performPromiseThenNode.execute((JSPromiseObject) promise, onAccepted, onRejected, null);
}
// On failure, an exception is thrown and this module's [[Status]] remains unlinked.

JSPromiseObject promise = moduleRecord.evaluate(realm);
JSFunctionObject onRejected = createTopLevelAwaitReject(context, realm);
JSFunctionObject onAccepted = createTopLevelAwaitResolve(context, realm);
// Non-standard: throw error from onRejected handler.
performPromiseThenNode.execute(promise, onAccepted, onRejected, null);

if (context.getLanguageOptions().esmEvalReturnsExports()) {
JSDynamicObject moduleNamespace = moduleRecord.getModuleNamespace();
assert moduleNamespace != null;
return moduleNamespace;
} else if (isAsync) {
} else if (context.isOptionTopLevelAwait() && moduleRecord.isAsyncEvaluation()) {
return promise;
} else {
return moduleRecord.getExecutionResultOrThrow();
Expand Down Expand Up @@ -700,47 +700,32 @@ private int innerModuleLinking(JSRealm realm, AbstractModuleRecord abstractModul

@TruffleBoundary
@Override
public Object moduleEvaluation(JSRealm realm, CyclicModuleRecord moduleRecord) {
public JSPromiseObject moduleEvaluation(JSRealm realm, CyclicModuleRecord moduleRecord) {
// Evaluate ( ) Concrete Method
CyclicModuleRecord module = moduleRecord;
assert module.getStatus() == Status.Linked || module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated : module.getStatus();
if (module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated) {
module = module.getCycleRoot();
}
if (module.getTopLevelCapability() != null) {
return (JSPromiseObject) module.getTopLevelCapability().getPromise();
}
Deque<CyclicModuleRecord> stack = new ArrayDeque<>(4);
if (realm.getContext().isOptionTopLevelAwait()) {
assert module.getStatus() == Status.Linked || module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated : module.getStatus();
if (module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated) {
module = module.getCycleRoot();
}
if (module.getTopLevelCapability() != null) {
return module.getTopLevelCapability().getPromise();
}
PromiseCapabilityRecord capability = NewPromiseCapabilityNode.createDefault(realm);
module.setTopLevelCapability(capability);
try {
innerModuleEvaluation(realm, module, stack, 0);
assert module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated;
assert module.getEvaluationError() == null;
if (!module.isAsyncEvaluation()) {
assert module.getStatus() == Status.Evaluated;
JSFunction.call(JSArguments.create(Undefined.instance, capability.getResolve(), Undefined.instance));
}
assert stack.isEmpty();
} catch (AbstractTruffleException e) {
handleModuleEvaluationError(module, stack, e);
}
return capability.getPromise();
} else {
try {
innerModuleEvaluation(realm, module, stack, 0);
} catch (AbstractTruffleException e) {
handleModuleEvaluationError(module, stack, e);
throw e;
}
PromiseCapabilityRecord capability = NewPromiseCapabilityNode.createDefault(realm);
module.setTopLevelCapability(capability);
try {
innerModuleEvaluation(realm, module, stack, 0);
assert module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated;
assert module.getEvaluationError() == null;

if (!module.isAsyncEvaluation()) {
assert module.getStatus() == Status.Evaluated;
JSFunction.call(JSArguments.create(Undefined.instance, capability.getResolve(), Undefined.instance));
}
assert stack.isEmpty();
Object result = module.getExecutionResult();
return result == null ? Undefined.instance : result;
} catch (AbstractTruffleException e) {
handleModuleEvaluationError(module, stack, e);
}
return (JSPromiseObject) capability.getPromise();
}

private static void handleModuleEvaluationError(CyclicModuleRecord module, Deque<CyclicModuleRecord> stack, AbstractTruffleException e) {
Expand All @@ -752,9 +737,7 @@ private static void handleModuleEvaluationError(CyclicModuleRecord module, Deque
assert module.getStatus() == Status.Evaluated && module.getEvaluationError() == e;

PromiseCapabilityRecord capability = module.getTopLevelCapability();
if (capability != null) {
JSFunction.call(JSArguments.create(Undefined.instance, capability.getReject(), getErrorObject(e)));
}
JSFunction.call(JSArguments.create(Undefined.instance, capability.getReject(), getErrorObject(e)));
}

private static Object getErrorObject(AbstractTruffleException e) {
Expand All @@ -769,13 +752,9 @@ private int innerModuleEvaluation(JSRealm realm, AbstractModuleRecord abstractMo
// InnerModuleEvaluation( module, stack, index )
int index = index0;
if (!(abstractModule instanceof CyclicModuleRecord moduleRecord)) {
Object result = abstractModule.evaluate(realm);
if (result instanceof JSPromiseObject promise) {
assert !JSPromise.isPending(promise);
if (JSPromise.isRejected(promise)) {
throw JSRuntime.getException(JSPromise.getPromiseResult(promise));
}
}
JSPromiseObject promise = abstractModule.evaluate(realm);
assert !JSPromise.isPending(promise);
JSPromise.throwIfRejected(promise, realm);
return index;
}
if (moduleRecord.getStatus() == Status.EvaluatingAsync || moduleRecord.getStatus() == Status.Evaluated) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or
* data (collectively the "Software"), free of charge and under any and all
* copyright rights in the Software, and any and all patent rights owned or
* freely licensable by each licensor hereunder covering either (i) the
* unmodified Software as contributed to or provided by such licensor, or (ii)
* the Larger Works (as defined below), to deal in both
*
* (a) the Software, and
*
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
*
* The above copyright notice and either this complete permission notice or at a
* minimum a reference to the UPL must be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oracle.truffle.js.test.regress;

import static com.oracle.truffle.js.runtime.JSContextOptions.UNHANDLED_REJECTIONS_NAME;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.io.IOAccess;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import com.oracle.truffle.js.test.JSTest;
import com.oracle.truffle.js.test.polyglot.MockFileSystem;

@RunWith(Parameterized.class)
public class GR61889 {

@Parameters(name = "unhandled-rejections={0}")
public static Iterable<String> data() {
return List.of("none", "throw", "warn");
}

@Parameter(0) public String unhandledRejections;

/**
* Do not treat rejected module evaluation promises as unhandled rejections.
*/
@Test
public void testRejectedModulePromiseIsHandled() throws IOException {
var fs = new MockFileSystem(Map.of(
"eval-throws-error.mjs", """
export function foo() {return 42}
throw new Error("Thrown from within the library");
""",
"eval-throws-non-error.mjs", """
export function foo() {return 42}
throw "Thrown from within the library";
"""));

try (var out = new ByteArrayOutputStream();
Context c = JSTest.newContextBuilder().//
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
out(out).err(out).build()) {
// Error thrown from another synchronously executed module.
for (String moduleCode : List.of(
"import * as throws from './eval-throws-error.mjs';",
"import * as throws from './eval-throws-non-error.mjs';")) {
try {
c.eval(Source.newBuilder("js", moduleCode, "main.mjs").buildLiteral());
fail("should have thrown");
} catch (PolyglotException e) {
assertFalse(e.getMessage(), e.isSyntaxError());
assertTrue(e.getMessage(), e.isGuestException());
assertThat(e.getMessage(), containsString("Thrown from within the library"));
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
}
}
assertThat("No unhandled rejection warnings", out.toString(), equalTo(""));
}

try (var out = new ByteArrayOutputStream();
Context c = JSTest.newContextBuilder().//
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
out(out).err(out).build()) {
// Error object thrown from the main module.
for (String moduleCode : List.of("""
export function foo() {return 42}
throw new Error("Thrown from within main module");
""", """
export function foo() {return 42}
throw "Thrown from within main module";
""")) {
try {
c.eval(Source.newBuilder("js", moduleCode, "main.mjs").buildLiteral());
fail("should have thrown");
} catch (PolyglotException e) {
assertFalse(e.getMessage(), e.isSyntaxError());
assertTrue(e.getMessage(), e.isGuestException());
assertThat(e.getMessage(), containsString("Thrown from within main module"));
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
}
assertThat("No unhandled rejection warnings", out.toString(), equalTo(""));
}
}
}

/**
* Do not treat rejected module loading promises as unhandled rejections.
*/
@Test
public void testRejectedLoadRequestedModulesPromiseIsHandled() throws IOException {
var fs = new MockFileSystem(Map.of());
try (var out = new ByteArrayOutputStream();
Context c = JSTest.newContextBuilder().//
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
out(out).err(out).build()) {
try {
c.eval(Source.newBuilder("js", """
import * as throws from "./does_not_exist.mjs";
""", "./throws-during-load.mjs").buildLiteral());
fail("should have thrown");
} catch (PolyglotException e) {
assertFalse(e.getMessage(), e.isSyntaxError());
assertTrue(e.getMessage(), e.isGuestException());
assertThat(e.getMessage(), containsString("Cannot find module"));
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
}
assertThat(out.toString(), equalTo(""));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
Expand Down Expand Up @@ -81,7 +81,6 @@
import com.oracle.truffle.js.runtime.builtins.JSPromise;
import com.oracle.truffle.js.runtime.builtins.JSPromiseObject;
import com.oracle.truffle.js.runtime.objects.AbstractModuleRecord;
import com.oracle.truffle.js.runtime.objects.CyclicModuleRecord;
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
import com.oracle.truffle.js.runtime.objects.JSObject;
import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord;
Expand Down Expand Up @@ -381,19 +380,9 @@ protected Object executeInRealm(VirtualFrame frame) {
// If link is an abrupt completion, reject the promise from import().
moduleRecord.link(realm);

// Evaluate() should always return a promise.
// Yet, if top-level-await is disabled, returns/throws the result instead.
Object evaluatePromise = moduleRecord.evaluate(realm);
if (context.isOptionTopLevelAwait() || !(moduleRecord instanceof CyclicModuleRecord cyclicModuleRecord)) {
assert evaluatePromise instanceof JSPromiseObject : evaluatePromise;
JSFunctionObject onFulfilled = createFulfilledClosure(context, realm, captures);
promiseThenNode.execute((JSPromiseObject) evaluatePromise, onFulfilled, onRejected);
} else {
// Rethrow any previous execution errors.
cyclicModuleRecord.getExecutionResultOrThrow();
var namespace = moduleRecord.getModuleNamespace();
callPromiseResolve.executeCall(JSArguments.createOneArg(Undefined.instance, importPromiseCapability.getResolve(), namespace));
}
JSPromiseObject evaluatePromise = moduleRecord.evaluate(realm);
JSFunctionObject onFulfilled = createFulfilledClosure(context, realm, captures);
promiseThenNode.execute(evaluatePromise, onFulfilled, onRejected);
} catch (AbstractTruffleException ex) {
rejectPromise(importPromiseCapability, ex);
}
Expand Down
Loading

0 comments on commit a4375db

Please sign in to comment.