Version 2.15.0-11.0.dev

Merge commit '5cec85d5ce49d13105e593f8fe6afb576ca5a3c2' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index f7073dd..20cdef0 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-08-05T11:33:04.746536",
+  "generated": "2021-08-10T10:51:47.272341",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -659,7 +659,7 @@
       "name": "status_file",
       "rootUri": "../pkg/status_file",
       "packageUri": "lib/",
-      "languageVersion": "2.3"
+      "languageVersion": "2.12"
     },
     {
       "name": "stream_channel",
diff --git a/pkg/dart_internal/pubspec.yaml b/pkg/dart_internal/pubspec.yaml
index 130631e..d871d5c 100644
--- a/pkg/dart_internal/pubspec.yaml
+++ b/pkg/dart_internal/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart_internal
-version: 0.2.1
+version: 0.2.2
 repository: https://github.com/dart-lang/sdk/tree/master/pkg/dart_internal
 description: >-
   This package is not intended for wide use. It provides a temporary API to
@@ -9,4 +9,4 @@
 environment:
   # Restrict the upper bound so that we can remove support for this in a later
   # version of the SDK without it being a breaking change.
-  sdk: ">=2.12.0 <2.15.0"
+  sdk: ">=2.12.0 <2.16.0"
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 669617b..fc56f09 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -138,6 +138,20 @@
   /// have been called.
   late final bool isAttach;
 
+  /// A list of evaluateNames for InstanceRef IDs.
+  ///
+  /// When providing variables for fields/getters or items in maps/arrays, we
+  /// need to provide an expression to the client that evaluates to that
+  /// variable so that functionality like "Add to Watch" or "Copy Value" can
+  /// work. For example, if a user expands a list named `myList` then the 1st
+  /// [Variable] returned should have an evaluateName of `myList[0]`. The `foo`
+  /// getter of that object would then have an evaluateName of `myList[0].foo`.
+  ///
+  /// Since those expressions aren't round-tripped as child variables are
+  /// requested we build them up as we send variables out, so we can append to
+  /// them when returning elements/map entries/fields/getters.
+  final _evaluateNamesForInstanceRefIds = <String, String>{};
+
   /// A list of all possible project paths that should be considered the users
   /// own code.
   ///
@@ -219,6 +233,26 @@
     // sendResponse();
   }
 
+  /// Builds an evaluateName given a parent VM InstanceRef ID and a suffix.
+  ///
+  /// If [parentInstanceRefId] is `null`, or we have no evaluateName for it,
+  /// will return null.
+  String? buildEvaluateName(
+    String suffix, {
+    required String? parentInstanceRefId,
+  }) {
+    final parentEvaluateName =
+        _evaluateNamesForInstanceRefIds[parentInstanceRefId];
+    return combineEvaluateName(parentEvaluateName, suffix);
+  }
+
+  /// Builds an evaluateName given a prefix and a suffix.
+  ///
+  /// If [prefix] is null, will return be null.
+  String? combineEvaluateName(String? prefix, String suffix) {
+    return prefix != null ? '$prefix$suffix' : null;
+  }
+
   /// configurationDone is called by the client when it has finished sending
   /// any initial configuration (such as breakpoints and exception pause
   /// settings).
@@ -489,11 +523,14 @@
         result,
         allowCallingToString: evaluateToStringInDebugViews,
       );
-      // TODO(dantup): We may need to store `expression` with this data
-      // to allow building nested evaluateNames.
+
       final variablesReference =
           _converter.isSimpleKind(result.kind) ? 0 : thread.storeData(result);
 
+      // Store the expression that gets this object as we may need it to
+      // compute evaluateNames for child objects later.
+      storeEvaluateName(result, expression);
+
       sendResponse(EvaluateResponseBody(
         result: resultString,
         variablesReference: variablesReference,
@@ -663,7 +700,7 @@
     // For local variables, we can just reuse the frameId as variablesReference
     // as variablesRequest handles stored data of type `Frame` directly.
     scopes.add(Scope(
-      name: 'Variables',
+      name: 'Locals',
       presentationHint: 'locals',
       variablesReference: args.frameId,
       expensive: false,
@@ -943,6 +980,14 @@
     sendResponse();
   }
 
+  /// Stores [evaluateName] as the expression that can be evaluated to get
+  /// [instanceRef].
+  void storeEvaluateName(vm.InstanceRef instanceRef, String? evaluateName) {
+    if (evaluateName != null) {
+      _evaluateNamesForInstanceRefIds[instanceRef.id!] = evaluateName;
+    }
+  }
+
   /// Overridden by sub-classes to handle when the client sends a
   /// `terminateRequest` (a request for a graceful shut down).
   Future<void> terminateImpl();
@@ -1032,19 +1077,55 @@
       final vars = vmData.vars;
       if (vars != null) {
         Future<Variable> convert(int index, vm.BoundVariable variable) {
+          // Store the expression that gets this object as we may need it to
+          // compute evaluateNames for child objects later.
+          storeEvaluateName(variable.value, variable.name);
           return _converter.convertVmResponseToVariable(
             thread,
             variable.value,
             name: variable.name,
             allowCallingToString: evaluateToStringInDebugViews &&
                 index <= maxToStringsPerEvaluation,
+            evaluateName: variable.name,
           );
         }
 
         variables.addAll(await Future.wait(vars.mapIndexed(convert)));
+
+        // Sort the variables by name.
+        variables.sortBy((v) => v.name);
       }
-    } else if (vmData is vm.MapAssociation) {
-      // TODO(dantup): Maps
+    } else if (data is vm.MapAssociation) {
+      final key = data.key;
+      final value = data.value;
+      if (key is vm.InstanceRef && value is vm.InstanceRef) {
+        // For a MapAssociation, we create a dummy set of variables for "key" and
+        // "value" so that each may be expanded if they are complex values.
+        variables.addAll([
+          Variable(
+            name: 'key',
+            value: await _converter.convertVmInstanceRefToDisplayString(
+              thread,
+              key,
+              allowCallingToString: evaluateToStringInDebugViews,
+            ),
+            variablesReference:
+                _converter.isSimpleKind(key.kind) ? 0 : thread.storeData(key),
+          ),
+          Variable(
+              name: 'value',
+              value: await _converter.convertVmInstanceRefToDisplayString(
+                thread,
+                value,
+                allowCallingToString: evaluateToStringInDebugViews,
+              ),
+              variablesReference: _converter.isSimpleKind(value.kind)
+                  ? 0
+                  : thread.storeData(value),
+              evaluateName:
+                  buildEvaluateName('', parentInstanceRefId: value.id)),
+        ]);
+      }
     } else if (vmData is vm.ObjRef) {
       final object =
           await _isolateManager.getObject(storedData.thread.isolate, vmData);
@@ -1056,13 +1137,10 @@
           variablesReference: 0,
         ));
       } else if (object is vm.Instance) {
-        // TODO(dantup): evaluateName
-        // should be built taking the parent into account, for ex. if
-        // args.variablesReference == thread.exceptionReference then we need to
-        // use some sythensized variable name like `frameExceptionExpression`.
         variables.addAll(await _converter.convertVmInstanceToVariablesList(
           thread,
           object,
+          evaluateName: buildEvaluateName('', parentInstanceRefId: vmData.id),
           allowCallingToString: evaluateToStringInDebugViews,
           startItem: childStart,
           numItems: childCount,
@@ -1076,8 +1154,6 @@
       }
     }
 
-    variables.sortBy((v) => v.name);
-
     sendResponse(VariablesResponseBody(variables: variables));
   }
 
@@ -1172,6 +1248,7 @@
         // string they logged regardless of the evaluateToStringInDebugViews
         // setting.
         allowCallingToString: true,
+        allowTruncatedValue: false,
         includeQuotesAroundString: false,
       );
     }
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index 4157cb0..d579bde 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -444,6 +444,7 @@
       // can add a variables scope for it so it can be examined.
       final exception = event.exception;
       if (exception != null) {
+        _adapter.storeEvaluateName(exception, threadExceptionExpression);
         thread.exceptionReference = thread.storeData(exception);
       }
 
diff --git a/pkg/dds/lib/src/dap/protocol_converter.dart b/pkg/dds/lib/src/dap/protocol_converter.dart
index c22b2a4..2d2f7240 100644
--- a/pkg/dds/lib/src/dap/protocol_converter.dart
+++ b/pkg/dds/lib/src/dap/protocol_converter.dart
@@ -61,17 +61,22 @@
     ThreadInfo thread,
     vm.InstanceRef ref, {
     required bool allowCallingToString,
+    bool allowTruncatedValue = true,
     bool includeQuotesAroundString = true,
   }) async {
     final isTruncated = ref.valueAsStringIsTruncated ?? false;
     if (ref.kind == vm.InstanceKind.kString && isTruncated) {
-      // Call toString() if allowed, otherwise (or if it returns null) fall back
-      // to the truncated value with "…" suffix.
-      var stringValue = allowCallingToString
+      // Call toString() if allowed (and we don't already have a value),
+      // otherwise (or if it returns null) fall back to the truncated value
+      // with "…" suffix.
+      var stringValue = allowCallingToString &&
+              (ref.valueAsString == null || !allowTruncatedValue)
           ? await _callToString(
               thread,
               ref,
-              includeQuotesAroundString: includeQuotesAroundString,
+              // Quotes are handled below, so they can be wrapped around the
+              // elipsis.
+              includeQuotesAroundString: false,
             )
           : null;
       stringValue ??= '${ref.valueAsString}…';
@@ -116,6 +121,7 @@
   Future<List<dap.Variable>> convertVmInstanceToVariablesList(
     ThreadInfo thread,
     vm.Instance instance, {
+    required String? evaluateName,
     required bool allowCallingToString,
     int? startItem = 0,
     int? numItems,
@@ -130,6 +136,8 @@
         await convertVmResponseToVariable(
           thread,
           instance,
+          name: null,
+          evaluateName: evaluateName,
           allowCallingToString: allowCallingToString,
         )
       ];
@@ -143,7 +151,9 @@
             (index, response) => convertVmResponseToVariable(
               thread,
               response,
-              name: '${start + index}',
+              name: '[${start + index}]',
+              evaluateName: _adapter.combineEvaluateName(
+                  evaluateName, '[${start + index}]'),
               allowCallingToString:
                   allowCallingToString && index <= maxToStringsPerEvaluation,
             ),
@@ -158,14 +168,26 @@
       return Future.wait(associations
           .sublist(start, numItems != null ? start + numItems : null)
           .mapIndexed((index, mapEntry) async {
+        final key = mapEntry.key;
+        final value = mapEntry.value;
         final callToString =
             allowCallingToString && index <= maxToStringsPerEvaluation;
-        final keyDisplay = await convertVmResponseToDisplayString(
-            thread, mapEntry.key,
+
+        final keyDisplay = await convertVmResponseToDisplayString(thread, key,
             allowCallingToString: callToString);
         final valueDisplay = await convertVmResponseToDisplayString(
-            thread, mapEntry.value,
+            thread, value,
             allowCallingToString: callToString);
+
+        // We only provide an evaluateName for the value, and only if the
+        // key is a simple value.
+        if (key is vm.InstanceRef &&
+            value is vm.InstanceRef &&
+            evaluateName != null &&
+            isSimpleKind(key.kind)) {
+          _adapter.storeEvaluateName(value, '$evaluateName[$keyDisplay]');
+        }
+
         return dap.Variable(
           name: '${start + index}',
           value: '$keyDisplay -> $valueDisplay',
@@ -175,11 +197,17 @@
     } else if (fields != null) {
       // Otherwise, show the fields from the instance.
       final variables = await Future.wait(fields.mapIndexed(
-          (index, field) async => convertVmResponseToVariable(
-              thread, field.value,
-              name: field.decl?.name ?? '<unnamed field>',
+        (index, field) async {
+          final name = field.decl?.name;
+          return convertVmResponseToVariable(thread, field.value,
+              name: name ?? '<unnamed field>',
+              evaluateName: name != null
+                  ? _adapter.combineEvaluateName(evaluateName, '.$name')
+                  : null,
               allowCallingToString:
-                  allowCallingToString && index <= maxToStringsPerEvaluation)));
+                  allowCallingToString && index <= maxToStringsPerEvaluation);
+        },
+      ));
 
       // Also evaluate the getters if evaluateGettersInDebugViews=true enabled.
       final service = _adapter.vmService;
@@ -202,6 +230,8 @@
             thread,
             response,
             name: getterName,
+            evaluateName:
+                _adapter.combineEvaluateName(evaluateName, '.$getterName'),
             allowCallingToString:
                 allowCallingToString && index <= maxToStringsPerEvaluation,
           );
@@ -210,6 +240,9 @@
         variables.addAll(await Future.wait(getterNames.mapIndexed(evaluate)));
       }
 
+      // Sort the fields/getters by name.
+      variables.sortBy((v) => v.name);
+
       return variables;
     } else {
       // For any other type that we don't produce variables for, return an empty
@@ -254,7 +287,8 @@
   Future<dap.Variable> convertVmResponseToVariable(
     ThreadInfo thread,
     vm.Response response, {
-    String? name,
+    required String? name,
+    required String? evaluateName,
     required bool allowCallingToString,
   }) async {
     if (response is vm.InstanceRef) {
@@ -265,6 +299,7 @@
 
       return dap.Variable(
         name: name ?? response.kind.toString(),
+        evaluateName: evaluateName,
         value: await convertVmResponseToDisplayString(
           thread,
           response,
diff --git a/pkg/dds/test/dap/integration/debug_eval_test.dart b/pkg/dds/test/dap/integration/debug_eval_test.dart
index ab24f0c..dc5b2a0 100644
--- a/pkg/dds/test/dap/integration/debug_eval_test.dart
+++ b/pkg/dds/test/dap/integration/debug_eval_test.dart
@@ -47,12 +47,12 @@
       );
 
       // Check we got a variablesReference that maps on to the fields.
-      expect(result.variablesReference, greaterThan(0));
+      expect(result.variablesReference, isPositive);
       await client.expectVariables(
         result.variablesReference,
         '''
-            isUtc: false
-          ''',
+            isUtc: false, eval: DateTime(2000, 1, 1).isUtc
+        ''',
       );
     });
 
@@ -111,7 +111,7 @@
         threadExceptionExpression,
         '_Exception',
       );
-      expect(result.variablesReference, greaterThan(0));
+      expect(result.variablesReference, isPositive);
     });
 
     test(
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 18b544c..3a58c37 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -159,7 +159,7 @@
 
       // SDK sources should have a sourceReference and no path.
       expect(topFrame.source!.path, isNull);
-      expect(topFrame.source!.sourceReference, greaterThan(0));
+      expect(topFrame.source!.sourceReference, isPositive);
 
       // Source code should contain the implementation/signature of print().
       final source = await client.getValidSource(topFrame.source!);
diff --git a/pkg/dds/test/dap/integration/debug_variables_test.dart b/pkg/dds/test/dap/integration/debug_variables_test.dart
index b5e357c..8acecb6 100644
--- a/pkg/dds/test/dap/integration/debug_variables_test.dart
+++ b/pkg/dds/test/dap/integration/debug_variables_test.dart
@@ -40,18 +40,18 @@
       // Check top two frames (in `foo` and in `main`).
       await client.expectScopeVariables(
         stack.stackFrames[0].id, // Top frame: foo
-        'Variables',
+        'Locals',
         '''
-            b: 2
-          ''',
+            b: 2, eval: b
+        ''',
       );
       await client.expectScopeVariables(
         stack.stackFrames[1].id, // Second frame: main
-        'Variables',
+        'Locals',
         '''
-            args: List (0 items)
-            myVariable: 1
-          ''',
+            args: List (0 items), eval: args
+            myVariable: 1, eval: myVariable
+        ''',
       );
     });
 
@@ -76,9 +76,9 @@
       await client.expectScopeVariables(
         topFrameId,
         'Exceptions',
-        '''
-            String: "my error"
-          ''',
+        r'''
+            String: "my error", eval: $_threadException
+        ''',
       );
     });
 
@@ -103,12 +103,11 @@
       await client.expectScopeVariables(
         topFrameId,
         'Exceptions',
-        // TODO(dantup): evaluateNames
-        '''
-            invalidValue: null
-            message: "Must not be null"
-            name: "args"
-          ''',
+        r'''
+            invalidValue: null, eval: $_threadException.invalidValue
+            message: "Must not be null", eval: $_threadException.message
+            name: "args", eval: $_threadException.name
+        ''',
       );
     });
 
@@ -128,8 +127,8 @@
         expectedName: 'myVariable',
         expectedDisplayString: 'DateTime',
         expectedVariables: '''
-            isUtc: false
-          ''',
+            isUtc: false, eval: myVariable.isUtc
+        ''',
       );
     });
 
@@ -157,19 +156,19 @@
         expectedName: 'myVariable',
         expectedDisplayString: 'DateTime',
         expectedVariables: '''
-            day: 1
-            hour: 0
-            isUtc: false
-            microsecond: 0
-            millisecond: 0
-            minute: 0
-            month: 1
-            runtimeType: Type (DateTime)
-            second: 0
-            timeZoneOffset: Duration
-            weekday: 6
-            year: 2000
-          ''',
+            day: 1, eval: myVariable.day
+            hour: 0, eval: myVariable.hour
+            isUtc: false, eval: myVariable.isUtc
+            microsecond: 0, eval: myVariable.microsecond
+            millisecond: 0, eval: myVariable.millisecond
+            minute: 0, eval: myVariable.minute
+            month: 1, eval: myVariable.month
+            runtimeType: Type (DateTime), eval: myVariable.runtimeType
+            second: 0, eval: myVariable.second
+            timeZoneOffset: Duration, eval: myVariable.timeZoneOffset
+            weekday: 6, eval: myVariable.weekday
+            year: 2000, eval: myVariable.year
+        ''',
         ignore: {
           // Don't check fields that may very based on timezone as it'll make
           // these tests fragile, and this isn't really what's being tested.
@@ -195,12 +194,11 @@
         stop.threadId!,
         expectedName: 'myVariable',
         expectedDisplayString: 'List (3 items)',
-        // TODO(dantup): evaluateNames
         expectedVariables: '''
-            0: "first"
-            1: "second"
-            2: "third"
-          ''',
+            [0]: "first", eval: myVariable[0]
+            [1]: "second", eval: myVariable[1]
+            [2]: "third", eval: myVariable[2]
+        ''',
       );
     });
 
@@ -219,22 +217,144 @@
         stop.threadId!,
         expectedName: 'myVariable',
         expectedDisplayString: 'List (3 items)',
-        // TODO(dantup): evaluateNames
         expectedVariables: '''
-            1: "second"
-          ''',
+            [1]: "second", eval: myVariable[1]
+        ''',
         start: 1,
         count: 1,
       );
     });
 
-    test('renders a simple map', () {
-      // TODO(dantup): Implement this (inc evaluateNames)
-    }, skip: true);
+    test('renders a simple map with keys/values', () async {
+      final client = dap.client;
+      final testFile = await dap.createTestFile(r'''
+void main(List<String> args) {
+  final myVariable = {
+    'zero': 0,
+    'one': 1,
+    'two': 2
+  };
+  print('Hello!'); // BREAKPOINT
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
 
-    test('renders a simple map subset', () {
-      // TODO(dantup): Implement this (inc evaluateNames)
-    }, skip: true);
+      final stop = await client.hitBreakpoint(testFile, breakpointLine);
+      final variables = await client.expectLocalVariable(
+        stop.threadId!,
+        expectedName: 'myVariable',
+        expectedDisplayString: 'Map (3 items)',
+        // For maps, we render a level of MapAssociates first, which show
+        // their index numbers. Expanding them has a Key and a Value "field"
+        // which correspond to the items.
+        expectedVariables: '''
+            0: "zero" -> 0
+            1: "one" -> 1
+            2: "two" -> 2
+        ''',
+      );
+
+      // Check one of the MapAssociation variables has the correct Key/Value
+      // inside.
+      expect(variables.variables, hasLength(3));
+      final variableOne = variables.variables[1];
+      expect(variableOne.variablesReference, isPositive);
+      await client.expectVariables(
+        variableOne.variablesReference,
+        '''
+            key: "one"
+            value: 1, eval: myVariable["one"]
+        ''',
+      );
+    });
+
+    test('renders a simple map subset', () async {
+      final client = dap.client;
+      final testFile = await dap.createTestFile(r'''
+void main(List<String> args) {
+  final myVariable = {
+    'zero': 0,
+    'one': 1,
+    'two': 2
+  };
+  print('Hello!'); // BREAKPOINT
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+
+      final stop = await client.hitBreakpoint(testFile, breakpointLine);
+      await client.expectLocalVariable(
+        stop.threadId!,
+        expectedName: 'myVariable',
+        expectedDisplayString: 'Map (3 items)',
+        // For maps, we render a level of MapAssociates first, which show
+        // their index numbers. Expanding them has a Key and a Value "field"
+        // which correspond to the items.
+        expectedVariables: '''
+            1: "one" -> 1
+        ''',
+        start: 1,
+        count: 1,
+      );
+    });
+
+    test('renders a complex map with keys/values', () async {
+      final client = dap.client;
+      final testFile = await dap.createTestFile(r'''
+void main(List<String> args) {
+  final myVariable = {
+    DateTime(2000, 1, 1): Exception("my error")
+  };
+  print('Hello!'); // BREAKPOINT
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+
+      final stop = await client.hitBreakpoint(testFile, breakpointLine);
+      final mapVariables = await client.expectLocalVariable(
+        stop.threadId!,
+        expectedName: 'myVariable',
+        expectedDisplayString: 'Map (1 item)',
+        expectedVariables: '''
+            0: DateTime -> _Exception
+        ''',
+      );
+
+      // Check one of the MapAssociation variables has the correct Key/Value
+      // inside.
+      expect(mapVariables.variables, hasLength(1));
+      final mapVariable = mapVariables.variables[0];
+      expect(mapVariable.variablesReference, isPositive);
+      final variables = await client.expectVariables(
+        mapVariable.variablesReference,
+        // We don't expect an evaluteName because the key is not a simple type.
+        '''
+            key: DateTime
+            value: _Exception
+        ''',
+      );
+
+      // Check the Key can be drilled into.
+      expect(variables.variables, hasLength(2));
+      final keyVariable = variables.variables[0];
+      expect(keyVariable.variablesReference, isPositive);
+      await client.expectVariables(
+        keyVariable.variablesReference,
+        '''
+            isUtc: false
+        ''',
+      );
+
+      // Check the Value can be drilled into.
+      final valueVariable = variables.variables[1];
+      expect(valueVariable.variablesReference, isPositive);
+      await client.expectVariables(
+        valueVariable.variablesReference,
+        '''
+            message: "my error"
+        ''',
+      );
+    });
     // These tests can be slow due to starting up the external server process.
   }, timeout: Timeout.none);
 }
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 52e8892..0099e3f 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -605,7 +605,7 @@
 
   /// A helper that finds a named variable in the Variables scope for the top
   /// frame and asserts its child variables (fields/getters/etc) match.
-  Future<void> expectLocalVariable(
+  Future<VariablesResponseBody> expectLocalVariable(
     int threadId, {
     required String expectedName,
     required String expectedDisplayString,
@@ -622,7 +622,7 @@
     );
     final topFrame = stack.stackFrames.first;
 
-    final variablesScope = await getValidScope(topFrame.id, 'Variables');
+    final variablesScope = await getValidScope(topFrame.id, 'Locals');
     final variables =
         await getValidVariables(variablesScope.variablesReference);
     final expectedVariable = variables.variables
@@ -632,7 +632,7 @@
     expect(expectedVariable.value, equals(expectedDisplayString));
 
     // Check the child fields.
-    await expectVariables(
+    return expectVariables(
       expectedVariable.variablesReference,
       expectedVariables,
       start: start,
@@ -713,7 +713,7 @@
       final type = v.type;
       final presentationHint = v.presentationHint;
 
-      buffer.write(v.name);
+      buffer.write('${v.name}: $value');
       if (evaluateName != null) {
         buffer.write(', eval: $evaluateName');
       }
@@ -723,12 +723,11 @@
       if (namedVariables != null) {
         buffer.write(', $namedVariables named items');
       }
-      buffer.write(': $value');
       if (type != null) {
-        buffer.write(' ($type)');
+        buffer.write(', $type');
       }
       if (presentationHint != null) {
-        buffer.write(' ($presentationHint)');
+        buffer.write(', $presentationHint');
       }
 
       return buffer.toString();
diff --git a/pkg/status_file/bin/lint.dart b/pkg/status_file/bin/lint.dart
index 17b84f47..fd9f6a0a 100644
--- a/pkg/status_file/bin/lint.dart
+++ b/pkg/status_file/bin/lint.dart
@@ -56,10 +56,11 @@
 }
 
 void lintStdIn({bool checkForDisjunctions = false}) {
-  List<String> strings = <String>[];
-  String readString;
+  var strings = <String>[];
   try {
-    while (null != (readString = stdin.readLineSync())) {
+    while (true) {
+      var readString = stdin.readLineSync();
+      if (readString == null) break;
       strings.add(readString);
     }
   } on StdinException {
@@ -126,7 +127,7 @@
     print("");
     return true;
   }
-  if (statusFile.path != null && statusFile.path.isNotEmpty) {
+  if (statusFile.path.isNotEmpty) {
     print("${statusFile.path}");
   }
   var errors = lintingErrors.toList();
diff --git a/pkg/status_file/bin/remove_non_essential_entries.dart b/pkg/status_file/bin/remove_non_essential_entries.dart
index 409fbcd..8f481c0 100644
--- a/pkg/status_file/bin/remove_non_essential_entries.dart
+++ b/pkg/status_file/bin/remove_non_essential_entries.dart
@@ -33,8 +33,9 @@
 import 'package:args/args.dart';
 import 'package:status_file/canonical_status_file.dart';
 import 'package:status_file/expectation.dart';
+import 'package:status_file/src/expression.dart';
 
-StatusEntry filterExpectations(
+StatusEntry? filterExpectations(
     StatusEntry entry, List<Expectation> expectationsToKeep) {
   List<Expectation> remaining = entry.expectations
       .where(
@@ -45,14 +46,14 @@
       : StatusEntry(entry.path, entry.lineNumber, remaining, entry.comment);
 }
 
-Map<String, Map<int, String>> issues;
+late Map<String, Map<int, String>> issues;
 
 String getIssueState(String project, int issue) {
-  Map projectIssues = issues[project];
+  var projectIssues = issues[project];
   if (projectIssues == null) {
     throw "Cannot find project $project, not one of {${issues.keys.join(",")}}";
   }
-  String state = projectIssues[issue] ?? "";
+  var state = projectIssues[issue] ?? "";
   return "\t$state";
 }
 
@@ -61,7 +62,7 @@
 // sorted by issue number then timestamp ascending.
 //
 // The first line is expected to contain the field names and is skipped.
-void parseIssueFile() async {
+Future<void> parseIssueFile() async {
   issues = {};
   String issuesLog = await File("issues.log").readAsString();
   List<String> lines = issuesLog.split("\n");
@@ -91,13 +92,13 @@
 ];
 
 String getIssueText(String comment, bool resolveState) {
-  int issue;
-  String prefix;
-  String project;
-  for (RegExp pattern in co19IssuePatterns) {
-    Match match = pattern.firstMatch(comment);
+  int? issue;
+  late String prefix;
+  late String project;
+  for (var pattern in co19IssuePatterns) {
+    var match = pattern.firstMatch(comment);
     if (match != null) {
-      issue = int.tryParse(match[1]);
+      issue = int.tryParse(match[1]!);
       if (issue != null) {
         prefix = "https://github.com/dart-lang/co19/issues/";
         project = "dart-lang/co19";
@@ -106,10 +107,10 @@
     }
   }
   if (issue == null) {
-    for (RegExp pattern in sdkIssuePatterns) {
-      Match match = pattern.firstMatch(comment);
+    for (var pattern in sdkIssuePatterns) {
+      var match = pattern.firstMatch(comment);
       if (match != null) {
-        issue = int.tryParse(match[1]);
+        issue = int.tryParse(match[1]!);
         if (issue != null) {
           prefix = "https://dartbug.com/";
           project = "dart-lang/sdk";
@@ -119,7 +120,7 @@
     }
   }
   if (issue != null) {
-    String state = resolveState ? getIssueState(project, issue) : "";
+    var state = resolveState ? getIssueState(project, issue) : "";
     return "$prefix$issue$state";
   } else {
     return "";
@@ -143,7 +144,7 @@
         entries.add(entry);
         hasStatusEntries = true;
       } else if (entry is StatusEntry) {
-        StatusEntry newEntry = entry;
+        StatusEntry? newEntry = entry;
         if (entry.comment == null) {
           newEntry = filterExpectations(entry, expectationsToKeep);
         } else if (removeComments) {
@@ -155,8 +156,9 @@
             String expectations = entry.expectations.toString();
             // Remove '[' and ']'.
             expectations = expectations.substring(1, expectations.length - 1);
-            String conditionPrefix =
-                section.condition != null ? "${section.condition}" : "";
+            String conditionPrefix = section.condition != Expression.always
+                ? "${section.condition}"
+                : "";
             String issueText = await getIssueText(comment, resolveIssueState);
             String statusLine = "$conditionPrefix\t$testName\t$expectations"
                 "\t$comment\t$issueText";
@@ -171,16 +173,18 @@
         throw "Unknown entry type ${entry.runtimeType}";
       }
     }
-    bool isDefaultSection = section.condition == null;
+
+    var isDefaultSection = section.condition == Expression.always;
     if (hasStatusEntries ||
         (isDefaultSection && section.sectionHeaderComments.isNotEmpty)) {
-      StatusSection newSection =
+      var newSection =
           StatusSection(section.condition, -1, section.sectionHeaderComments);
       newSection.entries.addAll(entries);
       sections.add(newSection);
     }
   }
-  StatusFile newStatusFile = StatusFile(statusFile.path);
+
+  var newStatusFile = StatusFile(statusFile.path);
   newStatusFile.sections.addAll(sections);
   return newStatusFile;
 }
diff --git a/pkg/status_file/lib/canonical_status_file.dart b/pkg/status_file/lib/canonical_status_file.dart
index 6f7f134..c20924f 100644
--- a/pkg/status_file/lib/canonical_status_file.dart
+++ b/pkg/status_file/lib/canonical_status_file.dart
@@ -95,7 +95,7 @@
 
     /// Checks if [currentLine] is a comment and returns the first regular
     /// expression match, or null otherwise.
-    Match commentEntryMatch(int currentLine) {
+    Match? commentEntryMatch(int currentLine) {
       if (currentLine < 1 || currentLine > lines.length) {
         return null;
       }
@@ -104,7 +104,7 @@
 
     /// Finds a section header on [currentLine] if the line is in range of
     /// [lines].
-    Match sectionHeaderMatch(int currentLine) {
+    Match? sectionHeaderMatch(int currentLine) {
       if (currentLine < 1 || currentLine > lines.length) {
         return null;
       }
@@ -178,14 +178,14 @@
     // The current section whose rules are being parsed. Initialized to an
     // implicit section that matches everything.
     StatusSection section =
-        new StatusSection(null, -1, implicitSectionHeaderComments);
+        new StatusSection(Expression.always, -1, implicitSectionHeaderComments);
     section.entries.addAll(entries);
     sections.add(section);
 
     for (; _lineCount <= lines.length; _lineCount++) {
       var line = lines[_lineCount - 1];
 
-      fail(String message, [List<String> errors]) {
+      fail(String message, [List<String>? errors]) {
         throw new SyntaxError(_shortPath, _lineCount, line, message, errors);
       }
 
@@ -199,7 +199,7 @@
       var match = _sectionPattern.firstMatch(line);
       if (match != null) {
         try {
-          var condition = Expression.parse(match[1].trim());
+          var condition = Expression.parse(match[1]!.trim());
           section =
               new StatusSection(condition, _lineCount, sectionHeaderComments);
           sections.add(section);
@@ -214,10 +214,10 @@
       // If it is in a new entry we should add to the current section.
       match = _entryPattern.firstMatch(line);
       if (match != null) {
-        var path = match[1].trim();
+        var path = match[1]!.trim();
         var expectations = <Expectation>[];
         // split expectations
-        match[2].split(",").forEach((name) {
+        match[2]!.split(",").forEach((name) {
           try {
             expectations.add(Expectation.find(name.trim()));
           } on ArgumentError {
@@ -229,7 +229,7 @@
               .add(new StatusEntry(path, _lineCount, expectations, null));
         } else {
           section.entries.add(new StatusEntry(
-              path, _lineCount, expectations, new Comment(match[3])));
+              path, _lineCount, expectations, new Comment(match[3]!)));
         }
         continue;
       }
@@ -264,8 +264,6 @@
   /// Throws a [SyntaxError] on the first found error.
   void validate(Environment environment) {
     for (var section in sections) {
-      if (section.condition == null) continue;
-
       var errors = <String>[];
       section.condition.validate(environment, errors);
 
@@ -299,8 +297,8 @@
 class StatusSection {
   /// The expression that determines when this section is applied.
   ///
-  /// May be `null` for paths that appear before any section header in the file.
-  /// In that case, the section always applies.
+  /// Will be [Expression.always] for paths that appear before any section
+  /// header in the file. In that case, the section always applies.
   final Expression condition;
 
   /// The one-based line number where the section appears in the file.
@@ -311,8 +309,7 @@
   final List<Entry> sectionHeaderComments;
 
   /// Returns true if this section should apply in the given [environment].
-  bool isEnabled(Environment environment) =>
-      condition == null || condition.evaluate(environment);
+  bool isEnabled(Environment environment) => condition.evaluate(environment);
 
   bool isEmpty() => !entries.any((entry) => entry is StatusEntry);
 
@@ -322,8 +319,8 @@
   String toString() {
     var buffer = new StringBuffer();
     sectionHeaderComments.forEach(buffer.writeln);
-    if (condition != null) {
-      buffer.writeln("[ ${condition} ]");
+    if (condition != Expression.always) {
+      buffer.writeln("[ $condition ]");
     }
     entries.forEach(buffer.writeln);
     return buffer.toString();
@@ -336,10 +333,10 @@
   Comment(this._comment);
 
   /// Returns the issue number embedded in [comment] or `null` if there is none.
-  int issueNumber(String comment) {
+  int? issueNumber(String comment) {
     var match = _issuePattern.firstMatch(comment);
     if (match == null) return null;
-    return int.parse(match[1]);
+    return int.parse(match[1]!);
   }
 
   @override
@@ -377,7 +374,7 @@
 class StatusEntry extends Entry {
   final String path;
   final List<Expectation> expectations;
-  final Comment comment;
+  final Comment? comment;
 
   StatusEntry(this.path, lineNumber, this.expectations, this.comment)
       : super(lineNumber);
diff --git a/pkg/status_file/lib/environment.dart b/pkg/status_file/lib/environment.dart
index 89e20ac..602c2a0 100644
--- a/pkg/status_file/lib/environment.dart
+++ b/pkg/status_file/lib/environment.dart
@@ -12,5 +12,5 @@
   void validate(String name, String value, List<String> errors);
 
   /// Looks up the value of the variable with [name].
-  String lookUp(String name);
+  String? lookUp(String name);
 }
diff --git a/pkg/status_file/lib/expectation.dart b/pkg/status_file/lib/expectation.dart
index 93979fb..6a0223c 100644
--- a/pkg/status_file/lib/expectation.dart
+++ b/pkg/status_file/lib/expectation.dart
@@ -206,17 +206,17 @@
   }
 
   final String _name;
-  final Expectation _group;
+  final Expectation? _group;
 
   /// Whether this expectation is a test outcome. If not, it's a "meta marker".
   final bool isOutcome;
 
-  Expectation._(this._name, {Expectation group, bool isMeta: false})
+  Expectation._(this._name, {Expectation? group, bool isMeta: false})
       : _group = group,
         isOutcome = !isMeta;
 
   bool canBeOutcomeOf(Expectation expectation) {
-    var outcome = this;
+    Expectation? outcome = this;
     if (outcome == ignore) return true;
 
     while (outcome != null) {
diff --git a/pkg/status_file/lib/src/disjunctive.dart b/pkg/status_file/lib/src/disjunctive.dart
index b94f17d..b768f69 100644
--- a/pkg/status_file/lib/src/disjunctive.dart
+++ b/pkg/status_file/lib/src/disjunctive.dart
@@ -102,14 +102,14 @@
   });
   var combinedMinSets = _combineMinSets(
       clauses.map((e) => [new LogicExpression.and(e)]).toList(), []);
-  List<List<Expression>> minCover = _findMinCover(combinedMinSets, []);
+  List<List<LogicExpression>> minCover = _findMinCover(combinedMinSets, []);
   var finalOperands = minCover.map((minSet) => _reduceMinSet(minSet)).toList();
   return new LogicExpression.or(finalOperands).normalize();
 }
 
 /// Computes all assignments of literals that make the [expression] evaluate to
 /// true.
-List<Expression> _satisfiableMinTerms(Expression expression) {
+List<Expression>? _satisfiableMinTerms(Expression expression) {
   var variables = _getVariables(expression);
   bool hasNotSatisfiableAssignment = false;
   List<Expression> satisfiableTerms = <Expression>[];
@@ -142,7 +142,7 @@
 /// which the [variables] was found.
 class TruthTableEnvironment extends Environment {
   final List<Expression> variables;
-  int configuration;
+  int configuration = -1;
 
   TruthTableEnvironment(this.variables);
 
@@ -172,10 +172,10 @@
 /// Combines [minSets] recursively as long as possible. Prime implicants (those
 /// that cannot be reduced further) are kept track of in [primeImplicants]. When
 /// finished the function returns all combined min sets.
-List<List<Expression>> _combineMinSets(
-    List<List<Expression>> minSets, List<List<Expression>> primeImplicants) {
+List<List<LogicExpression>> _combineMinSets(List<List<LogicExpression>> minSets,
+    List<List<LogicExpression>> primeImplicants) {
   List<List<LogicExpression>> combined = <List<LogicExpression>>[];
-  var addedInThisIteration = new Set<List<Expression>>();
+  var addedInThisIteration = new Set<List<LogicExpression>>();
   for (var i = 0; i < minSets.length; i++) {
     var minSet = minSets[i];
     var combinedMinSet = false;
@@ -262,8 +262,9 @@
 /// minimum set cover is NP-hard, and we are not trying to be really cleaver
 /// here. The implicants that cover only a single truth assignment can be
 /// directly added to [cover].
-List<List<Expression>> _findMinCover(
-    List<List<Expression>> primaryImplicants, List<List<Expression>> cover) {
+List<List<LogicExpression>> _findMinCover(
+    List<List<LogicExpression>> primaryImplicants,
+    List<List<LogicExpression>> cover) {
   var minCover = primaryImplicants.toList()..addAll(cover);
   if (cover.isEmpty) {
     var allImplicants = primaryImplicants.toList();
@@ -332,9 +333,9 @@
 
 /// Finds the first occurrence of [expressionToFind] in [expressions] or
 /// returns null.
-Expression _findFirst<Expression>(
+Expression? _findFirst<Expression>(
     expressionToFind, List<Expression> expressions) {
-  return expressions.firstWhere(
+  return expressions.cast<Expression?>().firstWhere(
       (otherExpression) => expressionToFind.compareTo(otherExpression) == 0,
       orElse: () => null);
 }
diff --git a/pkg/status_file/lib/src/expression.dart b/pkg/status_file/lib/src/expression.dart
index 16a5bbd..9baf6aa 100644
--- a/pkg/status_file/lib/src/expression.dart
+++ b/pkg/status_file/lib/src/expression.dart
@@ -9,6 +9,11 @@
 
 /// A parsed Boolean expression AST.
 abstract class Expression implements Comparable<Expression> {
+  /// An expression that always evaluates to true.
+  ///
+  /// Used for the section at the top of a status file with no expression.
+  static const Expression always = _AlwaysExpression();
+
   /// Parses Boolean expressions in a .status file for Dart.
   ///
   /// The grammar is:
@@ -26,6 +31,8 @@
   static Expression parse(String expression) =>
       new _ExpressionParser(expression).parse();
 
+  const Expression();
+
   /// Validates that this expression does not contain any invalid uses of
   /// variables.
   ///
@@ -111,6 +118,29 @@
   }
 }
 
+/// An expression that always evaluates to true.
+///
+/// Used for the implicit section at the top of a status file that always
+/// matches.
+class _AlwaysExpression extends Expression {
+  const _AlwaysExpression();
+
+  @override
+  int _compareToMyType(covariant _AlwaysExpression other) => 0;
+
+  @override
+  int get _typeComparison => 0;
+
+  @override
+  bool evaluate(Environment environment) => true;
+
+  @override
+  Expression normalize() => this;
+
+  @override
+  void validate(Environment environment, List<String> errors) {}
+}
+
 /// Tests whether a given variable is or is not equal some literal value, as in:
 /// ```
 /// $variable == someValue
@@ -156,7 +186,7 @@
 
   // Comparisons come before variables so that "$compiler == ..." and
   // "$runtime == ..." appear on the left in status expressions.
-  int get _typeComparison => 0;
+  int get _typeComparison => 1;
 
   String toString() => "\$${left.name} ${negate ? '!=' : '=='} $right";
 }
@@ -203,7 +233,7 @@
     return _compareBool(negate, other.negate);
   }
 
-  int get _typeComparison => 1;
+  int get _typeComparison => 2;
 
   String toString() => "${negate ? "!" : ""}\$${variable.name}";
 }
@@ -237,7 +267,7 @@
     }
   }
 
-  Expression normalize() {
+  LogicExpression normalize() {
     // Normalize the order of the clauses. Since there is no short-circuiting,
     // a || b means the same as b || a. Picking a standard order lets us
     // identify and collapse identical expressions that only differ by clause
@@ -281,7 +311,7 @@
     return 0;
   }
 
-  int get _typeComparison => 2;
+  int get _typeComparison => 3;
 
   String toString() {
     String parenthesize(Expression operand) {
@@ -365,7 +395,7 @@
           "Expected identifier in expression, got ${_scanner.current}");
     }
 
-    var left = new Variable(_scanner.current);
+    var left = new Variable(_scanner.current!);
     _scanner.advance();
 
     if (!negate &&
@@ -378,7 +408,7 @@
             "Expected value in expression, got ${_scanner.current}");
       }
 
-      var right = _scanner.advance();
+      var right = _scanner.advance()!;
       return new ComparisonExpression(left, right, isNotEquals);
     } else {
       return new VariableExpression(left, negate: negate);
@@ -401,7 +431,7 @@
   /// The token strings being iterated.
   final Iterator<String> tokenIterator;
 
-  String current;
+  String? current;
 
   _Scanner(String expression) : tokenIterator = tokenize(expression).iterator {
     advance();
@@ -414,7 +444,7 @@
 
     return _tokenPattern
         .allMatches(expression)
-        .map((match) => match[0])
+        .map((match) => match[0]!)
         .toList();
   }
 
@@ -424,7 +454,7 @@
   // All non-identifier tokens are one or two characters,
   // so a longer token must be an identifier.
   bool get isIdentifier =>
-      current.length > 2 || _identifierPattern.hasMatch(current);
+      current!.length > 2 || _identifierPattern.hasMatch(current!);
 
   /// If the current token is [token], consumes it and returns `true`.
   bool match(String token) {
@@ -435,7 +465,7 @@
   }
 
   /// Consumes the current token and returns it.
-  String advance() {
+  String? advance() {
     var previous = current;
     current = tokenIterator.moveNext() ? tokenIterator.current : null;
     return previous;
diff --git a/pkg/status_file/lib/status_file.dart b/pkg/status_file/lib/status_file.dart
index debc28f..86a37a5 100644
--- a/pkg/status_file/lib/status_file.dart
+++ b/pkg/status_file/lib/status_file.dart
@@ -45,7 +45,7 @@
 class StatusFile {
   final String path;
   final List<StatusSection> sections = [];
-  final List<String> _comments = [];
+  final List<String?> _comments = [];
 
   int _lineCount = 0;
 
@@ -58,13 +58,10 @@
     var lines = new File(path).readAsLinesSync();
     _comments.length = lines.length + 1;
 
-    // The current section whose rules are being parsed.
-    StatusSection section;
-
     for (var line in lines) {
       _lineCount++;
 
-      fail(String message, [List<String> errors]) {
+      fail(String message, [List<String>? errors]) {
         throw new SyntaxError(_shortPath, _lineCount, line, message, errors);
       }
 
@@ -86,9 +83,8 @@
       var match = _sectionPattern.firstMatch(source);
       if (match != null) {
         try {
-          var condition = Expression.parse(match[1].trim());
-          section = new StatusSection(condition, _lineCount);
-          sections.add(section);
+          var condition = Expression.parse(match[1]!.trim());
+          sections.add(new StatusSection(condition, _lineCount));
         } on FormatException {
           fail("Status expression syntax error");
         }
@@ -98,10 +94,10 @@
       // Otherwise, it should be a new entry under the current section.
       match = _entryPattern.firstMatch(source);
       if (match != null) {
-        var path = match[1].trim();
+        var path = match[1]!.trim();
         // TODO(whesse): Handle test names ending in a wildcard (*).
         var expectations = <Expectation>[];
-        for (var name in match[2].split(",")) {
+        for (var name in match[2]!.split(",")) {
           name = name.trim();
           try {
             expectations.add(Expectation.find(name));
@@ -114,12 +110,11 @@
 
         // If we haven't found a section header yet, create an implicit section
         // that matches everything.
-        if (section == null) {
-          section = new StatusSection(null, -1);
-          sections.add(section);
+        if (sections.isEmpty) {
+          sections.add(new StatusSection(Expression.always, -1));
         }
 
-        section.entries
+        sections.last.entries
             .add(new StatusEntry(path, _lineCount, expectations, issue));
         continue;
       }
@@ -138,8 +133,6 @@
     // TODO(rnystrom): It would be more useful if it reported all of the errors
     // instead of stopping on the first.
     for (var section in sections) {
-      if (section.condition == null) continue;
-
       var errors = <String>[];
       section.condition.validate(environment, errors);
 
@@ -158,11 +151,11 @@
   }
 
   /// Returns the issue number embedded in [comment] or `null` if there is none.
-  int _issueNumber(String comment) {
+  int? _issueNumber(String comment) {
     var match = _issuePattern.firstMatch(comment);
     if (match == null) return null;
 
-    return int.parse(match[1]);
+    return int.parse(match[1]!);
   }
 
   String toString() {
@@ -192,7 +185,7 @@
     var lastLine = 0;
     var needBlankLine = false;
 
-    void writeLine(String text, int line) {
+    void writeLine(String? text, int line) {
       var comment = _comments[line];
       if (text == null && comment == null) {
         // There's no comment on this line, so it's blank.
@@ -216,17 +209,15 @@
     }
 
     void writeText(String text, int line) {
-      if (line != null) {
-        while (++lastLine < line) {
-          writeLine(null, lastLine);
-        }
+      while (++lastLine < line) {
+        writeLine(null, lastLine);
       }
 
       writeLine(text, line);
     }
 
     for (var section in sections) {
-      if (section.condition != null) {
+      if (section.condition != Expression.always) {
         writeText("[ ${section.condition} ]", section.lineNumber);
       }
 
@@ -254,8 +245,8 @@
 class StatusSection {
   /// The expression that determines when this section is applied.
   ///
-  /// May be `null` for paths that appear before any section header in the file.
-  /// In that case, the section always applies.
+  /// Will be [Expression.always] for paths that appear before any section
+  /// header in the file. In that case, the section always applies.
   final Expression condition;
 
   /// The one-based line number where the entry appears in the file.
@@ -264,8 +255,7 @@
   final List<StatusEntry> entries = [];
 
   /// Returns true if this section should apply in the given [environment].
-  bool isEnabled(Environment environment) =>
-      condition == null || condition.evaluate(environment);
+  bool isEnabled(Environment environment) => condition.evaluate(environment);
 
   StatusSection(this.condition, this.lineNumber);
 }
@@ -278,7 +268,7 @@
   final int lineNumber;
 
   final List<Expectation> expectations;
-  final int issue;
+  final int? issue;
 
   StatusEntry(this.path, this.lineNumber, this.expectations, this.issue);
 }
@@ -289,7 +279,7 @@
   final int lineNumber;
   final String line;
   final String message;
-  final List<String> errors;
+  final List<String>? errors;
 
   SyntaxError(this.file, this.lineNumber, this.line, this.message, this.errors);
 
@@ -298,10 +288,8 @@
     buffer.writeln('$message in "$file" line $lineNumber:');
     buffer.writeln(line);
 
-    if (errors != null) {
-      for (var error in errors) {
-        buffer.writeln("- ${error.replaceAll('\n', '\n  ')}");
-      }
+    for (var error in errors ?? const []) {
+      buffer.writeln("- ${error.replaceAll('\n', '\n  ')}");
     }
 
     return buffer.toString().trimRight();
diff --git a/pkg/status_file/lib/status_file_linter.dart b/pkg/status_file/lib/status_file_linter.dart
index d1282ab..7f1e29b 100644
--- a/pkg/status_file/lib/status_file_linter.dart
+++ b/pkg/status_file/lib/status_file_linter.dart
@@ -115,10 +115,9 @@
 
 /// Checks that each section expression have been normalized.
 Iterable<LintingError> lintNormalizedSection(StatusSection section) {
-  if (section.condition == null) return const [];
   var nonNormalized = section.condition.toString();
   var normalized = section.condition.normalize().toString();
-  if (section.condition.toString() != normalized) {
+  if (nonNormalized != normalized) {
     return [
       new LintingError(
           section.lineNumber,
@@ -184,11 +183,11 @@
   if (witness != null) {
     return [
       new LintingError(
-          witness.second.lineNumber,
+          witness.second!.lineNumber,
           "Section expressions are not correctly ordered in file. "
-          "'${witness.first.condition}' on line ${witness.first.lineNumber} "
-          "should come before '${witness.second.condition}' at line "
-          "${witness.second.lineNumber}.")
+          "'${witness.first!.condition}' on line ${witness.first!.lineNumber} "
+          "should come before '${witness.second!.condition}' at line "
+          "${witness.second!.lineNumber}.")
     ];
   }
   return [];
@@ -198,14 +197,12 @@
 Iterable<LintingError> lintSectionHeaderDuplicates(
     List<StatusSection> sections) {
   var errors = <LintingError>[];
-  var sorted = sections.where((section) => section.condition != null).toList()
+  var sorted = sections.toList()
     ..sort((a, b) => a.condition.compareTo(b.condition));
   for (var i = 1; i < sorted.length; i++) {
     var section = sorted[i];
     var previousSection = sorted[i - 1];
-    if (section.condition != null &&
-        previousSection.condition != null &&
-        section.condition.compareTo(previousSection.condition) == 0) {
+    if (section.condition.compareTo(previousSection.condition) == 0) {
       errors.add(new LintingError(
           section.lineNumber,
           "The condition "
@@ -216,7 +213,7 @@
   return errors;
 }
 
-ListNotEqualWitness<T> _findNotEqualWitness<T>(List<T> first, List<T> second) {
+ListNotEqualWitness<T>? _findNotEqualWitness<T>(List<T> first, List<T> second) {
   if (first.isEmpty && second.isEmpty) {
     return null;
   }
@@ -233,7 +230,7 @@
 }
 
 class ListNotEqualWitness<T> {
-  final T first;
-  final T second;
+  final T? first;
+  final T? second;
   ListNotEqualWitness(this.first, this.second);
 }
diff --git a/pkg/status_file/lib/status_file_normalizer.dart b/pkg/status_file/lib/status_file_normalizer.dart
index 71d90e3..73e1727 100644
--- a/pkg/status_file/lib/status_file_normalizer.dart
+++ b/pkg/status_file/lib/status_file_normalizer.dart
@@ -2,6 +2,8 @@
 // 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:status_file/src/expression.dart';
+
 import 'canonical_status_file.dart';
 import 'dart:convert';
 
@@ -52,10 +54,9 @@
   List<StatusSection> newSections = [];
   // Copy over all sections and normalize all the expressions.
   oldStatusFile.sections.forEach((section) {
-    if (section.condition != null && section.isEmpty()) {
-      return;
-    }
-    if (section.condition != null) {
+    if (section.condition != Expression.always) {
+      if (section.isEmpty()) return;
+
       newSections.add(new StatusSection(section.condition.normalize(),
           section.lineNumber, section.sectionHeaderComments)
         ..entries.addAll(section.entries));
@@ -63,23 +64,17 @@
       newSections.add(section);
     }
   });
+
   // Sort the headers
-  newSections.sort((a, b) {
-    if (a.condition == null) {
-      return -1;
-    } else if (b.condition == null) {
-      return 1;
-    }
-    return a.condition.compareTo(b.condition);
-  });
+  newSections.sort((a, b) => a.condition.compareTo(b.condition));
+
   // See if we can combine section headers by simple comparison.
-  StatusFile newStatusFile = new StatusFile(statusFile.path);
+  var newStatusFile = new StatusFile(statusFile.path);
   newStatusFile.sections.add(newSections[0]);
   for (var i = 1; i < newSections.length; i++) {
     var previousSection = newSections[i - 1];
     var currentSection = newSections[i];
-    if (previousSection.condition != null &&
-        previousSection.condition.compareTo(currentSection.condition) == 0) {
+    if (previousSection.condition.compareTo(currentSection.condition) == 0) {
       newStatusFile.sections.last.entries.addAll(currentSection.entries);
     } else {
       newStatusFile.sections.add(currentSection);
diff --git a/pkg/status_file/pubspec.yaml b/pkg/status_file/pubspec.yaml
index ae78d40..1536b63 100644
--- a/pkg/status_file/pubspec.yaml
+++ b/pkg/status_file/pubspec.yaml
@@ -2,7 +2,7 @@
 # This package is not intended for consumption on pub.dev. DO NOT publish.
 publish_to: none
 environment:
-  sdk: "^2.3.0"
+  sdk: "^2.12.0"
 dependencies:
   path: "^1.4.0"
   args: "^1.4.4"
diff --git a/pkg/status_file/test/linter_test.dart b/pkg/status_file/test/linter_test.dart
index dcd6e06..f97b6ca 100644
--- a/pkg/status_file/test/linter_test.dart
+++ b/pkg/status_file/test/linter_test.dart
@@ -100,7 +100,7 @@
 vm/tests: Skip # this comment is valid
 """,
       "Error at line 1: Expression contains '||'. Please split the expression "
-      "into multiple separate sections.",
+          "into multiple separate sections.",
       disjunctions: true);
 }
 
@@ -118,7 +118,7 @@
 a_test: Pass
 """,
       "Error at line 1: Test paths are not alphabetically ordered in "
-      "section. a_test should come before vm/tests.");
+          "section. a_test should come before vm/tests.");
 }
 
 void testCheckForAlphabeticalOrderingOfPaths_okOrdering() {
@@ -139,7 +139,7 @@
 xyz_test: Skip
 """,
       "Error at line 1: The status entry 'a_test: Pass' is duplicated on lines "
-      "2 and 3.");
+          "2 and 3.");
 }
 
 void testCheckForCorrectOrderingInSections_invalidRuntimeBeforeCompiler() {
@@ -148,7 +148,7 @@
 a_test: Pass
 """,
       r"Error at line 1: Condition expression should be '$compiler == dart2js "
-      r"&& $runtime == ff' but was '$runtime == ff && $compiler == dart2js'.");
+          r"&& $runtime == ff' but was '$runtime == ff && $compiler == dart2js'.");
 }
 
 void testCheckForCorrectOrderingInSections_invalidRuntimeBeforeMode() {
@@ -157,7 +157,7 @@
 a_test: Pass
 """,
       r"Error at line 1: Condition expression should be '$mode == debug && "
-      r"$runtime == ff' but was '$runtime == ff && $mode == debug'.");
+          r"$runtime == ff' but was '$runtime == ff && $mode == debug'.");
 }
 
 void testCheckForCorrectOrderingInSections_invalidSystemBeforeMode() {
@@ -166,7 +166,7 @@
 a_test: Pass
 """,
       r"Error at line 1: Condition expression should be '$mode == debug && "
-      r"$system == win' but was '$system == win && $mode == debug'.");
+          r"$system == win' but was '$system == win && $mode == debug'.");
 }
 
 void testCheckForCorrectOrderingInSections_invalidStrongBeforeKernel() {
@@ -175,7 +175,7 @@
 a_test: Pass
 """,
       r"Error at line 1: Condition expression should be '!$kernel && !$strong' "
-      r"but was '!$strong && !$kernel'.");
+          r"but was '!$strong && !$kernel'.");
 }
 
 void testCheckForCorrectOrderingInSections_invalidOrdering() {
@@ -184,8 +184,8 @@
 a_test: Pass
 """,
       r"Error at line 1: Condition expression should be '$builder_tag == "
-      r"strong && $compiler == dart2js && !$browser' but was "
-      r"'$compiler == dart2js && $builder_tag == strong && !$browser'.");
+          r"strong && $compiler == dart2js && !$browser' but was "
+          r"'$compiler == dart2js && $builder_tag == strong && !$browser'.");
 }
 
 void testCheckForCorrectOrderingInSections_okOrdering() {
@@ -203,8 +203,8 @@
 a_test: Pass
 """,
       r"Error at line 1: Section expressions are not correctly ordered in file."
-      r" '$compiler == dart2js' on line 4 should come before '$runtime == ff' "
-      r"at line 1.");
+          r" '$compiler == dart2js' on line 4 should come before '$runtime == ff' "
+          r"at line 1.");
 }
 
 void checkLintNormalizedSection_invalidAlphabeticalOrderingVariableArguments() {
@@ -216,8 +216,8 @@
 a_test: Pass
 """,
       r"Error at line 1: Section expressions are not correctly ordered in file."
-      r" '$runtime == chrome' on line 4 should come before '$runtime == ff' at "
-      r"line 1.");
+          r" '$runtime == chrome' on line 4 should come before '$runtime == ff' at "
+          r"line 1.");
 }
 
 void checkLintNormalizedSection_invalidOrderingWithNotEqual() {
@@ -233,8 +233,8 @@
 a_test: Pass
 """,
       r"Error at line 4: Section expressions are not correctly ordered in file."
-      r" '$runtime == ff' on line 7 should come before '$runtime != ff' at "
-      r"line 4.");
+          r" '$runtime == ff' on line 7 should come before '$runtime != ff' at "
+          r"line 4.");
 }
 
 void checkLintNormalizedSection_invalidOrderingWithNegation() {
@@ -251,7 +251,7 @@
 
 """,
       r"Error at line 4: Section expressions are not correctly ordered in file."
-      r" '$checked' on line 7 should come before '!$checked' at line 4.");
+          r" '$checked' on line 7 should come before '!$checked' at line 4.");
 }
 
 void checkLintNormalizedSection_correctOrdering() {
@@ -285,5 +285,5 @@
 a_test: Pass
 """,
       r"Error at line 4: The condition '!$browser' is duplicated on lines 1 "
-      r"and 4.");
+          r"and 4.");
 }
diff --git a/pkg/status_file/test/normalize_test.dart b/pkg/status_file/test/normalize_test.dart
index 92869c3..27c5985 100644
--- a/pkg/status_file/test/normalize_test.dart
+++ b/pkg/status_file/test/normalize_test.dart
@@ -69,8 +69,8 @@
         "$entriesInNormalized. Those two numbers are not the same.");
   }
   for (var section in original.sections) {
-    section.entries.where((entry) => entry is StatusEntry).forEach((entry) =>
-        findInStatusFile(normalized, entry, section.condition?.normalize(),
+    section.entries.whereType<StatusEntry>().forEach((entry) =>
+        findInStatusFile(normalized, entry, section.condition.normalize(),
             warnOnDuplicateHeader: warnOnDuplicateHeader));
   }
 }
@@ -87,12 +87,7 @@
     {bool warnOnDuplicateHeader = false}) {
   int foundEntryPosition = -1;
   for (var section in statusFile.sections) {
-    if (section.condition == null && condition != null ||
-        section.condition != null && condition == null) {
-      continue;
-    }
-    if (section.condition != null &&
-        section.condition.normalize().compareTo(condition) != 0) {
+    if (section.condition.normalize().compareTo(condition) != 0) {
       continue;
     }
     var matchingEntries = section.entries
diff --git a/pkg/status_file/test/repo_status_files_test.dart b/pkg/status_file/test/repo_status_files_test.dart
index 9f14fe7..fccd043 100644
--- a/pkg/status_file/test/repo_status_files_test.dart
+++ b/pkg/status_file/test/repo_status_files_test.dart
@@ -19,8 +19,8 @@
       if (!entry.path.endsWith(".status")) continue;
       try {
         new StatusFile.read(entry.path);
-      } catch (err) {
-        Expect.fail("Could not parse '${entry.path}'.\n$err");
+      } catch (err, stack) {
+        Expect.fail("Could not parse '${entry.path}'.\n$err\n$stack");
       }
     }
   }
diff --git a/pkg/status_file/test/status_expression_dnf_test.dart b/pkg/status_file/test/status_expression_dnf_test.dart
index 475b664..546395a 100644
--- a/pkg/status_file/test/status_expression_dnf_test.dart
+++ b/pkg/status_file/test/status_expression_dnf_test.dart
@@ -55,13 +55,13 @@
   // https://en.wikipedia.org/wiki/Quine%E2%80%93McCluskey_algorithm
   shouldDnfTo(
       r"$a && !$b && !$c && !$d || $a && !$b && !$c && $d || "
-      r"$a && !$b && $c && !$d || $a && !$b && $c && $d",
+          r"$a && !$b && $c && !$d || $a && !$b && $c && $d",
       r"$a && !$b");
 
   shouldDnfTo(
       r"!$a && $b && !$c && !$d || $a && !$b && !$c && !$d || "
-      r"$a && !$b && $c && !$d || $a && !$b && $c && $d || $a && $b && !$c && !$d ||"
-      r" $a && $b && $c && $d || $a && !$b && !$c && $d || $a && $b && $c && !$d",
+          r"$a && !$b && $c && !$d || $a && !$b && $c && $d || $a && $b && !$c && !$d ||"
+          r" $a && $b && $c && $d || $a && !$b && !$c && $d || $a && $b && $c && !$d",
       r"$a && !$b || $a && $c || $b && !$c && !$d");
 
   // Test that an expression is converted to dnf and minified correctly.
diff --git a/pkg/status_file/test/status_expression_test.dart b/pkg/status_file/test/status_expression_test.dart
index 7fb224a..b79d57b 100644
--- a/pkg/status_file/test/status_expression_test.dart
+++ b/pkg/status_file/test/status_expression_test.dart
@@ -17,7 +17,7 @@
   }
 
   /// Looks up the value of the variable with [name].
-  String lookUp(String name) => _values[name];
+  String? lookUp(String name) => _values[name];
 
   operator []=(String key, String value) => _values[key] = value;
 }
@@ -33,8 +33,8 @@
 }
 
 void testExpression() {
-  var expression = Expression
-      .parse(r" $mode == debug && ($arch == chromium || $arch == dartc) ");
+  var expression = Expression.parse(
+      r" $mode == debug && ($arch == chromium || $arch == dartc) ");
   Expect.equals(r"$mode == debug && ($arch == chromium || $arch == dartc)",
       expression.toString());
 
diff --git a/runtime/bin/BUILD.gn b/runtime/bin/BUILD.gn
index 438cec8..355d3ba 100644
--- a/runtime/bin/BUILD.gn
+++ b/runtime/bin/BUILD.gn
@@ -131,6 +131,10 @@
     sources = [
       "elf_loader.cc",
       "elf_loader.h",
+      "virtual_memory.h",
+      "virtual_memory_fuchsia.cc",
+      "virtual_memory_posix.cc",
+      "virtual_memory_win.cc",
     ]
     deps = invoker.deps
   }
diff --git a/runtime/bin/elf_loader.cc b/runtime/bin/elf_loader.cc
index 43b152d..a5ff8c0 100644
--- a/runtime/bin/elf_loader.cc
+++ b/runtime/bin/elf_loader.cc
@@ -2,13 +2,9 @@
 // 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.
 
-#include <bin/elf_loader.h>
-#include <bin/file.h>
-#include <platform/elf.h>
-#include <platform/globals.h>
-#include <vm/cpu.h>
-#include <vm/virtual_memory.h>
+#include "bin/elf_loader.h"
 
+#include "platform/globals.h"
 #if defined(DART_HOST_OS_FUCHSIA)
 #include <sys/mman.h>
 #endif
@@ -16,6 +12,10 @@
 #include <memory>
 #include <utility>
 
+#include "bin/file.h"
+#include "bin/virtual_memory.h"
+#include "platform/elf.h"
+
 namespace dart {
 namespace bin {
 
@@ -369,7 +369,6 @@
 bool LoadedElf::LoadSegments() {
   // Calculate the total amount of virtual memory needed.
   uword total_memory = 0;
-  uword maximum_alignment = PageSize();
   for (uword i = 0; i < header_.num_program_headers; ++i) {
     const dart::elf::ProgramHeader header = program_table_[i];
 
@@ -381,14 +380,12 @@
         total_memory);
     CHECK_ERROR(Utils::IsPowerOfTwo(header.alignment),
                 "Alignment must be a power of two.");
-    maximum_alignment =
-        Utils::Maximum(maximum_alignment, static_cast<uword>(header.alignment));
   }
   total_memory = Utils::RoundUp(total_memory, PageSize());
 
-  base_.reset(VirtualMemory::AllocateAligned(
-      total_memory, /*alignment=*/maximum_alignment,
-      /*is_executable=*/false, "dart-compiled-image"));
+  base_.reset(VirtualMemory::Allocate(total_memory,
+                                      /*is_executable=*/false,
+                                      "dart-compiled-image"));
   CHECK_ERROR(base_ != nullptr, "Could not reserve virtual memory.");
 
   for (uword i = 0; i < header_.num_program_headers; ++i) {
diff --git a/runtime/bin/virtual_memory.h b/runtime/bin/virtual_memory.h
new file mode 100644
index 0000000..3ec25ed
--- /dev/null
+++ b/runtime/bin/virtual_memory.h
@@ -0,0 +1,73 @@
+// Copyright (c) 2021, 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.
+
+#ifndef RUNTIME_BIN_VIRTUAL_MEMORY_H_
+#define RUNTIME_BIN_VIRTUAL_MEMORY_H_
+
+#include "platform/allocation.h"
+#include "platform/globals.h"
+
+namespace dart {
+namespace bin {
+
+class VirtualMemory {
+ public:
+  enum Protection {
+    kNoAccess,
+    kReadOnly,
+    kReadWrite,
+    kReadExecute,
+    kReadWriteExecute
+  };
+
+  // The reserved memory is unmapped on destruction.
+  ~VirtualMemory();
+
+  void release() {
+    address_ = nullptr;
+    size_ = 0;
+  }
+
+  uword start() const { return reinterpret_cast<uword>(address_); }
+  uword end() const { return reinterpret_cast<uword>(address_) + size_; }
+  void* address() const { return address_; }
+  intptr_t size() const { return size_; }
+
+  // Changes the protection of the virtual memory area.
+  static void Protect(void* address, intptr_t size, Protection mode);
+  void Protect(Protection mode) { return Protect(address(), size(), mode); }
+
+  // Reserves and commits a virtual memory segment with size. If a segment of
+  // the requested size cannot be allocated, NULL is returned.
+  static VirtualMemory* Allocate(intptr_t size,
+                                 bool is_executable,
+                                 const char* name);
+
+  static void Init() { page_size_ = CalculatePageSize(); }
+
+  // Returns the cached page size. Use only if Init() has been called.
+  static intptr_t PageSize() {
+    ASSERT(page_size_ != 0);
+    return page_size_;
+  }
+
+ private:
+  static intptr_t CalculatePageSize();
+
+  // These constructors are only used internally when reserving new virtual
+  // spaces. They do not reserve any virtual address space on their own.
+  VirtualMemory(void* address, size_t size) : address_(address), size_(size) {}
+
+  void* address_;
+  size_t size_;
+
+  static uword page_size_;
+
+  DISALLOW_IMPLICIT_CONSTRUCTORS(VirtualMemory);
+};
+
+}  // namespace bin
+}  // namespace dart
+
+#endif  // RUNTIME_BIN_VIRTUAL_MEMORY_H_
diff --git a/runtime/bin/virtual_memory_fuchsia.cc b/runtime/bin/virtual_memory_fuchsia.cc
new file mode 100644
index 0000000..2ba42b5
--- /dev/null
+++ b/runtime/bin/virtual_memory_fuchsia.cc
@@ -0,0 +1,110 @@
+// Copyright (c) 2021, 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.
+
+#include "platform/globals.h"
+#if defined(DART_HOST_OS_FUCHSIA)
+
+#include "bin/virtual_memory.h"
+
+#include <zircon/process.h>
+#include <zircon/status.h>
+#include <zircon/syscalls.h>
+
+#include "platform/assert.h"
+#include "platform/utils.h"
+
+namespace dart {
+namespace bin {
+
+uword VirtualMemory::page_size_ = 0;
+
+intptr_t VirtualMemory::CalculatePageSize() {
+  const intptr_t page_size = getpagesize();
+  ASSERT(page_size != 0);
+  ASSERT(Utils::IsPowerOfTwo(page_size));
+  return page_size;
+}
+
+VirtualMemory* VirtualMemory::Allocate(intptr_t size,
+                                       bool is_executable,
+                                       const char* name) {
+  ASSERT(Utils::IsAligned(size, page_size_));
+  zx_handle_t vmar = zx_vmar_root_self();
+  zx_handle_t vmo = ZX_HANDLE_INVALID;
+  zx_status_t status = zx_vmo_create(size, 0u, &vmo);
+  if (status != ZX_OK) {
+    return nullptr;
+  }
+
+  if (name != nullptr) {
+    zx_object_set_property(vmo, ZX_PROP_NAME, name, strlen(name));
+  }
+
+  if (is_executable) {
+    // Add ZX_RIGHT_EXECUTE permission to VMO, so it can be mapped
+    // into memory as executable (now or later).
+    status = zx_vmo_replace_as_executable(vmo, ZX_HANDLE_INVALID, &vmo);
+    if (status != ZX_OK) {
+      zx_handle_close(vmo);
+      return nullptr;
+    }
+  }
+
+  const zx_vm_option_t region_options =
+      ZX_VM_PERM_READ | ZX_VM_PERM_WRITE |
+      (is_executable ? ZX_VM_PERM_EXECUTE : 0);
+  uword base;
+  status = zx_vmar_map(vmar, region_options, 0, vmo, 0u, size, &base);
+  zx_handle_close(vmo);
+  if (status != ZX_OK) {
+    return nullptr;
+  }
+
+  return new VirtualMemory(reinterpret_cast<void*>(base), size);
+}
+
+VirtualMemory::~VirtualMemory() {
+  if (address_ != nullptr) {
+    zx_status_t status = zx_vmar_unmap(
+        zx_vmar_root_self(), reinterpret_cast<uword>(address_), size_);
+    if (status != ZX_OK) {
+      FATAL("zx_vmar_unmap failed: %s\n", zx_status_get_string(status));
+    }
+  }
+}
+
+void VirtualMemory::Protect(void* address, intptr_t size, Protection mode) {
+  const uword start_address = reinterpret_cast<uword>(address);
+  const uword end_address = start_address + size;
+  const uword page_address = Utils::RoundDown(start_address, PageSize());
+  uint32_t prot = 0;
+  switch (mode) {
+    case kNoAccess:
+      prot = 0;
+      break;
+    case kReadOnly:
+      prot = ZX_VM_PERM_READ;
+      break;
+    case kReadWrite:
+      prot = ZX_VM_PERM_READ | ZX_VM_PERM_WRITE;
+      break;
+    case kReadExecute:
+      prot = ZX_VM_PERM_READ | ZX_VM_PERM_EXECUTE;
+      break;
+    case kReadWriteExecute:
+      prot = ZX_VM_PERM_READ | ZX_VM_PERM_WRITE | ZX_VM_PERM_EXECUTE;
+      break;
+  }
+  zx_status_t status = zx_vmar_protect(zx_vmar_root_self(), prot, page_address,
+                                       end_address - page_address);
+  if (status != ZX_OK) {
+    FATAL("zx_vmar_protect(0x%lx, 0x%lx) failed: %s\n", page_address,
+          end_address - page_address, zx_status_get_string(status));
+  }
+}
+
+}  // namespace bin
+}  // namespace dart
+
+#endif  // defined(DART_HOST_OS_FUCHSIA)
diff --git a/runtime/bin/virtual_memory_posix.cc b/runtime/bin/virtual_memory_posix.cc
new file mode 100644
index 0000000..3618564
--- /dev/null
+++ b/runtime/bin/virtual_memory_posix.cc
@@ -0,0 +1,110 @@
+// Copyright (c) 2021, 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.
+
+#include "platform/globals.h"
+#if defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX) ||            \
+    defined(DART_HOST_OS_MACOS)
+
+#include "bin/virtual_memory.h"
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "platform/assert.h"
+#include "platform/utils.h"
+
+namespace dart {
+namespace bin {
+
+// standard MAP_FAILED causes "error: use of old-style cast" as it
+// defines MAP_FAILED as ((void *) -1)
+#undef MAP_FAILED
+#define MAP_FAILED reinterpret_cast<void*>(-1)
+
+uword VirtualMemory::page_size_ = 0;
+
+intptr_t VirtualMemory::CalculatePageSize() {
+  const intptr_t page_size = getpagesize();
+  ASSERT(page_size != 0);
+  ASSERT(Utils::IsPowerOfTwo(page_size));
+  return page_size;
+}
+
+VirtualMemory* VirtualMemory::Allocate(intptr_t size,
+                                       bool is_executable,
+                                       const char* name) {
+  ASSERT(Utils::IsAligned(size, PageSize()));
+
+  const int prot = PROT_READ | PROT_WRITE | (is_executable ? PROT_EXEC : 0);
+
+  int map_flags = MAP_PRIVATE | MAP_ANONYMOUS;
+#if (defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS))
+  if (is_executable && IsAtLeastOS10_14()) {
+    map_flags |= MAP_JIT;
+  }
+#endif  // defined(DART_HOST_OS_MACOS)
+
+  // Some 64-bit microarchitectures store only the low 32-bits of targets as
+  // part of indirect branch prediction, predicting that the target's upper bits
+  // will be same as the call instruction's address. This leads to misprediction
+  // for indirect calls crossing a 4GB boundary. We ask mmap to place our
+  // generated code near the VM binary to avoid this.
+  void* hint = is_executable ? reinterpret_cast<void*>(&Allocate) : nullptr;
+  void* address = mmap(hint, size, prot, map_flags, -1, 0);
+  if (address == MAP_FAILED) {
+    return nullptr;
+  }
+  return new VirtualMemory(address, size);
+}
+
+VirtualMemory::~VirtualMemory() {
+  if (address_ != nullptr) {
+    if (munmap(address_, size_) != 0) {
+      int error = errno;
+      const int kBufferSize = 1024;
+      char error_buf[kBufferSize];
+      FATAL("munmap error: %d (%s)", error,
+            Utils::StrError(error, error_buf, kBufferSize));
+    }
+  }
+}
+
+void VirtualMemory::Protect(void* address, intptr_t size, Protection mode) {
+  uword start_address = reinterpret_cast<uword>(address);
+  uword end_address = start_address + size;
+  uword page_address = Utils::RoundDown(start_address, PageSize());
+  int prot = 0;
+  switch (mode) {
+    case kNoAccess:
+      prot = PROT_NONE;
+      break;
+    case kReadOnly:
+      prot = PROT_READ;
+      break;
+    case kReadWrite:
+      prot = PROT_READ | PROT_WRITE;
+      break;
+    case kReadExecute:
+      prot = PROT_READ | PROT_EXEC;
+      break;
+    case kReadWriteExecute:
+      prot = PROT_READ | PROT_WRITE | PROT_EXEC;
+      break;
+  }
+  if (mprotect(reinterpret_cast<void*>(page_address),
+               end_address - page_address, prot) != 0) {
+    int error = errno;
+    const int kBufferSize = 1024;
+    char error_buf[kBufferSize];
+    FATAL("mprotect error: %d (%s)", error,
+          Utils::StrError(error, error_buf, kBufferSize));
+  }
+}
+
+}  // namespace bin
+}  // namespace dart
+
+#endif  // defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX) ||     \
+        // defined(DART_HOST_OS_MACOS)
diff --git a/runtime/bin/virtual_memory_win.cc b/runtime/bin/virtual_memory_win.cc
new file mode 100644
index 0000000..b6075ed
--- /dev/null
+++ b/runtime/bin/virtual_memory_win.cc
@@ -0,0 +1,79 @@
+// Copyright (c) 2021, 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.
+
+#include "platform/globals.h"
+#if defined(DART_HOST_OS_WINDOWS)
+
+#include "bin/virtual_memory.h"
+
+#include "platform/assert.h"
+#include "platform/utils.h"
+
+namespace dart {
+namespace bin {
+
+uword VirtualMemory::page_size_ = 0;
+
+intptr_t VirtualMemory::CalculatePageSize() {
+  SYSTEM_INFO info;
+  GetSystemInfo(&info);
+  const intptr_t page_size = info.dwPageSize;
+  ASSERT(page_size != 0);
+  ASSERT(Utils::IsPowerOfTwo(page_size));
+  return page_size;
+}
+
+VirtualMemory* VirtualMemory::Allocate(intptr_t size,
+                                       bool is_executable,
+                                       const char* name) {
+  ASSERT(Utils::IsAligned(size, PageSize()));
+  int prot = is_executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
+  void* address = VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, prot);
+  if (address == nullptr) {
+    return nullptr;
+  }
+  return new VirtualMemory(address, size);
+}
+
+VirtualMemory::~VirtualMemory() {
+  if (address_ != nullptr) {
+    if (VirtualFree(address_, 0, MEM_RELEASE) == 0) {
+      FATAL("VirtualFree failed: Error code %d\n", GetLastError());
+    }
+  }
+}
+
+void VirtualMemory::Protect(void* address, intptr_t size, Protection mode) {
+  uword start_address = reinterpret_cast<uword>(address);
+  uword end_address = start_address + size;
+  uword page_address = Utils::RoundDown(start_address, PageSize());
+  DWORD prot = 0;
+  switch (mode) {
+    case kNoAccess:
+      prot = PAGE_NOACCESS;
+      break;
+    case kReadOnly:
+      prot = PAGE_READONLY;
+      break;
+    case kReadWrite:
+      prot = PAGE_READWRITE;
+      break;
+    case kReadExecute:
+      prot = PAGE_EXECUTE_READ;
+      break;
+    case kReadWriteExecute:
+      prot = PAGE_EXECUTE_READWRITE;
+      break;
+  }
+  DWORD old_prot = 0;
+  if (VirtualProtect(reinterpret_cast<void*>(page_address),
+                     end_address - page_address, prot, &old_prot) == 0) {
+    FATAL("VirtualProtect failed %d\n", GetLastError());
+  }
+}
+
+}  // namespace bin
+}  // namespace dart
+
+#endif  // defined(DART_HOST_OS_WINDOWS)
diff --git a/runtime/lib/isolate.cc b/runtime/lib/isolate.cc
index 43f124f..aa86706 100644
--- a/runtime/lib/isolate.cc
+++ b/runtime/lib/isolate.cc
@@ -21,7 +21,6 @@
 #include "vm/message_handler.h"
 #include "vm/message_snapshot.h"
 #include "vm/object.h"
-#include "vm/object_graph_copy.h"
 #include "vm/object_store.h"
 #include "vm/port.h"
 #include "vm/resolver.h"
@@ -108,28 +107,13 @@
 
   const Dart_Port destination_port_id = port.Id();
   const bool can_send_any_object = isolate->origin_id() == port.origin_id();
-
-  if (ApiObjectConverter::CanConvert(obj.ptr())) {
-    PortMap::PostMessage(
-        Message::New(destination_port_id, obj.ptr(), Message::kNormalPriority));
-  } else {
-    const bool same_group = FLAG_enable_isolate_groups &&
-                            PortMap::IsReceiverInThisIsolateGroup(
-                                destination_port_id, isolate->group());
-    if (same_group) {
-      const auto& copy = Object::Handle(CopyMutableObjectGraph(obj));
-      auto handle = isolate->group()->api_state()->AllocatePersistentHandle();
-      handle->set_ptr(copy.ptr());
-      std::unique_ptr<Message> message(
-          new Message(destination_port_id, handle, Message::kNormalPriority));
-      PortMap::PostMessage(std::move(message));
-    } else {
-      // TODO(turnidge): Throw an exception when the return value is false?
-      PortMap::PostMessage(WriteMessage(can_send_any_object, obj,
-                                        destination_port_id,
-                                        Message::kNormalPriority));
-    }
-  }
+  const bool same_group =
+      FLAG_enable_isolate_groups && PortMap::IsReceiverInThisIsolateGroup(
+                                        destination_port_id, isolate->group());
+  // TODO(turnidge): Throw an exception when the return value is false?
+  PortMap::PostMessage(WriteMessage(can_send_any_object, same_group, obj,
+                                    destination_port_id,
+                                    Message::kNormalPriority));
   return Object::null();
 }
 
@@ -581,11 +565,17 @@
 }
 
 ObjectPtr IsolateSpawnState::BuildArgs(Thread* thread) {
-  return DeserializeMessage(thread, serialized_args_.get());
+  const Object& result =
+      Object::Handle(DeserializeMessage(thread, serialized_args_.get()));
+  serialized_args_.reset();
+  return result.ptr();
 }
 
 ObjectPtr IsolateSpawnState::BuildMessage(Thread* thread) {
-  return DeserializeMessage(thread, serialized_message_.get());
+  const Object& result =
+      Object::Handle(DeserializeMessage(thread, serialized_message_.get()));
+  serialized_message_.reset();
+  return result.ptr();
 }
 
 static void ThrowIsolateSpawnException(const String& message) {
@@ -810,7 +800,8 @@
     {
       // If parent isolate died, we ignore the fact that we cannot notify it.
       PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
-                                        message, state_->parent_port(),
+                                        /* same_group */ false, message,
+                                        state_->parent_port(),
                                         Message::kNormalPriority));
     }
 
@@ -877,21 +868,20 @@
       bool fatal_errors = fatalErrors.IsNull() ? true : fatalErrors.value();
       Dart_Port on_exit_port = onExit.IsNull() ? ILLEGAL_PORT : onExit.Id();
       Dart_Port on_error_port = onError.IsNull() ? ILLEGAL_PORT : onError.Id();
+      const bool in_new_isolate_group = newIsolateGroup.value();
 
       // We first try to serialize the message.  In case the message is not
       // serializable this will throw an exception.
       SerializedObjectBuffer message_buffer;
-      {
-        message_buffer.set_message(WriteMessage(
-            /* can_send_any_object */ true, message, ILLEGAL_PORT,
-            Message::kNormalPriority));
-      }
+      message_buffer.set_message(WriteMessage(
+          /* can_send_any_object */ true,
+          /* same_group */ FLAG_enable_isolate_groups && !in_new_isolate_group,
+          message, ILLEGAL_PORT, Message::kNormalPriority));
 
       const char* utf8_package_config =
           packageConfig.IsNull() ? NULL : String2UTF8(packageConfig);
       const char* utf8_debug_name =
           debugName.IsNull() ? NULL : String2UTF8(debugName);
-      const bool in_new_isolate_group = newIsolateGroup.value();
 
       std::unique_ptr<IsolateSpawnState> state(new IsolateSpawnState(
           port.Id(), isolate->origin_id(), String2UTF8(script_uri), func,
@@ -967,13 +957,14 @@
   SerializedObjectBuffer arguments_buffer;
   SerializedObjectBuffer message_buffer;
   {
-    arguments_buffer.set_message(WriteMessage(/* can_send_any_object */ false,
-                                              args, ILLEGAL_PORT,
-                                              Message::kNormalPriority));
+    arguments_buffer.set_message(WriteMessage(
+        /* can_send_any_object */ false,
+        /* same_group */ false, args, ILLEGAL_PORT, Message::kNormalPriority));
   }
   {
     message_buffer.set_message(WriteMessage(/* can_send_any_object */ false,
-                                            message, ILLEGAL_PORT,
+                                            /* same_group */ false, message,
+                                            ILLEGAL_PORT,
                                             Message::kNormalPriority));
   }
 
@@ -1047,8 +1038,9 @@
   // Ensure message writer (and it's resources, e.g. forwarding tables) are
   // cleaned up before handling interrupts.
   {
-    PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false, msg,
-                                      port.Id(), Message::kOOBPriority));
+    PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
+                                      /* same_group */ false, msg, port.Id(),
+                                      Message::kOOBPriority));
   }
 
   // Drain interrupts before running so any IMMEDIATE operations on the current
diff --git a/runtime/lib/vmservice.cc b/runtime/lib/vmservice.cc
index 51ff5ea..f3dba9a 100644
--- a/runtime/lib/vmservice.cc
+++ b/runtime/lib/vmservice.cc
@@ -62,7 +62,7 @@
   // Serialize message.
   // TODO(turnidge): Throw an exception when the return value is false?
   bool result = PortMap::PostMessage(WriteMessage(
-      /* can_send_any_object */ false, message, sp.Id(),
+      /* can_send_any_object */ false, /* same_group */ false, message, sp.Id(),
       Message::kOOBPriority));
   return Bool::Get(result).ptr();
 #else
diff --git a/runtime/vm/benchmark_test.cc b/runtime/vm/benchmark_test.cc
index 2b5683b..2fe4811 100644
--- a/runtime/vm/benchmark_test.cc
+++ b/runtime/vm/benchmark_test.cc
@@ -519,8 +519,8 @@
   for (intptr_t i = 0; i < kLoopCount; i++) {
     StackZone zone(thread);
     std::unique_ptr<Message> message =
-        WriteMessage(/* can_send_any_object */ true, null_object, ILLEGAL_PORT,
-                     Message::kNormalPriority);
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                     null_object, ILLEGAL_PORT, Message::kNormalPriority);
 
     // Read object back from the snapshot.
     ReadMessage(thread, message.get());
@@ -541,8 +541,8 @@
   for (intptr_t i = 0; i < kLoopCount; i++) {
     StackZone zone(thread);
     std::unique_ptr<Message> message =
-        WriteMessage(/* can_send_any_object */ true, smi_object, ILLEGAL_PORT,
-                     Message::kNormalPriority);
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                     smi_object, ILLEGAL_PORT, Message::kNormalPriority);
 
     // Read object back from the snapshot.
     ReadMessage(thread, message.get());
@@ -565,8 +565,8 @@
   for (intptr_t i = 0; i < kLoopCount; i++) {
     StackZone zone(thread);
     std::unique_ptr<Message> message = WriteMessage(
-        /* can_send_any_object */ true, array_object, ILLEGAL_PORT,
-        Message::kNormalPriority);
+        /* can_send_any_object */ true, /* same_group */ false, array_object,
+        ILLEGAL_PORT, Message::kNormalPriority);
 
     // Read object back from the snapshot.
     ReadMessage(thread, message.get());
@@ -598,8 +598,8 @@
   for (intptr_t i = 0; i < kLoopCount; i++) {
     StackZone zone(thread);
     std::unique_ptr<Message> message =
-        WriteMessage(/* can_send_any_object */ true, map, ILLEGAL_PORT,
-                     Message::kNormalPriority);
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                     map, ILLEGAL_PORT, Message::kNormalPriority);
 
     // Read object back from the snapshot.
     ReadMessage(thread, message.get());
diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc
index dcfdb13..8acd172 100644
--- a/runtime/vm/dart_api_impl.cc
+++ b/runtime/vm/dart_api_impl.cc
@@ -2164,8 +2164,8 @@
 
   const Object& object = Object::Handle(Z, Api::UnwrapHandle(handle));
   return PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
-                                           object, port_id,
-                                           Message::kNormalPriority));
+                                           /* same_group */ false, object,
+                                           port_id, Message::kNormalPriority));
 }
 
 DART_EXPORT Dart_Handle Dart_NewSendPort(Dart_Port port_id) {
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index 458cc40..35236ef 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -124,8 +124,8 @@
 
 static std::unique_ptr<Message> SerializeMessage(Dart_Port dest_port,
                                                  const Instance& obj) {
-  return WriteMessage(/* can_send_any_object */ false, obj, dest_port,
-                      Message::kNormalPriority);
+  return WriteMessage(/* can_send_any_object */ false, /* same_group */ false,
+                      obj, dest_port, Message::kNormalPriority);
 }
 
 static std::unique_ptr<Message> SerializeMessage(Zone* zone,
@@ -1007,8 +1007,9 @@
   element = Capability::New(capability);
   msg.SetAt(2, element);
 
-  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false, msg,
-                                    main_port(), Message::kOOBPriority));
+  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
+                                    /* same_group */ false, msg, main_port(),
+                                    Message::kOOBPriority));
 }
 
 void IsolateGroup::set_object_store(ObjectStore* object_store) {
@@ -1327,38 +1328,7 @@
   }
 
   // Parse the message.
-  Object& msg_obj = Object::Handle(zone);
-  if (message->IsPersistentHandle()) {
-    // msg_array = [
-    //     <message>,
-    //     <collection-lib-objects-to-rehash>,
-    //     <core-lib-objects-to-rehash>,
-    // ]
-    const auto& msg_array = Array::Handle(
-        zone, Array::RawCast(message->persistent_handle()->ptr()));
-    ASSERT(msg_array.Length() == 3);
-    msg_obj = msg_array.At(0);
-    if (msg_array.At(1) != Object::null()) {
-      const auto& objects_to_rehash = Object::Handle(zone, msg_array.At(1));
-      auto& result = Object::Handle(zone);
-      result = DartLibraryCalls::RehashObjectsInDartCollection(
-          thread, objects_to_rehash);
-      if (result.ptr() != Object::null()) {
-        msg_obj = result.ptr();
-      }
-    }
-    if (msg_array.At(2) != Object::null()) {
-      const auto& objects_to_rehash = Object::Handle(zone, msg_array.At(2));
-      auto& result = Object::Handle(zone);
-      result =
-          DartLibraryCalls::RehashObjectsInDartCore(thread, objects_to_rehash);
-      if (result.ptr() != Object::null()) {
-        msg_obj = result.ptr();
-      }
-    }
-  } else {
-    msg_obj = ReadMessage(thread, message.get());
-  }
+  Object& msg_obj = Object::Handle(zone, ReadMessage(thread, message.get()));
   if (msg_obj.IsError()) {
     // An error occurred while reading the message.
     return ProcessUnhandledException(Error::Cast(msg_obj));
@@ -3306,8 +3276,8 @@
     element = Smi::New(Isolate::kBeforeNextEventAction);
     msg.SetAt(2, element);
     std::unique_ptr<Message> message = WriteMessage(
-        /* can_send_any_object */ false, msg, main_port(),
-        Message::kOOBPriority);
+        /* can_send_any_object */ false, /* same_group */ false, msg,
+        main_port(), Message::kOOBPriority);
     bool posted = PortMap::PostMessage(std::move(message));
     ASSERT(posted);
   }
diff --git a/runtime/vm/message_snapshot.cc b/runtime/vm/message_snapshot.cc
index c3ab4ec..e65fc1c 100644
--- a/runtime/vm/message_snapshot.cc
+++ b/runtime/vm/message_snapshot.cc
@@ -19,6 +19,7 @@
 #include "vm/heap/weak_table.h"
 #include "vm/longjump.h"
 #include "vm/object.h"
+#include "vm/object_graph_copy.h"
 #include "vm/object_store.h"
 #include "vm/symbols.h"
 #include "vm/type_testing_stubs.h"
@@ -3601,11 +3602,18 @@
 }
 
 std::unique_ptr<Message> WriteMessage(bool can_send_any_object,
+                                      bool same_group,
                                       const Object& obj,
                                       Dart_Port dest_port,
                                       Message::Priority priority) {
   if (ApiObjectConverter::CanConvert(obj.ptr())) {
     return Message::New(dest_port, obj.ptr(), priority);
+  } else if (same_group) {
+    const Object& copy = Object::Handle(CopyMutableObjectGraph(obj));
+    auto handle =
+        IsolateGroup::Current()->api_state()->AllocatePersistentHandle();
+    handle->set_ptr(copy.ptr());
+    return std::make_unique<Message>(dest_port, handle, priority);
   }
 
   Thread* thread = Thread::Current();
@@ -3652,6 +3660,37 @@
 ObjectPtr ReadMessage(Thread* thread, Message* message) {
   if (message->IsRaw()) {
     return message->raw_obj();
+  } else if (message->IsPersistentHandle()) {
+    // msg_array = [
+    //     <message>,
+    //     <collection-lib-objects-to-rehash>,
+    //     <core-lib-objects-to-rehash>,
+    // ]
+    Zone* zone = thread->zone();
+    Object& msg_obj = Object::Handle(zone);
+    const auto& msg_array = Array::Handle(
+        zone, Array::RawCast(message->persistent_handle()->ptr()));
+    ASSERT(msg_array.Length() == 3);
+    msg_obj = msg_array.At(0);
+    if (msg_array.At(1) != Object::null()) {
+      const auto& objects_to_rehash = Object::Handle(zone, msg_array.At(1));
+      auto& result = Object::Handle(zone);
+      result = DartLibraryCalls::RehashObjectsInDartCollection(
+          thread, objects_to_rehash);
+      if (result.ptr() != Object::null()) {
+        msg_obj = result.ptr();
+      }
+    }
+    if (msg_array.At(2) != Object::null()) {
+      const auto& objects_to_rehash = Object::Handle(zone, msg_array.At(2));
+      auto& result = Object::Handle(zone);
+      result =
+          DartLibraryCalls::RehashObjectsInDartCore(thread, objects_to_rehash);
+      if (result.ptr() != Object::null()) {
+        msg_obj = result.ptr();
+      }
+    }
+    return msg_obj.ptr();
   } else {
     RELEASE_ASSERT(message->IsSnapshot());
     MessageDeserializer deserializer(thread, message);
diff --git a/runtime/vm/message_snapshot.h b/runtime/vm/message_snapshot.h
index d06640e..be7eb8a 100644
--- a/runtime/vm/message_snapshot.h
+++ b/runtime/vm/message_snapshot.h
@@ -14,6 +14,7 @@
 namespace dart {
 
 std::unique_ptr<Message> WriteMessage(bool can_send_any_object,
+                                      bool same_group,
                                       const Object& obj,
                                       Dart_Port dest_port,
                                       Message::Priority priority);
diff --git a/runtime/vm/object_graph_copy.cc b/runtime/vm/object_graph_copy.cc
index ea1edf5..76d65b6 100644
--- a/runtime/vm/object_graph_copy.cc
+++ b/runtime/vm/object_graph_copy.cc
@@ -562,7 +562,10 @@
           Class::NumNativeFieldsOf(class_table_->At(cid)) != 0;
       if (has_native_fields) {
         exception_msg_ =
-            "Illegal argument in isolate message: (object has native fields)";
+            OS::SCreate(zone_,
+                        "Illegal argument in isolate message: (object extends "
+                        "NativeWrapper - %s)",
+                        Class::Handle(class_table_->At(cid)).ToCString());
         return false;
       }
       return true;
@@ -576,17 +579,28 @@
   }
 
     switch (cid) {
+      HANDLE_ILLEGAL_CASE(FunctionType)
+      HANDLE_ILLEGAL_CASE(DynamicLibrary)
       HANDLE_ILLEGAL_CASE(MirrorReference)
+      HANDLE_ILLEGAL_CASE(Pointer)
       HANDLE_ILLEGAL_CASE(ReceivePort)
       HANDLE_ILLEGAL_CASE(StackTrace)
       HANDLE_ILLEGAL_CASE(UserTag)
-      HANDLE_ILLEGAL_CASE(DynamicLibrary)
-      HANDLE_ILLEGAL_CASE(Pointer)
+#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_ =
-              "Illegal argument in isolate message: (object is a closure)";
+          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());
diff --git a/runtime/vm/service_isolate.cc b/runtime/vm/service_isolate.cc
index d840e7b..acc453f 100644
--- a/runtime/vm/service_isolate.cc
+++ b/runtime/vm/service_isolate.cc
@@ -92,8 +92,9 @@
       sp, VM_SERVICE_SERVER_INFO_MESSAGE_ID, false /* ignored */,
       Bool::Handle() /* ignored */));
   ASSERT(!message.IsNull());
-  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false, message,
-                                    port_, Message::kNormalPriority));
+  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
+                                    /* same_group */ false, message, port_,
+                                    Message::kNormalPriority));
 }
 
 void ServiceIsolate::ControlWebServer(const SendPort& sp,
@@ -102,8 +103,9 @@
   const Array& message = Array::Handle(MakeServerControlMessage(
       sp, VM_SERVICE_WEB_SERVER_CONTROL_MESSAGE_ID, enable, silenceOutput));
   ASSERT(!message.IsNull());
-  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false, message,
-                                    port_, Message::kNormalPriority));
+  PortMap::PostMessage(WriteMessage(/* can_send_any_object */ false,
+                                    /* same_group */ false, message, port_,
+                                    Message::kNormalPriority));
 }
 
 void ServiceIsolate::SetServerAddress(const char* address) {
@@ -228,7 +230,8 @@
                  name.ToCString(), Dart_GetMainPortId());
   }
   return PortMap::PostMessage(WriteMessage(
-      /* can_send_any_object */ false, list, port_, Message::kNormalPriority));
+      /* can_send_any_object */ false, /* same_group */ false, list, port_,
+      Message::kNormalPriority));
 }
 
 bool ServiceIsolate::SendIsolateShutdownMessage() {
@@ -253,7 +256,8 @@
                  name.ToCString(), Dart_GetMainPortId());
   }
   return PortMap::PostMessage(WriteMessage(
-      /* can_send_any_object */ false, list, port_, Message::kNormalPriority));
+      /* can_send_any_object */ false, /* same_group */ false, list, port_,
+      Message::kNormalPriority));
 }
 
 void ServiceIsolate::SendServiceExitMessage() {
diff --git a/runtime/vm/snapshot_test.cc b/runtime/vm/snapshot_test.cc
index 9c488ee..d42c2da 100644
--- a/runtime/vm/snapshot_test.cc
+++ b/runtime/vm/snapshot_test.cc
@@ -129,8 +129,8 @@
   // Write snapshot with object content.
   const Object& null_object = Object::Handle();
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, null_object, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   null_object, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   const Object& serialized_object =
@@ -151,8 +151,8 @@
   // Write snapshot with object content.
   const Smi& smi = Smi::Handle(Smi::New(124));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, smi, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false, smi,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   const Object& serialized_object =
@@ -174,8 +174,8 @@
   // Write snapshot with object content.
   const Smi& smi = Smi::Handle(Smi::New(-1));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, smi, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false, smi,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   const Object& serialized_object =
@@ -194,8 +194,8 @@
 Dart_CObject* SerializeAndDeserializeMint(Zone* zone, const Mint& mint) {
   // Write snapshot with object content.
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, mint, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false, mint,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   {
     // Switch to a regular zone, where VM handle allocation is allowed.
@@ -265,8 +265,8 @@
   // Write snapshot with object content.
   const Double& dbl = Double::Handle(Double::New(101.29));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, dbl, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false, dbl,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   const Object& serialized_object =
@@ -288,7 +288,7 @@
   // Write snapshot with true object.
   const Bool& bl = Bool::True();
   std::unique_ptr<Message> message = WriteMessage(
-      /* can_send_any_object */ true, bl, ILLEGAL_PORT,
+      /* can_send_any_object */ true, /* same_group */ false, bl, ILLEGAL_PORT,
       Message::kNormalPriority);
 
   // Read object back from the snapshot.
@@ -313,7 +313,7 @@
   // Write snapshot with false object.
   const Bool& bl = Bool::False();
   std::unique_ptr<Message> message = WriteMessage(
-      /* can_send_any_object */ true, bl, ILLEGAL_PORT,
+      /* can_send_any_object */ true, /* same_group */ false, bl, ILLEGAL_PORT,
       Message::kNormalPriority);
 
   // Read object back from the snapshot.
@@ -334,8 +334,8 @@
   // Write snapshot with object content.
   const Capability& capability = Capability::Handle(Capability::New(12345));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, capability, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   capability, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   Capability& obj = Capability::Handle();
@@ -357,8 +357,8 @@
   {                                                                            \
     const Object& before = Object::Handle(object);                             \
     std::unique_ptr<Message> message =                                         \
-        WriteMessage(/* can_send_any_object */ true, before, ILLEGAL_PORT,     \
-                     Message::kNormalPriority);                                \
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,   \
+                     before, ILLEGAL_PORT, Message::kNormalPriority);          \
     const Object& after = Object::Handle(ReadMessage(thread, message.get()));  \
     EXPECT(before.ptr() == after.ptr());                                       \
   }
@@ -384,8 +384,8 @@
   // Write snapshot with object content.
   String& str = String::Handle(String::New(cstr));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, str, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false, str,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   String& serialized_str = String::Handle();
@@ -425,8 +425,8 @@
     array.SetAt(i, smi);
   }
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, array, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   array, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   Array& serialized_array = Array::Handle();
@@ -458,8 +458,8 @@
     array.SetAt(i, smi);
   }
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, array, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   array, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   Array& serialized_array = Array::Handle();
@@ -537,8 +537,8 @@
   const int kArrayLength = 0;
   Array& array = Array::Handle(Array::New(kArrayLength));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, array, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   array, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   Array& serialized_array = Array::Handle();
@@ -563,8 +563,8 @@
     typed_data.SetUint8(i, i);
   }
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, typed_data, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   typed_data, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   TypedData& serialized_typed_data = TypedData::Handle();
@@ -593,8 +593,8 @@
       array.Set##darttype((i * scale), i);                                     \
     }                                                                          \
     std::unique_ptr<Message> message =                                         \
-        WriteMessage(/* can_send_any_object */ true, array, ILLEGAL_PORT,      \
-                     Message::kNormalPriority);                                \
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,   \
+                     array, ILLEGAL_PORT, Message::kNormalPriority);           \
     TypedData& serialized_array = TypedData::Handle();                         \
     serialized_array ^= ReadMessage(thread, message.get());                    \
     for (int i = 0; i < kArrayLength; i++) {                                   \
@@ -613,8 +613,8 @@
                                reinterpret_cast<uint8_t*>(data), length));     \
     intptr_t scale = array.ElementSizeInBytes();                               \
     std::unique_ptr<Message> message =                                         \
-        WriteMessage(/* can_send_any_object */ true, array, ILLEGAL_PORT,      \
-                     Message::kNormalPriority);                                \
+        WriteMessage(/* can_send_any_object */ true, /* same_group */ false,   \
+                     array, ILLEGAL_PORT, Message::kNormalPriority);           \
     ExternalTypedData& serialized_array = ExternalTypedData::Handle();         \
     serialized_array ^= ReadMessage(thread, message.get());                    \
     for (int i = 0; i < length; i++) {                                         \
@@ -655,8 +655,8 @@
   TypedData& typed_data = TypedData::Handle(
       TypedData::New(kTypedDataUint8ArrayCid, kTypedDataLength));
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ true, typed_data, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ true, /* same_group */ false,
+                   typed_data, ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot.
   TypedData& serialized_typed_data = TypedData::Handle();
@@ -772,8 +772,8 @@
   Object& obj = Object::Handle(Api::UnwrapHandle(result));
 
   // Serialize the object into a message.
-  return WriteMessage(/* can_send_any_object */ false, obj, ILLEGAL_PORT,
-                      Message::kNormalPriority);
+  return WriteMessage(/* can_send_any_object */ false, /* same_group */ false,
+                      obj, ILLEGAL_PORT, Message::kNormalPriority);
 }
 
 static void CheckString(Dart_Handle dart_string, const char* expected) {
@@ -781,8 +781,8 @@
   String& str = String::Handle();
   str ^= Api::UnwrapHandle(dart_string);
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ false, str, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ false, /* same_group */ false, str,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot into a C structure.
   ApiNativeScope scope;
@@ -798,8 +798,8 @@
   String& str = String::Handle();
   str ^= Api::UnwrapHandle(dart_string);
   std::unique_ptr<Message> message =
-      WriteMessage(/* can_send_any_object */ false, str, ILLEGAL_PORT,
-                   Message::kNormalPriority);
+      WriteMessage(/* can_send_any_object */ false, /* same_group */ false, str,
+                   ILLEGAL_PORT, Message::kNormalPriority);
 
   // Read object back from the snapshot into a C structure.
   ApiNativeScope scope;
@@ -902,8 +902,8 @@
       Smi& smi = Smi::Handle();
       smi ^= Api::UnwrapHandle(smi_result);
       std::unique_ptr<Message> message =
-          WriteMessage(/* can_send_any_object */ false, smi, ILLEGAL_PORT,
-                       Message::kNormalPriority);
+          WriteMessage(/* can_send_any_object */ false, /* same_group */ false,
+                       smi, ILLEGAL_PORT, Message::kNormalPriority);
 
       // Read object back from the snapshot into a C structure.
       ApiNativeScope scope;
diff --git a/runtime/vm/virtual_memory.h b/runtime/vm/virtual_memory.h
index c674d1c..dc9b2d7 100644
--- a/runtime/vm/virtual_memory.h
+++ b/runtime/vm/virtual_memory.h
@@ -76,13 +76,6 @@
 
   static VirtualMemory* ForImagePage(void* pointer, uword size);
 
-  void release() {
-    // Make sure no pages would be leaked.
-    const uword size_ = size();
-    ASSERT(address() == reserved_.pointer() && size_ == reserved_.size());
-    reserved_ = MemoryRegion(nullptr, 0);
-  }
-
  private:
   static intptr_t CalculatePageSize();
 
diff --git a/runtime/vm/virtual_memory_fuchsia.cc b/runtime/vm/virtual_memory_fuchsia.cc
index 7f7347d..3eb51e8 100644
--- a/runtime/vm/virtual_memory_fuchsia.cc
+++ b/runtime/vm/virtual_memory_fuchsia.cc
@@ -7,8 +7,6 @@
 
 #include "vm/virtual_memory.h"
 
-#include <sys/mman.h>
-#include <unistd.h>
 #include <zircon/process.h>
 #include <zircon/status.h>
 #include <zircon/syscalls.h>
@@ -160,6 +158,7 @@
     if (status != ZX_OK) {
       LOG_ERR("zx_vmo_replace_as_executable() failed: %s\n",
               zx_status_get_string(status));
+      zx_handle_close(vmo);
       return NULL;
     }
   }
@@ -173,6 +172,7 @@
   if (status != ZX_OK) {
     LOG_ERR("zx_vmar_map(%u, 0x%lx, 0x%lx) failed: %s\n", region_options, base,
             size, zx_status_get_string(status));
+    zx_handle_close(vmo);
     return NULL;
   }
   void* region_ptr = reinterpret_cast<void*>(base);
diff --git a/runtime/vm/virtual_memory_posix.cc b/runtime/vm/virtual_memory_posix.cc
index 14ff79d..2291d5e 100644
--- a/runtime/vm/virtual_memory_posix.cc
+++ b/runtime/vm/virtual_memory_posix.cc
@@ -80,17 +80,16 @@
   page_size_ = CalculatePageSize();
 
 #if defined(DART_COMPRESSED_POINTERS)
+  ASSERT(compressed_heap_ == nullptr);
+  compressed_heap_ = Reserve(kCompressedHeapSize, kCompressedHeapAlignment);
   if (compressed_heap_ == nullptr) {
-    compressed_heap_ = Reserve(kCompressedHeapSize, kCompressedHeapAlignment);
-    if (compressed_heap_ == nullptr) {
-      int error = errno;
-      const int kBufferSize = 1024;
-      char error_buf[kBufferSize];
-      FATAL("Failed to reserve region for compressed heap: %d (%s)", error,
-            Utils::StrError(error, error_buf, kBufferSize));
-    }
-    VirtualMemoryCompressedHeap::Init(compressed_heap_->address());
+    int error = errno;
+    const int kBufferSize = 1024;
+    char error_buf[kBufferSize];
+    FATAL("Failed to reserve region for compressed heap: %d (%s)", error,
+          Utils::StrError(error, error_buf, kBufferSize));
   }
+  VirtualMemoryCompressedHeap::Init(compressed_heap_->address());
 #endif  // defined(DART_COMPRESSED_POINTERS)
 
 #if defined(DUAL_MAPPING_SUPPORTED)
diff --git a/runtime/vm/virtual_memory_win.cc b/runtime/vm/virtual_memory_win.cc
index 2e380c0..79eee52 100644
--- a/runtime/vm/virtual_memory_win.cc
+++ b/runtime/vm/virtual_memory_win.cc
@@ -56,14 +56,13 @@
   page_size_ = CalculatePageSize();
 
 #if defined(DART_COMPRESSED_POINTERS)
+  ASSERT(compressed_heap_ == nullptr);
+  compressed_heap_ = Reserve(kCompressedHeapSize, kCompressedHeapAlignment);
   if (compressed_heap_ == nullptr) {
-    compressed_heap_ = Reserve(kCompressedHeapSize, kCompressedHeapAlignment);
-    if (compressed_heap_ == nullptr) {
-      int error = GetLastError();
-      FATAL("Failed to reserve region for compressed heap: %d", error);
-    }
-    VirtualMemoryCompressedHeap::Init(compressed_heap_->address());
+    int error = GetLastError();
+    FATAL("Failed to reserve region for compressed heap: %d", error);
   }
+  VirtualMemoryCompressedHeap::Init(compressed_heap_->address());
 #endif  // defined(DART_COMPRESSED_POINTERS)
 }
 
diff --git a/tests/lib/isolate/fast_copy_during_initial_message_test.dart b/tests/lib/isolate/fast_copy_during_initial_message_test.dart
new file mode 100644
index 0000000..375de09
--- /dev/null
+++ b/tests/lib/isolate/fast_copy_during_initial_message_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2021, 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.
+
+// VMOptions=--enable-isolate-groups --enable-fast-object-copy
+
+import "dart:isolate";
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+
+echo(message) {
+  var string = message[0] as String;
+  var replyPort = message[1] as SendPort;
+  replyPort.send(string);
+}
+
+main() {
+  asyncStart();
+
+  // This string is constructed at runtime, so it is not const and won't be
+  // identical because of canonicalization. It will only be identical if it is
+  // sent by pointer.
+  var sentString = "xyz" * 2;
+
+  var port;
+  port = new RawReceivePort((message) {
+    var receivedString = message as String;
+
+    Expect.identical(sentString, receivedString);
+
+    port.close();
+    asyncEnd();
+  });
+
+  Isolate.spawn(echo, [sentString, port.sendPort]);
+}
diff --git a/tests/lib_2/isolate/fast_copy_during_initial_message_test.dart b/tests/lib_2/isolate/fast_copy_during_initial_message_test.dart
new file mode 100644
index 0000000..15c3978
--- /dev/null
+++ b/tests/lib_2/isolate/fast_copy_during_initial_message_test.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2021, 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.
+
+// @dart = 2.9
+
+// VMOptions=--enable-isolate-groups --enable-fast-object-copy
+
+import "dart:isolate";
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+
+echo(message) {
+  var string = message[0] as String;
+  var replyPort = message[1] as SendPort;
+  replyPort.send(string);
+}
+
+main() {
+  asyncStart();
+
+  // This string is constructed at runtime, so it is not const and won't be
+  // identical because of canonicalization. It will only be identical if it is
+  // sent by pointer.
+  var sentString = "xyz" * 2;
+
+  var port;
+  port = new RawReceivePort((message) {
+    var receivedString = message as String;
+
+    Expect.identical(sentString, receivedString);
+
+    port.close();
+    asyncEnd();
+  });
+
+  Isolate.spawn(echo, [sentString, port.sendPort]);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 5d291c9..3cfedd8 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 10
+PRERELEASE 11
 PRERELEASE_PATCH 0
\ No newline at end of file