Skip to content

Commit e9b5679

Browse files
mkustermanncommit-bot@chromium.org
authored andcommitted
[vm/compiler] Make closures have no context if no variables are closed over
As a side-effect of making closures without captured variables have an empty context, we not only avoid a memory leak, but also allow such closures to be sent across [SendPort]s. Issue #36983 Issue #45603 TEST=vm/dart{_2,}/isolates/closures_without_captured_variables_test Change-Id: I5e8845203059ff625f7cff3f072d845b5e48ba36 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/212297 Commit-Queue: Martin Kustermann <[email protected]> Reviewed-by: Slava Egorov <[email protected]>
1 parent 9bec5d6 commit e9b5679

File tree

7 files changed

+220
-5
lines changed

7 files changed

+220
-5
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2021, 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+
// VMOptions=--enable-isolate-groups
6+
7+
// The tests in this file will only succeed when isolate groups are enabled
8+
// (hence the VMOptions above).
9+
10+
import 'dart:async';
11+
import 'dart:isolate';
12+
13+
import 'package:expect/expect.dart';
14+
15+
import 'fast_object_copy_test.dart' show ClassWithNativeFields;
16+
17+
main() async {
18+
final rp = ReceivePort();
19+
20+
testNormalEnclosingFunction(rp);
21+
testNormalNestedEnclosingFunction(rp);
22+
testNormalNestedEnclosingFunction2(rp);
23+
24+
final si = StreamIterator(rp);
25+
for (int i = 0; i < 3; ++i) {
26+
Expect.isTrue(await si.moveNext());
27+
Expect.equals(42, (si.current)());
28+
}
29+
si.cancel(); // closes the port
30+
}
31+
32+
testNormalEnclosingFunction(ReceivePort rp) {
33+
final invalidObject = ClassWithNativeFields();
34+
final normalObject = Object();
35+
36+
captureInvalidObject() => invalidObject;
37+
captureNormalObject() => normalObject;
38+
captureNothing() => 42;
39+
40+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
41+
42+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
43+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
44+
45+
// Should not throw, since the [captureNothing] closure should not have a
46+
// parent context and therefore not transitively refer [rp].
47+
rp.sendPort.send(captureNothing);
48+
}
49+
50+
testNormalNestedEnclosingFunction(ReceivePort rp) {
51+
final invalidObject = ClassWithNativeFields();
52+
final normalObject = Object();
53+
nested() {
54+
captureInvalidObject() => invalidObject;
55+
captureNormalObject() => normalObject;
56+
captureNothing() => 42;
57+
58+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
59+
60+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
61+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
62+
63+
// Should not throw, since the [captureNothing] closure should not have a
64+
// parent context and therefore not transitively refer [rp].
65+
rp.sendPort.send(captureNothing);
66+
}
67+
68+
nested();
69+
}
70+
71+
testNormalNestedEnclosingFunction2(ReceivePort rp) {
72+
final invalidObject = ClassWithNativeFields();
73+
final normalObject = Object();
74+
75+
captureInvalidObject() {
76+
local() => invalidObject;
77+
return local;
78+
}
79+
80+
captureNormalObject() {
81+
local() => normalObject;
82+
return local;
83+
}
84+
85+
captureNothing() => 42;
86+
87+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
88+
89+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
90+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
91+
92+
// Should not throw, since the [captureNothing] closure should not have a
93+
// parent context and therefore not transitively refer [rp].
94+
rp.sendPort.send(captureNothing);
95+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2021, 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+
// VMOptions=--enable-isolate-groups
6+
7+
// The tests in this file will only succeed when isolate groups are enabled
8+
// (hence the VMOptions above).
9+
10+
// @dart=2.9
11+
12+
import 'dart:async';
13+
import 'dart:isolate';
14+
15+
import 'package:expect/expect.dart';
16+
17+
import 'fast_object_copy_test.dart' show ClassWithNativeFields;
18+
19+
main() async {
20+
final rp = ReceivePort();
21+
22+
testNormalEnclosingFunction(rp);
23+
testNormalNestedEnclosingFunction(rp);
24+
testNormalNestedEnclosingFunction2(rp);
25+
26+
final si = StreamIterator(rp);
27+
for (int i = 0; i < 3; ++i) {
28+
Expect.isTrue(await si.moveNext());
29+
Expect.equals(42, (si.current)());
30+
}
31+
si.cancel(); // closes the port
32+
}
33+
34+
testNormalEnclosingFunction(ReceivePort rp) {
35+
final invalidObject = ClassWithNativeFields();
36+
final normalObject = Object();
37+
38+
captureInvalidObject() => invalidObject;
39+
captureNormalObject() => normalObject;
40+
captureNothing() => 42;
41+
42+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
43+
44+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
45+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
46+
47+
// Should not throw, since the [captureNothing] closure should not have a
48+
// parent context and therefore not transitively refer [rp].
49+
rp.sendPort.send(captureNothing);
50+
}
51+
52+
testNormalNestedEnclosingFunction(ReceivePort rp) {
53+
final invalidObject = ClassWithNativeFields();
54+
final normalObject = Object();
55+
nested() {
56+
captureInvalidObject() => invalidObject;
57+
captureNormalObject() => normalObject;
58+
captureNothing() => 42;
59+
60+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
61+
62+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
63+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
64+
65+
// Should not throw, since the [captureNothing] closure should not have a
66+
// parent context and therefore not transitively refer [rp].
67+
rp.sendPort.send(captureNothing);
68+
}
69+
70+
nested();
71+
}
72+
73+
testNormalNestedEnclosingFunction2(ReceivePort rp) {
74+
final invalidObject = ClassWithNativeFields();
75+
final normalObject = Object();
76+
77+
captureInvalidObject() {
78+
local() => invalidObject;
79+
return local;
80+
}
81+
82+
captureNormalObject() {
83+
local() => normalObject;
84+
return local;
85+
}
86+
87+
captureNothing() => 42;
88+
89+
Expect.throwsArgumentError(() => rp.sendPort.send(captureInvalidObject));
90+
91+
// TODO(http://dartbug.com/36983): Avoid capturing more than needed.
92+
Expect.throwsArgumentError(() => rp.sendPort.send(captureNormalObject));
93+
94+
// Should not throw, since the [captureNothing] closure should not have a
95+
// parent context and therefore not transitively refer [rp].
96+
rp.sendPort.send(captureNothing);
97+
}

runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5252,7 +5252,7 @@ Fragment StreamingFlowGraphBuilder::BuildFunctionNode(
52525252
bool has_valid_annotation,
52535253
bool has_pragma,
52545254
intptr_t func_decl_offset) {
5255-
intptr_t offset = ReaderOffset();
5255+
const intptr_t offset = ReaderOffset();
52565256

52575257
FunctionNodeHelper function_node_helper(this);
52585258
function_node_helper.ReadUntilExcluding(FunctionNodeHelper::kTypeParameters);
@@ -5385,7 +5385,11 @@ Fragment StreamingFlowGraphBuilder::BuildFunctionNode(
53855385

53865386
Fragment instructions;
53875387
instructions += Constant(function);
5388-
instructions += LoadLocal(parsed_function()->current_context_var());
5388+
if (scopes()->IsClosureWithEmptyContext(offset)) {
5389+
instructions += NullConstant();
5390+
} else {
5391+
instructions += LoadLocal(parsed_function()->current_context_var());
5392+
}
53895393
instructions += flow_graph_builder_->AllocateClosure();
53905394
LocalVariable* closure = MakeTemporary();
53915395

runtime/vm/compiler/frontend/scope_builder.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ void ScopeBuilder::VisitTypeParameterType() {
14921492

14931493
void ScopeBuilder::HandleLocalFunction(intptr_t parent_kernel_offset) {
14941494
// "Peek" ahead into the function node
1495-
intptr_t offset = helper_.ReaderOffset();
1495+
const intptr_t offset = helper_.ReaderOffset();
14961496

14971497
FunctionNodeHelper function_node_helper(&helper_);
14981498
function_node_helper.ReadUntilExcluding(FunctionNodeHelper::kTypeParameters);
@@ -1534,6 +1534,14 @@ void ScopeBuilder::HandleLocalFunction(intptr_t parent_kernel_offset) {
15341534

15351535
VisitFunctionNode(); // read function node.
15361536

1537+
// Remember if this closure and all closures nested within it don't
1538+
// capture any variables from outer scopes.
1539+
if (scope_->function_level() == 1) {
1540+
if (scope_->NumCapturedVariables() == 0) {
1541+
result_->closure_offsets_without_captures.Add(offset);
1542+
}
1543+
}
1544+
15371545
ExitScope(function_node_helper.position_, function_node_helper.end_position_);
15381546
depth_ = saved_depth_state;
15391547
current_function_scope_ = saved_function_scope;

runtime/vm/compiler/frontend/scope_builder.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ class ScopeBuildingResult : public ZoneAllocated {
189189
yield_context_variable(NULL),
190190
raw_variable_counter_(0) {}
191191

192+
bool IsClosureWithEmptyContext(intptr_t function_node_offset) {
193+
for (intptr_t i = 0; i < closure_offsets_without_captures.length(); ++i) {
194+
if (closure_offsets_without_captures[i] == function_node_offset) {
195+
return true;
196+
}
197+
}
198+
return false;
199+
}
200+
192201
IntMap<LocalVariable*> locals;
193202
IntMap<LocalScope*> scopes;
194203
GrowableArray<FunctionScope> function_scopes;
@@ -230,6 +239,10 @@ class ScopeBuildingResult : public ZoneAllocated {
230239
// For-in iterators, one per for-in nesting level.
231240
GrowableArray<LocalVariable*> iterator_variables;
232241

242+
// Remembers closure function kernel offsets that do not capture any
243+
// variables.
244+
GrowableArray<intptr_t> closure_offsets_without_captures;
245+
233246
private:
234247
DISALLOW_COPY_AND_ASSIGN(ScopeBuildingResult);
235248
};

tests/lib/isolate/illegal_msg_function_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// VMOptions=--enable-isolate-groups
65
// VMOptions=--no-enable-isolate-groups
76

87
library illegal_msg_function_test;

tests/lib_2/isolate/illegal_msg_function_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
// @dart = 2.9
66

7-
// VMOptions=--enable-isolate-groups
87
// VMOptions=--no-enable-isolate-groups
98

109
library illegal_msg_function_test;

0 commit comments

Comments
 (0)