mirror of
https://github.com/dart-lang/sdk
synced 2024-10-03 00:29:50 +00:00
[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:
parent
a133b75438
commit
eabb2b3aca
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue