[vm/infra] Nascent AOT IL tests infrastructure.

Our current unit testing infrastructure does not make it possible to
test AOT compilation pipeline end-to-end, because it does not run TFA
when generating Kernel.

This makes it challenging to write regression tests for certain issues.

Instead we extend test runner with a capability to perform IL matching
when running AOT tests.

runtime/docs/infa/il_tests.md provides details on how to write such tests.

TEST=manually

Change-Id: I6f5220b814f4a5d8c053efacd3711df495dea404
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/214961
Commit-Queue: Slava Egorov <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
This commit is contained in:
Vyacheslav Egorov 2021-09-30 13:19:42 +00:00 committed by commit-bot@chromium.org
parent 7ffd140a4e
commit 8740a4f10f
10 changed files with 412 additions and 27 deletions

View file

@ -725,7 +725,12 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration
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 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration
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 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration
// 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 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration
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 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration
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,

View file

@ -211,6 +211,11 @@ class TestFile extends _TestFileBase {
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 @@ class TestFile extends _TestFileBase {
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 @@ class TestFile extends _TestFileBase {
sharedObjects = [],
otherResources = [],
experiments = [],
ilMatches = [],
super(null, null, []);
TestFile._(Path suiteDirectory, Path path, List<StaticError> expectedErrors,
@ -376,7 +383,8 @@ class TestFile extends _TestFileBase {
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 @@ class TestFile extends _TestFileBase {
/// 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 @@ class _MultitestFile extends _TestFileBase implements TestFile {
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;

189
pkg/vm/bin/compare_il.dart Normal file
View file

@ -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');
}
}

35
pkg/vm/tool/compare_il Executable file
View file

@ -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" $@

View file

@ -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" %*

View file

@ -29,10 +29,8 @@ SDK_DIR="$CUR_DIR/../../.."
# 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" $@

View file

@ -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)
```

View file

@ -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 @@ DEFINE_FLAG(charp,
"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 @@ void Log::Flush(const intptr_t cursor) {
TerminateString();
const char* str = &buffer_[cursor];
ASSERT(str != nullptr);
printer_("%s", str);
printer_(str);
buffer_.TruncateTo(cursor);
}

View file

@ -22,11 +22,11 @@ class LogBlock;
#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();

View file

@ -18,26 +18,12 @@ namespace dart {
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_);