Version 2.12.0-50.0.dev

Merge commit 'bb6714c051f177e0c8d1d33ad85de562d76abf62' into 'dev'
diff --git a/BUILD.gn b/BUILD.gn
index f1332bf..3962d40 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -68,6 +68,12 @@
   }
 }
 
+# A separate target and not included in group("runtime"). This way the target\
+# "runtime" does not get many executables extra as build output.
+group("run_ffi_unit_tests") {
+  deps = [ "runtime/bin/ffi_unit_test:run_ffi_unit_tests" ]
+}
+
 group("runtime_kernel") {
   if (targetting_fuchsia) {
     # Fuchsia has run_vm_tests marked testonly.
diff --git a/DEPS b/DEPS
index f53c12e..4dc0fef 100644
--- a/DEPS
+++ b/DEPS
@@ -102,7 +102,7 @@
 
   "chromedriver_tag": "83.0.4103.39",
   "dartdoc_rev" : "6935dcd8f2d78cf1797e6365b4b71505e6579659",
-  "ffi_rev": "a90bd424116fb6f416337db67425171f2dc4c98f",
+  "ffi_rev": "a5d4232cd38562c75a3ed847baa340e399538028",
   "fixnum_rev": "16d3890c6dc82ca629659da1934e412292508bba",
   "glob_rev": "e9f4e6b7ae8abe5071461cf8f47191bb19cf7ef6",
   "html_rev": "22f17e97fedeacaa1e945cf84d8016284eed33a6",
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index 4ee4034..98e3a03 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -17,6 +17,9 @@
 import 'package:analysis_server/src/services/completion/completion_performance.dart';
 import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
 import 'package:analysis_server/src/services/completion/token_details/token_detail_builder.dart';
+import 'package:analysis_server/src/services/completion/yaml/analysis_options_generator.dart';
+import 'package:analysis_server/src/services/completion/yaml/fix_data_generator.dart';
+import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
 import 'package:analyzer/dart/analysis/results.dart';
 import 'package:analyzer/dart/analysis/session.dart';
 import 'package:analyzer/exception/exception.dart';
@@ -145,6 +148,25 @@
         request.replacementOffset, request.replacementLength, suggestions);
   }
 
+  /// Return the suggestions that should be presented in the YAML [file] at the
+  /// given [offset].
+  List<CompletionSuggestion> computeYamlSuggestions(String file, int offset) {
+    var provider = server.resourceProvider;
+    if (AnalysisEngine.isAnalysisOptionsFileName(file)) {
+      var generator = AnalysisOptionsGenerator(provider);
+      return generator.getSuggestions(file, offset);
+    }
+    var fileName = provider.pathContext.basename(file);
+    if (fileName == AnalysisEngine.PUBSPEC_YAML_FILE) {
+      var generator = PubspecGenerator(provider);
+      return generator.getSuggestions(file, offset);
+    } else if (fileName == AnalysisEngine.FIX_DATA_FILE) {
+      var generator = FixDataGenerator(provider);
+      return generator.getSuggestions(file, offset);
+    }
+    return <CompletionSuggestion>[];
+  }
+
   /// Process a `completion.getSuggestionDetails` request.
   void getSuggestionDetails(Request request) async {
     var params = CompletionGetSuggestionDetailsParams.fromRequest(request);
@@ -297,6 +319,24 @@
       if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
         return;
       }
+      if (file.endsWith('.yaml')) {
+        // Return the response without results.
+        var completionId = (_nextCompletionId++).toString();
+        server.sendResponse(CompletionGetSuggestionsResult(completionId)
+            .toResponse(request.id));
+        // Send a notification with results.
+        sendCompletionNotification(
+          completionId,
+          0, // replacementOffset
+          0, // replacementLength,
+          computeYamlSuggestions(file, offset),
+          null,
+          null,
+          null,
+          null,
+        );
+        return;
+      }
 
       var resolvedUnit = await server.getResolvedUnit(file);
       server.requestStatistics?.addItemTimeNow(request, 'resolvedUnit');
diff --git a/pkg/analysis_server/lib/src/server/driver.dart b/pkg/analysis_server/lib/src/server/driver.dart
index 734177d..db988fa 100644
--- a/pkg/analysis_server/lib/src/server/driver.dart
+++ b/pkg/analysis_server/lib/src/server/driver.dart
@@ -38,176 +38,12 @@
 import 'package:telemetry/crash_reporting.dart';
 import 'package:telemetry/telemetry.dart' as telemetry;
 
-/// Commandline argument parser. (Copied from analyzer/lib/options.dart)
-/// TODO(pquitslund): replaces with a simple [ArgParser] instance
-/// when the args package supports ignoring unrecognized
-/// options/flags (https://github.com/dart-lang/args/issues/9).
-/// TODO(devoncarew): Consider removing the ability to support unrecognized
-/// flags for the analysis server.
-class CommandLineParser {
-  final List<String> _knownFlags;
-  final ArgParser _parser;
-
-  /// Creates a new command line parser
-  CommandLineParser()
-      : _knownFlags = <String>[],
-        _parser = ArgParser(allowTrailingOptions: true);
-
-  ArgParser get parser => _parser;
-
-  /// Defines a flag.
-  /// See [ArgParser.addFlag()].
-  void addFlag(String name,
-      {String abbr,
-      String help,
-      bool defaultsTo = false,
-      bool negatable = true,
-      void Function(bool value) callback,
-      bool hide = false}) {
-    _knownFlags.add(name);
-    _parser.addFlag(name,
-        abbr: abbr,
-        help: help,
-        defaultsTo: defaultsTo,
-        negatable: negatable,
-        callback: callback,
-        hide: hide);
-  }
-
-  /// Defines an option that takes multiple values.
-  /// See [ArgParser.addMultiOption].
-  void addMultiOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
-      Iterable<String> defaultsTo,
-      void Function(List<String> values) callback,
-      bool splitCommas = true,
-      bool hide = false}) {
-    _knownFlags.add(name);
-    _parser.addMultiOption(name,
-        abbr: abbr,
-        help: help,
-        valueHelp: valueHelp,
-        allowed: allowed,
-        allowedHelp: allowedHelp,
-        defaultsTo: defaultsTo,
-        callback: callback,
-        splitCommas: splitCommas,
-        hide: hide);
-  }
-
-  /// Defines a value-taking option.
-  /// See [ArgParser.addOption()].
-  void addOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      List<String> allowed,
-      Map<String, String> allowedHelp,
-      String defaultsTo,
-      void Function(Object) callback,
-      bool hide = false}) {
-    _knownFlags.add(name);
-    _parser.addOption(name,
-        abbr: abbr,
-        help: help,
-        valueHelp: valueHelp,
-        allowed: allowed,
-        allowedHelp: allowedHelp,
-        defaultsTo: defaultsTo,
-        callback: callback,
-        hide: hide);
-  }
-
-  /// Generates a string displaying usage information for the defined options.
-  /// See [ArgParser.usage].
-  String getUsage() => _parser.usage;
-
-  /// Parses [args], a list of command-line arguments, matches them against the
-  /// flags and options defined by this parser, and returns the result. The
-  /// values of any defined variables are captured in the given map.
-  /// See [ArgParser].
-  ArgResults parse(List<String> args, Map<String, String> definedVariables) =>
-      _parser.parse(
-          _filterUnknowns(parseDefinedVariables(args, definedVariables)));
-
-  List<String> parseDefinedVariables(
-      List<String> args, Map<String, String> definedVariables) {
-    var count = args.length;
-    var remainingArgs = <String>[];
-    for (var i = 0; i < count; i++) {
-      var arg = args[i];
-      if (arg == '--') {
-        while (i < count) {
-          remainingArgs.add(args[i++]);
-        }
-      } else if (arg.startsWith('-D')) {
-        definedVariables[arg.substring(2)] = args[++i];
-      } else {
-        remainingArgs.add(arg);
-      }
-    }
-    return remainingArgs;
-  }
-
-  List<String> _filterUnknowns(List<String> args) {
-    // TODO(devoncarew): Consider dropping support for the
-    // --ignore-unrecognized-flags option.
-
-    // Only filter args if the ignore flag is specified.
-    if (args.contains('--ignore-unrecognized-flags')) {
-      // Filter all unrecognized flags and options.
-      var filtered = <String>[];
-      for (var i = 0; i < args.length; ++i) {
-        var arg = args[i];
-        if (arg.startsWith('--') && arg.length > 2) {
-          var option = arg.substring(2);
-          // remove any leading 'no-'
-          if (option.startsWith('no-')) {
-            option = option.substring(3);
-          }
-          // strip the last '=value'
-          var equalsOffset = option.lastIndexOf('=');
-          if (equalsOffset != -1) {
-            option = option.substring(0, equalsOffset);
-          }
-          // check the option
-          if (!_knownFlags.contains(option)) {
-            //"eat" params by advancing to the next flag/option
-            i = _getNextFlagIndex(args, i);
-          } else {
-            filtered.add(arg);
-          }
-        } else {
-          filtered.add(arg);
-        }
-      }
-
-      return filtered;
-    } else {
-      return args;
-    }
-  }
-
-  int _getNextFlagIndex(List<String> args, int i) {
-    for (; i < args.length; ++i) {
-      if (args[i].startsWith('--')) {
-        return i;
-      }
-    }
-    return i;
-  }
-}
-
 /// The [Driver] class represents a single running instance of the analysis
 /// server application.  It is responsible for parsing command line options
 /// and starting the HTTP and/or stdio servers.
 class Driver implements ServerStarter {
   /// The name of the application that is used to start a server.
-  static const BINARY_NAME = 'server';
+  static const BINARY_NAME = 'analysis_server';
 
   /// The name of the option used to set the identifier for the client.
   static const String CLIENT_ID = 'client-id';
@@ -286,7 +122,7 @@
   @override
   void start(List<String> arguments, [SendPort sendPort]) {
     var parser = _createArgParser();
-    var results = parser.parse(arguments, <String, String>{});
+    var results = parser.parse(arguments);
 
     var analysisServerOptions = AnalysisServerOptions();
     analysisServerOptions.newAnalysisDriverLog =
@@ -370,7 +206,7 @@
     }
 
     if (results[HELP_OPTION]) {
-      _printUsage(parser.parser, analytics, fromHelp: true);
+      _printUsage(parser, analytics, fromHelp: true);
       return null;
     }
 
@@ -418,7 +254,7 @@
       } on FormatException {
         print('Invalid port number: ${results[PORT_OPTION]}');
         print('');
-        _printUsage(parser.parser, analytics);
+        _printUsage(parser, analytics);
         exitCode = 1;
         return null;
       }
@@ -450,7 +286,7 @@
   void startAnalysisServer(
     ArgResults results,
     AnalysisServerOptions analysisServerOptions,
-    CommandLineParser parser,
+    ArgParser parser,
     DartSdkManager dartSdkManager,
     CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
     InstrumentationService instrumentationService,
@@ -639,8 +475,8 @@
   }
 
   /// Create and return the parser used to parse the command-line arguments.
-  CommandLineParser _createArgParser() {
-    var parser = CommandLineParser();
+  ArgParser _createArgParser() {
+    var parser = ArgParser();
     parser.addFlag(HELP_OPTION,
         abbr: 'h', negatable: false, help: 'Print this usage information.');
     parser.addOption(CLIENT_ID,
@@ -716,6 +552,8 @@
     parser.addFlag('enable-instrumentation', hide: true);
     // Removed 11/12/2020.
     parser.addOption('file-read-mode', hide: true);
+    // Removed 11/12/2020.
+    parser.addFlag('ignore-unrecognized-flags', hide: true);
     // Removed 11/8/2020.
     parser.addFlag('preview-dart-2', hide: true);
     // Removed 11/12/2020.
@@ -752,8 +590,11 @@
   }
 
   /// Print information about how to use the server.
-  void _printUsage(ArgParser parser, telemetry.Analytics analytics,
-      {bool fromHelp = false}) {
+  void _printUsage(
+    ArgParser parser,
+    telemetry.Analytics analytics, {
+    bool fromHelp = false,
+  }) {
     print('Usage: $BINARY_NAME [flags]');
     print('');
     print('Supported flags are:');
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
new file mode 100644
index 0000000..7f9228a
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/lint/registry.dart';
+import 'package:analyzer/src/task/options.dart';
+
+/// A completion generator that can produce completion suggestions for analysis
+/// options files.
+class AnalysisOptionsGenerator extends YamlCompletionGenerator {
+  /// The producer representing the known valid structure of an analysis options
+  /// file.
+  // TODO(brianwilkerson) We need to support multiple valid formats.
+  //  For example, the lint rules can either be a list or a map, but we only
+  //  suggest list items.
+  static const MapProducer analysisOptionsProducer = MapProducer({
+    AnalyzerOptions.analyzer: MapProducer({
+      AnalyzerOptions.enableExperiment: EmptyProducer(),
+      AnalyzerOptions.errors: EmptyProducer(),
+      AnalyzerOptions.exclude: EmptyProducer(),
+      AnalyzerOptions.language: MapProducer({
+        AnalyzerOptions.strictInference: EmptyProducer(),
+        AnalyzerOptions.strictRawTypes: EmptyProducer(),
+      }),
+      AnalyzerOptions.optionalChecks: MapProducer({
+        AnalyzerOptions.chromeOsManifestChecks: EmptyProducer(),
+      }),
+      AnalyzerOptions.plugins: EmptyProducer(),
+      AnalyzerOptions.strong_mode: MapProducer({
+        AnalyzerOptions.declarationCasts: EmptyProducer(),
+        AnalyzerOptions.implicitCasts: EmptyProducer(),
+        AnalyzerOptions.implicitDynamic: EmptyProducer(),
+      }),
+    }),
+    // TODO(brianwilkerson) Create a producer to produce `package:` URIs.
+    AnalyzerOptions.include: EmptyProducer(),
+    // TODO(brianwilkerson) Create constants for 'linter' and 'rules'.
+    'linter': MapProducer({
+      'rules': ListProducer(LintRuleProducer()),
+    }),
+  });
+
+  /// Initialize a newly created suggestion generator for analysis options
+  /// files.
+  AnalysisOptionsGenerator(ResourceProvider resourceProvider)
+      : super(resourceProvider);
+
+  @override
+  Producer get topLevelProducer => analysisOptionsProducer;
+}
+
+class LintRuleProducer extends Producer {
+  /// Initialize a location whose valid values are the names of the registered
+  /// lint rules.
+  const LintRuleProducer();
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    for (var rule in Registry.ruleRegistry.rules) {
+      yield identifier(rule.name);
+    }
+  }
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
new file mode 100644
index 0000000..4fd54bf
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analyzer/file_system/file_system.dart';
+
+/// A completion generator that can produce completion suggestions for fix data
+/// files.
+class FixDataGenerator extends YamlCompletionGenerator {
+  /// The producer representing the known valid structure of a fix data file.
+  static const MapProducer fixDataProducer = MapProducer({
+    'version': EmptyProducer(),
+    'transforms': ListProducer(MapProducer({
+      'title': EmptyProducer(),
+      'date': EmptyProducer(),
+      'bulkApply': BooleanProducer(),
+      'element': MapProducer({
+        // TODO(brianwilkerson) Support suggesting uris.
+        'uris': EmptyProducer(),
+        'class': EmptyProducer(),
+        'constant': EmptyProducer(),
+        'constructor': EmptyProducer(),
+        'enum': EmptyProducer(),
+        'extension': EmptyProducer(),
+        'field': EmptyProducer(),
+        'function': EmptyProducer(),
+        'getter': EmptyProducer(),
+        'method': EmptyProducer(),
+        'mixin': EmptyProducer(),
+        'setter': EmptyProducer(),
+        'typedef': EmptyProducer(),
+        'variable': EmptyProducer(),
+        'inClass': EmptyProducer(),
+        'inEnum': EmptyProducer(),
+        'inExtension': EmptyProducer(),
+        'inMixin': EmptyProducer(),
+      }),
+      'changes': ListProducer(MapProducer({
+        // TODO(brianwilkerson) Create a way to tailor the list of additional
+        //  keys based on the kind when a kind has already been provided.
+        'kind': EnumProducer([
+          'addParameter',
+          'addTypeParameter',
+          'removeParameter',
+          'rename',
+          'renameParameter',
+        ]),
+        'index': EmptyProducer(),
+        'name': EmptyProducer(),
+        'style': EmptyProducer(),
+        'argumentValue': MapProducer({
+          'expression': EmptyProducer(),
+          'statements': EmptyProducer(),
+          // TODO(brianwilkerson) Figure out how to support 'variables'.
+          'variables': EmptyProducer(),
+        }),
+        'extends': EmptyProducer(),
+        'oldName': EmptyProducer(),
+        'newName': EmptyProducer(),
+      })),
+    })),
+  });
+
+  /// Initialize a newly created suggestion generator for fix data files.
+  FixDataGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+
+  @override
+  Producer get topLevelProducer => fixDataProducer;
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
new file mode 100644
index 0000000..834adac
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
@@ -0,0 +1,127 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analyzer/file_system/file_system.dart';
+
+/// An object that represents the location of a Boolean value.
+class BooleanProducer extends Producer {
+  /// Initialize a location whose valid values are Booleans.
+  const BooleanProducer();
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    yield identifier('true');
+    yield identifier('false');
+  }
+}
+
+/// An object that represents the location of an arbitrary value. They serve as
+/// placeholders when there are no reasonable suggestions for a given location.
+class EmptyProducer extends Producer {
+  /// Initialize a location whose valid values are arbitrary.
+  const EmptyProducer();
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    // Returns nothing.
+  }
+}
+
+/// An object that represents the location of a value from a finite set of
+/// choices.
+class EnumProducer extends Producer {
+  /// The list of valid values at this location.
+  final List<String> values;
+
+  /// Initialize a location whose valid values are in the list of [values].
+  const EnumProducer(this.values);
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    for (var value in values) {
+      yield identifier(value);
+    }
+  }
+}
+
+/// An object that represents the location of a possibly relative file path.
+class FilePathProducer extends Producer {
+  /// The resource provider used to access the content of the file in which
+  /// completion was requested.
+  final ResourceProvider provider;
+
+  /// Initialize a location whose valid values are file paths.
+  FilePathProducer(this.provider);
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    // TODO(brianwilkerson) Implement this.
+  }
+}
+
+/// An object that represents the location of an element in a list.
+class ListProducer extends Producer {
+  /// The producer used to produce suggestions for an element of the list.
+  final Producer element;
+
+  /// Initialize a location whose valid values are determined by the [element]
+  /// producer.
+  const ListProducer(this.element);
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    for (var suggestion in element.suggestions()) {
+      // TODO(brianwilkerson) Consider prepending the suggestion with a hyphen
+      //  when the current node isn't already preceded by a hyphen. The
+      //  cleanest way to do this is probably to access the [element] producer
+      //  in the place where we're choosing a producer in that situation.
+      // suggestion.completion = '- ${suggestion.completion}';
+      yield suggestion;
+    }
+  }
+}
+
+/// An object that represents the location of the keys in a map.
+class MapProducer extends Producer {
+  /// A table from the value of a key to the producer used to make suggestions
+  /// for the value following the key.
+  final Map<String, Producer> children;
+
+  /// Initialize a location whose valid values are the keys of a map as encoded
+  /// by the map of [children].
+  const MapProducer(this.children);
+
+  @override
+  Iterable<CompletionSuggestion> suggestions() sync* {
+    for (var entry in children.entries) {
+      if (entry.value is ListProducer) {
+        yield identifier('${entry.key}:');
+      } else {
+        yield identifier('${entry.key}: ');
+      }
+    }
+  }
+}
+
+/// An object that represents a specific location in the structure of the valid
+/// YAML representation and can produce completion suggestions appropriate for
+/// that location.
+abstract class Producer {
+  /// Initialize a newly created instance of this class.
+  const Producer();
+
+  /// A utility method used to create a suggestion for the [identifier].
+  CompletionSuggestion identifier(String identifier) => CompletionSuggestion(
+      CompletionSuggestionKind.IDENTIFIER,
+      1000,
+      identifier,
+      identifier.length,
+      0,
+      false,
+      false);
+
+  /// Return the completion suggestions appropriate to this location.
+  Iterable<CompletionSuggestion> suggestions();
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
new file mode 100644
index 0000000..e38bddc
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analyzer/file_system/file_system.dart';
+
+/// A completion generator that can produce completion suggestions for pubspec
+/// files.
+class PubspecGenerator extends YamlCompletionGenerator {
+  /// The producer representing the known valid structure of a pubspec file.
+  static const MapProducer pubspecProducer = MapProducer({
+    'name': EmptyProducer(),
+    'version': EmptyProducer(),
+    'description': EmptyProducer(),
+    'homepage': EmptyProducer(),
+    'repository': EmptyProducer(),
+    'issue_tracker': EmptyProducer(),
+    'documentation': EmptyProducer(),
+    'executables': EmptyProducer(),
+    'publish_to': EmptyProducer(),
+    'environment': MapProducer({
+      'flutter': EmptyProducer(),
+      'sdk': EmptyProducer(),
+    }),
+    'dependencies': EmptyProducer(),
+    'dev_dependencies': EmptyProducer(),
+    // TODO(brianwilkerson) Suggest names already listed under 'dependencies'
+    //  and 'dev_dependencies'.
+    'dependency_overrides': EmptyProducer(),
+  });
+
+  /// Initialize a newly created suggestion generator for pubspec files.
+  PubspecGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+
+  @override
+  Producer get topLevelProducer => pubspecProducer;
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
new file mode 100644
index 0000000..3d0b64c
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
@@ -0,0 +1,224 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:yaml/yaml.dart';
+
+/// A completion generator that can produce completion suggestions for files
+/// containing a YAML structure.
+abstract class YamlCompletionGenerator {
+  /// The resource provider used to access the content of the file in which
+  /// completion was requested.
+  final ResourceProvider resourceProvider;
+
+  /// Initialize a newly created generator to use the [resourceProvider] to
+  /// access the content of the file in which completion was requested.
+  YamlCompletionGenerator(this.resourceProvider);
+
+  /// Return the producer used to produce suggestions at the top-level of the
+  /// file.
+  Producer get topLevelProducer;
+
+  /// Return the completion suggestions appropriate for the given [offset] in
+  /// the file at the given [filePath].
+  List<CompletionSuggestion> getSuggestions(String filePath, int offset) {
+    var file = resourceProvider.getFile(filePath);
+    String content;
+    try {
+      content = file.readAsStringSync();
+    } on FileSystemException {
+      // If the file doesn't exist or can't be read, then there are no
+      // suggestions.
+      return const <CompletionSuggestion>[];
+    }
+    var root = _parseYaml(content);
+    if (root == null) {
+      // If the contents can't be parsed, then there are no suggestions.
+      return const <CompletionSuggestion>[];
+    }
+    var path = _pathToOffset(root, offset);
+    var completionNode = path.last;
+    if (completionNode is YamlScalar) {
+      var value = completionNode.value;
+      if (value == null) {
+        return getSuggestionsForPath(path, offset);
+      } else if (value is String && completionNode.style == ScalarStyle.PLAIN) {
+        return getSuggestionsForPath(path, offset);
+      }
+    } else {
+      return getSuggestionsForPath(path, offset);
+    }
+    // There are no completions at the given location.
+    return const <CompletionSuggestion>[];
+  }
+
+  /// Given a [path] to the node in which completions are being requested and
+  /// the offset of the cursor, return the completions appropriate at that
+  /// location.
+  List<CompletionSuggestion> getSuggestionsForPath(
+      List<YamlNode> path, int offset) {
+    var producer = _producerForPath(path);
+    if (producer == null) {
+      return const <CompletionSuggestion>[];
+    }
+    var invalidSuggestions = _siblingsOnPath(path);
+    var suggestions = <CompletionSuggestion>[];
+    for (var suggestion in producer.suggestions()) {
+      if (!invalidSuggestions.contains(suggestion.completion)) {
+        suggestions.add(suggestion);
+      }
+    }
+    return suggestions;
+  }
+
+  /// Return the result of parsing the file [content] into a YAML node.
+  YamlNode _parseYaml(String content) {
+    try {
+      return loadYamlNode(content);
+    } on YamlException {
+      // If the file can't be parsed, then fall through to return `null`.
+    }
+    return null;
+  }
+
+  /// Return a list containing the node containing the [offset] and all of the
+  /// nodes between that and the [root] node. The root node is first in the list
+  /// and the node containing the offset is the last element in the list.
+  List<YamlNode> _pathToOffset(YamlNode root, int offset) {
+    var path = <YamlNode>[];
+    var node = root;
+    while (node != null) {
+      path.add(node);
+      node = node.childContainingOffset(offset);
+    }
+    return path;
+  }
+
+  /// Return the producer that should be used to produce completion suggestions
+  /// for the last node in the node [path].
+  Producer _producerForPath(List<YamlNode> path) {
+    var producer = topLevelProducer;
+    for (var i = 0; i < path.length - 1; i++) {
+      var node = path[i];
+      if (node is YamlMap && producer is MapProducer) {
+        var key = node.keyAtValue(path[i + 1]);
+        if (key is YamlScalar) {
+          producer = (producer as MapProducer).children[key.value];
+        } else {
+          return null;
+        }
+      } else if (node is YamlList && producer is ListProducer) {
+        producer = (producer as ListProducer).element;
+      } else {
+        return producer;
+      }
+    }
+    return producer;
+  }
+
+  /// Return a list of the suggestions that should not be suggested because they
+  /// are already in the structure.
+  List<String> _siblingsOnPath(List<YamlNode> path) {
+    List<String> siblingsInList(YamlList list, YamlNode currentElement) {
+      var siblings = <String>[];
+      for (var element in list.nodes) {
+        if (element != currentElement &&
+            element is YamlScalar &&
+            element.value is String) {
+          siblings.add(element.value);
+        }
+      }
+      return siblings;
+    }
+
+    List<String> siblingsInMap(YamlMap map, YamlNode currentKey) {
+      var siblings = <String>[];
+      for (var key in map.nodes.keys) {
+        if (key != currentKey && key is YamlScalar && key.value is String) {
+          siblings.add('${key.value}: ');
+        }
+      }
+      return siblings;
+    }
+
+    var length = path.length;
+    if (length < 2) {
+      return const <String>[];
+    }
+    var node = path[length - 1];
+    if (node is YamlList) {
+      return siblingsInList(node, null);
+    } else if (node is YamlMap) {
+      return siblingsInMap(node, null);
+    }
+    var parent = path[length - 2];
+    if (parent is YamlList) {
+      return siblingsInList(parent, node);
+    } else if (parent is YamlMap) {
+      return siblingsInMap(parent, node);
+    }
+    return const <String>[];
+  }
+}
+
+extension on YamlMap {
+  /// Return the node representing the key that corresponds to the value
+  /// represented by the [value] node.
+  YamlNode keyAtValue(YamlNode value) {
+    for (var entry in nodes.entries) {
+      if (entry.value == value) {
+        return entry.key;
+      }
+    }
+    return null;
+  }
+}
+
+extension on YamlNode {
+  /// Return the child of this node that contains the given [offset], or `null`
+  /// if none of the children contains the offset.
+  YamlNode childContainingOffset(int offset) {
+    var node = this;
+    if (node is YamlList) {
+      for (var element in node.nodes) {
+        if (element.containsOffset(offset)) {
+          return element;
+        }
+      }
+      for (var element in node.nodes) {
+        if (element is YamlScalar && element.value == null) {
+          // TODO(brianwilkerson) Testing for a null value probably gets
+          //  confused when there are multiple null values.
+          return element;
+        }
+      }
+    } else if (node is YamlMap) {
+      for (var entry in node.nodes.entries) {
+        if ((entry.key as YamlNode).containsOffset(offset)) {
+          return entry.key;
+        }
+        var value = entry.value;
+        if (value.containsOffset(offset) ||
+            (value is YamlScalar && value.value == null)) {
+          // TODO(brianwilkerson) Testing for a null value probably gets
+          //  confused when there are multiple null values or when there is a
+          //  null value before the node that actually contains the offset.
+          return entry.value;
+        }
+      }
+    }
+    return null;
+  }
+
+  /// Return `true` if this node contains the given [offset].
+  bool containsOffset(int offset) {
+    // TODO(brianwilkerson) Nodes at the end of the file contain any trailing
+    //  whitespace. This needs to be accounted for, here or elsewhere.
+    var nodeOffset = span.start.offset;
+    var nodeEnd = nodeOffset + span.length;
+    return nodeOffset <= offset && offset <= nodeEnd;
+  }
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/add_async.dart b/pkg/analysis_server/lib/src/services/correction/dart/add_async.dart
index ef18db3..59739f3 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/add_async.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/add_async.dart
@@ -5,24 +5,92 @@
 import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 
 class AddAsync extends CorrectionProducer {
+  /// A flag indicating whether this producer is producing a fix in the case
+  /// where a function is missing a return at the end.
+  final bool isForMissingReturn;
+
+  /// Initialize a newly created producer.
+  AddAsync(this.isForMissingReturn);
+
   @override
   FixKind get fixKind => DartFixKind.ADD_ASYNC;
 
   @override
   Future<void> compute(ChangeBuilder builder) async {
-    var body = node.thisOrAncestorOfType<FunctionBody>();
-    if (body != null && body.keyword == null) {
-      var typeProvider = this.typeProvider;
-      await builder.addDartFileEdit(file, (builder) {
-        builder.convertFunctionFromSyncToAsync(body, typeProvider);
-      });
+    if (isForMissingReturn) {
+      var parent = node.parent;
+      DartType returnType;
+      FunctionBody body;
+      if (parent is FunctionDeclaration) {
+        returnType = parent.returnType.type;
+        body = parent.functionExpression.body;
+      } else if (parent is MethodDeclaration) {
+        returnType = parent.returnType.type;
+        body = parent.body;
+      }
+      if (body == null) {
+        return;
+      }
+      if (_isFutureVoid(returnType) && _hasNoReturns(body)) {
+        await builder.addDartFileEdit(file, (builder) {
+          builder.addSimpleInsertion(body.offset, 'async ');
+        });
+      }
+    } else {
+      var body = node.thisOrAncestorOfType<FunctionBody>();
+      if (body != null && body.keyword == null) {
+        var typeProvider = this.typeProvider;
+        await builder.addDartFileEdit(file, (builder) {
+          builder.convertFunctionFromSyncToAsync(body, typeProvider);
+        });
+      }
     }
   }
 
+  /// Return `true` if there are no return statements in the given function
+  /// [body].
+  bool _hasNoReturns(FunctionBody body) {
+    var finder = _ReturnFinder();
+    body.accept(finder);
+    return !finder.foundReturn;
+  }
+
+  /// Return `true` if the [type] is `Future<void>`.
+  bool _isFutureVoid(DartType type) {
+    if (type is InterfaceType && type.isDartAsyncFuture) {
+      return type.typeArguments[0].isVoid;
+    }
+    return false;
+  }
+
   /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
-  static AddAsync newInstance() => AddAsync();
+  static AddAsync missingReturn() => AddAsync(true);
+
+  /// Return an instance of this class. Used as a tear-off in `FixProcessor`.
+  static AddAsync newInstance() => AddAsync(false);
+}
+
+/// An AST visitor used to find return statements in function bodies.
+class _ReturnFinder extends RecursiveAstVisitor<void> {
+  /// A flag indicating whether a return statement was visited.
+  bool foundReturn = false;
+
+  /// Initialize a newly created visitor.
+  _ReturnFinder();
+
+  @override
+  void visitFunctionExpression(FunctionExpression node) {
+    // Return statements within closures aren't counted.
+  }
+
+  @override
+  void visitReturnStatement(ReturnStatement node) {
+    foundReturn = true;
+  }
 }
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_to_set_literal.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_set_literal.dart
index f374346..f6a8db3 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/convert_to_set_literal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_to_set_literal.dart
@@ -178,6 +178,11 @@
           parent2.type?.type?.element == typeProvider.setElement) {
         return true;
       }
+    } else if (parent.parent is InvocationExpression) {
+      var parameterElement = creation.staticParameterElement;
+      if (parameterElement?.type?.element == typeProvider.setElement) {
+        return true;
+      }
     }
     return _hasUnambiguousElement(creation);
   }
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index 881714d..5995aaf 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -646,6 +646,9 @@
     CompileTimeErrorCode.AWAIT_IN_WRONG_CONTEXT: [
       AddAsync.newInstance,
     ],
+    CompileTimeErrorCode.BODY_MIGHT_COMPLETE_NORMALLY: [
+      AddAsync.missingReturn,
+    ],
     CompileTimeErrorCode.CAST_TO_NON_TYPE: [
       ChangeTo.classOrMixin,
       CreateClass.newInstance,
@@ -959,6 +962,9 @@
     HintCode.MISSING_REQUIRED_PARAM_WITH_DETAILS: [
       AddMissingRequiredArgument.newInstance,
     ],
+    HintCode.MISSING_RETURN: [
+      AddAsync.missingReturn,
+    ],
     HintCode.NULLABLE_TYPE_IN_CATCH_CLAUSE: [
       RemoveQuestionMark.newInstance,
     ],
diff --git a/pkg/analysis_server/test/src/services/completion/test_all.dart b/pkg/analysis_server/test/src/services/completion/test_all.dart
index 6055c7b..19d31b3 100644
--- a/pkg/analysis_server/test/src/services/completion/test_all.dart
+++ b/pkg/analysis_server/test/src/services/completion/test_all.dart
@@ -6,10 +6,12 @@
 
 import 'dart/test_all.dart' as dart;
 import 'filtering/test_all.dart' as filtering;
+import 'yaml/test_all.dart' as yaml;
 
 void main() {
   defineReflectiveSuite(() {
     dart.main();
     filtering.main();
+    yaml.main();
   }, name: 'completion');
 }
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/analysis_options_generator_test.dart b/pkg/analysis_server/test/src/services/completion/yaml/analysis_options_generator_test.dart
new file mode 100644
index 0000000..c86dc28
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/completion/yaml/analysis_options_generator_test.dart
@@ -0,0 +1,155 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/completion/yaml/analysis_options_generator.dart';
+import 'package:analyzer/src/task/options.dart';
+import 'package:linter/src/rules.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'yaml_generator_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(AnalysisOptionsGeneratorTest);
+  });
+}
+
+@reflectiveTest
+class AnalysisOptionsGeneratorTest extends YamlGeneratorTest {
+  @override
+  String get fileName => 'analysis_options.yaml';
+
+  @override
+  AnalysisOptionsGenerator get generator =>
+      AnalysisOptionsGenerator(resourceProvider);
+
+  void test_analyzer() {
+    getCompletions('''
+analyzer:
+  ^
+''');
+    assertSuggestion('${AnalyzerOptions.enableExperiment}: ');
+  }
+
+  void test_empty() {
+    getCompletions('^');
+    assertSuggestion('${AnalyzerOptions.analyzer}: ');
+    assertSuggestion('${AnalyzerOptions.include}: ');
+    // TODO(brianwilkerson) Replace this with a constant.
+    assertSuggestion('linter: ');
+  }
+
+  void test_linter() {
+    getCompletions('''
+linter:
+  ^
+''');
+    assertSuggestion('rules:');
+  }
+
+  void test_linter_rules() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    ^
+''');
+    assertSuggestion('annotate_overrides');
+  }
+
+  void test_linter_rules_listItem_first() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - ^
+    - annotate_overrides
+''');
+    assertSuggestion('avoid_as');
+    assertNoSuggestion('annotate_overrides');
+  }
+
+  void test_linter_rules_listItem_last() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - annotate_overrides
+    - ^
+''');
+    assertSuggestion('avoid_as');
+    assertNoSuggestion('annotate_overrides');
+  }
+
+  void test_linter_rules_listItem_middle() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - annotate_overrides
+    - ^
+    - avoid_empty_else
+''');
+    assertSuggestion('avoid_as');
+    assertNoSuggestion('annotate_overrides');
+    assertNoSuggestion('avoid_empty_else');
+  }
+
+  void test_linter_rules_listItem_nonDuplicate() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - annotate_overrides
+    - ^
+''');
+    assertNoSuggestion('annotate_overrides');
+  }
+
+  void test_linter_rules_listItem_only() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - ^
+''');
+    assertSuggestion('annotate_overrides');
+  }
+
+  void test_linter_rules_listItem_partial() {
+    registerLintRules();
+    getCompletions('''
+linter:
+  rules:
+    - ann^
+''');
+    assertSuggestion('annotate_overrides');
+  }
+
+  @failingTest
+  void test_topLevel_afterOtherKeys() {
+    // This test fails because the cursor is considered to be inside the exclude
+    // list, and we don't suggest values there.
+    getCompletions('''
+analyzer:
+  exclude:
+    - '*.g.dart'
+^
+''');
+    assertSuggestion('${AnalyzerOptions.include}: ');
+  }
+
+  @failingTest
+  void test_topLevel_afterOtherKeys_partial() {
+    // This test fails because the YAML parser can't recover from this kind of
+    // invalid input.
+    getCompletions('''
+analyzer:
+  exclude:
+    - '*.g.dart'
+li^
+''');
+    assertSuggestion('linter');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/fix_data_generator_test.dart b/pkg/analysis_server/test/src/services/completion/yaml/fix_data_generator_test.dart
new file mode 100644
index 0000000..4634360
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/completion/yaml/fix_data_generator_test.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/completion/yaml/fix_data_generator.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'yaml_generator_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FixDataGeneratorTest);
+  });
+}
+
+@reflectiveTest
+class FixDataGeneratorTest extends YamlGeneratorTest {
+  @override
+  String get fileName => 'fix_data.yaml';
+
+  @override
+  FixDataGenerator get generator => FixDataGenerator(resourceProvider);
+
+  void test_empty() {
+    getCompletions('^');
+    assertSuggestion('version: ');
+    assertSuggestion('transforms:');
+  }
+
+  void test_transforms_changes_listItem_last() {
+    getCompletions('''
+transforms:
+  - changes:
+    - kind: rename
+      ^
+''');
+    assertSuggestion('newName: ');
+    assertNoSuggestion('kind: ');
+  }
+
+  void test_transforms_changes_listItem_only() {
+    getCompletions('''
+transforms:
+  - changes:
+    - ^
+''');
+    assertSuggestion('kind: ');
+  }
+
+  void test_transforms_element() {
+    getCompletions('''
+transforms:
+  - element:
+      ^
+''');
+    assertSuggestion('uris: ');
+  }
+
+  void test_transforms_listItem_last() {
+    getCompletions('''
+transforms:
+  - title: ''
+    ^
+''');
+    assertSuggestion('date: ');
+    assertNoSuggestion('title: ');
+  }
+
+  void test_transforms_listItem_only() {
+    getCompletions('''
+transforms:
+  - ^
+''');
+    assertSuggestion('title: ');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
new file mode 100644
index 0000000..7247af0
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'yaml_generator_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(PubspecGeneratorTest);
+  });
+}
+
+@reflectiveTest
+class PubspecGeneratorTest extends YamlGeneratorTest {
+  @override
+  String get fileName => 'pubspec.yaml';
+
+  @override
+  PubspecGenerator get generator => PubspecGenerator(resourceProvider);
+
+  void test_empty() {
+    getCompletions('^');
+    assertSuggestion('name: ');
+  }
+
+  void test_environment() {
+    getCompletions('''
+environment:
+  ^
+''');
+    assertSuggestion('flutter: ');
+    assertSuggestion('sdk: ');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/test_all.dart b/pkg/analysis_server/test/src/services/completion/yaml/test_all.dart
new file mode 100644
index 0000000..0d82611
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/completion/yaml/test_all.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'analysis_options_generator_test.dart' as analysis_options_generator;
+import 'fix_data_generator_test.dart' as fix_data_generator;
+import 'pubspec_generator_test.dart' as pubspec_generator;
+
+void main() {
+  defineReflectiveSuite(() {
+    analysis_options_generator.main();
+    fix_data_generator.main();
+    pubspec_generator.main();
+  }, name: 'yaml');
+}
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/yaml_generator_test_support.dart b/pkg/analysis_server/test/src/services/completion/yaml/yaml_generator_test_support.dart
new file mode 100644
index 0000000..7e4cc84
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/completion/yaml/yaml_generator_test_support.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:test/test.dart';
+
+abstract class YamlGeneratorTest with ResourceProviderMixin {
+  /// The completion results produced by [getCompletions].
+  /* late */ List<CompletionSuggestion> results;
+
+  /// Return the name of the file being tested.
+  String get fileName;
+
+  /// Return the generator to be used for this test.
+  YamlCompletionGenerator get generator;
+
+  /// Assert that there is no suggestion with the given [completion].
+  void assertNoSuggestion(String completion) {
+    for (var suggestion in results) {
+      if (suggestion.completion == completion) {
+        var buffer = StringBuffer();
+        buffer.writeln("Unexpected suggestion of '$completion' in:");
+        for (var suggestion in results) {
+          buffer.writeln("  '${suggestion.completion}'");
+        }
+        fail(buffer.toString());
+      }
+    }
+  }
+
+  /// Assert that there is a suggestion with the given [completion].
+  void assertSuggestion(String completion) {
+    for (var suggestion in results) {
+      if (suggestion.completion == completion) {
+        return;
+      }
+    }
+    var buffer = StringBuffer();
+    buffer.writeln("Missing suggestion of '$completion', found:");
+    for (var suggestion in results) {
+      buffer.writeln("  '${suggestion.completion}'");
+    }
+    fail(buffer.toString());
+  }
+
+  /// Compute the completions in the given [content]. The location of the
+  /// completion request is encoded in the content as a caret (`^`).
+  void getCompletions(String content) {
+    // Extract the completion location from the [content].
+    var completionOffset = content.indexOf('^');
+    expect(completionOffset, isNot(equals(-1)), reason: 'missing ^');
+    var nextOffset = content.indexOf('^', completionOffset + 1);
+    expect(nextOffset, equals(-1), reason: 'too many ^');
+    content = content.substring(0, completionOffset) +
+        content.substring(completionOffset + 1);
+    // Add the file to the file system.
+    var file = newFile('/home/test/$fileName', content: content);
+    // Generate completions.
+    results = generator.getSuggestions(file.path, completionOffset);
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/add_async_test.dart b/pkg/analysis_server/test/src/services/correction/fix/add_async_test.dart
index 8047f30..f4c1745 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/add_async_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/add_async_test.dart
@@ -4,7 +4,7 @@
 
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server/src/services/linter/lint_names.dart';
-import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/dart/analysis/experiments.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -14,6 +14,7 @@
 void main() {
   defineReflectiveSuite(() {
     defineReflectiveTests(AddAsyncTest);
+    defineReflectiveTests(AddAsyncWithNullSafetyTest);
     defineReflectiveTests(AvoidReturningNullForFutureTest);
   });
 }
@@ -69,7 +70,7 @@
   await foo;
   return 1;
 }
-''', errorFilter: (AnalysisError error) {
+''', errorFilter: (error) {
       return error.errorCode == CompileTimeErrorCode.UNDEFINED_IDENTIFIER_AWAIT;
     });
   }
@@ -84,7 +85,7 @@
 void takeFutureCallback(Future callback()) {}
 
 void doStuff() => takeFutureCallback(() async => await 1);
-''', errorFilter: (AnalysisError error) {
+''', errorFilter: (error) {
       return error.errorCode == CompileTimeErrorCode.UNDEFINED_IDENTIFIER_AWAIT;
     });
   }
@@ -100,6 +101,42 @@
 ''');
   }
 
+  Future<void> test_missingReturn_hasReturn() async {
+    await resolveTestCode('''
+Future<int> f(bool b) {
+  if (b) {
+    return 0;
+  }
+}
+''');
+    await assertNoFix(errorFilter: (error) {
+      return error.errorCode ==
+          CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION;
+    });
+  }
+
+  Future<void> test_missingReturn_notVoid() async {
+    await resolveTestCode('''
+Future<int> f() {
+  print('');
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_missingReturn_void() async {
+    await resolveTestCode('''
+Future<void> f() {
+  print('');
+}
+''');
+    await assertHasFix('''
+Future<void> f() async {
+  print('');
+}
+''');
+  }
+
   Future<void> test_nullFunctionBody() async {
     await resolveTestCode('''
 var F = await;
@@ -121,7 +158,7 @@
   await foo();
   return 42;
 }
-''', errorFilter: (AnalysisError error) {
+''', errorFilter: (error) {
       return error.errorCode == CompileTimeErrorCode.AWAIT_IN_WRONG_CONTEXT;
     });
   }
@@ -179,6 +216,95 @@
 }
 
 @reflectiveTest
+class AddAsyncWithNullSafetyTest extends FixProcessorTest {
+  @override
+  List<String> get experiments => [EnableString.non_nullable];
+
+  @override
+  FixKind get kind => DartFixKind.ADD_ASYNC;
+
+  Future<void> test_missingReturn_method_hasReturn() async {
+    await resolveTestCode('''
+class C {
+  Future<int> m(bool b) {
+    if (b) {
+      return 0;
+    }
+  }
+}
+''');
+    await assertNoFix(errorFilter: (error) {
+      return error.errorCode ==
+          CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_METHOD;
+    });
+  }
+
+  Future<void> test_missingReturn_method_notVoid() async {
+    await resolveTestCode('''
+class C {
+  Future<int> m() {
+    print('');
+  }
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_missingReturn_method_void() async {
+    await resolveTestCode('''
+class C {
+  Future<void> m() {
+    print('');
+  }
+}
+''');
+    await assertHasFix('''
+class C {
+  Future<void> m() async {
+    print('');
+  }
+}
+''');
+  }
+
+  Future<void> test_missingReturn_topLevel_hasReturn() async {
+    await resolveTestCode('''
+Future<int> f(bool b) {
+  if (b) {
+    return 0;
+  }
+}
+''');
+    await assertNoFix(errorFilter: (error) {
+      return error.errorCode ==
+          CompileTimeErrorCode.RETURN_OF_INVALID_TYPE_FROM_FUNCTION;
+    });
+  }
+
+  Future<void> test_missingReturn_topLevel_notVoid() async {
+    await resolveTestCode('''
+Future<int> f() {
+  print('');
+}
+''');
+    await assertNoFix();
+  }
+
+  Future<void> test_missingReturn_topLevel_void() async {
+    await resolveTestCode('''
+Future<void> f() {
+  print('');
+}
+''');
+    await assertHasFix('''
+Future<void> f() async {
+  print('');
+}
+''');
+  }
+}
+
+@reflectiveTest
 class AvoidReturningNullForFutureTest extends FixProcessorLintTest {
   @override
   FixKind get kind => DartFixKind.ADD_ASYNC;
diff --git a/pkg/analysis_server/test/src/services/correction/fix/convert_to_set_literal_test.dart b/pkg/analysis_server/test/src/services/correction/fix/convert_to_set_literal_test.dart
index f12ee52..28674cb 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/convert_to_set_literal_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/convert_to_set_literal_test.dart
@@ -83,10 +83,7 @@
 ''');
   }
 
-  @failingTest
   Future<void> test_from_inferred() async {
-    // _setWouldBeInferred does not check for inference based on the parameter
-    // type.
     await resolveTestCode('''
 void f(Set<int> s) {}
 var s = f(Set.from([]));
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index acc8ac7..4fc82c5 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -1360,17 +1360,17 @@
       _errorReporter.reportErrorForNode(
           HintCode.INVALID_REQUIRED_OPTIONAL_POSITIONAL_PARAM,
           param,
-          [param.identifier.name]);
+          [_formalParameterNameOrEmpty(param)]);
     }
     for (final param in nonNamedParamsWithRequired.where((p) => p.isRequired)) {
       _errorReporter.reportErrorForNode(
           HintCode.INVALID_REQUIRED_POSITIONAL_PARAM,
           param,
-          [param.identifier.name]);
+          [_formalParameterNameOrEmpty(param)]);
     }
     for (final param in namedParamsWithRequiredAndDefault) {
       _errorReporter.reportErrorForNode(HintCode.INVALID_REQUIRED_NAMED_PARAM,
-          param, [param.identifier.name]);
+          param, [_formalParameterNameOrEmpty(param)]);
     }
   }
 
@@ -1629,6 +1629,11 @@
     return true;
   }
 
+  static String _formalParameterNameOrEmpty(FormalParameter node) {
+    var identifier = node.identifier;
+    return identifier?.name ?? '';
+  }
+
   static bool _hasNonVirtualAnnotation(ExecutableElement element) {
     if (element == null) {
       return false;
diff --git a/pkg/analyzer/lib/src/generated/engine.dart b/pkg/analyzer/lib/src/generated/engine.dart
index e0368a4..63e7b03 100644
--- a/pkg/analyzer/lib/src/generated/engine.dart
+++ b/pkg/analyzer/lib/src/generated/engine.dart
@@ -101,6 +101,10 @@
   /// The file name used for analysis options files.
   static const String ANALYSIS_OPTIONS_YAML_FILE = 'analysis_options.yaml';
 
+  /// The file name used for the files containing the data for the data-driven
+  /// fixes.
+  static const String FIX_DATA_FILE = 'fix_data.yaml';
+
   /// The file name used for pubspec files.
   static const String PUBSPEC_YAML_FILE = 'pubspec.yaml';
 
diff --git a/pkg/analyzer/test/src/diagnostics/invalid_required_positional_param_test.dart b/pkg/analyzer/test/src/diagnostics/invalid_required_positional_param_test.dart
index 39caa76..072131e 100644
--- a/pkg/analyzer/test/src/diagnostics/invalid_required_positional_param_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/invalid_required_positional_param_test.dart
@@ -21,7 +21,7 @@
     writeTestPackageConfigWithMeta();
   }
 
-  test_requiredPositionalParameter() async {
+  test_ofFunction_first() async {
     await assertErrorsInCode(r'''
 import 'package:meta/meta.dart';
 
@@ -31,7 +31,7 @@
     ]);
   }
 
-  test_requiredPositionalParameter_asSecond() async {
+  test_ofFunction_second() async {
     await assertErrorsInCode(r'''
 import 'package:meta/meta.dart';
 
@@ -41,6 +41,26 @@
     ]);
   }
 
+  test_ofGenericFunctionType_named() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+typedef F = void Function(@required int a);
+''', [
+      error(HintCode.INVALID_REQUIRED_POSITIONAL_PARAM, 60, 15),
+    ]);
+  }
+
+  test_ofGenericFunctionType_unnamed() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+typedef F = void Function(@required int);
+''', [
+      error(HintCode.INVALID_REQUIRED_POSITIONAL_PARAM, 60, 13),
+    ]);
+  }
+
   test_valid() async {
     await assertNoErrorsInCode(r'''
 m1() => null;
diff --git a/pkg/analyzer_plugin/lib/utilities/analyzer_converter.dart b/pkg/analyzer_plugin/lib/utilities/analyzer_converter.dart
index f7347d2..a46f3bd 100644
--- a/pkg/analyzer_plugin/lib/utilities/analyzer_converter.dart
+++ b/pkg/analyzer_plugin/lib/utilities/analyzer_converter.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/element/element.dart' as analyzer;
-import 'package:analyzer/dart/element/type.dart' as analyzer;
 import 'package:analyzer/diagnostic/diagnostic.dart' as analyzer;
 import 'package:analyzer/error/error.dart' as analyzer;
 import 'package:analyzer/exception/exception.dart' as analyzer;
@@ -11,10 +10,7 @@
 import 'package:analyzer/source/line_info.dart' as analyzer;
 import 'package:analyzer/src/generated/engine.dart' as analyzer;
 import 'package:analyzer/src/generated/source.dart' as analyzer;
-import 'package:analyzer/src/generated/utilities_dart.dart' as analyzer;
 import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
-import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin;
-import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 
 /// An object used to convert between objects defined by the 'analyzer' package
 /// and those defined by the plugin protocol.
diff --git a/pkg/compiler/lib/src/js/js.dart b/pkg/compiler/lib/src/js/js.dart
index c2f8403..dd31e3b 100644
--- a/pkg/compiler/lib/src/js/js.dart
+++ b/pkg/compiler/lib/src/js/js.dart
@@ -18,11 +18,14 @@
 String prettyPrint(Node node,
     {bool enableMinification: false,
     bool allowVariableMinification: true,
+    bool preferSemicolonToNewlineInMinifiedOutput: false,
     Renamer renamerForNames: JavaScriptPrintingOptions.identityRenamer}) {
   // TODO(johnniwinther): Do we need all the options here?
   JavaScriptPrintingOptions options = new JavaScriptPrintingOptions(
       shouldCompressOutput: enableMinification,
       minifyLocalVariables: allowVariableMinification,
+      preferSemicolonToNewlineInMinifiedOutput:
+          preferSemicolonToNewlineInMinifiedOutput,
       renamerForNames: renamerForNames);
   SimpleJavaScriptPrintingContext context =
       new SimpleJavaScriptPrintingContext();
diff --git a/pkg/compiler/lib/src/js/size_estimator.dart b/pkg/compiler/lib/src/js/size_estimator.dart
new file mode 100644
index 0000000..60d0354
--- /dev/null
+++ b/pkg/compiler/lib/src/js/size_estimator.dart
@@ -0,0 +1,1042 @@
+// Copyright (c) 2020, 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.
+
+library js.size_estimator;
+
+import 'package:js_ast/js_ast.dart';
+import 'package:js_ast/src/characters.dart' as charCodes;
+import 'package:js_ast/src/precedence.dart';
+
+import '../js_backend/string_reference.dart';
+import '../js_backend/type_reference.dart';
+import '../js_emitter/metadata_collector.dart';
+
+/// [SizeEstimator] is a [NodeVisitor] designed to produce a consistent size
+/// estimate for a given JavaScript AST. [SizeEstimator] trades accuracy for
+/// stability and performance. In addition, [SizeEstimator] assumes we will emit
+/// production quality minified JavaScript.
+class SizeEstimator implements NodeVisitor {
+  int charCount = 0;
+  bool inForInit = false;
+  bool atStatementBegin = false;
+  bool pendingSemicolon = false;
+  bool pendingSpace = false;
+
+  static final String variableSizeEstimate = '#';
+  static final String nameSizeEstimate = '###';
+
+  String sizeEstimate(Node node) {
+    if (node is VariableDeclaration || node is VariableUse) {
+      // We assume all [VariableDeclaration] and [VariableUse] nodes are
+      // locals.
+      return variableSizeEstimate;
+    } else if (node is Name ||
+        node is Parameter ||
+        node is VariableDeclaration ||
+        node is VariableUse) {
+      return nameSizeEstimate;
+    } else if (node is LiteralString) {
+      assert(!node.isFinalized);
+      // We assume all non-final literal strings are minified names, and thus
+      // use the nameSizeEstimate.
+      return nameSizeEstimate;
+    } else if (node is BoundMetadataEntry) {
+      // Value is an int.
+      return '####';
+    } else if (node is TypeReference) {
+      // Type references vary in size. Some references look like:
+      // '<typeHolder>$.<type>' where <typeHolder> is a one byte local and
+      // <type> is roughly 3 bytes. However, we also have to initialize the type
+      // in the holder, some like ab:f("QQ<b7c>"), ie 16 bytes. For two
+      // occurences we will have on average 13 bytes. For a more detailed
+      // estimate, we'd have to partially finalize the results.
+      return '###_###_###_#';
+    } else if (node is StringReference) {
+      // Worst case we have to inline the string so size of string + 2 bytes for
+      // quotes.
+      return "'${node.constant}'";
+    } else {
+      throw UnsupportedError('$node type is not supported');
+    }
+  }
+
+  /// Always emit a newline, even under `enableMinification`.
+  void forceLine() {
+    out('\n'); // '\n'
+  }
+
+  void out(String s) {
+    if (s.length > 0) {
+      // We can elide a semicolon in some cases, but for simplicity we
+      // assume a semicolon is needed here.
+      if (pendingSemicolon) {
+        emit(';'); // ';'
+      }
+
+      // We can elide a pending space in some cases, but for simplicity we
+      // assume a space is needed here.
+      if (pendingSpace) {
+        emit(' '); // ' '
+      }
+      pendingSpace = false;
+      pendingSemicolon = false;
+      emit(s); // str
+    }
+  }
+
+  void outSemicolonLn() {
+    pendingSemicolon = true;
+  }
+
+  void emit(String s) {
+    charCount += s.length;
+  }
+
+  void visit(Node node) {
+    node.accept(this);
+  }
+
+  void visitCommaSeparated(List<Node> nodes, int hasRequiredType,
+      {bool newInForInit, bool newAtStatementBegin}) {
+    for (int i = 0; i < nodes.length; i++) {
+      if (i != 0) {
+        atStatementBegin = false;
+        out(','); // ','
+      }
+      visitNestedExpression(nodes[i], hasRequiredType,
+          newInForInit: newInForInit, newAtStatementBegin: newAtStatementBegin);
+    }
+  }
+
+  void visitAll(List<Node> nodes) {
+    nodes.forEach(visit);
+  }
+
+  @override
+  void visitProgram(Program program) {
+    if (program.body.isNotEmpty) {
+      visitAll(program.body);
+    }
+  }
+
+  Statement unwrapBlockIfSingleStatement(Statement body) {
+    Statement result = body;
+    while (result is Block) {
+      Block block = result;
+      if (block.statements.length != 1) break;
+      result = block.statements.single;
+    }
+    return result;
+  }
+
+  bool blockBody(Statement body, {bool needsSeparation}) {
+    if (body is Block) {
+      blockOut(body);
+      return true;
+    }
+    if (needsSeparation) {
+      out(' '); // ' '
+    }
+    visit(body);
+    return false;
+  }
+
+  void blockOutWithoutBraces(Node node) {
+    if (node is Block) {
+      Block block = node;
+      block.statements.forEach(blockOutWithoutBraces);
+    } else {
+      visit(node);
+    }
+  }
+
+  int blockOut(Block node) {
+    out('{'); // '{'
+    node.statements.forEach(blockOutWithoutBraces);
+    out('}'); // '}'
+    int closingPosition = charCount - 1;
+    return closingPosition;
+  }
+
+  @override
+  void visitBlock(Block block) {
+    blockOut(block);
+  }
+
+  @override
+  void visitExpressionStatement(ExpressionStatement node) {
+    visitNestedExpression(node.expression, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: true);
+    outSemicolonLn();
+  }
+
+  @override
+  void visitEmptyStatement(EmptyStatement node) {
+    out(';'); // ';'
+  }
+
+  void ifOut(If node) {
+    Statement then = unwrapBlockIfSingleStatement(node.then);
+    Statement elsePart = node.otherwise;
+    bool hasElse = node.hasElse;
+
+    out('if('); // 'if('
+    visitNestedExpression(node.condition, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+    blockBody(then, needsSeparation: false);
+    if (hasElse) {
+      out('else'); // 'else'
+      if (elsePart is If) {
+        pendingSpace = true;
+        ifOut(elsePart);
+      } else {
+        blockBody(unwrapBlockIfSingleStatement(elsePart),
+            needsSeparation: true);
+      }
+    }
+  }
+
+  @override
+  void visitIf(If node) {
+    ifOut(node);
+  }
+
+  @override
+  void visitFor(For loop) {
+    out('for('); // 'for('
+    if (loop.init != null) {
+      visitNestedExpression(loop.init, EXPRESSION,
+          newInForInit: true, newAtStatementBegin: false);
+    }
+    out(';'); // ';'
+    if (loop.condition != null) {
+      visitNestedExpression(loop.condition, EXPRESSION,
+          newInForInit: false, newAtStatementBegin: false);
+    }
+    out(';'); // ';'
+    if (loop.update != null) {
+      visitNestedExpression(loop.update, EXPRESSION,
+          newInForInit: false, newAtStatementBegin: false);
+    }
+    out(')'); // ')'
+    blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false);
+  }
+
+  @override
+  void visitForIn(ForIn loop) {
+    out('for('); // 'for('
+    visitNestedExpression(loop.leftHandSide, EXPRESSION,
+        newInForInit: true, newAtStatementBegin: false);
+    out(' in'); // ' in'
+    pendingSpace = true;
+    visitNestedExpression(loop.object, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+    blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false);
+  }
+
+  @override
+  void visitWhile(While loop) {
+    out('while('); // 'while('
+    visitNestedExpression(loop.condition, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+    blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false);
+  }
+
+  @override
+  void visitDo(Do loop) {
+    out('do'); // 'do'
+    if (blockBody(unwrapBlockIfSingleStatement(loop.body),
+        needsSeparation: true)) {}
+    out('while('); // 'while('
+    visitNestedExpression(loop.condition, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+    outSemicolonLn();
+  }
+
+  @override
+  void visitContinue(Continue node) {
+    if (node.targetLabel == null) {
+      out('continue'); // 'continue'
+    } else {
+      out('continue ${node.targetLabel}'); // 'continue ${node.targetLabel}'
+    }
+    outSemicolonLn();
+  }
+
+  @override
+  void visitBreak(Break node) {
+    if (node.targetLabel == null) {
+      out('break');
+    } else {
+      out('break ${node.targetLabel}');
+    }
+    outSemicolonLn();
+  }
+
+  @override
+  void visitReturn(Return node) {
+    out('return'); // 'return'
+    if (node.value != null) {
+      pendingSpace = true;
+      visitNestedExpression(node.value, EXPRESSION,
+          newInForInit: false, newAtStatementBegin: false);
+    }
+    outSemicolonLn();
+  }
+
+  @override
+  void visitDartYield(DartYield node) {
+    if (node.hasStar) {
+      out('yield*'); // 'yield*'
+    } else {
+      out('yield'); // 'yield'
+    }
+    pendingSpace = true;
+    visitNestedExpression(node.expression, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    outSemicolonLn();
+  }
+
+  @override
+  void visitThrow(Throw node) {
+    out('throw'); // 'throw'
+    pendingSpace = true;
+    visitNestedExpression(node.expression, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    outSemicolonLn();
+  }
+
+  @override
+  void visitTry(Try node) {
+    out('try'); // 'try'
+    blockBody(node.body, needsSeparation: true);
+    if (node.catchPart != null) {
+      visit(node.catchPart);
+    }
+    if (node.finallyPart != null) {
+      out('finally'); // 'finally'
+      blockBody(node.finallyPart, needsSeparation: true);
+    }
+  }
+
+  @override
+  void visitCatch(Catch node) {
+    out('catch('); // 'catch('
+    visitNestedExpression(node.declaration, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+    blockBody(node.body, needsSeparation: false);
+  }
+
+  @override
+  void visitSwitch(Switch node) {
+    out('switch('); // 'switch('
+    visitNestedExpression(node.key, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out('){'); // '){
+    visitAll(node.cases);
+    out('}'); // '}'
+  }
+
+  @override
+  void visitCase(Case node) {
+    out('case'); // 'case'
+    pendingSpace = true;
+    visitNestedExpression(node.expression, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(':'); // ':'
+    if (!node.body.statements.isEmpty) {
+      blockOutWithoutBraces(node.body);
+    }
+  }
+
+  @override
+  void visitDefault(Default node) {
+    out('default:'); // 'default:'
+    if (!node.body.statements.isEmpty) {
+      blockOutWithoutBraces(node.body);
+    }
+  }
+
+  @override
+  void visitLabeledStatement(LabeledStatement node) {
+    Statement body = unwrapBlockIfSingleStatement(node.body);
+    // `label: break label;`
+    // Does not work on IE. The statement is a nop, so replace it by an empty
+    // statement.
+    // See:
+    // https://connect.microsoft.com/IE/feedback/details/891889/parser-bugs
+    if (body is Break && body.targetLabel == node.label) {
+      visit(new EmptyStatement());
+      return;
+    }
+    out('${node.label}:');
+    blockBody(body, needsSeparation: false);
+  }
+
+  int functionOut(Fun fun, Node name, VarCollector vars) {
+    out('function'); // 'function'
+    if (name != null) {
+      out(' '); // ' '
+      // Name must be a [Decl]. Therefore only test for primary expressions.
+      visitNestedExpression(name, PRIMARY,
+          newInForInit: false, newAtStatementBegin: false);
+    }
+    out('('); // '('
+    if (fun.params != null) {
+      visitCommaSeparated(fun.params, PRIMARY,
+          newInForInit: false, newAtStatementBegin: false);
+    }
+    out(')'); // ')'
+    switch (fun.asyncModifier) {
+      case AsyncModifier.sync:
+        break;
+      case AsyncModifier.async:
+        out(' async'); // ' async'
+        break;
+      case AsyncModifier.syncStar:
+        out(' sync*'); // ' sync*'
+        break;
+      case AsyncModifier.asyncStar:
+        out(' async*'); // ' async*'
+        break;
+    }
+    int closingPosition = blockOut(fun.body);
+    return closingPosition;
+  }
+
+  @override
+  visitFunctionDeclaration(FunctionDeclaration declaration) {
+    VarCollector vars = new VarCollector();
+    vars.visitFunctionDeclaration(declaration);
+    functionOut(declaration.function, declaration.name, vars);
+  }
+
+  visitNestedExpression(Expression node, int requiredPrecedence,
+      {bool newInForInit, bool newAtStatementBegin}) {
+    bool needsParentheses = !node.isFinalized ||
+        // a - (b + c).
+        (requiredPrecedence != EXPRESSION &&
+            node.precedenceLevel < requiredPrecedence) ||
+        // for (a = (x in o); ... ; ... ) { ... }
+        (newInForInit && node is Binary && node.op == "in") ||
+        // (function() { ... })().
+        // ({a: 2, b: 3}.toString()).
+        (newAtStatementBegin &&
+            (node is NamedFunction ||
+                node is Fun ||
+                node is ObjectInitializer));
+    if (needsParentheses) {
+      inForInit = false;
+      atStatementBegin = false;
+      out('('); // '('
+      visit(node);
+      out(')'); // ')'
+    } else {
+      inForInit = newInForInit;
+      atStatementBegin = newAtStatementBegin;
+      visit(node);
+    }
+  }
+
+  @override
+  visitVariableDeclarationList(VariableDeclarationList list) {
+    out('var '); // 'var '
+    List<Node> nodes = list.declarations;
+    if (inForInit) {
+      visitCommaSeparated(nodes, ASSIGNMENT,
+          newInForInit: inForInit, newAtStatementBegin: false);
+    } else {
+      for (int i = 0; i < nodes.length; i++) {
+        Node node = nodes[i];
+        if (i > 0) {
+          atStatementBegin = false;
+          out(','); // ','
+        }
+        visitNestedExpression(node, ASSIGNMENT,
+            newInForInit: inForInit, newAtStatementBegin: false);
+      }
+    }
+  }
+
+  void _outputIncDec(String op, Expression variable, [Expression alias]) {
+    // We can eliminate the space preceding the inc/dec in some cases,
+    // but for estimation purposes we assume the worst case.
+    if (op == '+') {
+      out(' ++');
+    } else {
+      out(' --');
+    }
+    visitNestedExpression(variable, UNARY,
+        newInForInit: inForInit, newAtStatementBegin: false);
+  }
+
+  @override
+  visitAssignment(Assignment assignment) {
+    /// To print assignments like `a = a + 1` and `a = a + b` compactly as
+    /// `++a` and `a += b` in the face of [DeferredExpression]s we detect the
+    /// pattern of the undeferred assignment.
+    String op = assignment.op;
+    Node leftHandSide = assignment.leftHandSide;
+    Node rightHandSide = assignment.value;
+    if ((op == '+' || op == '-') &&
+        leftHandSide is VariableUse &&
+        rightHandSide is LiteralNumber &&
+        rightHandSide.value == "1") {
+      // Output 'a += 1' as '++a' and 'a -= 1' as '--a'.
+      _outputIncDec(op, assignment.leftHandSide);
+      return;
+    }
+    if (!assignment.isCompound &&
+        leftHandSide is VariableUse &&
+        rightHandSide is Binary) {
+      Node rLeft = rightHandSide.left;
+      Node rRight = rightHandSide.right;
+      String op = rightHandSide.op;
+      if (op == '+' ||
+          op == '-' ||
+          op == '/' ||
+          op == '*' ||
+          op == '%' ||
+          op == '^' ||
+          op == '&' ||
+          op == '|') {
+        if (rLeft is VariableUse && rLeft.name == leftHandSide.name) {
+          // Output 'a = a + 1' as '++a' and 'a = a - 1' as '--a'.
+          if ((op == '+' || op == '-') &&
+              rRight is LiteralNumber &&
+              rRight.value == "1") {
+            _outputIncDec(op, assignment.leftHandSide, rightHandSide.left);
+            return;
+          }
+          // Output 'a = a + b' as 'a += b'.
+          visitNestedExpression(assignment.leftHandSide, CALL,
+              newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+          assert(op.length == 1);
+          out('$op='); // '$op='
+          visitNestedExpression(rRight, ASSIGNMENT,
+              newInForInit: inForInit, newAtStatementBegin: false);
+          return;
+        }
+      }
+    }
+    visitNestedExpression(assignment.leftHandSide, CALL,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+    if (assignment.value != null) {
+      if (op != null) out(op);
+      out('='); // '='
+      visitNestedExpression(assignment.value, ASSIGNMENT,
+          newInForInit: inForInit, newAtStatementBegin: false);
+    }
+  }
+
+  @override
+  visitVariableInitialization(VariableInitialization initialization) {
+    visitAssignment(initialization);
+  }
+
+  @override
+  visitConditional(Conditional cond) {
+    visitNestedExpression(cond.condition, LOGICAL_OR,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+    out('?'); // '?'
+    // The then part is allowed to have an 'in'.
+    visitNestedExpression(cond.then, ASSIGNMENT,
+        newInForInit: false, newAtStatementBegin: false);
+    out(':'); // ':'
+    visitNestedExpression(cond.otherwise, ASSIGNMENT,
+        newInForInit: inForInit, newAtStatementBegin: false);
+  }
+
+  @override
+  visitNew(New node) {
+    out('new'); // 'new'
+    visitNestedExpression(node.target, LEFT_HAND_SIDE,
+        newInForInit: inForInit, newAtStatementBegin: false);
+    out('('); // '('
+    visitCommaSeparated(node.arguments, ASSIGNMENT,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+  }
+
+  @override
+  visitCall(Call call) {
+    visitNestedExpression(call.target, CALL,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+    out('('); // '('
+    visitCommaSeparated(call.arguments, ASSIGNMENT,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+  }
+
+  @override
+  void visitBinary(Binary binary) {
+    Expression left = binary.left;
+    Expression right = binary.right;
+    String op = binary.op;
+    int leftPrecedenceRequirement;
+    int rightPrecedenceRequirement;
+    switch (op) {
+      case ',':
+        //  x, (y, z) <=> (x, y), z.
+        leftPrecedenceRequirement = EXPRESSION;
+        rightPrecedenceRequirement = EXPRESSION;
+        break;
+      case "||":
+        leftPrecedenceRequirement = LOGICAL_OR;
+        // x || (y || z) <=> (x || y) || z.
+        rightPrecedenceRequirement = LOGICAL_OR;
+        break;
+      case "&&":
+        leftPrecedenceRequirement = LOGICAL_AND;
+        // x && (y && z) <=> (x && y) && z.
+        rightPrecedenceRequirement = LOGICAL_AND;
+        break;
+      case "|":
+        leftPrecedenceRequirement = BIT_OR;
+        // x | (y | z) <=> (x | y) | z.
+        rightPrecedenceRequirement = BIT_OR;
+        break;
+      case "^":
+        leftPrecedenceRequirement = BIT_XOR;
+        // x ^ (y ^ z) <=> (x ^ y) ^ z.
+        rightPrecedenceRequirement = BIT_XOR;
+        break;
+      case "&":
+        leftPrecedenceRequirement = BIT_AND;
+        // x & (y & z) <=> (x & y) & z.
+        rightPrecedenceRequirement = BIT_AND;
+        break;
+      case "==":
+      case "!=":
+      case "===":
+      case "!==":
+        leftPrecedenceRequirement = EQUALITY;
+        rightPrecedenceRequirement = RELATIONAL;
+        break;
+      case "<":
+      case ">":
+      case "<=":
+      case ">=":
+      case "instanceof":
+      case "in":
+        leftPrecedenceRequirement = RELATIONAL;
+        rightPrecedenceRequirement = SHIFT;
+        break;
+      case ">>":
+      case "<<":
+      case ">>>":
+        leftPrecedenceRequirement = SHIFT;
+        rightPrecedenceRequirement = ADDITIVE;
+        break;
+      case "+":
+      case "-":
+        leftPrecedenceRequirement = ADDITIVE;
+        // We cannot remove parenthesis for "+" because
+        //   x + (y + z) <!=> (x + y) + z:
+        // Example:
+        //   "a" + (1 + 2) => "a3";
+        //   ("a" + 1) + 2 => "a12";
+        rightPrecedenceRequirement = MULTIPLICATIVE;
+        break;
+      case "*":
+      case "/":
+      case "%":
+        leftPrecedenceRequirement = MULTIPLICATIVE;
+        // We cannot remove parenthesis for "*" because of precision issues.
+        rightPrecedenceRequirement = UNARY;
+        break;
+      default:
+        throw UnsupportedError("Forgot operator: $op");
+    }
+
+    visitNestedExpression(left, leftPrecedenceRequirement,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+
+    if (op == "in" || op == "instanceof") {
+      // There are cases where the space is not required but without further
+      // analysis we cannot know.
+      out(' $op '); // ' $op '
+    } else {
+      out(op); // '$op'
+    }
+    visitNestedExpression(right, rightPrecedenceRequirement,
+        newInForInit: inForInit, newAtStatementBegin: false);
+  }
+
+  @override
+  void visitPrefix(Prefix unary) {
+    String op = unary.op;
+    switch (op) {
+      case "delete":
+      case "void":
+      case "typeof":
+      case "+":
+      case "++":
+      case "-":
+      case "--":
+        // We may be able to eliminate the space in some cases, but for
+        // estimation we assume the worst case.
+        out('$op '); // '$op '
+        break;
+      default:
+        out('$op'); // '$op'
+    }
+    visitNestedExpression(unary.argument, UNARY,
+        newInForInit: inForInit, newAtStatementBegin: false);
+  }
+
+  @override
+  void visitPostfix(Postfix postfix) {
+    visitNestedExpression(postfix.argument, CALL,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+    out(postfix.op); // '${postfix.op}'
+  }
+
+  @override
+  void visitVariableUse(VariableUse ref) {
+    // For simplicity and stability we use a constant name size estimate.
+    // In production this is:
+    // '${localNamer.getName(ref.name)'
+    out(sizeEstimate(ref));
+  }
+
+  @override
+  void visitThis(This node) {
+    out('this'); // 'this'
+  }
+
+  @override
+  void visitVariableDeclaration(VariableDeclaration decl) {
+    // '${localNamer.getName(decl.name)'
+    out(sizeEstimate(decl));
+  }
+
+  @override
+  void visitParameter(Parameter param) {
+    // For simplicity and stability we use a constant name size estimate.
+    // In production this is:
+    // '${localNamer.getName(param.name)'
+    out(sizeEstimate(param));
+  }
+
+  bool isDigit(int charCode) {
+    return charCodes.$0 <= charCode && charCode <= charCodes.$9;
+  }
+
+  bool isValidJavaScriptId(String field) {
+    if (field.length < 3) return false;
+    // Ignore the leading and trailing string-delimiter.
+    for (int i = 1; i < field.length - 1; i++) {
+      // TODO(floitsch): allow more characters.
+      int charCode = field.codeUnitAt(i);
+      if (!(charCodes.$a <= charCode && charCode <= charCodes.$z ||
+          charCodes.$A <= charCode && charCode <= charCodes.$Z ||
+          charCode == charCodes.$$ ||
+          charCode == charCodes.$_ ||
+          i != 1 && isDigit(charCode))) {
+        return false;
+      }
+    }
+    // TODO(floitsch): normally we should also check that the field is not a
+    // reserved word.  We don't generate fields with reserved word names except
+    // for 'super'.
+    if (field == '"super"') return false;
+    if (field == '"catch"') return false;
+    return true;
+  }
+
+  @override
+  void visitAccess(PropertyAccess access) {
+    visitNestedExpression(access.receiver, CALL,
+        newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+    Node selector = access.selector;
+    if (selector is LiteralString) {
+      String fieldWithQuotes = selector.value;
+      if (isValidJavaScriptId(fieldWithQuotes)) {
+        if (access.receiver is LiteralNumber) {
+          // We can eliminate the space in some cases, but for simplicity we
+          // always assume it is necessary.
+          out(' '); // ' '
+        }
+
+        // '.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}'
+        out('.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}');
+        return;
+      }
+    } else if (selector is Name) {
+      Node receiver = access.receiver;
+      if (receiver is LiteralNumber) {
+        // We can eliminate the space in some cases, but for simplicity we
+        // always assume it is necessary.
+        out(' '); // ' '
+      }
+      out('.'); // '.'
+      selector.accept(this);
+      return;
+    }
+    out('['); // '['
+    visitNestedExpression(access.selector, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(']'); // ']
+  }
+
+  @override
+  void visitNamedFunction(NamedFunction namedFunction) {
+    VarCollector vars = new VarCollector();
+    vars.visitNamedFunction(namedFunction);
+    functionOut(namedFunction.function, namedFunction.name, vars);
+  }
+
+  @override
+  void visitFun(Fun fun) {
+    VarCollector vars = new VarCollector();
+    vars.visitFun(fun);
+    functionOut(fun, null, vars);
+  }
+
+  @override
+  visitDeferredExpression(DeferredExpression node) {
+    if (node.isFinalized) {
+      // Continue printing with the expression value.
+      assert(node.precedenceLevel == node.value.precedenceLevel);
+      node.value.accept(this);
+    } else {
+      out(sizeEstimate(node));
+    }
+  }
+
+  outputNumberWithRequiredWhitespace(String number) {
+    int charCode = number.codeUnitAt(0);
+    if (charCode == charCodes.$MINUS) {
+      // We can eliminate the space in some cases, but for simplicity we
+      // always assume it is necessary.
+      out(' ');
+    }
+    out(number); // '${number}'
+  }
+
+  @override
+  visitDeferredNumber(DeferredNumber node) {
+    if (node.isFinalized) {
+      outputNumberWithRequiredWhitespace("${node.value}");
+    } else {
+      out(sizeEstimate(node));
+    }
+  }
+
+  @override
+  visitDeferredString(DeferredString node) {
+    if (node.isFinalized) {
+      out(node.value);
+    } else {
+      out(sizeEstimate(node));
+    }
+  }
+
+  @override
+  visitLiteralBool(LiteralBool node) {
+    out(node.value ? '!0' : '!1');
+  }
+
+  @override
+  void visitLiteralString(LiteralString node) {
+    if (node.isFinalized) {
+      out(node.value); // '${node.value}'
+    } else {
+      out(sizeEstimate(node));
+    }
+  }
+
+  @override
+  visitStringConcatenation(StringConcatenation node) {
+    node.visitChildren(this);
+  }
+
+  @override
+  visitName(Name node) {
+    // For simplicity and stability we use a constant name size estimate.
+    // In production this is:
+    // '${options.renamerForNames(node)}'
+    out(sizeEstimate(node));
+  }
+
+  @override
+  visitParentheses(Parentheses node) {
+    out('('); // '('
+    visitNestedExpression(node.enclosed, EXPRESSION,
+        newInForInit: false, newAtStatementBegin: false);
+    out(')'); // ')'
+  }
+
+  @override
+  visitLiteralNumber(LiteralNumber node) {
+    outputNumberWithRequiredWhitespace(node.value);
+  }
+
+  @override
+  void visitLiteralNull(LiteralNull node) {
+    out('null'); // 'null'
+  }
+
+  @override
+  void visitArrayInitializer(ArrayInitializer node) {
+    out('['); // '['
+    List<Expression> elements = node.elements;
+    for (int i = 0; i < elements.length; i++) {
+      Expression element = elements[i];
+      if (element is ArrayHole) {
+        // Note that array holes must have a trailing "," even if they are
+        // in last position. Otherwise `[,]` (having length 1) would become
+        // equal to `[]` (the empty array)
+        // and [1,,] (array with 1 and a hole) would become [1,] = [1].
+        out(','); // ','
+        continue;
+      }
+      visitNestedExpression(element, ASSIGNMENT,
+          newInForInit: false, newAtStatementBegin: false);
+      // We can skip the trailing "," for the last element (since it's not
+      // an array hole).
+      if (i != elements.length - 1) out(','); // ','
+    }
+    out(']'); // ']'
+  }
+
+  @override
+  void visitArrayHole(ArrayHole node) {
+    throw UnsupportedError("Unreachable");
+  }
+
+  @override
+  void visitObjectInitializer(ObjectInitializer node) {
+    // Print all the properties on one line until we see a function-valued
+    // property.  Ideally, we would use a proper pretty-printer to make the
+    // decision based on layout.
+    bool exitOneLinerMode(Expression value) {
+      return value is Fun ||
+          value is ArrayInitializer && value.elements.any((e) => e is Fun);
+    }
+
+    bool isOneLiner = true;
+    List<Property> properties = node.properties;
+    out('{'); // '{'
+    for (int i = 0; i < properties.length; i++) {
+      Node value = properties[i].value;
+      if (isOneLiner && exitOneLinerMode(value)) isOneLiner = false;
+      if (i != 0) {
+        out(','); // ','
+      }
+      if (!isOneLiner) {
+        forceLine();
+      }
+      visit(properties[i]);
+    }
+    out('}'); // '}'
+  }
+
+  @override
+  void visitProperty(Property node) {
+    Node name = node.name;
+    if (name is LiteralString) {
+      String text = name.value;
+      if (isValidJavaScriptId(text)) {
+        // '${text.substring(1, text.length - 1)}
+        out('${text.substring(1, text.length - 1)}');
+      } else {
+        out(text); // '$text'
+      }
+    } else if (name is Name) {
+      node.name.accept(this);
+    } else if (name is DeferredExpression) {
+      out(sizeEstimate(name));
+    } else {
+      assert(name is LiteralNumber);
+      LiteralNumber nameNumber = node.name;
+      out(nameNumber.value); // '${nameNumber.value}'
+    }
+    out(':'); // ':'
+    visitNestedExpression(node.value, ASSIGNMENT,
+        newInForInit: false, newAtStatementBegin: false);
+  }
+
+  @override
+  void visitRegExpLiteral(RegExpLiteral node) {
+    out(node.pattern); // '${node.pattern}'
+  }
+
+  @override
+  void visitLiteralExpression(LiteralExpression node) {
+    String template = node.template;
+    List<Expression> inputs = node.inputs;
+
+    List<String> parts = template.split('#');
+    int inputsLength = inputs == null ? 0 : inputs.length;
+    if (parts.length != inputsLength + 1) {
+      throw UnsupportedError('Wrong number of arguments for JS: $template');
+    }
+    // Code that uses JS must take care of operator precedences, and
+    // put parenthesis if needed.
+    out(parts[0]); // '${parts[0]}'
+    for (int i = 0; i < inputsLength; i++) {
+      visit(inputs[i]);
+      out(parts[i + 1]); // '${parts[i + 1]}'
+    }
+  }
+
+  @override
+  void visitLiteralStatement(LiteralStatement node) {
+    out(node.code); // '${node.code}'
+  }
+
+  void visitInterpolatedNode(InterpolatedNode node) {
+    throw UnsupportedError('InterpolatedStatements are not supported');
+  }
+
+  @override
+  void visitInterpolatedExpression(InterpolatedExpression node) =>
+      visitInterpolatedNode(node);
+
+  @override
+  void visitInterpolatedLiteral(InterpolatedLiteral node) =>
+      visitInterpolatedNode(node);
+
+  @override
+  void visitInterpolatedParameter(InterpolatedParameter node) =>
+      visitInterpolatedNode(node);
+
+  @override
+  void visitInterpolatedSelector(InterpolatedSelector node) =>
+      visitInterpolatedNode(node);
+
+  @override
+  void visitInterpolatedStatement(InterpolatedStatement node) {
+    throw UnsupportedError('InterpolatedStatements are not supported');
+  }
+
+  @override
+  void visitInterpolatedDeclaration(InterpolatedDeclaration node) {
+    visitInterpolatedNode(node);
+  }
+
+  @override
+  void visitComment(Comment node) {
+    // We assume output is compressed and thus do not output comments.
+  }
+
+  @override
+  void visitAwait(Await node) {
+    out('await '); // 'await '
+    visit(node.expression);
+  }
+}
+
+int EstimateSize(Node node) {
+  var estimator = SizeEstimator();
+  estimator.visit(node);
+  return estimator.charCount;
+}
diff --git a/pkg/compiler/lib/src/js_backend/namer_names.dart b/pkg/compiler/lib/src/js_backend/namer_names.dart
index dd8c765..ce57087 100644
--- a/pkg/compiler/lib/src/js_backend/namer_names.dart
+++ b/pkg/compiler/lib/src/js_backend/namer_names.dart
@@ -83,6 +83,9 @@
   int get hashCode => base.hashCode * 13 + prefix.hashCode;
 
   @override
+  bool get isFinalized => prefix.isFinalized && base.isFinalized;
+
+  @override
   int compareTo(jsAst.Name other) {
     _NamerName otherNamerName;
     if (other is ModularName) {
@@ -209,6 +212,7 @@
 
   TokenName(this._scope, this.key);
 
+  @override
   bool get isFinalized => _name != null;
 
   @override
@@ -276,6 +280,9 @@
   }
 
   @override
+  bool get isFinalized => _target.isFinalized;
+
+  @override
   bool operator ==(other) => _target == other;
 
   @override
diff --git a/pkg/compiler/lib/src/js_backend/string_reference.dart b/pkg/compiler/lib/src/js_backend/string_reference.dart
index 95fa795..b8c41aa 100644
--- a/pkg/compiler/lib/src/js_backend/string_reference.dart
+++ b/pkg/compiler/lib/src/js_backend/string_reference.dart
@@ -115,16 +115,19 @@
   }
 
   set value(js.Expression value) {
-    assert(_value == null && value != null);
+    assert(!isFinalized && value != null);
     _value = value;
   }
 
   @override
   js.Expression get value {
-    assert(_value != null, 'StringReference is unassigned');
+    assert(isFinalized, 'StringReference is unassigned');
     return _value;
   }
 
+  @override
+  bool get isFinalized => _value != null;
+
   // Precedence will be CALL or LEFT_HAND_SIDE depending on what expression the
   // reference is resolved to.
   @override
@@ -139,7 +142,7 @@
   }
 
   @override
-  Iterable<js.Node> get containedNodes => _value == null ? const [] : [_value];
+  Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
 }
 
 /// A [StringReferenceResource] is a deferred JavaScript expression determined
@@ -159,13 +162,13 @@
   StringReferenceResource._(this._value, this.sourceInformation);
 
   set value(js.Expression value) {
-    assert(_value == null && value != null);
+    assert(!isFinalized && value != null);
     _value = value;
   }
 
   @override
   js.Expression get value {
-    assert(_value != null, 'StringReferenceResource is unassigned');
+    assert(isFinalized, 'StringReferenceResource is unassigned');
     return _value;
   }
 
@@ -173,6 +176,9 @@
   int get precedenceLevel => value.precedenceLevel;
 
   @override
+  bool get isFinalized => _value != null;
+
+  @override
   StringReferenceResource withSourceInformation(
       js.JavaScriptNodeSourceInformation newSourceInformation) {
     if (newSourceInformation == sourceInformation) return this;
@@ -181,7 +187,7 @@
   }
 
   @override
-  Iterable<js.Node> get containedNodes => _value == null ? const [] : [_value];
+  Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
 
   @override
   void visitChildren<T>(js.NodeVisitor<T> visitor) {
diff --git a/pkg/compiler/lib/src/js_backend/type_reference.dart b/pkg/compiler/lib/src/js_backend/type_reference.dart
index de1a5f3..f33591d 100644
--- a/pkg/compiler/lib/src/js_backend/type_reference.dart
+++ b/pkg/compiler/lib/src/js_backend/type_reference.dart
@@ -132,16 +132,19 @@
   }
 
   set value(js.Expression value) {
-    assert(_value == null && value != null);
+    assert(!isFinalized && value != null);
     _value = value;
   }
 
   @override
   js.Expression get value {
-    assert(_value != null, 'TypeReference is unassigned');
+    assert(isFinalized, 'TypeReference is unassigned');
     return _value;
   }
 
+  @override
+  bool get isFinalized => _value != null;
+
   // Precedence will be CALL or LEFT_HAND_SIDE depending on what expression the
   // reference is resolved to.
   @override
@@ -156,7 +159,7 @@
   }
 
   @override
-  Iterable<js.Node> get containedNodes => _value == null ? const [] : [_value];
+  Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
 }
 
 /// A [TypeReferenceResource] is a deferred JavaScript expression determined by
@@ -176,17 +179,20 @@
   TypeReferenceResource._(this._value, this.sourceInformation);
 
   set value(js.Expression value) {
-    assert(_value == null && value != null);
+    assert(!isFinalized && value != null);
     _value = value;
   }
 
   @override
   js.Expression get value {
-    assert(_value != null, 'TypeReferenceResource is unassigned');
+    assert(isFinalized, 'TypeReferenceResource is unassigned');
     return _value;
   }
 
   @override
+  bool get isFinalized => _value != null;
+
+  @override
   int get precedenceLevel => value.precedenceLevel;
 
   @override
@@ -198,7 +204,7 @@
   }
 
   @override
-  Iterable<js.Node> get containedNodes => _value == null ? const [] : [_value];
+  Iterable<js.Node> get containedNodes => isFinalized ? [_value] : const [];
 
   @override
   void visitChildren<T>(js.NodeVisitor<T> visitor) {
diff --git a/pkg/compiler/lib/src/js_emitter/metadata_collector.dart b/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
index 8066ff1..6fac2f9 100644
--- a/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
+++ b/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
@@ -41,15 +41,16 @@
   void markSeen(jsAst.TokenCounter visitor);
 }
 
-class _BoundMetadataEntry extends _MetadataEntry {
+class BoundMetadataEntry extends _MetadataEntry {
   int _value = -1;
   @override
   int _rc = 0;
   @override
   final jsAst.Expression entry;
 
-  _BoundMetadataEntry(this.entry);
+  BoundMetadataEntry(this.entry);
 
+  @override
   bool get isFinalized => _value != -1;
 
   finalize(int value) {
@@ -75,7 +76,7 @@
   int compareTo(covariant _MetadataEntry other) => other._rc - this._rc;
 
   @override
-  String toString() => '_BoundMetadataEntry($hashCode,rc=$_rc,_value=$_value)';
+  String toString() => 'BoundMetadataEntry($hashCode,rc=$_rc,_value=$_value)';
 }
 
 class _MetadataList extends jsAst.DeferredExpression {
@@ -114,8 +115,8 @@
   }
 
   /// A map used to canonicalize the entries of metadata.
-  Map<OutputUnit, Map<String, _BoundMetadataEntry>> _metadataMap =
-      <OutputUnit, Map<String, _BoundMetadataEntry>>{};
+  Map<OutputUnit, Map<String, BoundMetadataEntry>> _metadataMap =
+      <OutputUnit, Map<String, BoundMetadataEntry>>{};
 
   /// A map with a token for a lists of JS expressions, one token for each
   /// output unit. Once finalized, the entries represent types including
@@ -128,8 +129,8 @@
   }
 
   /// A map used to canonicalize the entries of types.
-  Map<OutputUnit, Map<DartType, _BoundMetadataEntry>> _typesMap =
-      <OutputUnit, Map<DartType, _BoundMetadataEntry>>{};
+  Map<OutputUnit, Map<DartType, BoundMetadataEntry>> _typesMap =
+      <OutputUnit, Map<DartType, BoundMetadataEntry>>{};
 
   MetadataCollector(this._options, this.reporter, this._emitter,
       this._rtiRecipeEncoder, this._elementEnvironment);
@@ -165,9 +166,9 @@
     String printed = jsAst.prettyPrint(node,
         enableMinification: _options.enableMinification,
         renamerForNames: nameToKey);
-    _metadataMap[outputUnit] ??= new Map<String, _BoundMetadataEntry>();
+    _metadataMap[outputUnit] ??= new Map<String, BoundMetadataEntry>();
     return _metadataMap[outputUnit].putIfAbsent(printed, () {
-      return new _BoundMetadataEntry(node);
+      return new BoundMetadataEntry(node);
     });
   }
 
@@ -177,36 +178,36 @@
   }
 
   jsAst.Expression addTypeInOutputUnit(DartType type, OutputUnit outputUnit) {
-    _typesMap[outputUnit] ??= new Map<DartType, _BoundMetadataEntry>();
+    _typesMap[outputUnit] ??= new Map<DartType, BoundMetadataEntry>();
     return _typesMap[outputUnit].putIfAbsent(type, () {
-      return new _BoundMetadataEntry(_computeTypeRepresentationNewRti(type));
+      return new BoundMetadataEntry(_computeTypeRepresentationNewRti(type));
     });
   }
 
   @override
   void finalizeTokens() {
-    void countTokensInTypes(Iterable<_BoundMetadataEntry> entries) {
+    void countTokensInTypes(Iterable<BoundMetadataEntry> entries) {
       jsAst.TokenCounter counter = new jsAst.TokenCounter();
       entries
-          .where((_BoundMetadataEntry e) => e._rc > 0)
-          .map((_BoundMetadataEntry e) => e.entry)
+          .where((BoundMetadataEntry e) => e._rc > 0)
+          .map((BoundMetadataEntry e) => e.entry)
           .forEach(counter.countTokens);
     }
 
-    jsAst.ArrayInitializer finalizeMap(Map<dynamic, _BoundMetadataEntry> map) {
-      bool isUsed(_BoundMetadataEntry entry) => entry.isUsed;
-      List<_BoundMetadataEntry> entries = map.values.where(isUsed).toList();
+    jsAst.ArrayInitializer finalizeMap(Map<dynamic, BoundMetadataEntry> map) {
+      bool isUsed(BoundMetadataEntry entry) => entry.isUsed;
+      List<BoundMetadataEntry> entries = map.values.where(isUsed).toList();
       entries.sort();
 
       // TODO(herhut): Bucket entries by index length and use a stable
       //               distribution within buckets.
       int count = 0;
-      for (_BoundMetadataEntry entry in entries) {
+      for (BoundMetadataEntry entry in entries) {
         entry.finalize(count++);
       }
 
       List<jsAst.Node> values =
-          entries.map((_BoundMetadataEntry e) => e.entry).toList();
+          entries.map((BoundMetadataEntry e) => e.entry).toList();
 
       return new jsAst.ArrayInitializer(values);
     }
diff --git a/pkg/compiler/test/js/debug_size_estimator.dart b/pkg/compiler/test/js/debug_size_estimator.dart
new file mode 100644
index 0000000..86b528c
--- /dev/null
+++ b/pkg/compiler/test/js/debug_size_estimator.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:compiler/src/js/size_estimator.dart';
+
+/// This class is supposed to be identical to [SizeEstimator] with the sole
+/// difference that it builds up strings to help debug estimates.
+class DebugSizeEstimator extends SizeEstimator {
+  StringBuffer resultBuffer = StringBuffer();
+  String get resultString => resultBuffer.toString();
+
+  @override
+  void emit(String s) {
+    resultBuffer.write(s);
+    super.emit(s);
+  }
+}
diff --git a/pkg/compiler/test/js/js_size_estimator_test.dart b/pkg/compiler/test/js/js_size_estimator_test.dart
new file mode 100644
index 0000000..cb5ce76
--- /dev/null
+++ b/pkg/compiler/test/js/js_size_estimator_test.dart
@@ -0,0 +1,125 @@
+// Copyright (c) 2020, 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.7
+
+import 'dart:convert' as json;
+import 'dart:io';
+
+import 'package:expect/expect.dart';
+import 'package:compiler/src/js/js.dart';
+import 'package:compiler/src/js/size_estimator.dart';
+import 'debug_size_estimator.dart';
+
+const String expressionsKey = 'expressions';
+const String statementsKey = 'statements';
+const String originalKey = 'original';
+const String expectedKey = 'expected';
+const String minifiedKey = 'minified';
+
+DebugSizeEstimator debugSizeEstimator(Node node) {
+  DebugSizeEstimator debugSizeEstimator = DebugSizeEstimator();
+  debugSizeEstimator.visit(node);
+
+  // Always verify the actual results from the [SizeEstimator].
+  // This is the actual test, though DebugSizeEstimator is pretty trivial.
+  int actualEstimate = EstimateSize(node);
+  Expect.equals(actualEstimate, debugSizeEstimator.charCount);
+  return debugSizeEstimator;
+}
+
+abstract class TestSuite {
+  String get key;
+  Node parse(String testCase);
+
+  String generateExpected(Node node) {
+    return debugSizeEstimator(node).resultString;
+  }
+
+  String generateMinified(Node node) {
+    return prettyPrint(node,
+        enableMinification: true,
+        preferSemicolonToNewlineInMinifiedOutput: true);
+  }
+
+  Map<String, String> goldenTestCase(goldenTestCaseJson) {
+    String original = goldenTestCaseJson[originalKey];
+    Node node = parse(original);
+    return {
+      originalKey: original,
+      expectedKey: generateExpected(node),
+      minifiedKey: generateMinified(node),
+    };
+  }
+
+  List<Map<String, String>> regenerateGoldens(currentGoldensJson) {
+    List<Map<String, String>> newGoldens = [];
+    for (var testCaseJson in currentGoldensJson) {
+      newGoldens.add(goldenTestCase(testCaseJson));
+    }
+    return newGoldens;
+  }
+
+  void verifyGoldens(goldensJson) {
+    for (var goldenTestCase in goldensJson) {
+      test(goldenTestCase[originalKey], goldenTestCase[expectedKey]);
+    }
+  }
+
+  void test(String original, String expected) {
+    var debugResults = debugSizeEstimator(parse(original));
+    Expect.equals(expected, debugResults.resultString);
+    Expect.equals(expected.length, debugResults.charCount);
+  }
+}
+
+class ExpressionTestSuite extends TestSuite {
+  @override
+  String get key => expressionsKey;
+
+  @override
+  Node parse(String expression) => js(expression);
+}
+
+class StatementTestSuite extends TestSuite {
+  @override
+  String get key => statementsKey;
+
+  @override
+  Node parse(String statement) => js.statement(statement);
+}
+
+List<TestSuite> testSuites = [
+  ExpressionTestSuite(),
+  StatementTestSuite(),
+];
+
+void generateGoldens(currentGoldens,
+    Map<String, List<Map<String, String>>> newGoldens, List<TestSuite> suites) {
+  for (var suite in suites) {
+    newGoldens[suite.key] = suite.regenerateGoldens(currentGoldens[suite.key]);
+  }
+}
+
+void testGoldens(currentGoldens, List<TestSuite> suites) {
+  for (var suite in suites) {
+    suite.verifyGoldens(currentGoldens[suite.key]);
+  }
+}
+
+void main(List<String> args) {
+  var goldenFile = 'pkg/compiler/test/js/size_estimator_expectations.json';
+  bool generate = args.contains('-g');
+  var currentGoldens = json.jsonDecode(File(goldenFile).readAsStringSync());
+
+  if (generate) {
+    Map<String, List<Map<String, String>>> newGoldens = {};
+    generateGoldens(currentGoldens, newGoldens, testSuites);
+
+    File(goldenFile).writeAsStringSync(
+        json.JsonEncoder.withIndent('  ').convert(newGoldens));
+  } else {
+    testGoldens(currentGoldens, testSuites);
+  }
+}
diff --git a/pkg/compiler/test/js/size_estimator_expectations.json b/pkg/compiler/test/js/size_estimator_expectations.json
new file mode 100644
index 0000000..57b911f
--- /dev/null
+++ b/pkg/compiler/test/js/size_estimator_expectations.json
@@ -0,0 +1,196 @@
+{
+  "expressions": [
+    {
+      "original": "1",
+      "expected": "1",
+      "minified": "1"
+    },
+    {
+      "original": "123456789",
+      "expected": "123456789",
+      "minified": "123456789"
+    },
+    {
+      "original": "true",
+      "expected": "!0",
+      "minified": "true"
+    },
+    {
+      "original": "false",
+      "expected": "!1",
+      "minified": "false"
+    },
+    {
+      "original": "a || b",
+      "expected": "#||#",
+      "minified": "a||b"
+    },
+    {
+      "original": "(a || b) && c",
+      "expected": "(#||#)&&#",
+      "minified": "(a||b)&&c"
+    },
+    {
+      "original": "a == true ? b : c",
+      "expected": "#==!0?#:#",
+      "minified": "a==true?b:c"
+    },
+    {
+      "original": "x = a + b * c",
+      "expected": "#=#+#*#",
+      "minified": "x=a+b*c"
+    },
+    {
+      "original": "a + (b == c) + d",
+      "expected": "#+(#==#)+#",
+      "minified": "a+(b==c)+d"
+    },
+    {
+      "original": "foo(bar)",
+      "expected": "#(#)",
+      "minified": "foo(bar)"
+    },
+    {
+      "original": "foo.bar(baz)",
+      "expected": "#.bar(#)",
+      "minified": "foo.bar(baz)"
+    },
+    {
+      "original": "foo({meaning: 42})",
+      "expected": "#({meaning:42})",
+      "minified": "foo({meaning:42})"
+    },
+    {
+      "original": "x = !x",
+      "expected": "#=!#",
+      "minified": "x=!x"
+    },
+    {
+      "original": "delete foo.bar",
+      "expected": "delete #.bar",
+      "minified": "delete foo.bar"
+    },
+    {
+      "original": "x in y",
+      "expected": "# in #",
+      "minified": "x in y"
+    },
+    {
+      "original": "x instanceof y",
+      "expected": "# instanceof #",
+      "minified": "x instanceof y"
+    },
+    {
+      "original": "x &= ~mask",
+      "expected": "#&=~#",
+      "minified": "x&=~mask"
+    },
+    {
+      "original": "await x++",
+      "expected": "await #++",
+      "minified": "await x++"
+    },
+    {
+      "original": "foo[x[bar]]",
+      "expected": "#[#[#]]",
+      "minified": "foo[x[bar]]"
+    },
+    {
+      "original": "x << 5",
+      "expected": "#<<5",
+      "minified": "x<<5"
+    },
+    {
+      "original": "x = ['a', 'b', 'c']",
+      "expected": "#=['a','b','c']",
+      "minified": "x=['a','b','c']"
+    },
+    {
+      "original": "a = {'b': 1, 'c': 2}",
+      "expected": "#={b:1,c:2}",
+      "minified": "a={b:1,c:2}"
+    },
+    {
+      "original": "foo([1, 2, 3])",
+      "expected": "#([1,2,3])",
+      "minified": "foo([1,2,3])"
+    },
+    {
+      "original": "a = b = c",
+      "expected": "#=#=#",
+      "minified": "a=b=c"
+    },
+    {
+      "original": "var a = \"\"",
+      "expected": "var #=\"\"",
+      "minified": "var a=\"\""
+    }
+  ],
+  "statements": [
+    {
+      "original": "{ 1; 2; 3; }",
+      "expected": "{1;2;3;}",
+      "minified": "{1;2;3}"
+    },
+    {
+      "original": "function foo() { /a/; 1; 2; }",
+      "expected": "function #(){/a/;1;2;}",
+      "minified": "function foo(){/a/;1;2}"
+    },
+    {
+      "original": "function foo() { return function bar() { return null; } }",
+      "expected": "function #(){return function #(){return null;};}",
+      "minified": "function foo(){return function bar(){return null}}"
+    },
+    {
+      "original": "if (x == true) { bar(); }",
+      "expected": "if(#==!0)#()",
+      "minified": "if(x==true)bar()"
+    },
+    {
+      "original": "if (x > 4 && y < 5) { 1; 2; }",
+      "expected": "if(#>4&&#<5){1;2;}",
+      "minified": "if(x>4&&y<5){1;2}"
+    },
+    {
+      "original": "if (x == true) { return true; } else if (y < 3 || z > 5) { return l != null ? 'a' : 4; } else { foo(); return; }",
+      "expected": "if(#==!0)return !0;else if(#<3||#>5)return #!=null?'a':4;else{#();return;}",
+      "minified": "if(x==true)return true;else if(y<3||z>5)return l!=null?'a':4;else{foo();return}"
+    },
+    {
+      "original": "for (var a = 0; a < 10; a++) { foo(a); }",
+      "expected": "for(var #=0;#<10;#++)#(#)",
+      "minified": "for(var a=0;a<10;a++)foo(a)"
+    },
+    {
+      "original": "for (var b in c) { var e = 1; foo(e); }",
+      "expected": "for(var # in #){var #=1;#(#);}",
+      "minified": "for(var b in c){var e=1;foo(e)}"
+    },
+    {
+      "original": "while (x != null) { foo(); }",
+      "expected": "while(#!=null)#()",
+      "minified": "while(x!=null)foo()"
+    },
+    {
+      "original": "do { print(1); do while (true) ; while (false); } while (a != b);",
+      "expected": "do{#(1);do while(!0);while(!1);}while(#!=#)",
+      "minified": "do{print(1);do while(true);while(false)}while(a!=b)"
+    },
+    {
+      "original": "switch (foo) { case 'a': case 'b': bar(); break; case 'c': 1; break; default: boo(); }",
+      "expected": "switch(#){case 'a':case 'b':#();break;case 'c':1;break;default:#();}",
+      "minified": "switch(foo){case'a':case'b':bar();break;case'c':1;break;default:boo()}"
+    },
+    {
+      "original": "foo.prototype.Goo = function(a) { return a.bar(); }",
+      "expected": "#.prototype.Goo=function(#){return #.bar();}",
+      "minified": "foo.prototype.Goo=function(a){return a.bar()}"
+    },
+    {
+      "original": "try { null = 4; } catch (e) { print(e); }",
+      "expected": "try{null=4;}catch(#){#(#);}",
+      "minified": "try{null=4}catch(e){print(e)}"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 9571c2a..5d79c5f 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -4428,6 +4428,33 @@
   js_ast.Expression _emitMethodCall(Expression receiver, Member target,
       Arguments arguments, InvocationExpression node) {
     var name = node.name.text;
+
+    /// Returns `true` when [node] represents an invocation of `List.add()` that
+    /// can be optimized.
+    ///
+    /// The optimized add operation can skip checks for a growable or modifiable
+    /// list and the element type is known to be invariant so it can skip the
+    /// type check.
+    bool isNativeListInvariantAdd(InvocationExpression node) {
+      if (node is MethodInvocation &&
+          node.isInvariant &&
+          node.name.name == 'add') {
+        // The call to add is marked as invariant, so the type check on the
+        // parameter to add is not needed.
+        var receiver = node.receiver;
+        if (receiver is VariableGet && receiver.variable.isFinal) {
+          // The receiver is a final variable, so it only contains the
+          // initializer value.
+          if (receiver.variable.initializer is ListLiteral) {
+            // The initializer is a list literal, so we know the list can be
+            // grown, modified, and is represented by a JavaScript Array.
+            return true;
+          }
+        }
+      }
+      return false;
+    }
+
     if (isOperatorMethodName(name) && arguments.named.isEmpty) {
       var argLength = arguments.positional.length;
       if (argLength == 0) {
@@ -4441,6 +4468,10 @@
     var jsReceiver = _visitExpression(receiver);
     var args = _emitArgumentList(arguments, target: target);
 
+    if (isNativeListInvariantAdd(node)) {
+      return js.call('#.push(#)', [jsReceiver, args]);
+    }
+
     var isCallingDynamicField = target is Member &&
         target.hasGetter &&
         _isDynamicOrFunction(target.getterType);
diff --git a/pkg/js_ast/lib/src/nodes.dart b/pkg/js_ast/lib/src/nodes.dart
index 82cdc4f..7a9298a 100644
--- a/pkg/js_ast/lib/src/nodes.dart
+++ b/pkg/js_ast/lib/src/nodes.dart
@@ -444,6 +444,8 @@
 
   bool get isCommaOperator => false;
 
+  bool get isFinalized => true;
+
   Statement toStatement() {
     throw new UnsupportedError('toStatement');
   }
@@ -1005,6 +1007,9 @@
 
   LiteralStringFromName(this.name) : super(null);
 
+  @override
+  bool get isFinalized => name.isFinalized;
+
   String get value => '"${name.name}"';
 
   void visitChildren<T>(NodeVisitor<T> visitor) {
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index f3875f3..b76ee89 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -145,6 +145,12 @@
   /// being visited, or null.
   Element _currentClassOrExtension;
 
+  /// If an extension declaration is being visited, the decorated type of the
+  /// type appearing in the `on` clause (this is the type of `this` inside the
+  /// extension declaration).  Null if an extension declaration is not being
+  /// visited.
+  DecoratedType _currentExtendedType;
+
   /// The [DecoratedType] of the innermost list or set literal being visited, or
   /// `null` if the visitor is not inside any list or set.
   ///
@@ -792,9 +798,12 @@
   }
 
   DecoratedType visitExtensionDeclaration(ExtensionDeclaration node) {
-    visitClassOrMixinOrExtensionDeclaration(node);
     _dispatch(node.typeParameters);
     _dispatch(node.extendedType);
+    _currentExtendedType =
+        _variables.decoratedTypeAnnotation(source, node.extendedType);
+    visitClassOrMixinOrExtensionDeclaration(node);
+    _currentExtendedType = null;
     return null;
   }
 
@@ -3148,22 +3157,8 @@
               .toList());
     } else {
       assert(_currentClassOrExtension is ExtensionElement);
-      final type = (_currentClassOrExtension as ExtensionElement).extendedType;
-
-      if (type is InterfaceType) {
-        var index = 0;
-        return DecoratedType(type, NullabilityNode.forInferredType(target),
-            typeArguments: type.typeArguments
-                .map((t) => DecoratedType(
-                    t,
-                    NullabilityNode.forInferredType(
-                        target.typeArgument(index++))))
-                .toList());
-      } else if (type is TypeParameterType) {
-        return DecoratedType(type, NullabilityNode.forInferredType(target));
-      } else {
-        _unimplemented(node, 'extension of $type (${type.runtimeType}');
-      }
+      assert(_currentExtendedType != null);
+      return _currentExtendedType;
     }
   }
 
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 1eb3737..d3075e1 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -2217,6 +2217,28 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_extension_on_generic_type() async {
+    var content = '''
+class C<T> {
+  final T value;
+  C(this.value);
+}
+extension E<T> on Future<C<T/*?*/>> {
+  Future<T> get asyncValue async => (await this).value;
+}
+''';
+    var expected = '''
+class C<T> {
+  final T value;
+  C(this.value);
+}
+extension E<T> on Future<C<T?>> {
+  Future<T?> get asyncValue async => (await this).value;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_extension_on_type_param_implementation() async {
     var content = '''
 abstract class C {
@@ -6408,11 +6430,11 @@
     await _checkSingleFileChanges(content, expected);
   }
 
-  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/39387')
   Future<void> test_this_inside_extension() async {
     var content = '''
 class C<T> {
   T field;
+  C(this.field);
 }
 extension on C<int> {
   f() {
@@ -6421,14 +6443,14 @@
 }
 extension on C<List<int>> {
   f() {
-    this.field = null;
+    this.field = [null];
   }
 }
 ''';
     var expected = '''
-
 class C<T> {
   T field;
+  C(this.field);
 }
 extension on C<int?> {
   f() {
diff --git a/pkg/nnbd_migration/test/edge_builder_test.dart b/pkg/nnbd_migration/test/edge_builder_test.dart
index 001a94b..e8f81c6 100644
--- a/pkg/nnbd_migration/test/edge_builder_test.dart
+++ b/pkg/nnbd_migration/test/edge_builder_test.dart
@@ -2929,7 +2929,6 @@
     // metadata was visited.
   }
 
-  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/39387')
   Future<void> test_extension_on_class_with_generic_type_arguments() async {
     await analyze('''
 class C<T> {}
@@ -2942,7 +2941,6 @@
     // adding assertion(s).
   }
 
-  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/39387')
   Future<void> test_extension_on_function_type() async {
     await analyze('''
 extension CurryFunction<R, S, T> on R Function(S, T) {
diff --git a/pkg/test_runner/lib/src/test_configurations.dart b/pkg/test_runner/lib/src/test_configurations.dart
index 4d3925a..3179fec 100644
--- a/pkg/test_runner/lib/src/test_configurations.dart
+++ b/pkg/test_runner/lib/src/test_configurations.dart
@@ -149,6 +149,12 @@
           // vm tests contain both cc tests (added here) and dart tests (added
           // in [TEST_SUITE_DIRECTORIES]).
           testSuites.add(VMTestSuite(configuration));
+        } else if (key == 'ffi_unit') {
+          // 'ffi_unit' contains cc non-DartVM unit tests.
+          //
+          // This is a separate suite from 'ffi', because we want to run the
+          // 'ffi' suite on many architectures, but 'ffi_unit' only on one.
+          testSuites.add(FfiTestSuite(configuration));
         } else if (configuration.compiler == Compiler.dart2analyzer) {
           if (key == 'analyze_library') {
             testSuites.add(AnalyzeLibraryTestSuite(configuration));
diff --git a/pkg/test_runner/lib/src/test_suite.dart b/pkg/test_runner/lib/src/test_suite.dart
index e788744..53af2c7 100644
--- a/pkg/test_runner/lib/src/test_suite.dart
+++ b/pkg/test_runner/lib/src/test_suite.dart
@@ -364,6 +364,128 @@
   VMUnitTest(this.name, this.expectation);
 }
 
+/// A specialized [TestSuite] that runs tests written in C to unit test
+/// the standalone (non-DartVM) C/C++ code.
+///
+/// The tests are compiled into an executable for all [targetAbis] by the
+/// build step.
+/// An executable lists its tests when run with the --list command line flag.
+/// Individual tests are run by specifying them on the command line.
+class FfiTestSuite extends TestSuite {
+  Map<String, String> runnerPaths;
+  final String dartDir;
+
+  static const targetAbis = [
+    "arm64_android",
+    "arm64_ios",
+    "arm64_linux",
+    "arm64_macos",
+    "arm_android",
+    "arm_ios",
+    "arm_linux",
+    "ia32_android",
+    "ia32_linux",
+    "ia32_win",
+    "x64_ios",
+    "x64_linux",
+    "x64_macos",
+    "x64_win",
+  ];
+
+  FfiTestSuite(TestConfiguration configuration)
+      : dartDir = Repository.dir.toNativePath(),
+        super(configuration, "ffi_unit", []) {
+    final binarySuffix = Platform.operatingSystem == 'windows' ? '.exe' : '';
+
+    // For running the tests we use multiple binaries, one for each target ABI.
+    runnerPaths = Map.fromIterables(
+        targetAbis,
+        targetAbis.map((String config) =>
+            '$buildDir/run_ffi_unit_tests_$config$binarySuffix'));
+  }
+
+  void findTestCases(TestCaseEvent onTest, Map testCache) {
+    final statusFiles =
+        statusFilePaths.map((statusFile) => "$dartDir/$statusFile").toList();
+    final expectations = ExpectationSet.read(statusFiles, configuration);
+
+    runnerPaths.forEach((runnerName, runnerPath) {
+      try {
+        for (final test in _listTests(runnerName, runnerPath)) {
+          _addTest(expectations, test, onTest);
+        }
+      } catch (error, s) {
+        print(
+            "Fatal error occurred while parsing tests from $runnerName: $error");
+        print(s);
+        exit(1);
+      }
+    });
+  }
+
+  void _addTest(
+      ExpectationSet testExpectations, FfiUnitTest test, TestCaseEvent onTest) {
+    final fullName = '${test.runnerName}/${test.name}';
+    var expectations = testExpectations.expectations(fullName);
+
+    // Get the expectation from the test itself.
+    final testExpectation = Expectation.find(test.expectation);
+
+    // Update the legacy status-file based expectations to include
+    // [testExpectation].
+    if (testExpectation != Expectation.pass) {
+      expectations = {...expectations, testExpectation};
+      expectations.remove(Expectation.pass);
+    }
+
+    // Update the new workflow based expectations to include [testExpectation].
+    final testFile = TestFile.vmUnitTest(
+        hasSyntaxError: false,
+        hasCompileError: testExpectation == Expectation.compileTimeError,
+        hasRuntimeError: testExpectation == Expectation.runtimeError,
+        hasStaticWarning: false,
+        hasCrash: testExpectation == Expectation.crash);
+
+    final args = [
+      // This test has no VM, but pipe through vmOptions as test options.
+      // Passing `--vm-options=--update` will update all test expectations.
+      ...configuration.vmOptions,
+      test.name,
+    ];
+    final command = ProcessCommand(
+        'run_ffi_unit_test', test.runnerPath, args, environmentOverrides);
+
+    _addTestCase(testFile, fullName, [command], expectations, onTest);
+  }
+
+  Iterable<FfiUnitTest> _listTests(String runnerName, String runnerPath) {
+    final result = Process.runSync(runnerPath, ["--list"]);
+    if (result.exitCode != 0) {
+      throw "Failed to list tests: '$runnerPath --list'. "
+          "Process exited with ${result.exitCode}";
+    }
+
+    return (result.stdout as String)
+        .split('\n')
+        .map((line) => line.trim())
+        .where((name) => name.isNotEmpty)
+        .map((String line) {
+      final parts = line.split(' ');
+      assert(parts.length == 2);
+      return FfiUnitTest(runnerName, runnerPath, parts[0], parts[1]);
+    });
+  }
+}
+
+class FfiUnitTest {
+  final String runnerName;
+  final String runnerPath;
+  final String name;
+  final String expectation;
+
+  FfiUnitTest(this.runnerName, this.runnerPath, this.name, this.expectation);
+}
+
 /// A standard [TestSuite] implementation that searches for tests in a
 /// directory, and creates [TestCase]s that compile and/or run them.
 class StandardTestSuite extends TestSuite {
diff --git a/pkg/test_runner/tool/update_static_error_tests.dart b/pkg/test_runner/tool/update_static_error_tests.dart
index 5364fc2..7432ff2 100644
--- a/pkg/test_runner/tool/update_static_error_tests.dart
+++ b/pkg/test_runner/tool/update_static_error_tests.dart
@@ -301,7 +301,7 @@
   return errors;
 }
 
-/// Find the most recently-built [binary] in any of the build directories.
+/// Find the most recently-built binary [name] in any of the build directories.
 String _findBinary(String name, String windowsExtension) {
   var binary = Platform.isWindows ? "$name.$windowsExtension" : name;
 
diff --git a/pkg/vm_service/test/get_stack_test.dart b/pkg/vm_service/test/get_stack_test.dart
index 6e61379..0f65938 100644
--- a/pkg/vm_service/test/get_stack_test.dart
+++ b/pkg/vm_service/test/get_stack_test.dart
@@ -74,17 +74,17 @@
     ]);
 
     expectFrames(result.asyncCausalFrames, [
-      [equals('AsyncCausal'), endsWith(' func10')],
-      [equals('AsyncCausal'), endsWith(' func9')],
-      [equals('AsyncCausal'), endsWith(' func8')],
-      [equals('AsyncCausal'), endsWith(' func7')],
-      [equals('AsyncCausal'), endsWith(' func6')],
-      [equals('AsyncCausal'), endsWith(' func5')],
-      [equals('AsyncCausal'), endsWith(' func4')],
-      [equals('AsyncCausal'), endsWith(' func3')],
-      [equals('AsyncCausal'), endsWith(' func2')],
-      [equals('AsyncCausal'), endsWith(' func1')],
-      [equals('AsyncCausal'), endsWith(' testMain')],
+      [equals('Regular'), endsWith(' func10')],
+      [equals('Regular'), endsWith(' func9')],
+      [equals('Regular'), endsWith(' func8')],
+      [equals('Regular'), endsWith(' func7')],
+      [equals('Regular'), endsWith(' func6')],
+      [equals('Regular'), endsWith(' func5')],
+      [equals('Regular'), endsWith(' func4')],
+      [equals('Regular'), endsWith(' func3')],
+      [equals('Regular'), endsWith(' func2')],
+      [equals('Regular'), endsWith(' func1')],
+      [equals('Regular'), endsWith(' testMain')],
     ]);
 
     expectFrames(result.awaiterFrames, [
@@ -126,7 +126,7 @@
     ]);
 
     expectFrames(result.asyncCausalFrames, [
-      [equals('AsyncCausal'), endsWith(' func10')],
+      [equals('Regular'), endsWith(' func10')],
       [equals('AsyncSuspensionMarker'), isNull],
       [equals('AsyncCausal'), endsWith(' func9')],
       [equals('AsyncSuspensionMarker'), isNull],
diff --git a/runtime/bin/ffi_unit_test/BUILD.gn b/runtime/bin/ffi_unit_test/BUILD.gn
new file mode 100644
index 0000000..d7c93ba
--- /dev/null
+++ b/runtime/bin/ffi_unit_test/BUILD.gn
@@ -0,0 +1,187 @@
+# Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import("../../platform/platform_sources.gni")
+import("../../vm/compiler/compiler_sources.gni")
+import("../../vm/vm_sources.gni")
+
+template("build_run_ffi_unit_tests") {
+  extra_configs = []
+  if (defined(invoker.extra_configs)) {
+    extra_configs += invoker.extra_configs
+  }
+
+  executable(target_name) {
+    configs += [] + extra_configs
+
+    defines = [
+      "TESTING",
+      "FFI_UNIT_TESTS",
+    ]
+
+    include_dirs = [
+      "../..",
+      "//third_party",
+    ]
+
+    constants = rebase_path(constants_sources, ".", "../../vm")
+    ffi_tests = rebase_path(ffi_sources_tests, ".", "../../vm/compiler")
+    platform = rebase_path(platform_sources, ".", "../../platform")
+
+    sources = [ "run_ffi_unit_tests.cc" ] + constants + ffi_tests + platform
+  }
+}
+
+config("define_target_arch_arm") {
+  defines = [ "TARGET_ARCH_ARM" ]
+}
+
+config("define_target_arch_arm64") {
+  defines = [ "TARGET_ARCH_ARM64" ]
+}
+
+config("define_target_arch_ia32") {
+  defines = [ "TARGET_ARCH_IA32" ]
+}
+
+config("define_target_arch_x64") {
+  defines = [ "TARGET_ARCH_X64" ]
+}
+
+config("define_target_os_android") {
+  defines = [ "TARGET_OS_ANDROID" ]
+}
+
+config("define_target_os_ios") {
+  defines = [ "TARGET_OS_IOS" ]
+}
+
+config("define_target_os_linux") {
+  defines = [ "TARGET_OS_LINUX" ]
+}
+
+config("define_target_os_macos") {
+  defines = [ "TARGET_OS_MACOS" ]
+}
+
+config("define_target_os_windows") {
+  defines = [ "TARGET_OS_WINDOWS" ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm_android") {
+  extra_configs = [
+    ":define_target_arch_arm",
+    ":define_target_os_android",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm_ios") {
+  extra_configs = [
+    ":define_target_arch_arm",
+    ":define_target_os_ios",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm_linux") {
+  extra_configs = [
+    ":define_target_arch_arm",
+    ":define_target_os_linux",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm64_android") {
+  extra_configs = [
+    ":define_target_arch_arm64",
+    ":define_target_os_android",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm64_ios") {
+  extra_configs = [
+    ":define_target_arch_arm64",
+    ":define_target_os_ios",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm64_macos") {
+  extra_configs = [
+    ":define_target_arch_arm64",
+    ":define_target_os_macos",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_arm64_linux") {
+  extra_configs = [
+    ":define_target_arch_arm64",
+    ":define_target_os_linux",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_ia32_android") {
+  extra_configs = [
+    ":define_target_arch_ia32",
+    ":define_target_os_android",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_ia32_linux") {
+  extra_configs = [
+    ":define_target_arch_ia32",
+    ":define_target_os_linux",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_ia32_win") {
+  extra_configs = [
+    ":define_target_arch_ia32",
+    ":define_target_os_windows",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_x64_ios") {
+  extra_configs = [
+    ":define_target_arch_x64",
+    ":define_target_os_ios",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_x64_linux") {
+  extra_configs = [
+    ":define_target_arch_x64",
+    ":define_target_os_linux",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_x64_macos") {
+  extra_configs = [
+    ":define_target_arch_x64",
+    ":define_target_os_macos",
+  ]
+}
+
+build_run_ffi_unit_tests("run_ffi_unit_tests_x64_win") {
+  extra_configs = [
+    ":define_target_arch_x64",
+    ":define_target_os_windows",
+  ]
+}
+
+group("run_ffi_unit_tests") {
+  deps = [
+    ":run_ffi_unit_tests_arm64_android",
+    ":run_ffi_unit_tests_arm64_ios",  # No other test coverage.
+    ":run_ffi_unit_tests_arm64_linux",
+    ":run_ffi_unit_tests_arm64_macos",
+    ":run_ffi_unit_tests_arm_android",  # SoftFP
+    ":run_ffi_unit_tests_arm_ios",  # No other test coverage.
+    ":run_ffi_unit_tests_arm_linux",  # HardFP
+    ":run_ffi_unit_tests_ia32_android",  # Emulator, no other test coverage.
+    ":run_ffi_unit_tests_ia32_linux",
+    ":run_ffi_unit_tests_ia32_win",
+    ":run_ffi_unit_tests_x64_ios",  # Simulator, no other test coverage.
+    ":run_ffi_unit_tests_x64_linux",
+    ":run_ffi_unit_tests_x64_macos",
+    ":run_ffi_unit_tests_x64_win",
+  ]
+}
diff --git a/runtime/bin/ffi_unit_test/run_ffi_unit_tests.cc b/runtime/bin/ffi_unit_test/run_ffi_unit_tests.cc
new file mode 100644
index 0000000..8fe8784
--- /dev/null
+++ b/runtime/bin/ffi_unit_test/run_ffi_unit_tests.cc
@@ -0,0 +1,115 @@
+// Copyright (c) 2020, 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.
+
+// A slimmed down version of bin/run_vm_tests.cc that only runs C++ non-DartVM
+// unit tests.
+//
+// By slimming it down to non-VM, we can run with the defines for all target
+// architectures and operating systems.
+
+#include "vm/compiler/ffi/unit_test.h"
+
+#include "platform/assert.h"
+#include "platform/syslog.h"
+
+namespace dart {
+namespace compiler {
+namespace ffi {
+
+static const char* const kNone = "No Test";
+static const char* const kList = "List all Tests";
+static const char* const kAll = "Run all Tests";
+static const char* run_filter = kNone;
+
+static const char* kCommandAll = "--all";
+static const char* kCommandList = "--list";
+static const char* kCommandUpdate = "--update";
+
+static int run_matches = 0;
+
+TestCaseBase* TestCaseBase::first_ = nullptr;
+TestCaseBase* TestCaseBase::tail_ = nullptr;
+bool TestCaseBase::update_expectations = false;
+
+TestCaseBase::TestCaseBase(const char* name, const char* expectation)
+    : next_(nullptr), name_(name), expectation_(expectation) {
+  ASSERT(strlen(expectation) > 0);
+  if (first_ == nullptr) {
+    first_ = this;
+  } else {
+    tail_->next_ = this;
+  }
+  tail_ = this;
+}
+
+void TestCaseBase::RunAll() {
+  TestCaseBase* test = first_;
+  while (test != nullptr) {
+    test->RunTest();
+    test = test->next_;
+  }
+}
+
+void TestCaseBase::RunTest() {
+  if (run_filter == kList) {
+    Syslog::Print("%s %s\n", this->name(), this->expectation());
+    run_matches++;
+  } else if (run_filter == kAll) {
+    this->Run();
+    run_matches++;
+  } else if (strcmp(run_filter, this->name()) == 0) {
+    this->Run();
+    run_matches++;
+  }
+}
+
+void RawTestCase::Run() {
+  Syslog::Print("Running test: %s\n", name());
+  (*run_)();
+  Syslog::Print("Done: %s\n", name());
+}
+
+static int Main(int argc, const char** argv) {
+  if (argc == 2 && strcmp(argv[1], kCommandList) == 0) {
+    run_filter = kList;
+    // List all tests and benchmarks and exit.
+    TestCaseBase::RunAll();
+    fflush(stdout);
+    return 0;
+  }
+  if (argc > 1 && strcmp(argv[1], kCommandUpdate) == 0) {
+    TestCaseBase::update_expectations = true;
+  }
+  if (strcmp(argv[argc - 1], kCommandAll) == 0) {
+    // Run all tests.
+    run_filter = kAll;
+  } else if (argc > 1) {
+    // Run only test with specific name.
+    run_filter = argv[argc - 1];
+  }
+
+  TestCaseBase::RunAll();
+
+  // Print a warning message if no tests or benchmarks were matched.
+  if (run_matches == 0) {
+    Syslog::PrintErr("No tests matched: %s\n", run_filter);
+    return 1;
+  }
+  if (Expect::failed()) {
+    Syslog::PrintErr(
+        "Some tests failed. Run the following command to update "
+        "expectations.\ntools/test.py --vm-options=--update ffi_unit");
+    return 255;
+  }
+
+  return 0;
+}
+
+}  // namespace ffi
+}  // namespace compiler
+}  // namespace dart
+
+int main(int argc, const char** argv) {
+  return dart::compiler::ffi::Main(argc, argv);
+}
diff --git a/runtime/platform/globals.h b/runtime/platform/globals.h
index ab51780..79b6a6b 100644
--- a/runtime/platform/globals.h
+++ b/runtime/platform/globals.h
@@ -339,17 +339,17 @@
 // Verify that host and target architectures match, we cannot
 // have a 64 bit Dart VM generating 32 bit code or vice-versa.
 #if defined(TARGET_ARCH_X64) || defined(TARGET_ARCH_ARM64)
-#if !defined(ARCH_IS_64_BIT)
+#if !defined(ARCH_IS_64_BIT) && !defined(FFI_UNIT_TESTS)
 #error Mismatched Host/Target architectures.
-#endif  // !defined(ARCH_IS_64_BIT)
+#endif  // !defined(ARCH_IS_64_BIT) && !defined(FFI_UNIT_TESTS)
 #elif defined(TARGET_ARCH_IA32) || defined(TARGET_ARCH_ARM)
 #if defined(HOST_ARCH_X64) && defined(TARGET_ARCH_ARM)
 // This is simarm_x64, which is the only case where host/target architecture
-// mismatch is allowed.
+// mismatch is allowed. Unless, we're running FFI unit tests.
 #define IS_SIMARM_X64 1
-#elif !defined(ARCH_IS_32_BIT)
+#elif !defined(ARCH_IS_32_BIT) && !defined(FFI_UNIT_TESTS)
 #error Mismatched Host/Target architectures.
-#endif  // !defined(ARCH_IS_32_BIT)
+#endif  // !defined(ARCH_IS_32_BIT) && !defined(FFI_UNIT_TESTS)
 #endif  // defined(TARGET_ARCH_IA32) || defined(TARGET_ARCH_ARM)
 
 // Determine whether we will be using the simulator.
diff --git a/runtime/tools/embedder_layering_check.py b/runtime/tools/embedder_layering_check.py
index d49b705..8e33bf0 100644
--- a/runtime/tools/embedder_layering_check.py
+++ b/runtime/tools/embedder_layering_check.py
@@ -20,7 +20,9 @@
 
 # Tests that don't match the simple case of *_test.cc.
 EXTRA_TEST_FILES = [
-    'runtime/bin/run_vm_tests.cc', 'runtime/vm/libfuzzer/dart_libfuzzer.cc'
+    'runtime/bin/run_vm_tests.cc',
+    'runtime/bin/ffi_unit_test/run_ffi_unit_tests.cc',
+    'runtime/vm/libfuzzer/dart_libfuzzer.cc'
 ]
 
 
diff --git a/runtime/vm/compilation_trace.cc b/runtime/vm/compilation_trace.cc
index 0db337d..5a86366 100644
--- a/runtime/vm/compilation_trace.cc
+++ b/runtime/vm/compilation_trace.cc
@@ -744,6 +744,8 @@
       fields_ = cls_.fields();
     }
 
+    SafepointWriteRwLocker ml(thread_,
+                              thread_->isolate_group()->program_lock());
     for (intptr_t i = 0; i < num_fields; i++) {
       field_name_ = ReadString();
       intptr_t guarded_cid = cid_map_[ReadInt()];
diff --git a/runtime/vm/compiler/compiler_sources.gni b/runtime/vm/compiler/compiler_sources.gni
index dd1d2fa..386dd86 100644
--- a/runtime/vm/compiler/compiler_sources.gni
+++ b/runtime/vm/compiler/compiler_sources.gni
@@ -204,3 +204,10 @@
   "assembler/disassembler_arm64.cc",
   "assembler/disassembler_x86.cc",
 ]
+
+ffi_sources_tests = [
+  "ffi/native_calling_convention_test.cc",
+  "ffi/native_location_test.cc",
+  "ffi/native_type_test.cc",
+  "ffi/unit_test_custom_zone.cc",
+]
diff --git a/runtime/vm/compiler/ffi/native_calling_convention.cc b/runtime/vm/compiler/ffi/native_calling_convention.cc
index fe3881c..5b21cf2 100644
--- a/runtime/vm/compiler/ffi/native_calling_convention.cc
+++ b/runtime/vm/compiler/ffi/native_calling_convention.cc
@@ -6,9 +6,12 @@
 
 #include "vm/compiler/ffi/native_location.h"
 #include "vm/compiler/ffi/native_type.h"
-#include "vm/cpu.h"
 #include "vm/zone_text_buffer.h"
 
+#if !defined(FFI_UNIT_TESTS)
+#include "vm/cpu.h"
+#endif
+
 namespace dart {
 
 namespace compiler {
@@ -17,6 +20,7 @@
 
 const intptr_t kNoFpuRegister = -1;
 
+#if !defined(FFI_UNIT_TESTS)
 // In Soft FP, floats and doubles get passed in integer registers.
 static bool SoftFpAbi() {
 #if defined(TARGET_ARCH_ARM)
@@ -25,6 +29,15 @@
   return false;
 #endif
 }
+#else  // !defined(FFI_UNIT_TESTS)
+static bool SoftFpAbi() {
+#if defined(TARGET_ARCH_ARM) && defined(TARGET_OS_ANDROID)
+  return true;
+#else
+  return false;
+#endif
+}
+#endif  // !defined(FFI_UNIT_TESTS)
 
 // In Soft FP, floats are treated as 4 byte ints, and doubles as 8 byte ints.
 static const NativeType& ConvertIfSoftFp(Zone* zone, const NativeType& rep) {
@@ -293,9 +306,11 @@
   return textBuffer.buffer();
 }
 
+#if !defined(FFI_UNIT_TESTS)
 const char* NativeCallingConvention::ToCString(bool multi_line) const {
   return ToCString(Thread::Current()->zone(), multi_line);
 }
+#endif
 
 }  // namespace ffi
 
diff --git a/runtime/vm/compiler/ffi/native_calling_convention.h b/runtime/vm/compiler/ffi/native_calling_convention.h
index bb10887..9faf620 100644
--- a/runtime/vm/compiler/ffi/native_calling_convention.h
+++ b/runtime/vm/compiler/ffi/native_calling_convention.h
@@ -42,7 +42,9 @@
   void PrintTo(BaseTextBuffer* f, bool multi_line = false) const;
   void PrintToMultiLine(BaseTextBuffer* f) const;
   const char* ToCString(Zone* zone, bool multi_line = false) const;
+#if !defined(FFI_UNIT_TESTS)
   const char* ToCString(bool multi_line = false) const;
+#endif
 
  private:
   NativeCallingConvention(const NativeLocations& argument_locations,
diff --git a/runtime/vm/compiler/ffi/native_calling_convention_test.cc b/runtime/vm/compiler/ffi/native_calling_convention_test.cc
new file mode 100644
index 0000000..23ed777
--- /dev/null
+++ b/runtime/vm/compiler/ffi/native_calling_convention_test.cc
@@ -0,0 +1,138 @@
+// Copyright (c) 2020, 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 "vm/compiler/ffi/unit_test.h"
+
+#include "platform/syslog.h"
+#include "vm/compiler/ffi/native_calling_convention.h"
+
+namespace dart {
+namespace compiler {
+namespace ffi {
+
+#if defined(TARGET_ARCH_ARM)
+const char* kArch = "arm";
+#elif defined(TARGET_ARCH_ARM64)
+const char* kArch = "arm64";
+#elif defined(TARGET_ARCH_IA32)
+const char* kArch = "ia32";
+#elif defined(TARGET_ARCH_X64)
+const char* kArch = "x64";
+#endif
+
+#if defined(TARGET_OS_ANDROID)
+const char* kOs = "android";
+#elif defined(TARGET_OS_IOS)
+const char* kOs = "ios";
+#elif defined(TARGET_OS_LINUX)
+const char* kOs = "linux";
+#elif defined(TARGET_OS_MACOS)
+const char* kOs = "macos";
+#elif defined(TARGET_OS_WINDOWS)
+const char* kOs = "win";
+#endif
+
+void WriteToFile(char* path, const char* contents) {
+  FILE* file;
+  file = fopen(path, "w");
+  if (file != nullptr) {
+    fprintf(file, "%s", contents);
+  } else {
+    Syslog::Print("Error %d \n", errno);
+  }
+  fclose(file);
+}
+
+void ReadFromFile(char* path, char** buffer_pointer) {
+  FILE* file = fopen(path, "rb");
+  if (file == nullptr) {
+    Syslog::Print("Error %d \n", errno);
+    return;
+  }
+
+  fseek(file, 0, SEEK_END);
+  size_t size = ftell(file);
+  rewind(file);
+
+  char* buffer = reinterpret_cast<char*>(malloc(sizeof(char) * (size + 1)));
+
+  fread(buffer, 1, size, file);
+  buffer[size] = 0;
+
+  fclose(file);
+  *buffer_pointer = buffer;
+}
+
+void RunSignatureTest(dart::Zone* zone,
+                      const char* name,
+                      const NativeTypes& argument_types,
+                      const NativeType& return_type) {
+  const auto& native_signature =
+      *new (zone) NativeFunctionType(argument_types, return_type);
+
+  const auto& native_calling_convention =
+      NativeCallingConvention::FromSignature(zone, native_signature);
+
+  const char* test_result =
+      native_calling_convention.ToCString(zone, /*multi_line=*/true);
+
+  const int kFilePathLength = 100;
+  char expectation_file_path[kFilePathLength];
+  Utils::SNPrint(expectation_file_path, kFilePathLength,
+                 "runtime/vm/compiler/ffi/unit_tests/%s/%s_%s.expect", name,
+                 kArch, kOs);
+
+  if (TestCaseBase::update_expectations) {
+    Syslog::Print("Updating %s\n", expectation_file_path);
+    WriteToFile(expectation_file_path, test_result);
+  }
+
+  char* expectation_file_contents = nullptr;
+  ReadFromFile(expectation_file_path, &expectation_file_contents);
+  EXPECT_NOTNULL(expectation_file_contents);
+  if (expectation_file_contents != nullptr) {
+    EXPECT_STREQ(expectation_file_contents, test_result);
+    free(expectation_file_contents);
+  }
+}
+
+UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_int8x10) {
+  const auto& int8type = *new (Z) NativePrimitiveType(kInt8);
+
+  auto& arguments = *new (Z) NativeTypes(Z, 10);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+  arguments.Add(&int8type);
+
+  RunSignatureTest(Z, "int8x10", arguments, int8type);
+}
+
+UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_floatx10) {
+  const auto& floatType = *new (Z) NativePrimitiveType(kFloat);
+
+  auto& arguments = *new (Z) NativeTypes(Z, 10);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+  arguments.Add(&floatType);
+
+  RunSignatureTest(Z, "floatx10", arguments, floatType);
+}
+
+}  // namespace ffi
+}  // namespace compiler
+}  // namespace dart
diff --git a/runtime/vm/compiler/ffi/native_location.cc b/runtime/vm/compiler/ffi/native_location.cc
index dce51c1..12439aa 100644
--- a/runtime/vm/compiler/ffi/native_location.cc
+++ b/runtime/vm/compiler/ffi/native_location.cc
@@ -12,6 +12,7 @@
 
 namespace ffi {
 
+#if !defined(FFI_UNIT_TESTS)
 bool NativeLocation::LocationCanBeExpressed(Location loc, Representation rep) {
   switch (loc.kind()) {
     case Location::Kind::kRegister:
@@ -74,6 +75,7 @@
   const Location loc = pair_loc.AsPairLocation()->At(index);
   return FromLocation(zone, loc, rep);
 }
+#endif
 
 const NativeRegistersLocation& NativeLocation::AsRegisters() const {
   ASSERT(IsRegisters());
@@ -90,6 +92,7 @@
   return static_cast<const NativeStackLocation&>(*this);
 }
 
+#if !defined(FFI_UNIT_TESTS)
 Location NativeRegistersLocation::AsLocation() const {
   ASSERT(IsExpressibleAsLocation());
   switch (num_regs()) {
@@ -126,6 +129,7 @@
   }
   UNREACHABLE();
 }
+#endif
 
 NativeRegistersLocation& NativeRegistersLocation::Split(Zone* zone,
                                                         intptr_t index) const {
@@ -206,10 +210,12 @@
   return other_stack.offset_in_bytes_ == offset_in_bytes_;
 }
 
+#if !defined(FFI_UNIT_TESTS)
 compiler::Address NativeLocationToStackSlotAddress(
     const NativeStackLocation& loc) {
   return compiler::Address(loc.base_register(), loc.offset_in_bytes());
 }
+#endif
 
 static void PrintRepresentations(BaseTextBuffer* f, const NativeLocation& loc) {
   f->AddString(" ");
@@ -271,9 +277,11 @@
   return textBuffer.buffer();
 }
 
+#if !defined(FFI_UNIT_TESTS)
 const char* NativeLocation::ToCString() const {
   return ToCString(Thread::Current()->zone());
 }
+#endif
 
 intptr_t SizeFromFpuRegisterKind(enum FpuRegisterKind kind) {
   switch (kind) {
diff --git a/runtime/vm/compiler/ffi/native_location.h b/runtime/vm/compiler/ffi/native_location.h
index c3cce14..3437e5c 100644
--- a/runtime/vm/compiler/ffi/native_location.h
+++ b/runtime/vm/compiler/ffi/native_location.h
@@ -10,17 +10,24 @@
 #endif  // defined(DART_PRECOMPILED_RUNTIME)
 
 #include "platform/assert.h"
-#include "vm/compiler/backend/locations.h"
 #include "vm/compiler/ffi/native_type.h"
 #include "vm/constants.h"
 #include "vm/growable_array.h"
 
+#if !defined(FFI_UNIT_TESTS)
+#include "vm/compiler/backend/locations.h"
+#endif
+
 namespace dart {
 
 class BaseTextBuffer;
 
 namespace compiler {
 
+namespace target {
+extern const int kWordSize;
+}
+
 namespace ffi {
 
 class NativeRegistersLocation;
@@ -55,6 +62,7 @@
 // inequality cannot be used to determine disjointness.
 class NativeLocation : public ZoneAllocated {
  public:
+#if !defined(FFI_UNIT_TESTS)
   static bool LocationCanBeExpressed(Location loc, Representation rep);
   static NativeLocation& FromLocation(Zone* zone,
                                       Location loc,
@@ -63,6 +71,7 @@
                                           Location loc,
                                           Representation rep,
                                           intptr_t index);
+#endif
 
   // The type of the data at this location.
   const NativeType& payload_type() const { return payload_type_; }
@@ -91,14 +100,18 @@
   virtual bool IsStack() const { return false; }
 
   virtual bool IsExpressibleAsLocation() const { return false; }
+#if !defined(FFI_UNIT_TESTS)
   virtual Location AsLocation() const {
     ASSERT(IsExpressibleAsLocation());
     UNREACHABLE();
   }
+#endif
 
   virtual void PrintTo(BaseTextBuffer* f) const;
   const char* ToCString(Zone* zone) const;
+#if !defined(FFI_UNIT_TESTS)
   const char* ToCString() const;
+#endif
 
   const NativeRegistersLocation& AsRegisters() const;
   const NativeFpuRegistersLocation& AsFpuRegisters() const;
@@ -171,7 +184,9 @@
   virtual bool IsExpressibleAsLocation() const {
     return num_regs() == 1 || num_regs() == 2;
   }
+#if !defined(FFI_UNIT_TESTS)
   virtual Location AsLocation() const;
+#endif
   intptr_t num_regs() const { return regs_->length(); }
   Register reg_at(intptr_t index) const { return regs_->At(index); }
 
@@ -238,10 +253,12 @@
   virtual bool IsExpressibleAsLocation() const {
     return fpu_reg_kind_ == kQuadFpuReg;
   }
+#if !defined(FFI_UNIT_TESTS)
   virtual Location AsLocation() const {
     ASSERT(IsExpressibleAsLocation());
     return Location::FpuRegisterLocation(fpu_reg());
   }
+#endif
   FpuRegisterKind fpu_reg_kind() const { return fpu_reg_kind_; }
   FpuRegister fpu_reg() const {
     ASSERT(fpu_reg_kind_ == kQuadFpuReg);
@@ -299,6 +316,8 @@
            size % compiler::target::kWordSize == 0 &&
            (size_slots == 1 || size_slots == 2);
   }
+
+#if !defined(FFI_UNIT_TESTS)
   virtual Location AsLocation() const;
 
   // ConstantInstr expects DoubleStackSlot for doubles, even on 64-bit systems.
@@ -308,6 +327,7 @@
     ASSERT(compiler::target::kWordSize == 8);
     return Location::DoubleStackSlot(offset_in_words(), base_register_);
   }
+#endif
 
   virtual NativeStackLocation& Split(Zone* zone, intptr_t index) const;
 
@@ -334,9 +354,11 @@
   DISALLOW_COPY_AND_ASSIGN(NativeStackLocation);
 };
 
+#if !defined(FFI_UNIT_TESTS)
 // Return a memory operand for stack slot locations.
 compiler::Address NativeLocationToStackSlotAddress(
     const NativeStackLocation& loc);
+#endif
 
 }  // namespace ffi
 
diff --git a/runtime/vm/compiler/ffi/native_location_test.cc b/runtime/vm/compiler/ffi/native_location_test.cc
new file mode 100644
index 0000000..ea1fb27
--- /dev/null
+++ b/runtime/vm/compiler/ffi/native_location_test.cc
@@ -0,0 +1,40 @@
+// Copyright (c) 2020, 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 "vm/compiler/ffi/unit_test.h"
+
+#include "vm/compiler/ffi/native_location.h"
+
+namespace dart {
+namespace compiler {
+namespace ffi {
+
+UNIT_TEST_CASE_WITH_ZONE(NativeStackLocation) {
+  const auto& native_type = *new (Z) NativePrimitiveType(kInt8);
+
+  const int kUnalignedStackLocation = 17;
+
+  const auto& native_location = *new (Z) NativeStackLocation(
+      native_type, native_type, SPREG, kUnalignedStackLocation);
+
+  EXPECT_EQ(kUnalignedStackLocation + native_type.SizeInBytes(),
+            native_location.StackTopInBytes());
+}
+
+UNIT_TEST_CASE_WITH_ZONE(NativeStackLocation_Split) {
+  const auto& native_type = *new (Z) NativePrimitiveType(kInt64);
+
+  const auto& native_location =
+      *new (Z) NativeStackLocation(native_type, native_type, SPREG, 0);
+
+  const auto& half_0 = native_location.Split(Z, 0);
+  const auto& half_1 = native_location.Split(Z, 1);
+
+  EXPECT_EQ(0, half_0.offset_in_bytes());
+  EXPECT_EQ(4, half_1.offset_in_bytes());
+}
+
+}  // namespace ffi
+}  // namespace compiler
+}  // namespace dart
diff --git a/runtime/vm/compiler/ffi/native_type.cc b/runtime/vm/compiler/ffi/native_type.cc
index 7c1faae..db06400 100644
--- a/runtime/vm/compiler/ffi/native_type.cc
+++ b/runtime/vm/compiler/ffi/native_type.cc
@@ -10,9 +10,9 @@
 #include "vm/constants.h"
 #include "vm/zone_text_buffer.h"
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 #include "vm/compiler/backend/locations.h"
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
 namespace dart {
 
@@ -127,7 +127,7 @@
   }
 }
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 bool NativePrimitiveType::IsExpressibleAsRepresentation() const {
   switch (representation_) {
     case kInt8:
@@ -170,7 +170,7 @@
       UNREACHABLE();
   }
 }
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
 bool NativePrimitiveType::Equals(const NativeType& other) const {
   if (!other.IsPrimitive()) {
@@ -249,12 +249,14 @@
   return *new (zone) NativePrimitiveType(fundamental_rep);
 }
 
+#if !defined(FFI_UNIT_TESTS)
 NativeType& NativeType::FromAbstractType(Zone* zone, const AbstractType& type) {
   // TODO(36730): Support composites.
   return NativeType::FromTypedDataClassId(zone, type.type_class_id());
 }
+#endif
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 static PrimitiveType fundamental_rep(Representation rep) {
   switch (rep) {
     case kUnboxedDouble:
@@ -277,7 +279,7 @@
                                                            Representation rep) {
   return *new (zone) NativePrimitiveType(fundamental_rep(rep));
 }
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
 const char* NativeType::ToCString(Zone* zone) const {
   ZoneTextBuffer textBuffer(zone);
@@ -285,9 +287,11 @@
   return textBuffer.buffer();
 }
 
+#if !defined(FFI_UNIT_TESTS)
 const char* NativeType::ToCString() const {
   return ToCString(Thread::Current()->zone());
 }
+#endif
 
 static const char* PrimitiveTypeToCString(PrimitiveType rep) {
   switch (rep) {
@@ -334,9 +338,11 @@
   return textBuffer.buffer();
 }
 
+#if !defined(FFI_UNIT_TESTS)
 const char* NativeFunctionType::ToCString() const {
   return ToCString(Thread::Current()->zone());
 }
+#endif
 
 void NativeFunctionType::PrintTo(BaseTextBuffer* f) const {
   f->AddString("(");
diff --git a/runtime/vm/compiler/ffi/native_type.h b/runtime/vm/compiler/ffi/native_type.h
index d2ca9c1..3c11341 100644
--- a/runtime/vm/compiler/ffi/native_type.h
+++ b/runtime/vm/compiler/ffi/native_type.h
@@ -9,11 +9,13 @@
 #include "platform/globals.h"
 #include "vm/allocation.h"
 #include "vm/growable_array.h"
-#include "vm/object.h"
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 #include "vm/compiler/backend/locations.h"
 #endif
+#if !defined(FFI_UNIT_TESTS)
+#include "vm/object.h"
+#endif
 
 namespace dart {
 
@@ -51,13 +53,15 @@
 // TODO(36730): Add composites.
 class NativeType : public ZoneAllocated {
  public:
+#if !defined(FFI_UNIT_TESTS)
   static NativeType& FromAbstractType(Zone* zone, const AbstractType& type);
+#endif
   static NativeType& FromTypedDataClassId(Zone* zone, classid_t class_id);
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
   static NativePrimitiveType& FromUnboxedRepresentation(Zone* zone,
                                                         Representation rep);
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
   virtual bool IsPrimitive() const { return false; }
   const NativePrimitiveType& AsPrimitive() const;
@@ -79,7 +83,7 @@
   // The alignment in bytes of this representation as member of a composite.
   virtual intptr_t AlignmentInBytesField() const = 0;
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
   // NativeTypes which are available as unboxed Representations.
   virtual bool IsExpressibleAsRepresentation() const { return false; }
 
@@ -91,7 +95,7 @@
     const auto& widened = WidenTo4Bytes(zone_);
     return widened.AsRepresentation();
   }
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
   virtual bool Equals(const NativeType& other) const { UNREACHABLE(); }
 
@@ -104,7 +108,9 @@
 
   virtual void PrintTo(BaseTextBuffer* f) const;
   const char* ToCString(Zone* zone) const;
+#if !defined(FFI_UNIT_TESTS)
   const char* ToCString() const;
+#endif
 
   virtual ~NativeType() {}
 
@@ -152,10 +158,10 @@
   virtual intptr_t AlignmentInBytesStack() const;
   virtual intptr_t AlignmentInBytesField() const;
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
   virtual bool IsExpressibleAsRepresentation() const;
   virtual Representation AsRepresentation() const;
-#endif  // !defined(DART_PRECOMPILED_RUNTIME)
+#endif  // !defined(DART_PRECOMPILED_RUNTIME) && !defined(FFI_UNIT_TESTS)
 
   virtual bool Equals(const NativeType& other) const;
   virtual NativePrimitiveType& Split(Zone* zone, intptr_t part) const;
@@ -181,8 +187,9 @@
 
   void PrintTo(BaseTextBuffer* f) const;
   const char* ToCString(Zone* zone) const;
-
+#if !defined(FFI_UNIT_TESTS)
   const char* ToCString() const;
+#endif
 
  private:
   const NativeTypes& argument_types_;
diff --git a/runtime/vm/compiler/ffi/native_type_test.cc b/runtime/vm/compiler/ffi/native_type_test.cc
new file mode 100644
index 0000000..abf275d
--- /dev/null
+++ b/runtime/vm/compiler/ffi/native_type_test.cc
@@ -0,0 +1,25 @@
+// Copyright (c) 2020, 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 "vm/compiler/ffi/unit_test.h"
+
+#include "vm/compiler/ffi/native_type.h"
+
+namespace dart {
+namespace compiler {
+namespace ffi {
+
+UNIT_TEST_CASE_WITH_ZONE(NativeType) {
+  const auto& native_type = *new (Z) NativePrimitiveType(kInt8);
+
+  EXPECT_EQ(1, native_type.SizeInBytes());
+  EXPECT(native_type.IsInt());
+  EXPECT(native_type.IsPrimitive());
+
+  EXPECT_STREQ("int8", native_type.ToCString(Z));
+}
+
+}  // namespace ffi
+}  // namespace compiler
+}  // namespace dart
diff --git a/runtime/vm/compiler/ffi/unit_test.h b/runtime/vm/compiler/ffi/unit_test.h
new file mode 100644
index 0000000..44c4f0d
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_test.h
@@ -0,0 +1,85 @@
+// Copyright (c) 2020, 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.
+
+// A slimmed down version of runtime/vm/unit_test.h that only runs C++
+// non-DartVM unit tests.
+
+#ifndef RUNTIME_VM_COMPILER_FFI_UNIT_TEST_H_
+#define RUNTIME_VM_COMPILER_FFI_UNIT_TEST_H_
+
+// Don't use the DartVM zone, so include this first.
+#include "vm/compiler/ffi/unit_test_custom_zone.h"
+
+#include "platform/globals.h"
+
+// The UNIT_TEST_CASE macro is used for tests.
+#define UNIT_TEST_CASE_WITH_EXPECTATION(name, expectation)                     \
+  void Dart_Test##name();                                                      \
+  static const dart::compiler::ffi::RawTestCase kRegister##name(               \
+      Dart_Test##name, #name, expectation);                                    \
+  void Dart_Test##name()
+
+#define UNIT_TEST_CASE(name) UNIT_TEST_CASE_WITH_EXPECTATION(name, "Pass")
+
+// The UNIT_TEST_CASE_WITH_ZONE macro is used for tests that need a custom
+// dart::Zone.
+#define UNIT_TEST_CASE_WITH_ZONE_WITH_EXPECTATION(name, expectation)           \
+  static void Dart_TestHelper##name(dart::Zone* Z);                            \
+  UNIT_TEST_CASE_WITH_EXPECTATION(name, expectation) {                         \
+    dart::Zone zone;                                                           \
+    Dart_TestHelper##name(&zone);                                              \
+  }                                                                            \
+  static void Dart_TestHelper##name(dart::Zone* Z)
+
+#define UNIT_TEST_CASE_WITH_ZONE(name)                                         \
+  UNIT_TEST_CASE_WITH_ZONE_WITH_EXPECTATION(name, "Pass")
+
+namespace dart {
+namespace compiler {
+namespace ffi {
+
+class TestCaseBase {
+ public:
+  explicit TestCaseBase(const char* name, const char* expectation);
+  virtual ~TestCaseBase() {}
+
+  const char* name() const { return name_; }
+  const char* expectation() const { return expectation_; }
+
+  virtual void Run() = 0;
+  void RunTest();
+
+  static void RunAll();
+  static void RunAllRaw();
+
+  static bool update_expectations;
+
+ private:
+  static TestCaseBase* first_;
+  static TestCaseBase* tail_;
+
+  TestCaseBase* next_;
+  const char* name_;
+  const char* expectation_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestCaseBase);
+};
+
+class RawTestCase : TestCaseBase {
+ public:
+  typedef void(RunEntry)();
+
+  RawTestCase(RunEntry* run, const char* name, const char* expectation)
+      : TestCaseBase(name, expectation), run_(run) {}
+  virtual void Run();
+
+ private:
+  RunEntry* const run_;
+};
+
+}  // namespace ffi
+}  // namespace compiler
+}  // namespace dart
+
+#endif  // RUNTIME_VM_COMPILER_FFI_UNIT_TEST_H_
diff --git a/runtime/vm/compiler/ffi/unit_test_custom_zone.cc b/runtime/vm/compiler/ffi/unit_test_custom_zone.cc
new file mode 100644
index 0000000..90f01cf
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_test_custom_zone.cc
@@ -0,0 +1,45 @@
+// Copyright (c) 2020, 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 "vm/compiler/ffi/unit_test_custom_zone.h"
+
+#include "vm/compiler/runtime_api.h"
+
+// Directly compile cc files into the custom zone, so that we do not get linker
+// errors from object files compiled against the DartVM Zone.
+#include "vm/compiler/ffi/native_calling_convention.cc"  // NOLINT
+#include "vm/compiler/ffi/native_location.cc"            // NOLINT
+#include "vm/compiler/ffi/native_type.cc"                // NOLINT
+#include "vm/zone_text_buffer.cc"                        // NOLINT
+
+namespace dart {
+
+void* ZoneAllocated::operator new(uintptr_t size, dart::Zone* zone) {
+  return reinterpret_cast<void*>(zone->AllocUnsafe(size));
+}
+
+Zone::~Zone() {
+  while (buffers_.size() > 0) {
+    free(buffers_.back());
+    buffers_.pop_back();
+  }
+}
+
+void* Zone::AllocUnsafe(intptr_t size) {
+  void* memory = malloc(size);
+  buffers_.push_back(memory);
+  return memory;
+}
+
+DART_EXPORT void Dart_PrepareToAbort() {
+  fprintf(stderr, "Dart_PrepareToAbort() not implemented!\n");
+  exit(1);
+}
+
+DART_EXPORT void Dart_DumpNativeStackTrace(void* context) {
+  fprintf(stderr, "Dart_DumpNativeStackTrace() not implemented!\n");
+  exit(1);
+}
+
+}  // namespace dart
diff --git a/runtime/vm/compiler/ffi/unit_test_custom_zone.h b/runtime/vm/compiler/ffi/unit_test_custom_zone.h
new file mode 100644
index 0000000..ac305a1
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_test_custom_zone.h
@@ -0,0 +1,51 @@
+// Copyright (c) 2020, 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_VM_COMPILER_FFI_UNIT_TEST_CUSTOM_ZONE_H_
+#define RUNTIME_VM_COMPILER_FFI_UNIT_TEST_CUSTOM_ZONE_H_
+
+#include <vector>
+
+// We use a custom zone here which doesn't depend on VM internals (e.g. handles,
+// thread, ...)
+#if defined(RUNTIME_VM_ZONE_H_)
+#error "We want our own zone implementation"
+#endif
+#define RUNTIME_VM_ZONE_H_
+
+namespace dart {
+
+class Zone {
+ public:
+  Zone() {}
+  ~Zone();
+
+  template <class ElementType>
+  inline ElementType* Alloc(intptr_t length) {
+    return static_cast<ElementType*>(AllocUnsafe(sizeof(ElementType) * length));
+  }
+
+  template <class ElementType>
+  inline ElementType* Realloc(ElementType* old_array,
+                              intptr_t old_length,
+                              intptr_t new_length) {
+    void* memory = AllocUnsafe(sizeof(ElementType) * new_length);
+    memmove(memory, old_array, sizeof(ElementType) * old_length);
+    return static_cast<ElementType*>(memory);
+  }
+
+  template <class ElementType>
+  void Free(ElementType* old_array, intptr_t len) {}
+
+  void* AllocUnsafe(intptr_t size);
+
+ private:
+  Zone(const Zone&) = delete;
+  void operator=(const Zone&) = delete;
+  std::vector<void*> buffers_;
+};
+
+}  // namespace dart
+
+#endif  // RUNTIME_VM_COMPILER_FFI_UNIT_TEST_CUSTOM_ZONE_H_
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_android.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_android.expect
new file mode 100644
index 0000000..8f237e3
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_android.expect
@@ -0,0 +1,12 @@
+v0 float
+v1 float
+v2 float
+v3 float
+v4 float
+v5 float
+v6 float
+v7 float
+S+0 float
+S+8 float
+=>
+v0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_ios.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_ios.expect
new file mode 100644
index 0000000..8f237e3
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_ios.expect
@@ -0,0 +1,12 @@
+v0 float
+v1 float
+v2 float
+v3 float
+v4 float
+v5 float
+v6 float
+v7 float
+S+0 float
+S+8 float
+=>
+v0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_linux.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_linux.expect
new file mode 100644
index 0000000..8f237e3
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_linux.expect
@@ -0,0 +1,12 @@
+v0 float
+v1 float
+v2 float
+v3 float
+v4 float
+v5 float
+v6 float
+v7 float
+S+0 float
+S+8 float
+=>
+v0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_macos.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_macos.expect
new file mode 100644
index 0000000..8f237e3
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm64_macos.expect
@@ -0,0 +1,12 @@
+v0 float
+v1 float
+v2 float
+v3 float
+v4 float
+v5 float
+v6 float
+v7 float
+S+0 float
+S+8 float
+=>
+v0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_android.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_android.expect
new file mode 100644
index 0000000..f563dbd
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_android.expect
@@ -0,0 +1,12 @@
+r0 int32[float]
+r1 int32[float]
+r2 int32[float]
+r3 int32[float]
+S+0 float
+S+4 float
+S+8 float
+S+12 float
+S+16 float
+S+20 float
+=>
+r0 int32[float]
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_ios.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_ios.expect
new file mode 100644
index 0000000..9ff44a7
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_ios.expect
@@ -0,0 +1,12 @@
+s0 float
+s1 float
+s2 float
+s3 float
+s4 float
+s5 float
+s6 float
+s7 float
+s8 float
+s9 float
+=>
+q0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_linux.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_linux.expect
new file mode 100644
index 0000000..9ff44a7
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/arm_linux.expect
@@ -0,0 +1,12 @@
+s0 float
+s1 float
+s2 float
+s3 float
+s4 float
+s5 float
+s6 float
+s7 float
+s8 float
+s9 float
+=>
+q0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_android.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_android.expect
new file mode 100644
index 0000000..c4390e7
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_android.expect
@@ -0,0 +1,12 @@
+S+0 float
+S+4 float
+S+8 float
+S+12 float
+S+16 float
+S+20 float
+S+24 float
+S+28 float
+S+32 float
+S+36 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_linux.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_linux.expect
new file mode 100644
index 0000000..c4390e7
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_linux.expect
@@ -0,0 +1,12 @@
+S+0 float
+S+4 float
+S+8 float
+S+12 float
+S+16 float
+S+20 float
+S+24 float
+S+28 float
+S+32 float
+S+36 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_win.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_win.expect
new file mode 100644
index 0000000..c4390e7
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/ia32_win.expect
@@ -0,0 +1,12 @@
+S+0 float
+S+4 float
+S+8 float
+S+12 float
+S+16 float
+S+20 float
+S+24 float
+S+28 float
+S+32 float
+S+36 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_ios.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_ios.expect
new file mode 100644
index 0000000..b650130
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_ios.expect
@@ -0,0 +1,12 @@
+xmm0 float
+xmm1 float
+xmm2 float
+xmm3 float
+xmm4 float
+xmm5 float
+xmm6 float
+xmm7 float
+S+0 float
+S+8 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_linux.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_linux.expect
new file mode 100644
index 0000000..b650130
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_linux.expect
@@ -0,0 +1,12 @@
+xmm0 float
+xmm1 float
+xmm2 float
+xmm3 float
+xmm4 float
+xmm5 float
+xmm6 float
+xmm7 float
+S+0 float
+S+8 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_macos.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_macos.expect
new file mode 100644
index 0000000..b650130
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_macos.expect
@@ -0,0 +1,12 @@
+xmm0 float
+xmm1 float
+xmm2 float
+xmm3 float
+xmm4 float
+xmm5 float
+xmm6 float
+xmm7 float
+S+0 float
+S+8 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_win.expect b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_win.expect
new file mode 100644
index 0000000..1244587
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/floatx10/x64_win.expect
@@ -0,0 +1,12 @@
+xmm0 float
+xmm1 float
+xmm2 float
+xmm3 float
+S+0 float
+S+8 float
+S+16 float
+S+24 float
+S+32 float
+S+40 float
+=>
+xmm0 float
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_android.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_android.expect
new file mode 100644
index 0000000..04f72e5f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_android.expect
@@ -0,0 +1,12 @@
+r0 int8
+r1 int8
+r2 int8
+r3 int8
+r4 int8
+r5 int8
+r6 int8
+r7 int8
+S+0 int8
+S+8 int8
+=>
+r0 int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_ios.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_ios.expect
new file mode 100644
index 0000000..04f72e5f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_ios.expect
@@ -0,0 +1,12 @@
+r0 int8
+r1 int8
+r2 int8
+r3 int8
+r4 int8
+r5 int8
+r6 int8
+r7 int8
+S+0 int8
+S+8 int8
+=>
+r0 int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_linux.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_linux.expect
new file mode 100644
index 0000000..04f72e5f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_linux.expect
@@ -0,0 +1,12 @@
+r0 int8
+r1 int8
+r2 int8
+r3 int8
+r4 int8
+r5 int8
+r6 int8
+r7 int8
+S+0 int8
+S+8 int8
+=>
+r0 int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_macos.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_macos.expect
new file mode 100644
index 0000000..04f72e5f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm64_macos.expect
@@ -0,0 +1,12 @@
+r0 int8
+r1 int8
+r2 int8
+r3 int8
+r4 int8
+r5 int8
+r6 int8
+r7 int8
+S+0 int8
+S+8 int8
+=>
+r0 int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_android.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_android.expect
new file mode 100644
index 0000000..9307d2f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_android.expect
@@ -0,0 +1,12 @@
+r0 int32[int8]
+r1 int32[int8]
+r2 int32[int8]
+r3 int32[int8]
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+=>
+r0 int32[int8]
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_ios.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_ios.expect
new file mode 100644
index 0000000..9307d2f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_ios.expect
@@ -0,0 +1,12 @@
+r0 int32[int8]
+r1 int32[int8]
+r2 int32[int8]
+r3 int32[int8]
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+=>
+r0 int32[int8]
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_linux.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_linux.expect
new file mode 100644
index 0000000..9307d2f
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/arm_linux.expect
@@ -0,0 +1,12 @@
+r0 int32[int8]
+r1 int32[int8]
+r2 int32[int8]
+r3 int32[int8]
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+=>
+r0 int32[int8]
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_android.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_android.expect
new file mode 100644
index 0000000..bf5f6c4
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_android.expect
@@ -0,0 +1,12 @@
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+S+24 int32[int8]
+S+28 int32[int8]
+S+32 int32[int8]
+S+36 int32[int8]
+=>
+eax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_linux.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_linux.expect
new file mode 100644
index 0000000..bf5f6c4
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_linux.expect
@@ -0,0 +1,12 @@
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+S+24 int32[int8]
+S+28 int32[int8]
+S+32 int32[int8]
+S+36 int32[int8]
+=>
+eax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_win.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_win.expect
new file mode 100644
index 0000000..bf5f6c4
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/ia32_win.expect
@@ -0,0 +1,12 @@
+S+0 int32[int8]
+S+4 int32[int8]
+S+8 int32[int8]
+S+12 int32[int8]
+S+16 int32[int8]
+S+20 int32[int8]
+S+24 int32[int8]
+S+28 int32[int8]
+S+32 int32[int8]
+S+36 int32[int8]
+=>
+eax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_ios.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_ios.expect
new file mode 100644
index 0000000..0bcad4b
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_ios.expect
@@ -0,0 +1,12 @@
+rdi int32[int8]
+rsi int32[int8]
+rdx int32[int8]
+rcx int32[int8]
+r8 int32[int8]
+r9 int32[int8]
+S+0 int32[int8]
+S+8 int32[int8]
+S+16 int32[int8]
+S+24 int32[int8]
+=>
+rax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_linux.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_linux.expect
new file mode 100644
index 0000000..0bcad4b
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_linux.expect
@@ -0,0 +1,12 @@
+rdi int32[int8]
+rsi int32[int8]
+rdx int32[int8]
+rcx int32[int8]
+r8 int32[int8]
+r9 int32[int8]
+S+0 int32[int8]
+S+8 int32[int8]
+S+16 int32[int8]
+S+24 int32[int8]
+=>
+rax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_macos.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_macos.expect
new file mode 100644
index 0000000..0bcad4b
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_macos.expect
@@ -0,0 +1,12 @@
+rdi int32[int8]
+rsi int32[int8]
+rdx int32[int8]
+rcx int32[int8]
+r8 int32[int8]
+r9 int32[int8]
+S+0 int32[int8]
+S+8 int32[int8]
+S+16 int32[int8]
+S+24 int32[int8]
+=>
+rax int8
diff --git a/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_win.expect b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_win.expect
new file mode 100644
index 0000000..94704e5
--- /dev/null
+++ b/runtime/vm/compiler/ffi/unit_tests/int8x10/x64_win.expect
@@ -0,0 +1,12 @@
+rcx int8
+rdx int8
+r8 int8
+r9 int8
+S+0 int8
+S+8 int8
+S+16 int8
+S+24 int8
+S+32 int8
+S+40 int8
+=>
+rax int8
diff --git a/runtime/vm/compiler/jit/compiler.cc b/runtime/vm/compiler/jit/compiler.cc
index cd282c3..90d3903 100644
--- a/runtime/vm/compiler/jit/compiler.cc
+++ b/runtime/vm/compiler/jit/compiler.cc
@@ -620,10 +620,13 @@
           CheckIfBackgroundCompilerIsBeingStopped(optimized());
         }
 
-        // Grab read program_lock outside of potential safepoint, that lock
+        // Grab write program_lock outside of potential safepoint, that lock
         // can't be waited for inside the safepoint.
-        SafepointReadRwLocker ml(thread(),
-                                 thread()->isolate_group()->program_lock());
+        // Initially read lock was added to guard direct_subclasses field
+        // access.
+        // Read lock was upgraded to write lock to guard dependent code updates.
+        SafepointWriteRwLocker ml(thread(),
+                                  thread()->isolate_group()->program_lock());
         // We have to ensure no mutators are running, because:
         //
         //   a) We allocate an instructions object, which might cause us to
diff --git a/runtime/vm/compiler/jit/jit_call_specializer.cc b/runtime/vm/compiler/jit/jit_call_specializer.cc
index 6ce72c4..05ce2af 100644
--- a/runtime/vm/compiler/jit/jit_call_specializer.cc
+++ b/runtime/vm/compiler/jit/jit_call_specializer.cc
@@ -205,6 +205,9 @@
       }
       ASSERT(field.IsOriginal());
       field.set_is_unboxing_candidate(false);
+      Thread* thread = Thread::Current();
+      SafepointWriteRwLocker ml(thread,
+                                thread->isolate_group()->program_lock());
       field.DeoptimizeDependentCode();
     } else {
       flow_graph()->parsed_function().AddToGuardedFields(&field);
diff --git a/runtime/vm/compiler/runtime_api.h b/runtime/vm/compiler/runtime_api.h
index 7611669..ce2d164 100644
--- a/runtime/vm/compiler/runtime_api.h
+++ b/runtime/vm/compiler/runtime_api.h
@@ -288,8 +288,10 @@
 static constexpr int kWordSize = 1 << kWordSizeLog2;
 static_assert(kWordSize == sizeof(word), "kWordSize should match sizeof(word)");
 // Our compiler code currently assumes this, so formally check it.
+#if !defined(FFI_UNIT_TESTS)
 static_assert(dart::kWordSize >= kWordSize,
               "Host word size smaller than target word size");
+#endif
 
 static constexpr word kBitsPerWordLog2 = kWordSizeLog2 + kBitsPerByteLog2;
 static constexpr word kBitsPerWord = 1 << kBitsPerWordLog2;
diff --git a/runtime/vm/constants_arm64.h b/runtime/vm/constants_arm64.h
index 21319ca..898f10e 100644
--- a/runtime/vm/constants_arm64.h
+++ b/runtime/vm/constants_arm64.h
@@ -1400,7 +1400,7 @@
   DISALLOW_IMPLICIT_CONSTRUCTORS(Instr);
 };
 
-const uword kBreakInstructionFiller = 0xD4200000D4200000L;  // brk #0; brk #0
+const uint64_t kBreakInstructionFiller = 0xD4200000D4200000L;  // brk #0; brk #0
 
 }  // namespace dart
 
diff --git a/runtime/vm/constants_x64.h b/runtime/vm/constants_x64.h
index ae421fd..80e37ab 100644
--- a/runtime/vm/constants_x64.h
+++ b/runtime/vm/constants_x64.h
@@ -473,7 +473,7 @@
 // becomes important to us.
 const int MAX_NOP_SIZE = 8;
 
-const uword kBreakInstructionFiller = 0xCCCCCCCCCCCCCCCCL;
+const uint64_t kBreakInstructionFiller = 0xCCCCCCCCCCCCCCCCL;
 
 }  // namespace dart
 
diff --git a/runtime/vm/debugger.cc b/runtime/vm/debugger.cc
index 36394b0..ceb0e2e 100644
--- a/runtime/vm/debugger.cc
+++ b/runtime/vm/debugger.cc
@@ -1919,8 +1919,11 @@
 DebuggerStackTrace* Debugger::CollectAsyncLazyStackTrace() {
   Thread* thread = Thread::Current();
   Zone* zone = thread->zone();
+  Isolate* isolate = thread->isolate();
 
   Code& code = Code::Handle(zone);
+  Code& inlined_code = Code::Handle(zone);
+  Array& deopt_frame = Array::Handle(zone);
   Smi& offset = Smi::Handle(zone);
   Function& function = Function::Handle(zone);
 
@@ -1932,8 +1935,16 @@
   const auto& pc_offset_array = GrowableObjectArray::ZoneHandle(
       zone, GrowableObjectArray::New(kDefaultStackAllocation));
   bool has_async = false;
+
+  std::function<void(StackFrame*)> on_sync_frame = [&](StackFrame* frame) {
+    code = frame->LookupDartCode();
+    AppendCodeFrames(thread, isolate, zone, stack_trace, frame, &code,
+                     &inlined_code, &deopt_frame);
+  };
+
   StackTraceUtils::CollectFramesLazy(thread, code_array, pc_offset_array,
-                                     /*skip_frames=*/0, &has_async);
+                                     /*skip_frames=*/0, &on_sync_frame,
+                                     &has_async);
 
   // If the entire stack is sync, return no trace.
   if (!has_async) {
@@ -1941,11 +1952,19 @@
   }
 
   const intptr_t length = code_array.Length();
-  for (intptr_t i = stack_trace->Length(); i < length; ++i) {
+  bool async_frames = false;
+  for (intptr_t i = 0; i < length; ++i) {
     code ^= code_array.At(i);
 
     if (code.raw() == StubCode::AsynchronousGapMarker().raw()) {
       stack_trace->AddMarker(ActivationFrame::kAsyncSuspensionMarker);
+      // Once we reach a gap, the rest is async.
+      async_frames = true;
+      continue;
+    }
+
+    // Skip the sync frames since they've been added (and un-inlined) above.
+    if (!async_frames) {
       continue;
     }
 
diff --git a/runtime/vm/heap/weak_code.cc b/runtime/vm/heap/weak_code.cc
index c1a3d53..97f832a 100644
--- a/runtime/vm/heap/weak_code.cc
+++ b/runtime/vm/heap/weak_code.cc
@@ -69,6 +69,9 @@
   }
 
   UpdateArrayTo(Object::null_array());
+
+  // TODO(dartbug.com/36097): This has to walk all mutator threads when
+  // disabling code.
   // Disable all code on stack.
   Code& code = Code::Handle();
   {
diff --git a/runtime/vm/isolate_reload.cc b/runtime/vm/isolate_reload.cc
index 4df27eb..b126b1c 100644
--- a/runtime/vm/isolate_reload.cc
+++ b/runtime/vm/isolate_reload.cc
@@ -1242,6 +1242,8 @@
   Class& cls = Class::Handle();
   Array& fields = Array::Handle();
   Field& field = Field::Handle();
+  Thread* thread = Thread::Current();
+  SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
   for (intptr_t cls_idx = bottom; cls_idx < top; cls_idx++) {
     if (!class_table->HasValidClassAt(cls_idx)) {
       // Skip.
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index 3395587..aeacffe 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -3889,7 +3889,15 @@
 }
 
 void Class::DisableCHAOptimizedCode(const Class& subclass) {
-  ASSERT(Thread::Current()->IsMutatorThread());
+  Thread* thread = Thread::Current();
+  ASSERT(thread->IsMutatorThread());
+  // TODO(dartbug.com/36097): The program_lock acquisition has to move up the
+  // call chain to ClassFinalizer::AllocateFinalizeClass() so that:
+  //   - no two threads allocate-finalize a class at the same time(we should
+  // use the logic similar to what is used in EnsureIsAllocateFinalized()).
+  //   - code is deoptimized before we violate optimization assumptions
+  // potentially done concurrently (AddDirectSubclass/AddDirectImplementor).
+  SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
   CHACodeArray a(*this);
   if (FLAG_trace_deoptimization && a.HasCodes()) {
     if (subclass.IsNull()) {
@@ -3928,7 +3936,15 @@
 #endif
 }
 
+ArrayPtr Class::dependent_code() const {
+  DEBUG_ASSERT(
+      IsolateGroup::Current()->program_lock()->IsCurrentThreadReader());
+  return raw_ptr()->dependent_code_;
+}
+
 void Class::set_dependent_code(const Array& array) const {
+  DEBUG_ASSERT(
+      IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter());
   StorePointer(&raw_ptr()->dependent_code_, array.raw());
 }
 
@@ -10405,11 +10421,15 @@
 }
 
 ArrayPtr Field::dependent_code() const {
+  DEBUG_ASSERT(
+      IsolateGroup::Current()->program_lock()->IsCurrentThreadReader());
   return raw_ptr()->dependent_code_;
 }
 
 void Field::set_dependent_code(const Array& array) const {
   ASSERT(IsOriginal());
+  DEBUG_ASSERT(
+      IsolateGroup::Current()->program_lock()->IsCurrentThreadWriter());
   StorePointer(&raw_ptr()->dependent_code_, array.raw());
 }
 
@@ -11058,6 +11078,8 @@
       THR_Print("    => %s\n", GuardedPropertiesAsCString());
     }
 
+    Thread* const thread = Thread::Current();
+    SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
     DeoptimizeDependentCode();
   }
 }
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 11fed21..098db27 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -1541,7 +1541,7 @@
   // Return the list of code objects that were compiled using CHA of this class.
   // These code objects will be invalidated if new subclasses of this class
   // are finalized.
-  ArrayPtr dependent_code() const { return raw_ptr()->dependent_code_; }
+  ArrayPtr dependent_code() const;
   void set_dependent_code(const Array& array) const;
 
   bool TraceAllocation(Isolate* isolate) const;
diff --git a/runtime/vm/object_reload.cc b/runtime/vm/object_reload.cc
index bea8d14..447dbc2 100644
--- a/runtime/vm/object_reload.cc
+++ b/runtime/vm/object_reload.cc
@@ -468,6 +468,8 @@
     }
   }
 
+  Thread* thread = Thread::Current();
+  SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
   const Array& field_list = Array::Handle(fields());
   Field& field = Field::Handle();
   for (intptr_t i = 0; i < field_list.Length(); i++) {
diff --git a/runtime/vm/runtime_entry.cc b/runtime/vm/runtime_entry.cc
index 0b4ceb7..f2c86a2 100644
--- a/runtime/vm/runtime_entry.cc
+++ b/runtime/vm/runtime_entry.cc
@@ -2574,15 +2574,20 @@
 
   if (Compiler::CanOptimizeFunction(thread, function)) {
     if (FLAG_background_compilation) {
-      Field& field = Field::Handle(zone, isolate->GetDeoptimizingBoxedField());
-      while (!field.IsNull()) {
-        if (FLAG_trace_optimization || FLAG_trace_field_guards) {
-          THR_Print("Lazy disabling unboxing of %s\n", field.ToCString());
+      {
+        SafepointWriteRwLocker ml(thread,
+                                  thread->isolate_group()->program_lock());
+        Field& field =
+            Field::Handle(zone, isolate->GetDeoptimizingBoxedField());
+        while (!field.IsNull()) {
+          if (FLAG_trace_optimization || FLAG_trace_field_guards) {
+            THR_Print("Lazy disabling unboxing of %s\n", field.ToCString());
+          }
+          field.set_is_unboxing_candidate(false);
+          field.DeoptimizeDependentCode();
+          // Get next field.
+          field = isolate->GetDeoptimizingBoxedField();
         }
-        field.set_is_unboxing_candidate(false);
-        field.DeoptimizeDependentCode();
-        // Get next field.
-        field = isolate->GetDeoptimizingBoxedField();
       }
       if (!BackgroundCompiler::IsDisabled(isolate,
                                           /* optimizing_compiler = */ true) &&
diff --git a/runtime/vm/stack_trace.cc b/runtime/vm/stack_trace.cc
index 572c222..5b146ff 100644
--- a/runtime/vm/stack_trace.cc
+++ b/runtime/vm/stack_trace.cc
@@ -287,6 +287,7 @@
     const GrowableObjectArray& code_array,
     const GrowableObjectArray& pc_offset_array,
     int skip_frames,
+    std::function<void(StackFrame*)>* on_sync_frames,
     bool* has_async) {
   if (has_async != nullptr) {
     *has_async = false;
@@ -326,6 +327,9 @@
     ASSERT(pc_offset > 0 && pc_offset <= code.Size());
     offset = Smi::New(pc_offset);
     pc_offset_array.Add(offset);
+    if (on_sync_frames != nullptr) {
+      (*on_sync_frames)(frame);
+    }
 
     // Either continue the loop (sync-async case) or find all await'ers and
     // return.
diff --git a/runtime/vm/stack_trace.h b/runtime/vm/stack_trace.h
index e3fd332..660746a 100644
--- a/runtime/vm/stack_trace.h
+++ b/runtime/vm/stack_trace.h
@@ -80,11 +80,15 @@
   ///       closure = closure.context[Context::kAsyncCompleterVarIndex]._future
   ///           ._resultOrListeners.callback;
   ///     }
+  ///
+  /// If [on_sync_frames] is non-nullptr, it will be called for every
+  /// synchronous frame which is collected.
   static void CollectFramesLazy(
       Thread* thread,
       const GrowableObjectArray& code_array,
       const GrowableObjectArray& pc_offset_array,
       int skip_frames,
+      std::function<void(StackFrame*)>* on_sync_frames = nullptr,
       bool* has_async = nullptr);
 
   /// Counts the number of stack frames.
diff --git a/runtime/vm/vm_sources.gni b/runtime/vm/vm_sources.gni
index fda79fb..665d37b 100644
--- a/runtime/vm/vm_sources.gni
+++ b/runtime/vm/vm_sources.gni
@@ -451,3 +451,15 @@
   "virtual_memory_test.cc",
   "zone_test.cc",
 ]
+
+constants_sources = [
+  "constants_arm.cc",
+  "constants_arm.h",
+  "constants_arm64.cc",
+  "constants_arm64.h",
+  "constants_base.h",
+  "constants_ia32.cc",
+  "constants_ia32.h",
+  "constants_x64.cc",
+  "constants_x64.h",
+]
diff --git a/sdk/lib/html/html_common/conversions_dart2js.dart b/sdk/lib/html/html_common/conversions_dart2js.dart
index c1fb0be..f0ed6d2 100644
--- a/sdk/lib/html/html_common/conversions_dart2js.dart
+++ b/sdk/lib/html/html_common/conversions_dart2js.dart
@@ -13,7 +13,29 @@
   return dict;
 }
 
-/// Converts a flat Dart map into a JavaScript object with properties.
+/// Converts values that occur within a Dart map for map conversion.
+///
+/// This includes other maps, lists, or values that don't need a conversion e.g.
+/// bool, String.
+_convertDartToNative_Value(Object? value) {
+  if (value == null) return value;
+  if (value is String || value is num || value is bool) return value;
+  if (value is Map) return convertDartToNative_Dictionary(value);
+  if (value is List) {
+    var array = JS('var', '[]');
+    value.forEach((element) {
+      JS('void', '#.push(#)', array, _convertDartToNative_Value(element));
+    });
+    value = array;
+  }
+  return value;
+}
+
+/// Converts a potentially nested Dart map to a JavaScript object with
+/// properties.
+///
+/// This method requires that the values within the map are either maps
+/// themselves, lists, or do not need a conversion.
 convertDartToNative_Dictionary(Map? dict, [void postCreate(Object? f)?]) {
   if (dict == null) return null;
   var object = JS('var', '{}');
@@ -21,7 +43,7 @@
     postCreate(object);
   }
   dict.forEach((key, value) {
-    JS('void', '#[#] = #', object, key, value);
+    JS('void', '#[#] = #', object, key, _convertDartToNative_Value(value));
   });
   return object;
 }
diff --git a/tools/VERSION b/tools/VERSION
index f08a7cd..6296136 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 12
 PATCH 0
-PRERELEASE 49
+PRERELEASE 50
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json
index 8d4be38..582dd82 100644
--- a/tools/bots/test_matrix.json
+++ b/tools/bots/test_matrix.json
@@ -418,6 +418,9 @@
     "flutter-frontend": {
       "__comment__": "This configuration is only used for a custom test runner. If it conflicts with a new configuration you are adding, you can make this configuration more specific by adding options."
     },
+    "vm-ffi-unit-test": {
+      "__comment__": "This configuration is only used for a custom test runner. If it conflicts with a new configuration you are adding, you can make this configuration more specific by adding options."
+    },
     "unittest-asserts-no-sdk-(linux|mac|win)": {
       "options": {
         "compiler": "dartk",
@@ -1102,10 +1105,26 @@
         "vm-precomp-ffi-qemu-linux-release-arm"
       ],
       "meta": {
-        "description": "This configuration is used for running FFI tests on qemu."
+        "description": "This configuration is used for running FFI tests on qemu and FFI unit tests."
       },
       "steps": [
         {
+          "name": "build run_ffi_unit_tests",
+          "script": "tools/build.py",
+          "arguments": [
+            "--arch=x64",
+            "--mode=debug",
+            "run_ffi_unit_tests"
+          ]
+        },
+        {
+          "name": "ffi unit tests",
+          "arguments": [
+            "-nvm-ffi-unit-test",
+            "ffi_unit"
+          ]
+        },
+        {
           "name": "build dart",
           "script": "tools/build.py",
           "arguments": [
@@ -2804,7 +2823,7 @@
         "analyzer-analysis-server-linux"
       ],
       "meta": {
-        "description": "Analyze analyzer related packages."
+        "description": "Analyze repository packages."
       },
       "steps": [
         {
@@ -2816,9 +2835,9 @@
         },
         {
           "name": "analyze runtime/tools/dartfuzz",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
             "runtime/tools/dartfuzz"
           ]
         },
@@ -2827,6 +2846,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/analysis_server"
           ]
         },
@@ -2835,6 +2855,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/analysis_server_client"
           ]
         },
@@ -2843,6 +2864,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/analyzer"
           ]
         },
@@ -2851,6 +2873,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/analyzer_cli"
           ]
         },
@@ -2859,14 +2882,15 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/analyzer_plugin"
           ]
         },
         {
           "name": "analyze pkg/async_helper",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
             "pkg/async_helper"
           ]
         },
@@ -2905,26 +2929,27 @@
         },
         {
           "name": "analyze pkg/dart_internal",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/dart_internal"
           ]
         },
         {
           "name": "analyze pkg/dev_compiler",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
-            "--fatal-lints",
+            "analyze",
+            "--fatal-infos",
             "pkg/dev_compiler"
           ]
         },
         {
           "name": "analyze pkg/expect",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
             "pkg/expect"
           ]
         },
@@ -2962,33 +2987,36 @@
         },
         {
           "name": "analyze pkg/meta",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/meta"
           ]
         },
         {
           "name": "analyze pkg/native_stack_traces",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
             "pkg/native_stack_traces"
           ]
         },
         {
           "name": "analyze pkg/smith",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/smith"
           ]
         },
         {
           "name": "analyze pkg/sourcemap_testing",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/sourcemap_testing"
           ]
         },
@@ -2997,6 +3025,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/status_file"
           ]
         },
@@ -3005,6 +3034,7 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/telemetry"
           ]
         },
@@ -3018,25 +3048,28 @@
         },
         {
           "name": "analyze pkg/testing",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/testing"
           ]
         },
         {
           "name": "analyze pkg/vm",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/vm"
           ]
         },
         {
           "name": "analyze pkg/vm_service",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/vm_service"
           ]
         },
@@ -3045,14 +3078,16 @@
           "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
             "analyze",
+            "--fatal-infos",
             "pkg/vm_snapshot_analysis"
           ]
         },
         {
           "name": "analyze pkg/dds",
-          "script": "out/ReleaseX64/dart-sdk/bin/dartanalyzer",
+          "script": "out/ReleaseX64/dart-sdk/bin/dart",
           "arguments": [
-            "--fatal-warnings",
+            "analyze",
+            "--fatal-infos",
             "pkg/dds"
           ]
         },