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