Skip to content

Commit eb4619d

Browse files
nshahanCommit Queue
authored andcommitted
[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 <[email protected]> Commit-Queue: Nicholas Shahan <[email protected]> Reviewed-by: Nate Biggs <[email protected]>
1 parent 41ff6c3 commit eb4619d

File tree

26 files changed

+646
-26
lines changed

26 files changed

+646
-26
lines changed

pkg/dev_compiler/lib/src/kernel/compiler_new.dart

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6551,6 +6551,19 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
65516551
// Otherwise generate this as a normal typed method call.
65526552
var jsName = _emitMemberName(name, member: target);
65536553
var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]);
6554+
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
6555+
var checkedInvocation = _rewriteInvocationWithHotReloadChecks(
6556+
jsReceiver,
6557+
jsName,
6558+
target,
6559+
node.arguments,
6560+
node.getStaticType(_staticTypeContext),
6561+
_nodeStart(node),
6562+
);
6563+
// As an optimization, avoid extra checks when the invocation code was
6564+
// compiled in the same generation that it is running.
6565+
return _emitHotReloadSafeInvocation(invocation, checkedInvocation);
6566+
}
65546567
return _isNullCheckableJsInterop(node.interfaceTarget)
65556568
? _wrapWithJsInteropNullCheck(invocation)
65566569
: invocation;
@@ -7636,6 +7649,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
76367649
..sourceInformation = _nodeStart(node);
76377650
if (_shouldRewriteInvocationWithHotReloadChecks(target)) {
76387651
var checkedCall = _rewriteInvocationWithHotReloadChecks(
7652+
fn.receiver,
7653+
fn.selector,
76397654
target,
76407655
node.arguments,
76417656
node.getStaticType(_staticTypeContext),
@@ -7678,6 +7693,10 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
76787693
/// time to include checks to preserve soundness in the presence of hot
76797694
/// reloads at runtime.
76807695
///
7696+
/// The compiled JavaScript [receiver] and [selector] should be passed so that
7697+
/// they can be reused for the validated invocation after the checks have
7698+
/// passed.
7699+
///
76817700
/// The checks are similar to the those performed when making a dynamic call.
76827701
/// The [arguments] are checked for the correct shape and runtime types.
76837702
/// Additionally after the invocation, the returned value is checked against
@@ -7689,7 +7708,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
76897708
/// The resulting expression for the validated call site will receive the
76907709
/// [originalCallSiteSourceLocation].
76917710
js_ast.Expression _rewriteInvocationWithHotReloadChecks(
7692-
Member target,
7711+
js_ast.Expression receiver,
7712+
js_ast.Expression selector,
7713+
Procedure target,
76937714
Arguments arguments,
76947715
DartType expectedReturnType,
76957716
SourceLocation? originalCallSiteSourceLocation,
@@ -7738,8 +7759,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
77387759
// still valid.
77397760
var checkResult = _emitScopedId('\$result');
77407761
_letVariables!.add(checkResult);
7741-
var jsTarget = _emitStaticTarget(target);
7742-
var jsTypeArguments = [
7762+
var typeArguments = [
77437763
// TODO(nshahan): Remove this check if we stop rewriting calls to SDK
77447764
// functions.
77457765
if (_reifyGenericFunction(target))
@@ -7748,9 +7768,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
77487768
var correctnessCheck = _runtimeCall(
77497769
'hotReloadCorrectnessChecks(#, #, #, #, #)',
77507770
[
7751-
jsTarget.receiver,
7752-
jsTarget.selector,
7753-
js_ast.ArrayInitializer(jsTypeArguments),
7771+
receiver,
7772+
selector,
7773+
js_ast.ArrayInitializer(typeArguments),
77547774
js_ast.ArrayInitializer(hoistedPositionalVariables),
77557775
hoistedNamedVariables.isEmpty
77567776
? js_ast.LiteralNull()
@@ -7766,14 +7786,18 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
77667786
: js_ast.Binary(',', letAssignments, checkAssignment);
77677787
// Create a new invocation of the original target but passing all the
77687788
// arguments via their let variables.
7769-
var validatedCallSite = js_ast.Call(jsTarget, [
7770-
...jsTypeArguments,
7771-
...hoistedPositionalVariables,
7772-
if (hoistedNamedVariables.isNotEmpty)
7773-
js_ast.ObjectInitializer([
7774-
for (var e in hoistedNamedVariables.entries)
7775-
js_ast.Property(js.string(e.key), e.value),
7776-
]),
7789+
var validatedCallSite = js.call('#.#(#)', [
7790+
receiver,
7791+
selector,
7792+
[
7793+
...typeArguments,
7794+
...hoistedPositionalVariables,
7795+
if (hoistedNamedVariables.isNotEmpty)
7796+
js_ast.ObjectInitializer([
7797+
for (var e in hoistedNamedVariables.entries)
7798+
js_ast.Property(js.string(e.key), e.value),
7799+
]),
7800+
],
77777801
])..sourceInformation = originalCallSiteSourceLocation;
77787802
// Cast the result of the checked call or the value returned from a
77797803
// `NoSuchMethod` invocation.

sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -744,13 +744,15 @@ Object? hotReloadCorrectnessChecks(
744744
),
745745
);
746746
}
747-
var functionType = JS<Object?>(
748-
'',
749-
'#[#][#]',
750-
receiver,
751-
name,
752-
JS_GET_NAME(JsGetName.SIGNATURE_NAME),
753-
);
747+
var functionType = _jsInstanceOf(receiver, Object)
748+
? getMethodType(receiver, name)
749+
: JS<Object?>(
750+
'',
751+
'#[#][#]',
752+
receiver,
753+
name,
754+
JS_GET_NAME(JsGetName.SIGNATURE_NAME),
755+
);
754756
if (functionType == null) {
755757
// Allow JavaScript interop calls without checking arguments.
756758
// TODO(nshahan): Potentially we should be checking arguments to static
@@ -1546,7 +1548,7 @@ bool isStateBearingSymbol(property) => JS<bool>(
15461548
/// copies the members of [classDeclaration] and its prototype's properties to
15471549
/// the existing class. Existing members not prefixed by a special identifier
15481550
/// are replaced (see [isStateBearingSymbol]).
1549-
declareClass(library, classIdentifier, classDeclaration) {
1551+
declareClass(Object library, Object classIdentifier, Object classDeclaration) {
15501552
var originalClass = JS<Object>('!', '#.#', library, classIdentifier);
15511553
if (JS<bool>('!', '# === void 0', originalClass)) {
15521554
JS('', '#.# = #', library, classIdentifier, classDeclaration);
@@ -1559,13 +1561,16 @@ declareClass(library, classIdentifier, classDeclaration) {
15591561
!isStateBearingSymbol(property),
15601562
originalClassProto,
15611563
);
1564+
// Reconcile instance members.
1565+
deleteClassMembers(originalClassProto, newClassProto);
15621566
copyProperties(originalClassProto, newClassProto, copyWhen: copyWhenProto);
15631567
var copyWhen = (property) => JS<bool>(
15641568
'!',
15651569
'# || # === void 0',
15661570
!isStateBearingSymbol(property),
15671571
originalClass,
15681572
);
1573+
// Reconcile static members.
15691574
deleteClassMembers(originalClass, classDeclaration);
15701575
copyProperties(originalClass, classDeclaration, copyWhen: copyWhen);
15711576
}
@@ -1579,10 +1584,12 @@ declareClass(library, classIdentifier, classDeclaration) {
15791584
///
15801585
/// Called from generated code.
15811586
void deleteClassMembers(Object oldClass, Object newClass) {
1582-
for (var name in getOwnNamesAndSymbols(oldClass)) {
1583-
if (JS<Object?>('', '#.#', newClass, name) == null &&
1584-
!isStateBearingSymbol(name)) {
1585-
JS('', 'delete #.#', oldClass, name);
1587+
var oldClassNamesAndSymbols = getOwnNamesAndSymbols(oldClass);
1588+
var newClassNamesAndSymbols = getOwnNamesAndSymbols(newClass);
1589+
for (var property in oldClassNamesAndSymbols) {
1590+
if (JS<bool>('', '!#.includes(#)', newClassNamesAndSymbols, property) &&
1591+
!isStateBearingSymbol(property)) {
1592+
JS('', 'delete #.#', oldClass, property);
15861593
}
15871594
}
15881595
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"exclude": [
3+
"vm"
4+
]
5+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
String deleted() {
13+
return 'hello';
14+
}
15+
}
16+
17+
helper() {
18+
c = C();
19+
retained = () => c!.deleted();
20+
return retained();
21+
}
22+
23+
Future<void> main() async {
24+
helper();
25+
await hotReload();
26+
Expect.throws<NoSuchMethodError>(
27+
helper,
28+
(error) => '$error'.contains('deleted'),
29+
);
30+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {}
12+
13+
helper() {
14+
return retained();
15+
}
16+
17+
Future<void> main() async {
18+
helper();
19+
await hotReload();
20+
Expect.throws<NoSuchMethodError>(
21+
helper,
22+
(error) => '$error'.contains('deleted'),
23+
);
24+
}
25+
26+
/** DIFF **/
27+
/*
28+
var retained;
29+
C? c;
30+
31+
-class C {
32+
- String deleted() {
33+
- return 'hello';
34+
- }
35+
-}
36+
+class C {}
37+
38+
helper() {
39+
- c = C();
40+
- retained = () => c!.deleted();
41+
return retained();
42+
}
43+
44+
*/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"exclude": [
3+
"chrome",
4+
"d8"
5+
]
6+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
String deleted() {
13+
return 'hello';
14+
}
15+
}
16+
17+
helper() {
18+
c = C();
19+
retained = () => c!.deleted();
20+
return retained();
21+
}
22+
23+
Future<void> main() async {
24+
helper();
25+
await hotReload();
26+
Expect.throws(
27+
helper,
28+
(error) => '$error'.contains('Lookup failed: deleted in @methods in C'),
29+
);
30+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {}
12+
13+
helper() {
14+
return retained();
15+
}
16+
17+
Future<void> main() async {
18+
helper();
19+
await hotReload();
20+
Expect.throws(
21+
helper,
22+
(error) => '$error'.contains('Lookup failed: deleted in @methods in C'),
23+
);
24+
}
25+
26+
/** DIFF **/
27+
/*
28+
var retained;
29+
C? c;
30+
31+
-class C {
32+
- String deleted() {
33+
- return 'hello';
34+
- }
35+
-}
36+
+class C {}
37+
38+
helper() {
39+
- c = C();
40+
- retained = () => c!.deleted();
41+
return retained();
42+
}
43+
44+
*/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"exclude": [
3+
"vm"
4+
]
5+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:expect/expect.dart';
6+
import 'package:reload_test/reload_test_utils.dart';
7+
8+
var retained;
9+
C? c;
10+
11+
class C {
12+
int parametersChange(int i) {
13+
return i + 10;
14+
}
15+
}
16+
17+
helper() {
18+
c = C();
19+
retained = () => c!.parametersChange(32);
20+
return retained!();
21+
}
22+
23+
Future<void> main() async {
24+
Expect.equals(42, helper());
25+
await hotReload();
26+
Expect.throws<TypeError>(
27+
helper,
28+
(error) => '$error'.contains("'int' is not a subtype of type 'String'"),
29+
);
30+
}

0 commit comments

Comments
 (0)