Version 2.15.0-167.0.dev

Merge commit '8740a4f10f887c4088b7a7a987e783b8ed7542d5' into 'dev'
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index 0a4ddd9..cb968a9 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -725,7 +725,12 @@
         tempDir, arguments, environmentOverrides));
 
     commands.add(
-        computeDartBootstrapCommand(tempDir, arguments, environmentOverrides));
+        computeGenSnapshotCommand(tempDir, arguments, environmentOverrides));
+
+    if (arguments.contains('--print-flow-graph-optimized')) {
+      commands.add(
+          computeILCompareCommand(tempDir, arguments, environmentOverrides));
+    }
 
     if (!_configuration.keepGeneratedFiles) {
       commands.add(computeRemoveKernelFileCommand(
@@ -777,7 +782,7 @@
         alwaysCompile: !_useSdk);
   }
 
-  Command computeDartBootstrapCommand(String tempDir, List<String> arguments,
+  Command computeGenSnapshotCommand(String tempDir, List<String> arguments,
       Map<String, String> environmentOverrides) {
     var buildDir = _configuration.buildDirectory;
     var exec = _configuration.genSnapshotPath;
@@ -821,6 +826,8 @@
       // The SIMARM precompiler assumes support for integer division, but the
       // Qemu arm cpus do not support integer division.
       if (_configuration.useQemu) '--no-use-integer-division',
+      if (arguments.contains('--print-flow-graph-optimized'))
+        '--redirect-isolate-log-to=$tempDir/out.il',
       ..._replaceDartFiles(arguments, tempKernelFile(tempDir)),
     ];
 
@@ -829,6 +836,21 @@
         alwaysCompile: !_useSdk);
   }
 
+  Command computeILCompareCommand(String tempDir, List<String> arguments,
+      Map<String, String> environmentOverrides) {
+    var pkgVmDir = Platform.script.resolve('../../../pkg/vm').toFilePath();
+    var compareIl = '$pkgVmDir/tool/compare_il$shellScriptExtension';
+
+    var args = [
+      arguments.firstWhere((arg) => arg.endsWith('_il_test.dart')),
+      '$tempDir/out.il',
+    ];
+
+    return CompilationCommand('compare_il', tempDir, bootstrapDependencies(),
+        compareIl, args, environmentOverrides,
+        alwaysCompile: !_useSdk);
+  }
+
   static const String ndkPath = "third_party/android_tools/ndk";
   String get abiTriple => _isArm || _isArmX64
       ? "arm-linux-androideabi"
@@ -943,6 +965,10 @@
   List<String> computeCompilerArguments(
       TestFile testFile, List<String> vmOptions, List<String> args) {
     return [
+      if (testFile.ilMatches.isNotEmpty) ...[
+        '--print-flow-graph-optimized',
+        '--print-flow-graph-filter=${testFile.ilMatches.join(',')}'
+      ],
       if (_enableAsserts) '--enable_asserts',
       ...filterVmOptions(vmOptions),
       ...testFile.sharedOptions,
diff --git a/pkg/test_runner/lib/src/test_file.dart b/pkg/test_runner/lib/src/test_file.dart
index 76faa68..8b61d68 100644
--- a/pkg/test_runner/lib/src/test_file.dart
+++ b/pkg/test_runner/lib/src/test_file.dart
@@ -211,6 +211,11 @@
       throw FormatException('Unknown feature "$name" in test $filePath');
     });
 
+    var ilMatches = filePath.endsWith('_il_test.dart')
+        ? _parseStringOption(filePath, contents, r'MatchIL\[AOT\]',
+            allowMultiple: true)
+        : const <String>[];
+
     // VM options.
     var vmOptions = <List<String>>[];
     var matches = _vmOptionsRegExp.allMatches(contents);
@@ -335,7 +340,8 @@
         vmOptions: vmOptions,
         sharedObjects: sharedObjects,
         otherResources: otherResources,
-        experiments: experiments);
+        experiments: experiments,
+        ilMatches: ilMatches);
   }
 
   /// A special fake test file for representing a VM unit test written in C++.
@@ -357,6 +363,7 @@
         sharedObjects = [],
         otherResources = [],
         experiments = [],
+        ilMatches = [],
         super(null, null, []);
 
   TestFile._(Path suiteDirectory, Path path, List<StaticError> expectedErrors,
@@ -376,7 +383,8 @@
       this.vmOptions,
       this.sharedObjects,
       this.otherResources,
-      this.experiments})
+      this.experiments,
+      this.ilMatches = const <String>[]})
       : super(suiteDirectory, path, expectedErrors) {
     assert(!isMultitest || dartOptions.isEmpty);
   }
@@ -403,6 +411,9 @@
   /// requirements, the test is implicitly skipped.
   final List<Feature> requirements;
 
+  /// List of functions which will have their IL verified (in AOT mode).
+  final List<String> ilMatches;
+
   final List<String> sharedOptions;
   final List<String> dartOptions;
   final List<String> dart2jsOptions;
@@ -482,6 +493,7 @@
   String get packages => _origin.packages;
 
   List<Feature> get requirements => _origin.requirements;
+  List<String> get ilMatches => _origin.ilMatches;
   List<String> get dart2jsOptions => _origin.dart2jsOptions;
   List<String> get dartOptions => _origin.dartOptions;
   List<String> get ddcOptions => _origin.ddcOptions;
diff --git a/pkg/vm/bin/compare_il.dart b/pkg/vm/bin/compare_il.dart
new file mode 100644
index 0000000..9d9febd
--- /dev/null
+++ b/pkg/vm/bin/compare_il.dart
@@ -0,0 +1,189 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This is a helper script which performs IL matching for AOT IL tests.
+// See runtime/docs/infra/il_tests.md for more information.
+
+import 'dart:io';
+
+void main(List<String> args) {
+  if (args.length != 2) {
+    throw 'Usage: compare_il <*_il_test.dart> <output.il>';
+  }
+
+  final testFile = args[0];
+  final ilFile = args[1];
+
+  final graphs = _extractGraphs(ilFile);
+
+  final expectations = _extractExpectations(testFile);
+
+  for (var expectation in expectations.entries) {
+    // Find a graph for this expectation. We expect that function names are
+    // unique enough to identify a specific graph.
+    final graph =
+        graphs.entries.singleWhere((e) => e.key.contains(expectation.key));
+
+    // Extract the list of opcodes, ignoring irrelevant things like
+    // ParallelMove.
+    final gotOpcodesIgnoringMoves = graph.value
+        .where((instr) => instr.opcode != 'ParallelMove')
+        .map((instr) => instr.opcode)
+        .toList();
+
+    // Check that expectations are the prefix of gotOpcodesIgnoringMoves.
+    print('Matching ${graph.key}');
+    for (var i = 0; i < expectation.value.length; i++) {
+      final gotOpcode = gotOpcodesIgnoringMoves[i];
+      final expectedOpcode = expectation.value[i];
+      if (gotOpcode != expectedOpcode) {
+        throw 'Failed to match graph of ${graph.key} to '
+            'expectations for ${expectation.key} at instruction ${i}: '
+            'got ${gotOpcode} expected ${expectedOpcode}';
+      }
+    }
+    print('... ok');
+  }
+
+  exit(0); // Success.
+}
+
+// IL instruction extracted from flow graph dump.
+class Instruction {
+  final String raw;
+
+  Instruction(this.raw);
+
+  String get opcode {
+    final match = instructionPattern.firstMatch(raw)!;
+    final op = match.namedGroup('opcode')!;
+    final blockType = match.namedGroup('block_type');
+
+    // Handle blocks which look like "B%d[%s]".
+    if (blockType != null) {
+      return blockTypes[blockType]!;
+    }
+
+    // Handle parallel moves specially.
+    if (op.startsWith('ParallelMove')) {
+      return 'ParallelMove';
+    }
+
+    // Handle branches.
+    if (op.startsWith(branchIfPrefix)) {
+      return 'Branch(${op.substring(branchIfPrefix.length)})';
+    }
+
+    // Normal instruction.
+    return op;
+  }
+
+  @override
+  String toString() => 'Instruction($opcode)';
+
+  static final instructionPattern = RegExp(
+      r'^\s*\d+:\s+(v\d+ <- )?(?<opcode>[^:[(]+(?<block_type>\[[\w ]+\])?)');
+
+  static const blockTypes = {
+    '[join]': 'JoinEntry',
+    '[target]': 'TargetEntry',
+    '[graph]': 'GraphEntry',
+    '[function entry]': 'FunctionEntry'
+  };
+
+  static const branchIfPrefix = 'Branch if ';
+}
+
+Map<String, List<Instruction>> _extractGraphs(String ilFile) {
+  final graphs = <String, List<Instruction>>{};
+
+  final reader = LineReader(ilFile);
+
+  var instructions = <Instruction>[];
+  while (reader.hasMore) {
+    if (reader.testNext('*** BEGIN CFG')) {
+      reader.next(); // Skip phase name.
+      final functionName = reader.next();
+      while (!reader.testNext('*** END CFG')) {
+        var curr = reader.next();
+
+        // If instruction line ends with '{' search for a matching '}' (it will
+        // be on its own line).
+        if (curr.endsWith('{')) {
+          do {
+            curr += '\n' + reader.current;
+          } while (reader.next() != '}');
+        }
+
+        instructions.add(Instruction(curr));
+      }
+
+      graphs[functionName] = instructions;
+      instructions = <Instruction>[];
+    } else {
+      reader.next();
+    }
+  }
+
+  return graphs;
+}
+
+Map<String, List<String>> _extractExpectations(String testFile) {
+  final expectations = <String, List<String>>{};
+
+  final reader = LineReader(testFile);
+
+  final matchILPattern = RegExp(r'^// MatchIL\[AOT\]=(?<value>.*)$');
+  final matcherPattern = RegExp(r'^// __ (?<value>.*)$');
+
+  var matchers = <String>[];
+  while (reader.hasMore) {
+    var functionName = reader.matchNext(matchILPattern);
+    if (functionName != null) {
+      // Read comment block which follows `// MatchIL[AOT]=...`.
+      while (reader.hasMore && reader.current.startsWith('//')) {
+        final match = matcherPattern.firstMatch(reader.next());
+        if (match != null) {
+          matchers.add(match.namedGroup('value')!);
+        }
+      }
+      expectations[functionName] = matchers;
+      matchers = <String>[];
+    } else {
+      reader.next();
+    }
+  }
+
+  return expectations;
+}
+
+class LineReader {
+  final List<String> lines;
+  int lineno = 0;
+
+  LineReader(String path) : lines = File(path).readAsLinesSync();
+
+  String get current => lines[lineno];
+
+  bool get hasMore => lineno < lines.length;
+
+  String next() {
+    final curr = current;
+    lineno++;
+    return curr;
+  }
+
+  bool testNext(String expected) {
+    if (current == expected) {
+      next();
+      return true;
+    }
+    return false;
+  }
+
+  String? matchNext(RegExp pattern) {
+    final m = pattern.firstMatch(current);
+    return m?.namedGroup('value');
+  }
+}
diff --git a/pkg/vm/tool/compare_il b/pkg/vm/tool/compare_il
new file mode 100755
index 0000000..65ad55e
--- /dev/null
+++ b/pkg/vm/tool/compare_il
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Script for comparing IL generated from IL tests.
+
+set -e
+
+function follow_links() {
+  file="$1"
+  while [ -h "$file" ]; do
+    # On Mac OS, readlink -f doesn't work.
+    file="$(readlink "$file")"
+  done
+  echo "$file"
+}
+
+# Unlike $0, $BASH_SOURCE points to the absolute path of this file.
+PROG_NAME="$(follow_links "$BASH_SOURCE")"
+
+# Handle the case where dart-sdk/bin has been symlinked to.
+CUR_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
+
+SDK_DIR="$CUR_DIR/../../.."
+
+# TODO(kustermann): For windows as well as for hosts running on arm, our
+# checked-in dart binaries must be adjusted.
+if [[ `uname` == 'Darwin' ]]; then
+  DART="$SDK_DIR/tools/sdks/dart-sdk/bin/dart"
+else
+  DART="$SDK_DIR/tools/sdks/dart-sdk/bin/dart"
+fi
+
+exec "$DART" $DART_VM_FLAGS "${SDK_DIR}/pkg/vm/bin/compare_il.dart" $@
diff --git a/pkg/vm/tool/compare_il.bat b/pkg/vm/tool/compare_il.bat
new file mode 100644
index 0000000..009aca7
--- /dev/null
+++ b/pkg/vm/tool/compare_il.bat
@@ -0,0 +1,17 @@
+@echo off
+REM Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+REM for details. All rights reserved. Use of this source code is governed by a
+REM BSD-style license that can be found in the LICENSE file.
+
+REM Script for comparing IL generated from IL tests.
+
+set SCRIPTPATH=%~dp0
+
+REM Does the path have a trailing slash? If so, remove it.
+if %SCRIPTPATH:~-1%==\ set SCRIPTPATH=%SCRIPTPATH:~0,-1%
+
+set SDK_DIR=%SCRIPTPATH%/../../../
+
+set DART=%SDK_DIR%/tools/sdks/dart-sdk/bin/dart.exe
+
+"%DART%" %DART_VM_OPTIONS% "%SDK_DIR%/pkg/vm/bin/compare_il.dart" %*
diff --git a/pkg/vm/tool/gen_kernel b/pkg/vm/tool/gen_kernel
index d0494c7..71f4f8a 100755
--- a/pkg/vm/tool/gen_kernel
+++ b/pkg/vm/tool/gen_kernel
@@ -29,10 +29,8 @@
 # checked-in dart binaries must be adjusted.
 if [[ `uname` == 'Darwin' ]]; then
   DART="$SDK_DIR/tools/sdks/dart-sdk/bin/dart"
-  OUT_DIR="$SDK_DIR/xcodebuild"
 else
   DART="$SDK_DIR/tools/sdks/dart-sdk/bin/dart"
-  OUT_DIR="$SDK_DIR/out"
 fi
 
 exec "$DART" $DART_VM_FLAGS "${SDK_DIR}/pkg/vm/bin/gen_kernel.dart" $@
diff --git a/runtime/docs/infra/il_tests.md b/runtime/docs/infra/il_tests.md
new file mode 100644
index 0000000..8400b6e
--- /dev/null
+++ b/runtime/docs/infra/il_tests.md
@@ -0,0 +1,86 @@
+# Writing IL tests for AOT compiler
+
+Usually optimized IL strongly depends on TFA results and which makes it
+difficult to test certain AOT optimizations through `run_vm_tests`.
+
+In such cases you can attempt to write an IL test instead. In these tests
+test runner will run full AOT pipeline (TFA + `gen_snapshot`), will instruct
+`gen_snapshot` to dump flow graphs of specific methods and then run
+`pkg/vm/tool/compare_il` helper script to compare expectations. Here is how you
+create an IL test.
+
+IL tests are placed in files ending with `_il_test.dart`.
+
+Each IL test should contain one or more _IL matching blocks_, which have the
+following format:
+
+```dart
+// MatchIL[AOT]=functionName
+//   comment
+// __ op
+//   comment
+// __ op
+// __ op
+// __ op
+```
+
+Each section starts with a `// MatchIL[AOT]=functionName` line which contains
+the name (or a substring of a name) of the function for which IL should be
+matched.
+
+`// MatchIL[AOT]=...` line is followed by some number of comment lines `//`,
+where lines starting with `// __ ` specify _an instruction matcher_ and the rest
+are ignored (they just act as normal comments).
+
+`gen_snapshot` will be instructed (via `--print-flow-graph-optimized` and
+`--print-flow-graph-filter=functionName,...` flags) to dump IL for all
+functions names specified in IL matching blocks.
+
+After that `pkg/vm/tool/compare_il` script will be used to compare the dumps
+to actual expectations: by checking that dumped flow graph starts with the
+expected sequence of commands (ignoring some instructions like `ParallelMove`).
+
+## Example
+
+```dart
+// MatchIL[AOT]=factorial
+// __ GraphEntry
+// __ FunctionEntry
+// __ CheckStackOverflow
+// __ Branch(EqualityCompare)
+@pragma('vm:never-inline')
+int factorial(int value) => value == 1 ? value : value * factorial(value - 1);
+```
+
+This test specifies that the graph for `factorial` should start with a sequence
+`GraphEntry`, `FunctionEntry`, `CheckStackOverflow`, `Branch(EqualityCompare)`.
+
+If the graph has a different shape the test will fail, e.g. given the graph
+
+```
+*** BEGIN CFG
+After AllocateRegisters
+==== file:///.../src/dart/sdk/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart_::_factorial (RegularFunction)
+  0: B0[graph]:0 {
+      v3 <- Constant(#1) [1, 1] T{_Smi}
+      v19 <- UnboxedConstant(#1 int64) T{_Smi}
+}
+  2: B1[function entry]:2 {
+      v2 <- Parameter(0) [-9223372036854775808, 9223372036854775807] T{int}
+}
+  4:     CheckStackOverflow:8(stack=0, loop=0)
+  5:     ParallelMove rcx <- S+2
+  6:     v17 <- BoxInt64(v2) [-9223372036854775808, 9223372036854775807] T{int}
+  7:     ParallelMove rax <- rax
+  8:     Branch if StrictCompare(===, v17 T{int}, v3) T{bool} goto (3, 4)
+```
+
+we will get:
+
+```
+Unhandled exception:
+Failed to match graph of ==== file:///.../src/dart/sdk/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart_::_factorial (RegularFunction) to expectations for factorial at instruction 3: got BoxInt64 expected Branch(EqualityCompare)
+#0      main (file:///.../src/dart/sdk/pkg/vm/bin/compare_il.dart:37:9)
+#1      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:285:32)
+#2      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:187:12)
+```
diff --git a/runtime/vm/log.cc b/runtime/vm/log.cc
index 998ff60..809df77 100644
--- a/runtime/vm/log.cc
+++ b/runtime/vm/log.cc
@@ -4,6 +4,7 @@
 
 #include "vm/log.h"
 
+#include "vm/dart.h"
 #include "vm/flags.h"
 #include "vm/isolate.h"
 #include "vm/thread.h"
@@ -28,8 +29,43 @@
             "Default: service isolate log messages are suppressed "
             "(specify 'vm-service' to log them).");
 
-Log::Log(LogPrinter printer)
-    : printer_(printer), manual_flush_(0), buffer_(0) {}
+DEFINE_FLAG(charp,
+            redirect_isolate_log_to,
+            nullptr,
+            "Log isolate messages into the given file.");
+
+namespace {
+class LogFile {
+ public:
+  static const LogFile& Instance() {
+    static LogFile log_file;
+    return log_file;
+  }
+
+  static void Print(const char* data) {
+    Dart::file_write_callback()(data, strlen(data), Instance().handle_);
+  }
+
+ private:
+  LogFile()
+      : handle_(Dart::file_open_callback()(FLAG_redirect_isolate_log_to,
+                                           /*write=*/true)) {}
+
+  ~LogFile() { Dart::file_close_callback()(handle_); }
+
+  void* handle_;
+};
+}  // namespace
+
+Log::Log(LogPrinter printer) : printer_(printer), manual_flush_(0), buffer_(0) {
+  if (printer_ == nullptr) {
+    if (FLAG_redirect_isolate_log_to == nullptr) {
+      printer_ = [](const char* data) { OS::PrintErr("%s", data); };
+    } else {
+      printer_ = &LogFile::Print;
+    }
+  }
+}
 
 Log::~Log() {
   // Did someone enable manual flushing and then forgot to Flush?
@@ -108,7 +144,7 @@
   TerminateString();
   const char* str = &buffer_[cursor];
   ASSERT(str != nullptr);
-  printer_("%s", str);
+  printer_(str);
   buffer_.TruncateTo(cursor);
 }
 
@@ -172,6 +208,13 @@
 }
 
 bool Log::ShouldFlush() const {
+#ifdef DART_TARGET_OS_ANDROID
+  // Android truncates on 1023 characters, flush more eagerly.
+  // Flush on newlines, because otherwise Android inserts newlines everywhere.
+  if (*(buffer_.end() - 1) == '\n') {
+    return true;
+  }
+#endif  // DART_TARGET_OS_ANDROID
   return ((manual_flush_ == 0) || FLAG_force_log_flush ||
           ((FLAG_force_log_flush_at_size > 0) &&
            (cursor() > FLAG_force_log_flush_at_size)));
diff --git a/runtime/vm/log.h b/runtime/vm/log.h
index f96c5ea..18c71e4 100644
--- a/runtime/vm/log.h
+++ b/runtime/vm/log.h
@@ -22,11 +22,11 @@
 
 #define THR_VPrint(format, args) Log::Current()->VPrint(format, args)
 
-typedef void (*LogPrinter)(const char* str, ...) PRINTF_ATTRIBUTE(1, 2);
+typedef void (*LogPrinter)(const char* data);
 
 class Log {
  public:
-  explicit Log(LogPrinter printer = OS::PrintErr);
+  explicit Log(LogPrinter printer = nullptr);
   ~Log();
 
   static Log* Current();
diff --git a/runtime/vm/log_test.cc b/runtime/vm/log_test.cc
index 6a926bb..d4d2864 100644
--- a/runtime/vm/log_test.cc
+++ b/runtime/vm/log_test.cc
@@ -18,26 +18,12 @@
 
 static const char* test_output_ = NULL;
 
-PRINTF_ATTRIBUTE(1, 2)
-static void TestPrinter(const char* format, ...) {
-  // Measure.
-  va_list args;
-  va_start(args, format);
-  intptr_t len = Utils::VSNPrint(NULL, 0, format, args);
-  va_end(args);
-
-  // Print string to buffer.
-  char* buffer = reinterpret_cast<char*>(malloc(len + 1));
-  va_list args2;
-  va_start(args2, format);
-  Utils::VSNPrint(buffer, (len + 1), format, args2);
-  va_end(args2);
-
+static void TestPrinter(const char* buffer) {
   if (test_output_ != NULL) {
     free(const_cast<char*>(test_output_));
     test_output_ = NULL;
   }
-  test_output_ = buffer;
+  test_output_ = strdup(buffer);
 
   // Also print to stdout to see the overall result.
   OS::PrintErr("%s", test_output_);
diff --git a/tools/VERSION b/tools/VERSION
index 08be743..d113f0b 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 166
+PRERELEASE 167
 PRERELEASE_PATCH 0
\ No newline at end of file