[dart2wasm] Catch JS exceptions in async functions

When generating `try` block around a CFG block of an async state
machine, generate `catch_all` if the Dart `catch` blocks wrapping the
CFG block can catch JS exceptions.

`catch_all` bodies are identical to the `catch` bodies we already
generate.

The check for whether to generate `catch_all` is reused from the
non-async code generator without changes.

Fixes #55457.

Change-Id: I9d89593599da592106e12efb77c07d68b6cfce5f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/363000
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Jackson Gardner <jacksongardner@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
This commit is contained in:
Ömer Sinan Ağacan 2024-04-18 08:57:39 +00:00 committed by Commit Queue
parent a133b75438
commit eabb2b3aca
3 changed files with 209 additions and 35 deletions

View file

@ -276,33 +276,64 @@ class _ExceptionHandlerStack {
void terminateTryBlocks() {
int nextHandlerIdx = _handlers.length - 1;
for (final int nCoveredHandlers in _tryBlockNumHandlers.reversed) {
codeGen.b.catch_(codeGen.translator.exceptionTag);
final stackTraceLocal = codeGen
.addLocal(codeGen.translator.stackTraceInfo.repr.nonNullableType);
codeGen.b.local_set(stackTraceLocal);
final exceptionLocal =
codeGen.addLocal(codeGen.translator.topInfo.nonNullableType);
codeGen.b.local_set(exceptionLocal);
for (int i = 0; i < nCoveredHandlers; i += 1) {
final handler = _handlers[nextHandlerIdx - i];
if (handler is Finalizer) {
handler.setContinuationRethrow(
() => codeGen.b.local_get(exceptionLocal),
() => codeGen.b.local_get(stackTraceLocal));
void generateCatchBody() {
// Set continuations of finalizers that can be reached by this `catch`
// (or `catch_all`) as "rethrow".
for (int i = 0; i < nCoveredHandlers; i += 1) {
final handler = _handlers[nextHandlerIdx - i];
if (handler is Finalizer) {
handler.setContinuationRethrow(
() => codeGen.b.local_get(exceptionLocal),
() => codeGen.b.local_get(stackTraceLocal));
}
}
// Set the untyped "current exception" variable. Catch blocks will do the
// type tests as necessary using this variable and set their exception
// and stack trace locals.
codeGen._setCurrentException(() => codeGen.b.local_get(exceptionLocal));
codeGen._setCurrentExceptionStackTrace(
() => codeGen.b.local_get(stackTraceLocal));
codeGen.jumpToTarget(_handlers[nextHandlerIdx].target);
}
// Set the untyped "current exception" variable. Catch blocks will do the
// type tests as necessary using this variable and set their exception
// and stack trace locals.
codeGen._setCurrentException(() => codeGen.b.local_get(exceptionLocal));
codeGen._setCurrentExceptionStackTrace(
() => codeGen.b.local_get(stackTraceLocal));
codeGen.b.catch_(codeGen.translator.exceptionTag);
codeGen.b.local_set(stackTraceLocal);
codeGen.b.local_set(exceptionLocal);
codeGen.jumpToTarget(_handlers[nextHandlerIdx].target);
generateCatchBody();
// Generate a `catch_all` to catch JS exceptions if any of the covered
// handlers can catch JS exceptions.
bool canHandleJSExceptions = false;
for (int handlerIdx = nextHandlerIdx;
handlerIdx > nextHandlerIdx - nCoveredHandlers;
handlerIdx -= 1) {
final handler = _handlers[handlerIdx];
canHandleJSExceptions |= handler.canHandleJSExceptions;
}
if (canHandleJSExceptions) {
codeGen.b.catch_all();
// We can't inspect the thrown object in a `catch_all` and get a stack
// trace, so we just attach the current stack trace.
codeGen.call(codeGen.translator.stackTraceCurrent.reference);
codeGen.b.local_set(stackTraceLocal);
// We create a generic JavaScript error.
codeGen.call(codeGen.translator.javaScriptErrorFactory.reference);
codeGen.b.local_set(exceptionLocal);
generateCatchBody();
}
codeGen.b.end(); // end catch
@ -323,20 +354,28 @@ abstract class _ExceptionHandler {
final StateTarget target;
_ExceptionHandler(this.target);
bool get canHandleJSExceptions;
}
class Catcher extends _ExceptionHandler {
final List<VariableDeclaration> _exceptionVars = [];
final List<VariableDeclaration> _stackTraceVars = [];
final AsyncCodeGenerator codeGen;
bool _canHandleJSExceptions = false;
Catcher.fromTryCatch(this.codeGen, TryCatch node, super.target) {
for (Catch catch_ in node.catches) {
_exceptionVars.add(catch_.exception!);
_stackTraceVars.add(catch_.stackTrace!);
_canHandleJSExceptions |=
guardCanMatchJSException(codeGen.translator, catch_.guard);
}
}
@override
bool get canHandleJSExceptions => _canHandleJSExceptions;
void setException(void Function() pushException) {
for (final exceptionVar in _exceptionVars) {
codeGen._setVariable(exceptionVar, pushException);
@ -373,6 +412,9 @@ class Finalizer extends _ExceptionHandler {
_stackTraceVar =
(node.parent as Block).statements[2] as VariableDeclaration;
@override
bool get canHandleJSExceptions => true;
void setContinuationFallthrough() {
codeGen._setVariable(_continuationVar, () {
codeGen.b.i64_const(continuationFallthrough);

View file

@ -1240,24 +1240,10 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
// Rethrow if all the catch blocks fall through
b.rethrow_(try_);
bool guardCanMatchJSException(DartType guard) {
if (guard is DynamicType) {
return true;
}
if (guard is InterfaceType) {
return translator.hierarchy
.isSubInterfaceOf(translator.javaScriptErrorClass, guard.classNode);
}
if (guard is TypeParameterType) {
return guardCanMatchJSException(guard.bound);
}
return false;
}
// If we have a catches that are generic enough to catch a JavaScript
// error, we need to put that into a catch_all block.
final Iterable<Catch> catchAllCatches =
node.catches.where((c) => guardCanMatchJSException(c.guard));
final Iterable<Catch> catchAllCatches = node.catches
.where((c) => guardCanMatchJSException(translator, c.guard));
if (catchAllCatches.isNotEmpty) {
// This catches any objects that aren't dart exceptions, such as
@ -3942,3 +3928,17 @@ extension MacroAssembler on w.InstructionsBuilder {
translator.closureLayouter.closureBaseStruct, FieldIndex.closureVtable);
}
}
bool guardCanMatchJSException(Translator translator, DartType guard) {
if (guard is DynamicType) {
return true;
}
if (guard is InterfaceType) {
return translator.hierarchy
.isSubInterfaceOf(translator.javaScriptErrorClass, guard.classNode);
}
if (guard is TypeParameterType) {
return guardCanMatchJSException(translator, guard.bound);
}
return false;
}

View file

@ -5,17 +5,29 @@
import 'dart:js_interop';
import 'dart:_wasm';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
// Catch JS exceptions in try-catch and try-finally, in sync and async
// functions. Also in `await`.
void main() async {
asyncStart();
defineThrowJSException();
jsExceptionCatch();
jsExceptionFinally();
jsExceptionCatchAsync();
jsExceptionFinallyAsync();
await jsExceptionCatchAsync();
await jsExceptionFinallyAsync();
await jsExceptionCatchAsyncDirect();
await jsExceptionFinallyAsyncDirect();
await jsExceptionFinallyPropagateAsync();
await jsExceptionFinallyPropagateAsyncDirect();
await jsExceptionTypeTest1();
await jsExceptionTypeTest2();
asyncEnd();
}
@JS()
@ -60,6 +72,9 @@ Future<void> throwJSExceptionAsync() async {
return throwJSException();
}
// A simple async function used to create suspension points.
Future<int> yield_() async => runtimeTrue() ? 1 : throw '';
// Catch a JS exception throw by `await` in `catch`.
Future<void> jsExceptionCatchAsync() async {
try {
@ -81,3 +96,120 @@ Future<void> jsExceptionFinallyAsync() async {
}
Expect.fail("Exception not caught");
}
// Catch a JS exception thrown without `await` in `catch`.
Future<void> jsExceptionCatchAsyncDirect() async {
try {
throwJSException();
} catch (e) {
return;
}
Expect.fail("Exception not caught");
}
// Catch a JS exception thrown without `await` in `finally`.
Future<void> jsExceptionFinallyAsyncDirect() async {
if (runtimeTrue()) {
try {
throwJSException();
} finally {
return;
}
}
Expect.fail("Exception not caught");
}
// Check that the finally blocks rethrow JS exceptions, when `await` throws.
Future<void> jsExceptionFinallyPropagateAsync() async {
int i = 0;
try {
if (runtimeTrue()) {
try {
await throwJSExceptionAsync();
} finally {
i += 1;
}
}
} catch (e) {
i += 1;
}
Expect.equals(i, 2);
}
// Check that the finally blocks rethrow JS exceptions, when a function directly throws (no `await`).
Future<void> jsExceptionFinallyPropagateAsyncDirect() async {
int i = 0;
try {
if (runtimeTrue()) {
try {
throwJSException();
} finally {
i += 1;
}
}
} catch (e) {
i += 1;
}
Expect.equals(i, 2);
}
// Catch JS exception and run type tests. Dummy `await` statements to generate
// suspension points before and after every statement. Type test should succeed
// in the same try-catch statement.
Future<void> jsExceptionTypeTest1() async {
bool exceptionCaught = false;
bool errorCaught = false;
try {
await yield_();
if (runtimeTrue()) {
try {
await yield_();
throwJSException();
await yield_();
} on Exception catch (_) {
await yield_();
exceptionCaught = true;
await yield_();
} on Error catch (_) {
await yield_();
errorCaught = true;
await yield_();
}
}
} catch (_) {
await yield_();
}
Expect.equals(exceptionCaught, false);
Expect.equals(errorCaught, true);
}
// Similar to `jsExceptionTypeTest1`, but the type test should succeed in a
// parent try-catch.
Future<void> jsExceptionTypeTest2() async {
bool exceptionCaught = false;
bool errorCaught = false;
try {
await yield_();
if (runtimeTrue()) {
try {
await yield_();
throwJSException();
await yield_();
} on Exception catch (_) {
await yield_();
exceptionCaught = true;
await yield_();
}
}
} on Exception catch (_) {
await yield_();
exceptionCaught = true;
await yield_();
} on Error catch (_) {
await yield_();
errorCaught = true;
await yield_();
}
Expect.equals(exceptionCaught, false);
Expect.equals(errorCaught, true);
}