[vm] Allow sending closures between isolates of the same isolate group

Closures can be shared if they have no state they capture (i.e. a null
context). Otherwise we copy them by also transitively copying the
parent context chain.

Issue https://github.com/dart-lang/sdk/issues/36097

TEST=Added tests to vm/dart{,_2}/isolates/fast_object_copy{,2}_test

Change-Id: Ie641b97806edd0c21f0a8d5c514f8e850823e165
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/209110
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
This commit is contained in:
Martin Kustermann 2021-09-02 19:45:55 +00:00 committed by commit-bot@chromium.org
parent 714e7e6f9e
commit 26ef8f6947
11 changed files with 415 additions and 44 deletions

View file

@ -19,6 +19,14 @@ import 'package:expect/expect.dart';
import 'fast_object_copy_test.dart'
show UserObject, SendReceiveTestBase, notAllocatableInTLAB;
topLevelClosure(a, b) {}
topLevelClosureG<T>(T a, T b) {}
Type getType<T>() => T;
class A<T> {
dynamic m<H>(T a, H b) => this;
}
// When running with isolate groups enabled, we can share all of the following
// objects.
final sharableObjects = [
@ -32,6 +40,36 @@ final sharableObjects = [
rp.close();
return sp;
})(),
() {
innerClosure(a, b) {}
return innerClosure;
}(),
() {
innerClosureG<T>(T a, T b) {}
return innerClosureG;
}(),
() {
innerClosureG<T>() {
innerClosureG2<H>(T a, H b) {}
return innerClosureG2;
}
return innerClosureG<int>();
}(),
() {
innerClosureG<T>(T a, T b) {}
final Function(int, int) partialInstantiatedInnerClosure = innerClosureG;
return partialInstantiatedInnerClosure;
}(),
() {
return topLevelClosureG;
}(),
() {
final Function(int, int) partialInstantiatedInnerClosure = topLevelClosureG;
return partialInstantiatedInnerClosure;
}(),
getType<void Function(int, double, Object)>(),
getType<T Function<T>(int, double, T)>(),
const [1, 2, 3],
const {1: 1, 2: 2, 3: 2},
const {1, 2, 3},
@ -40,13 +78,40 @@ final sharableObjects = [
Int32x4(1, 2, 3, 4),
];
final copyableClosures = <dynamic>[
() {
final a = A<int>();
final Function<T>(int, T) genericMethod = a.m;
return genericMethod;
}(),
() {
final a = A<int>();
final Function(int, double) partialInstantiatedMethod = a.m;
return partialInstantiatedMethod;
}(),
() {
final a = Object();
dynamic inner() => a;
return inner;
}(),
() {
foo(var arg) {
return () => arg;
}
return foo(1);
}(),
];
class SendReceiveTest extends SendReceiveTestBase {
Future runTests() async {
await testSharable();
await testSharable2();
await testCopyableClosures();
}
Future testSharable() async {
print('testSharable');
final sharableObjectsCopy = await sendReceive([
...sharableObjects,
]);
@ -57,6 +122,7 @@ class SendReceiveTest extends SendReceiveTestBase {
}
Future testSharable2() async {
print('testSharable2');
final sharableObjectsCopy = await sendReceive([
notAllocatableInTLAB,
...sharableObjects,
@ -68,6 +134,27 @@ class SendReceiveTest extends SendReceiveTestBase {
Expect.identical(sharableObjects[i], sharableObjectsCopy[i + 1]);
}
}
Future testCopyableClosures() async {
print('testCopyableClosures');
final copy = await sendReceive([
notAllocatableInTLAB,
...copyableClosures,
]);
for (int i = 0; i < copyableClosures.length; ++i) {
Expect.notIdentical(copyableClosures[i], copy[1 + i]);
Expect.equals(copyableClosures[i].runtimeType, copy[1 + i].runtimeType);
}
final copy2 = await sendReceive([
...copyableClosures,
notAllocatableInTLAB,
]);
for (int i = 0; i < copyableClosures.length; ++i) {
Expect.notIdentical(copyableClosures[i], copy2[i]);
Expect.equals(copyableClosures[i].runtimeType, copy2[i].runtimeType);
}
}
}
main() async {

View file

@ -24,7 +24,9 @@ import 'dart:typed_data';
import 'package:expect/expect.dart';
class ClassWithNativeFields extends NativeFieldWrapperClass1 {}
class ClassWithNativeFields extends NativeFieldWrapperClass1 {
void m() {}
}
final Uint8List largeExternalTypedData =
File(Platform.resolvedExecutable).readAsBytesSync()..[0] = 42;
@ -221,6 +223,8 @@ class SendReceiveTest extends SendReceiveTestBase {
await testSlowOnly();
await testWeakProperty();
await testForbiddenClosures();
}
Future testTransferrable() async {
@ -644,6 +648,38 @@ class SendReceiveTest extends SendReceiveTestBase {
Expect.equals('bar', (expando4Copy[expando4Copy] as Map)['foo']);
}
}
Future testForbiddenClosures() async {
print('testForbiddenClosures');
final nonCopyableClosures = <dynamic>[
(() {
final a = ClassWithNativeFields();
return a.m;
})(),
(() {
final a = ClassWithNativeFields();
dynamic inner() => a;
return inner;
})(),
(() {
foo(var arg) {
return () => arg;
}
return foo(ClassWithNativeFields());
})(),
];
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(() => sendPort.send(closure));
}
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(() => sendPort.send([closure]));
}
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(
() => sendPort.send([notAllocatableInTLAB, closure]));
}
}
}
main() async {

View file

@ -21,6 +21,14 @@ import 'package:expect/expect.dart';
import 'fast_object_copy_test.dart'
show UserObject, SendReceiveTestBase, notAllocatableInTLAB;
topLevelClosure(a, b) {}
topLevelClosureG<T>(T a, T b) {}
Type getType<T>() => T;
class A<T> {
dynamic m<H>(T a, H b) => this;
}
// When running with isolate groups enabled, we can share all of the following
// objects.
final sharableObjects = [
@ -34,6 +42,35 @@ final sharableObjects = [
rp.close();
return sp;
})(),
() {
innerClosure(a, b) {}
return innerClosure;
}(),
() {
innerClosureG<T>(T a, T b) {}
return innerClosureG;
}(),
() {
innerClosureG<T>() {
innerClosureG2<H>(T a, H b) {}
return innerClosureG2;
}
return innerClosureG<int>();
}(),
() {
innerClosureG<T>(T a, T b) {}
final Function(int, int) partialInstantiatedInnerClosure = innerClosureG;
return partialInstantiatedInnerClosure;
}(),
() {
return topLevelClosureG;
}(),
() {
final Function(int, int) partialInstantiatedInnerClosure = topLevelClosureG;
return partialInstantiatedInnerClosure;
}(),
getType<void Function(int, double, Object)>(),
const [1, 2, 3],
const {1: 1, 2: 2, 3: 2},
const {1, 2, 3},
@ -42,13 +79,40 @@ final sharableObjects = [
Int32x4(1, 2, 3, 4),
];
final copyableClosures = <dynamic>[
() {
final a = A<int>();
final Function<T>(int, T) genericMethod = a.m;
return genericMethod;
}(),
() {
final a = A<int>();
final Function(int, double) partialInstantiatedMethod = a.m;
return partialInstantiatedMethod;
}(),
() {
final a = Object();
dynamic inner() => a;
return inner;
}(),
() {
foo(var arg) {
return () => arg;
}
return foo(1);
}(),
];
class SendReceiveTest extends SendReceiveTestBase {
Future runTests() async {
await testSharable();
await testSharable2();
await testCopyableClosures();
}
Future testSharable() async {
print('testSharable');
final sharableObjectsCopy = await sendReceive([
...sharableObjects,
]);
@ -59,6 +123,7 @@ class SendReceiveTest extends SendReceiveTestBase {
}
Future testSharable2() async {
print('testSharable2');
final sharableObjectsCopy = await sendReceive([
notAllocatableInTLAB,
...sharableObjects,
@ -70,6 +135,27 @@ class SendReceiveTest extends SendReceiveTestBase {
Expect.identical(sharableObjects[i], sharableObjectsCopy[i + 1]);
}
}
Future testCopyableClosures() async {
print('testCopyableClosures');
final copy = await sendReceive([
notAllocatableInTLAB,
...copyableClosures,
]);
for (int i = 0; i < copyableClosures.length; ++i) {
Expect.notIdentical(copyableClosures[i], copy[1 + i]);
Expect.equals(copyableClosures[i].runtimeType, copy[1 + i].runtimeType);
}
final copy2 = await sendReceive([
...copyableClosures,
notAllocatableInTLAB,
]);
for (int i = 0; i < copyableClosures.length; ++i) {
Expect.notIdentical(copyableClosures[i], copy2[i]);
Expect.equals(copyableClosures[i].runtimeType, copy2[i].runtimeType);
}
}
}
main() async {

View file

@ -26,7 +26,9 @@ import 'dart:typed_data';
import 'package:expect/expect.dart';
class ClassWithNativeFields extends NativeFieldWrapperClass1 {}
class ClassWithNativeFields extends NativeFieldWrapperClass1 {
void m() {}
}
final Uint8List largeExternalTypedData =
File(Platform.resolvedExecutable).readAsBytesSync()..[0] = 42;
@ -223,6 +225,8 @@ class SendReceiveTest extends SendReceiveTestBase {
await testSlowOnly();
await testWeakProperty();
await testForbiddenClosures();
}
Future testTransferrable() async {
@ -646,6 +650,38 @@ class SendReceiveTest extends SendReceiveTestBase {
Expect.equals('bar', (expando4Copy[expando4Copy] as Map)['foo']);
}
}
Future testForbiddenClosures() async {
print('testForbiddenClosures');
final nonCopyableClosures = <dynamic>[
(() {
final a = ClassWithNativeFields();
return a.m;
})(),
(() {
final a = ClassWithNativeFields();
dynamic inner() => a;
return inner;
})(),
(() {
foo(var arg) {
return () => arg;
}
return foo(ClassWithNativeFields());
})(),
];
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(() => sendPort.send(closure));
}
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(() => sendPort.send([closure]));
}
for (final closure in nonCopyableClosures) {
Expect.throwsArgumentError(
() => sendPort.send([notAllocatableInTLAB, closure]));
}
}
}
main() async {

View file

@ -3066,22 +3066,20 @@ void MessageSerializer::Trace(Object* object) {
}
ILLEGAL(FunctionType)
ILLEGAL(DynamicLibrary)
ILLEGAL(MirrorReference)
ILLEGAL(Pointer)
ILLEGAL(ReceivePort)
ILLEGAL(StackTrace)
ILLEGAL(UserTag)
#undef ILLEGAL
switch (cid) {
#define ILLEGAL(type) case kFfi##type##Cid:
CLASS_LIST_FFI(ILLEGAL)
// From "dart:ffi" we handle only Pointer/DynamicLibrary specially, since
// those are the only non-abstract classes (so we avoid checking more cids
// here that cannot happen in reality)
ILLEGAL(DynamicLibrary)
ILLEGAL(Pointer)
ILLEGAL(FfiDynamicLibrary)
ILLEGAL(FfiPointer)
#undef ILLEGAL
IllegalObject(*object,
"Native objects (from dart:ffi) such as Pointers and "
"Structs cannot be passed between isolates.");
}
if (cid >= kNumPredefinedCids || cid == kInstanceCid ||
cid == kByteBufferCid) {

View file

@ -29,7 +29,6 @@
V(Code) \
V(CodeSourceMap) \
V(CompressedStackMaps) \
V(Context) \
V(ContextScope) \
V(DynamicLibrary) \
V(Error) \
@ -138,7 +137,7 @@ static ObjectPtr Marker() {
}
DART_FORCE_INLINE
static bool CanShareObject(uword tags) {
static bool CanShareObject(ObjectPtr obj, uword tags) {
if ((tags & UntaggedObject::CanonicalBit::mask_in_place()) != 0) {
return true;
}
@ -164,6 +163,11 @@ static bool CanShareObject(uword tags) {
if (cid == kCapabilityCid) return true;
if (cid == kRegExpCid) return true;
if (cid == kClosureCid) {
// We can share a closure iff it doesn't close over any state.
return Closure::RawCast(obj)->untag()->context() == Object::null();
}
return false;
}
@ -259,6 +263,9 @@ void UpdateLengthField(intptr_t cid, ObjectPtr from, ObjectPtr to) {
if (cid == kArrayCid) {
static_cast<UntaggedArray*>(to.untag())->length_ =
static_cast<UntaggedArray*>(from.untag())->length_;
} else if (cid == kContextCid) {
static_cast<UntaggedContext*>(to.untag())->num_variables_ =
static_cast<UntaggedContext*>(from.untag())->num_variables_;
} else if (IsTypedDataClassId(cid)) {
static_cast<UntaggedTypedDataBase*>(to.untag())->length_ =
static_cast<UntaggedTypedDataBase*>(from.untag())->length_;
@ -579,33 +586,18 @@ class ObjectCopyBase {
}
switch (cid) {
HANDLE_ILLEGAL_CASE(FunctionType)
// From "dart:ffi" we handle only Pointer/DynamicLibrary specially, since
// those are the only non-abstract classes (so we avoid checking more cids
// here that cannot happen in reality)
HANDLE_ILLEGAL_CASE(DynamicLibrary)
HANDLE_ILLEGAL_CASE(MirrorReference)
HANDLE_ILLEGAL_CASE(Pointer)
HANDLE_ILLEGAL_CASE(FfiDynamicLibrary)
HANDLE_ILLEGAL_CASE(FfiPointer)
HANDLE_ILLEGAL_CASE(FunctionType)
HANDLE_ILLEGAL_CASE(MirrorReference)
HANDLE_ILLEGAL_CASE(ReceivePort)
HANDLE_ILLEGAL_CASE(StackTrace)
HANDLE_ILLEGAL_CASE(UserTag)
#define CASE(type) case kFfi##type##Cid:
CLASS_LIST_FFI(CASE)
#undef CASE
exception_msg_ =
"Native objects (from dart:ffi) such as Pointers and "
"Structs cannot be passed between isolates.";
return false;
case kClosureCid: {
if (!Function::IsImplicitStaticClosureFunction(
Closure::FunctionOf(Closure::RawCast(object)))) {
exception_msg_ = OS::SCreate(
zone_,
"Illegal argument in isolate message: (object is a closure - %s)",
Function::Handle(Closure::FunctionOf(Closure::RawCast(object)))
.ToCString());
return false;
}
ASSERT(Closure::ContextOf(Closure::RawCast(object)) == Object::null());
return true;
}
default:
return true;
}
@ -672,6 +664,16 @@ class FastObjectCopyBase : public ObjectCopyBase {
}
}
void ForwardContextPointers(intptr_t context_length,
ObjectPtr src,
ObjectPtr dst,
intptr_t offset,
intptr_t end_offset) {
for (; offset < end_offset; offset += kWordSize) {
ForwardPointer(src, dst, offset);
}
}
DART_FORCE_INLINE
void ForwardCompressedPointer(ObjectPtr src, ObjectPtr dst, intptr_t offset) {
auto value = LoadCompressedPointer(src, offset);
@ -681,7 +683,7 @@ class FastObjectCopyBase : public ObjectCopyBase {
}
auto value_decompressed = value.Decompress(heap_base_);
const uword tags = TagsFromUntaggedObject(value_decompressed.untag());
if (CanShareObject(tags)) {
if (CanShareObject(value_decompressed, tags)) {
StoreCompressedPointerNoBarrier(dst, offset, value);
return;
}
@ -703,6 +705,36 @@ class FastObjectCopyBase : public ObjectCopyBase {
StoreCompressedPointerNoBarrier(dst, offset, to);
}
// TODO(rmacnak): Can be removed if Contexts are compressed.
DART_FORCE_INLINE
void ForwardPointer(ObjectPtr src, ObjectPtr dst, intptr_t offset) {
auto value = LoadPointer(src, offset);
if (!value.IsHeapObject()) {
StorePointerNoBarrier(dst, offset, value);
return;
}
const uword tags = TagsFromUntaggedObject(value.untag());
if (CanShareObject(value, tags)) {
StorePointerNoBarrier(dst, offset, value);
return;
}
ObjectPtr existing_to = fast_forward_map_.ForwardedObject(value);
if (existing_to != Marker()) {
StorePointerNoBarrier(dst, offset, existing_to);
return;
}
if (UNLIKELY(!CanCopyObject(tags, value))) {
ASSERT(exception_msg_ != nullptr);
StorePointerNoBarrier(dst, offset, Object::null());
return;
}
auto to = Forward(tags, value);
StorePointerNoBarrier(dst, offset, to);
}
ObjectPtr Forward(uword tags, ObjectPtr from) {
const intptr_t header_size = UntaggedObject::SizeTag::decode(tags);
const auto cid = UntaggedObject::ClassIdTag::decode(tags);
@ -827,6 +859,16 @@ class SlowObjectCopyBase : public ObjectCopyBase {
}
}
void ForwardContextPointers(intptr_t context_length,
const Object& src,
const Object& dst,
intptr_t offset,
intptr_t end_offset) {
for (; offset < end_offset; offset += kWordSize) {
ForwardPointer(src, dst, offset);
}
}
DART_FORCE_INLINE
void ForwardCompressedLargeArrayPointer(const Object& src,
const Object& dst,
@ -839,7 +881,7 @@ class SlowObjectCopyBase : public ObjectCopyBase {
auto value_decompressed = value.Decompress(heap_base_);
const uword tags = TagsFromUntaggedObject(value_decompressed.untag());
if (CanShareObject(tags)) {
if (CanShareObject(value_decompressed, tags)) {
StoreCompressedLargeArrayPointerBarrier(dst.ptr(), offset,
value_decompressed);
return;
@ -874,7 +916,7 @@ class SlowObjectCopyBase : public ObjectCopyBase {
}
auto value_decompressed = value.Decompress(heap_base_);
const uword tags = TagsFromUntaggedObject(value_decompressed.untag());
if (CanShareObject(tags)) {
if (CanShareObject(value_decompressed, tags)) {
StoreCompressedPointerBarrier(dst.ptr(), offset, value_decompressed);
return;
}
@ -896,6 +938,37 @@ class SlowObjectCopyBase : public ObjectCopyBase {
tmp_ = Forward(tags, tmp_); // Only this can cause allocation.
StoreCompressedPointerBarrier(dst.ptr(), offset, tmp_.ptr());
}
// TODO(rmacnak): Can be removed if Contexts are compressed.
DART_FORCE_INLINE
void ForwardPointer(const Object& src, const Object& dst, intptr_t offset) {
auto value = LoadPointer(src.ptr(), offset);
if (!value.IsHeapObject()) {
StorePointerNoBarrier(dst.ptr(), offset, value);
return;
}
const uword tags = TagsFromUntaggedObject(value.untag());
if (CanShareObject(value, tags)) {
StorePointerBarrier(dst.ptr(), offset, value);
return;
}
ObjectPtr existing_to = slow_forward_map_.ForwardedObject(value);
if (existing_to != Marker()) {
StorePointerBarrier(dst.ptr(), offset, existing_to);
return;
}
if (UNLIKELY(!CanCopyObject(tags, value))) {
ASSERT(exception_msg_ != nullptr);
StorePointerNoBarrier(dst.ptr(), offset, Object::null());
return;
}
tmp_ = value;
tmp_ = Forward(tags, tmp_); // Only this can cause allocation.
StorePointerBarrier(dst.ptr(), offset, tmp_.ptr());
}
ObjectPtr Forward(uword tags, const Object& from) {
const intptr_t cid = UntaggedObject::ClassIdTag::decode(tags);
intptr_t size = UntaggedObject::SizeTag::decode(tags);
@ -1083,6 +1156,18 @@ class ObjectCopy : public Base {
UntagClosure(from)->entry_point_);
}
void CopyContext(typename Types::Context from, typename Types::Context to) {
const intptr_t length = Context::NumVariables(Types::GetContextPtr(from));
UntagContext(to)->num_variables_ = UntagContext(from)->num_variables_;
Base::ForwardCompressedPointer(from, to,
OFFSET_OF(UntaggedContext, parent_));
Base::ForwardContextPointers(
length, from, to, Context::variable_offset(0),
Context::variable_offset(0) + kWordSize * length);
}
void CopyArray(typename Types::Array from, typename Types::Array to) {
const intptr_t length = Smi::Value(UntagArray(from)->length());
Base::StoreCompressedArrayPointers(
@ -1671,7 +1756,7 @@ class ObjectGraphCopier {
return result_array.ptr();
}
const uword tags = TagsFromUntaggedObject(root.ptr().untag());
if (CanShareObject(tags)) {
if (CanShareObject(root.ptr(), tags)) {
result_array.SetAt(0, root);
return result_array.ptr();
}

View file

@ -2179,6 +2179,9 @@ class UntaggedContext : public UntaggedObject {
VARIABLE_POINTER_FIELDS(ObjectPtr, element, data)
friend class Object;
friend void UpdateLengthField(intptr_t,
ObjectPtr,
ObjectPtr); // num_variables_
};
class UntaggedContextScope : public UntaggedObject {

View file

@ -7,10 +7,15 @@
// VMOptions=--no-enable-isolate-groups
import "dart:isolate";
import "dart:io";
import "dart:async";
import "package:expect/expect.dart";
import "package:async_helper/async_helper.dart";
final bool isolateGroupsEnabled =
Platform.executableArguments.contains('--enable-isolate-groups');
void toplevel(port, message) {
port.send("toplevel:$message");
}
@ -175,6 +180,9 @@ void _call(initPort) {
}
void testUnsendable(name, func) {
// Isolate group support does allow sending closures.
if (isolateGroupsEnabled) return;
asyncStart();
Isolate.spawn(nop, func).then<void>((v) => throw "allowed spawn direct?",
onError: (e, s) {

View file

@ -13,11 +13,16 @@ library MessageTest;
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'dart:typed_data';
final bool isolateGroupsEnabled =
Platform.executableArguments.contains('--enable-isolate-groups');
void echoMain(msg) {
SendPort replyTo = msg[0];
SendPort pong = msg[1];
@ -408,7 +413,14 @@ void runTests(SendPort ping, Queue checks) {
Expect.equals(42, x.fun()); // //# fun: continued
}); // //# fun: continued
Expect.throws(() => ping.send(new E(new E(E.fooFun).instanceFun)));
if (isolateGroupsEnabled) {
ping.send(new E(new E(E.fooFun).instanceFun));
checks.add((x) {
Expect.equals(1234, (x as E).fun());
});
} else {
Expect.throws(() => ping.send(new E(new E(E.fooFun).instanceFun)));
}
F nonConstF = new F();
ping.send(nonConstF);

View file

@ -9,10 +9,15 @@
// VMOptions=--no-enable-isolate-groups
import "dart:isolate";
import "dart:io";
import "dart:async";
import "package:expect/expect.dart";
import "package:async_helper/async_helper.dart";
final bool isolateGroupsEnabled =
Platform.executableArguments.contains('--enable-isolate-groups');
void toplevel(port, message) {
port.send("toplevel:$message");
}
@ -151,8 +156,8 @@ Future<SendPort> echoPort(callback(value)) {
completer.complete(p);
initPort.close();
});
return Isolate.spawn(_echo, [replyPort, initPort.sendPort]).then(
(isolate) => completer.future);
return Isolate.spawn(_echo, [replyPort, initPort.sendPort])
.then((isolate) => completer.future);
}
void _echo(msg) {
@ -178,6 +183,9 @@ void _call(initPort) {
}
void testUnsendable(name, func) {
// Isolate group support does allow sending closures.
if (isolateGroupsEnabled) return;
asyncStart();
Isolate.spawn(nop, func).then((v) => throw "allowed spawn direct?",
onError: (e, s) {

View file

@ -15,11 +15,16 @@ library MessageTest;
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
import 'dart:typed_data';
final bool isolateGroupsEnabled =
Platform.executableArguments.contains('--enable-isolate-groups');
void echoMain(msg) {
SendPort replyTo = msg[0];
SendPort pong = msg[1];
@ -410,7 +415,14 @@ void runTests(SendPort ping, Queue checks) {
Expect.equals(42, x.fun()); // //# fun: continued
}); // //# fun: continued
Expect.throws(() => ping.send(new E(new E(null).instanceFun)));
if (isolateGroupsEnabled) {
ping.send(new E(new E(null).instanceFun));
checks.add((x) {
Expect.equals(1234, (x as E).fun());
});
} else {
Expect.throws(() => ping.send(new E(new E(null).instanceFun)));
}
F nonConstF = new F();
ping.send(nonConstF);