[ddc] Add hot reload checks in aysnc bodies

Treats async code like function expressions when adding extra
checks for correctness after a hot reload. This is intended to handle
the cases where code that was statically sound and enqueued before the
reload actually runs after a reload when the soundness may no longer
be valid.

Change-Id: I42cd010175126748937d7df830875bc5b850a565
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444368
Reviewed-by: Nate Biggs <natebiggs@google.com>
Commit-Queue: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
Nicholas Shahan
2025-09-30 15:06:34 -07:00
committed by Commit Queue
parent 5199e1493e
commit cff9ab2c8e
15 changed files with 448 additions and 11 deletions
@@ -873,6 +873,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
final FutureOrNormalizer _futureOrNormalizer;
bool _inFunctionExpression = false;
bool _inAsyncExpression = false;
/// Returns whether or not [uri] can be hot reloaded.
///
@@ -5072,8 +5073,14 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
}
js_ast.Statement _emitFunctionScopedBody(FunctionNode f) {
var savedInAsyncExpression = _inAsyncExpression;
if (f.asyncMarker != AsyncMarker.Sync) {
_inAsyncExpression = true;
}
var jsBody = _visitStatement(f.body!);
return _emitScopedBody(f, jsBody);
var body = _emitScopedBody(f, jsBody);
_inAsyncExpression = savedInAsyncExpression;
return body;
}
js_ast.Statement _emitScopedBody(FunctionNode f, js_ast.Statement body) {
@@ -7732,7 +7739,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
/// include checks to preserve soundness in the presence of hot reloads at
/// runtime.
bool _shouldRewriteInvocationWithHotReloadChecks(Member target) =>
_inFunctionExpression &&
(_inFunctionExpression || _inAsyncExpression) &&
!_isBuildingSdk &&
!usesJSInterop(target) &&
target != _assertInteropMethod;
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,43 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/expect.dart';
import 'package:expect/async_helper.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been deleted.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int deleted() {
return 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return deleted();
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var e = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
Expect.contains(
"NoSuchMethodError: 'deleted'\n"
'Method was deleted during a hot reload and is no longer callable.',
e.toString(),
);
Expect.isTrue(setAfterAwait);
}
@@ -0,0 +1,59 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/expect.dart';
import 'package:expect/async_helper.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been deleted.
bool setBeforeAwait = false;
bool setAfterAwait = false;
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return 99;
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var e = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
Expect.contains(
"NoSuchMethodError: 'deleted'\n"
'Method was deleted during a hot reload and is no longer callable.',
e.toString(),
);
Expect.isTrue(setAfterAwait);
}
/** DIFF **/
/*
bool setBeforeAwait = false;
bool setAfterAwait = false;
-int deleted() {
- return 10;
-}
-
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
- return deleted();
+ return 99;
}
Future<void> main() async {
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"chrome",
"d8"
]
}
@@ -0,0 +1,37 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been deleted.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int deleted() {
return 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return deleted();
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
Expect.equals(10, await helperFuture);
Expect.isTrue(setAfterAwait);
}
@@ -0,0 +1,53 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been deleted.
bool setBeforeAwait = false;
bool setAfterAwait = false;
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return 99;
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
Expect.equals(10, await helperFuture);
Expect.isTrue(setAfterAwait);
}
/** DIFF **/
/*
bool setBeforeAwait = false;
bool setAfterAwait = false;
-int deleted() {
- return 10;
-}
-
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
- return deleted();
+ return 99;
}
Future<void> main() async {
*/
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,42 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been updated.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int changed(String s) {
return s.length;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return changed('hello');
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var error = await asyncExpectThrows<TypeError>(helperFuture);
Expect.contains(
"type 'String' is not a subtype of type 'int'",
error.toString(),
);
Expect.isTrue(setAfterAwait);
}
@@ -0,0 +1,64 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been updated.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int changed(int i) {
return i - 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return changed(99);
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var error = await asyncExpectThrows<TypeError>(helperFuture);
Expect.contains(
"type 'String' is not a subtype of type 'int'",
error.toString(),
);
Expect.isTrue(setAfterAwait);
}
/** DIFF **/
/*
bool setBeforeAwait = false;
bool setAfterAwait = false;
-int changed(String s) {
- return s.length;
+int changed(int i) {
+ return i - 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
- return changed('hello');
+ return changed(99);
}
Future<void> main() async {
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"chrome",
"d8"
]
}
@@ -0,0 +1,42 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been updated.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int changed(String s) {
return s.length;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return changed('hello');
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var error = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
Expect.contains(
"Class 'String' has no instance method '-'",
error.toString(),
);
Expect.isTrue(setAfterAwait);
}
@@ -0,0 +1,64 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
/// Shows the behavior when async code enqueued before a hot restart runs
/// after the restart and tries to interact with code that has been updated.
bool setBeforeAwait = false;
bool setAfterAwait = false;
int changed(int i) {
return i - 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
return changed(99);
}
Future<void> main() async {
var helperCompleter = Completer<void>();
var helperFuture = helper(helperCompleter.future);
Expect.isTrue(setBeforeAwait);
Expect.isFalse(setAfterAwait);
await hotReload();
Expect.isFalse(setAfterAwait);
helperCompleter.complete();
var error = await asyncExpectThrows<NoSuchMethodError>(helperFuture);
Expect.contains(
"Class 'String' has no instance method '-'",
error.toString(),
);
Expect.isTrue(setAfterAwait);
}
/** DIFF **/
/*
bool setBeforeAwait = false;
bool setAfterAwait = false;
-int changed(String s) {
- return s.length;
+int changed(int i) {
+ return i - 10;
}
Future<int> helper(Future<void> timingControl) async {
setBeforeAwait = true;
await timingControl;
setAfterAwait = true;
- return changed('hello');
+ return changed(99);
}
Future<void> main() async {
*/
@@ -13,11 +13,13 @@ class Foo {
int x = 42;
}
helper() => Foo().x;
Future<void> main() async {
Expect.type<int>(Foo().x);
Expect.equals(42, Foo().x);
Expect.type<int>(helper());
Expect.equals(42, helper());
await hotReload();
Expect.type<String>(Foo().x);
Expect.equals('42', Foo().x);
Expect.type<String>(helper());
Expect.equals('42', helper());
}
@@ -13,13 +13,15 @@ class Foo {
String x = '42';
}
helper() => Foo().x;
Future<void> main() async {
Expect.type<int>(Foo().x);
Expect.equals(42, Foo().x);
Expect.type<int>(helper());
Expect.equals(42, helper());
await hotReload();
Expect.type<String>(Foo().x);
Expect.equals('42', Foo().x);
Expect.type<String>(helper());
Expect.equals('42', helper());
}
/** DIFF **/
@@ -31,5 +33,5 @@ Future<void> main() async {
+ String x = '42';
}
Future<void> main() async {
helper() => Foo().x;
*/