[ddc] Add hot reload checks for instance methods

Handles errors and type changes on a hot reload when call sites
that were statically valid are retained and run after the reload.

Change-Id: I8bebbd7bc7acc97f55ff930e8f456f99146fbf21
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/440082
Reviewed-by: Mark Zhou <markzipan@google.com>
Commit-Queue: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Nicholas Shahan
2025-07-28 16:16:06 -07:00
committed by Commit Queue
parent 41ff6c38a6
commit eb4619d139
26 changed files with 646 additions and 26 deletions
@@ -6551,6 +6551,19 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
// Otherwise generate this as a normal typed method call.
var jsName = _emitMemberName(name, member: target);
var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]);
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
jsReceiver,
jsName,
target,
node.arguments,
node.getStaticType(_staticTypeContext),
_nodeStart(node),
);
// As an optimization, avoid extra checks when the invocation code was
// compiled in the same generation that it is running.
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
}
return _isNullCheckableJsInterop(node.interfaceTarget)
? _wrapWithJsInteropNullCheck(invocation)
: invocation;
@@ -7636,6 +7649,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
..sourceInformation = _nodeStart(node);
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
var checkedCall = _rewriteInvocationWithHotReloadChecks(
fn.receiver,
fn.selector,
target,
node.arguments,
node.getStaticType(_staticTypeContext),
@@ -7678,6 +7693,10 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
/// time to include checks to preserve soundness in the presence of hot
/// reloads at runtime.
///
/// The compiled JavaScript [receiver] and [selector] should be passed so that
/// they can be reused for the validated invocation after the checks have
/// passed.
///
/// The checks are similar to the those performed when making a dynamic call.
/// The [arguments] are checked for the correct shape and runtime types.
/// Additionally after the invocation, the returned value is checked against
@@ -7689,7 +7708,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
/// The resulting expression for the validated call site will receive the
/// [originalCallSiteSourceLocation].
js_ast.Expression _rewriteInvocationWithHotReloadChecks(
Member target,
js_ast.Expression receiver,
js_ast.Expression selector,
Procedure target,
Arguments arguments,
DartType expectedReturnType,
SourceLocation? originalCallSiteSourceLocation,
@@ -7738,8 +7759,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
// still valid.
var checkResult = _emitScopedId('\$result');
_letVariables!.add(checkResult);
var jsTarget = _emitStaticTarget(target);
var jsTypeArguments = [
var typeArguments = [
// TODO(nshahan): Remove this check if we stop rewriting calls to SDK
// functions.
if (_reifyGenericFunction(target))
@@ -7748,9 +7768,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
var correctnessCheck = _runtimeCall(
'hotReloadCorrectnessChecks(#, #, #, #, #)',
[
jsTarget.receiver,
jsTarget.selector,
js_ast.ArrayInitializer(jsTypeArguments),
receiver,
selector,
js_ast.ArrayInitializer(typeArguments),
js_ast.ArrayInitializer(hoistedPositionalVariables),
hoistedNamedVariables.isEmpty
? js_ast.LiteralNull()
@@ -7766,14 +7786,18 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
: js_ast.Binary(',', letAssignments, checkAssignment);
// Create a new invocation of the original target but passing all the
// arguments via their let variables.
var validatedCallSite = js_ast.Call(jsTarget, [
...jsTypeArguments,
...hoistedPositionalVariables,
if (hoistedNamedVariables.isNotEmpty)
js_ast.ObjectInitializer([
for (var e in hoistedNamedVariables.entries)
js_ast.Property(js.string(e.key), e.value),
]),
var validatedCallSite = js.call('#.#(#)', [
receiver,
selector,
[
...typeArguments,
...hoistedPositionalVariables,
if (hoistedNamedVariables.isNotEmpty)
js_ast.ObjectInitializer([
for (var e in hoistedNamedVariables.entries)
js_ast.Property(js.string(e.key), e.value),
]),
],
])..sourceInformation = originalCallSiteSourceLocation;
// Cast the result of the checked call or the value returned from a
// `NoSuchMethod` invocation.
@@ -744,13 +744,15 @@ Object? hotReloadCorrectnessChecks(
),
);
}
var functionType = JS<Object?>(
'',
'#[#][#]',
receiver,
name,
JS_GET_NAME(JsGetName.SIGNATURE_NAME),
);
var functionType = _jsInstanceOf(receiver, Object)
? getMethodType(receiver, name)
: JS<Object?>(
'',
'#[#][#]',
receiver,
name,
JS_GET_NAME(JsGetName.SIGNATURE_NAME),
);
if (functionType == null) {
// Allow JavaScript interop calls without checking arguments.
// TODO(nshahan): Potentially we should be checking arguments to static
@@ -1546,7 +1548,7 @@ bool isStateBearingSymbol(property) => JS<bool>(
/// copies the members of [classDeclaration] and its prototype's properties to
/// the existing class. Existing members not prefixed by a special identifier
/// are replaced (see [isStateBearingSymbol]).
declareClass(library, classIdentifier, classDeclaration) {
declareClass(Object library, Object classIdentifier, Object classDeclaration) {
var originalClass = JS<Object>('!', '#.#', library, classIdentifier);
if (JS<bool>('!', '# === void 0', originalClass)) {
JS('', '#.# = #', library, classIdentifier, classDeclaration);
@@ -1559,6 +1561,8 @@ declareClass(library, classIdentifier, classDeclaration) {
!isStateBearingSymbol(property),
originalClassProto,
);
// Reconcile instance members.
deleteClassMembers(originalClassProto, newClassProto);
copyProperties(originalClassProto, newClassProto, copyWhen: copyWhenProto);
var copyWhen = (property) => JS<bool>(
'!',
@@ -1566,6 +1570,7 @@ declareClass(library, classIdentifier, classDeclaration) {
!isStateBearingSymbol(property),
originalClass,
);
// Reconcile static members.
deleteClassMembers(originalClass, classDeclaration);
copyProperties(originalClass, classDeclaration, copyWhen: copyWhen);
}
@@ -1579,10 +1584,12 @@ declareClass(library, classIdentifier, classDeclaration) {
///
/// Called from generated code.
void deleteClassMembers(Object oldClass, Object newClass) {
for (var name in getOwnNamesAndSymbols(oldClass)) {
if (JS<Object?>('', '#.#', newClass, name) == null &&
!isStateBearingSymbol(name)) {
JS('', 'delete #.#', oldClass, name);
var oldClassNamesAndSymbols = getOwnNamesAndSymbols(oldClass);
var newClassNamesAndSymbols = getOwnNamesAndSymbols(newClass);
for (var property in oldClassNamesAndSymbols) {
if (JS<bool>('', '!#.includes(#)', newClassNamesAndSymbols, property) &&
!isStateBearingSymbol(property)) {
JS('', 'delete #.#', oldClass, property);
}
}
}
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,30 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
String deleted() {
return 'hello';
}
}
helper() {
c = C();
retained = () => c!.deleted();
return retained();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) => '$error'.contains('deleted'),
);
}
@@ -0,0 +1,44 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {}
helper() {
return retained();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) => '$error'.contains('deleted'),
);
}
/** DIFF **/
/*
var retained;
C? c;
-class C {
- String deleted() {
- return 'hello';
- }
-}
+class C {}
helper() {
- c = C();
- retained = () => c!.deleted();
return retained();
}
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"chrome",
"d8"
]
}
@@ -0,0 +1,30 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
String deleted() {
return 'hello';
}
}
helper() {
c = C();
retained = () => c!.deleted();
return retained();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws(
helper,
(error) => '$error'.contains('Lookup failed: deleted in @methods in C'),
);
}
@@ -0,0 +1,44 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {}
helper() {
return retained();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws(
helper,
(error) => '$error'.contains('Lookup failed: deleted in @methods in C'),
);
}
/** DIFF **/
/*
var retained;
C? c;
-class C {
- String deleted() {
- return 'hello';
- }
-}
+class C {}
helper() {
- c = C();
- retained = () => c!.deleted();
return retained();
}
*/
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,30 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
int parametersChange(int i) {
return i + 10;
}
}
helper() {
c = C();
retained = () => c!.parametersChange(32);
return retained!();
}
Future<void> main() async {
Expect.equals(42, helper());
await hotReload();
Expect.throws<TypeError>(
helper,
(error) => '$error'.contains("'int' is not a subtype of type 'String'"),
);
}
@@ -0,0 +1,48 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
int parametersChange(String s) {
return s.length;
}
}
helper() {
return retained!();
}
Future<void> main() async {
Expect.equals(42, helper());
await hotReload();
Expect.throws<TypeError>(
helper,
(error) => '$error'.contains("'int' is not a subtype of type 'String'"),
);
}
/** DIFF **/
/*
C? c;
class C {
- int parametersChange(int i) {
- return i + 10;
+ int parametersChange(String s) {
+ return s.length;
}
}
helper() {
- c = C();
- retained = () => c!.parametersChange(32);
return retained!();
}
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"chrome",
"d8"
]
}
@@ -0,0 +1,31 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
int parametersChange(int i) {
return i + 10;
}
}
helper() {
c = C();
retained = () => c!.parametersChange(32);
return retained!();
}
Future<void> main() async {
Expect.equals(42, helper());
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) =>
'$error'.contains("Class 'int' has no instance getter 'length'."),
);
}
@@ -0,0 +1,49 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
var retained;
C? c;
class C {
int parametersChange(String s) {
return s.length;
}
}
helper() {
return retained!();
}
Future<void> main() async {
Expect.equals(42, helper());
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) =>
'$error'.contains("Class 'int' has no instance getter 'length'."),
);
}
/** DIFF **/
/*
C? c;
class C {
- int parametersChange(int i) {
- return i + 10;
+ int parametersChange(String s) {
+ return s.length;
}
}
helper() {
- c = C();
- retained = () => c!.parametersChange(32);
return retained!();
}
*/
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,30 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
int Function()? retained;
C? c;
class C {
String returnChange() {
return 'hello';
}
}
helper() {
c = C();
retained = () => c!.returnChange().length;
return retained!();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<TypeError>(
helper,
(error) => '$error'.contains("'double' is not a subtype of type 'String'"),
);
}
@@ -0,0 +1,48 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
int Function()? retained;
C? c;
class C {
double returnChange() {
return 3.14;
}
}
helper() {
return retained!();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<TypeError>(
helper,
(error) => '$error'.contains("'double' is not a subtype of type 'String'"),
);
}
/** DIFF **/
/*
C? c;
class C {
- String returnChange() {
- return 'hello';
+ double returnChange() {
+ return 3.14;
}
}
helper() {
- c = C();
- retained = () => c!.returnChange().length;
return retained!();
}
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"chrome",
"d8"
]
}
@@ -0,0 +1,30 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
int Function()? retained;
C? c;
class C {
String returnChange() {
return 'hello';
}
}
helper() {
c = C();
retained = () => c!.returnChange().length;
return retained!();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) => '$error'.contains("'length'"),
);
}
@@ -0,0 +1,48 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
int Function()? retained;
C? c;
class C {
double returnChange() {
return 3.14;
}
}
helper() {
return retained!();
}
Future<void> main() async {
helper();
await hotReload();
Expect.throws<NoSuchMethodError>(
helper,
(error) => '$error'.contains("'length'"),
);
}
/** DIFF **/
/*
C? c;
class C {
- String returnChange() {
- return 'hello';
+ double returnChange() {
+ return 3.14;
}
}
helper() {
- c = C();
- retained = () => c!.returnChange().length;
return retained!();
}
*/
@@ -0,0 +1,5 @@
{
"exclude": [
"vm"
]
}
@@ -0,0 +1,33 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
// Adapted from:
// https://github.com/dart-lang/sdk/blob/1a486499bf73ee5b007abbe522b94869a1f36d02/runtime/vm/isolate_reload_test.cc#L3985
// Tests reload succeeds when instance format changes.
// Change: Bar {a, b}, Foo : Bar {c:42} -> Bar {c:42}, Foo : Bar {}
// Validate: c keeps the value in the retained Foo object.
class Bar {
var a;
var b;
}
class Foo extends Bar {
var c;
}
var f;
Future<void> main() async {
f = Foo();
f.c = 42;
Expect.equals(42, f.c);
await hotReload();
Expect.equals(null, f.c);
}
@@ -0,0 +1,50 @@
// 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 'package:expect/expect.dart';
import 'package:reload_test/reload_test_utils.dart';
// Adapted from:
// https://github.com/dart-lang/sdk/blob/1a486499bf73ee5b007abbe522b94869a1f36d02/runtime/vm/isolate_reload_test.cc#L3985
// Tests reload succeeds when instance format changes.
// Change: Bar {a, b}, Foo : Bar {c:42} -> Bar {c:42}, Foo : Bar {}
// Validate: c keeps the value in the retained Foo object.
class Bar {
var c;
}
class Foo extends Bar {}
var f;
Future<void> main() async {
f = Foo();
f.c = 42;
Expect.equals(42, f.c);
await hotReload();
Expect.equals(null, f.c);
}
/** DIFF **/
/*
// Validate: c keeps the value in the retained Foo object.
class Bar {
- var a;
- var b;
-}
-
-class Foo extends Bar {
var c;
}
+class Foo extends Bar {}
+
var f;
Future<void> main() async {
*/
@@ -0,0 +1,6 @@
{
"exclude": [
"d8",
"chrome"
]
}