diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart index cb968a9cadcc..a32b3945f229 100644 --- a/pkg/test_runner/lib/src/compiler_configuration.dart +++ b/pkg/test_runner/lib/src/compiler_configuration.dart @@ -828,6 +828,9 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration if (_configuration.useQemu) '--no-use-integer-division', if (arguments.contains('--print-flow-graph-optimized')) '--redirect-isolate-log-to=$tempDir/out.il', + if (arguments.contains('--print-flow-graph-optimized') && + (_configuration.isMinified || arguments.contains('--obfuscate'))) + '--save-obfuscation_map=$tempDir/renames.json', ..._replaceDartFiles(arguments, tempKernelFile(tempDir)), ]; @@ -844,6 +847,7 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration var args = [ arguments.firstWhere((arg) => arg.endsWith('_il_test.dart')), '$tempDir/out.il', + if (arguments.contains('--obfuscate')) '$tempDir/renames.json', ]; return CompilationCommand('compare_il', tempDir, bootstrapDependencies(), @@ -965,9 +969,10 @@ class PrecompilerCompilerConfiguration extends CompilerConfiguration List computeCompilerArguments( TestFile testFile, List vmOptions, List args) { return [ - if (testFile.ilMatches.isNotEmpty) ...[ + if (testFile.isVmIntermediateLanguageTest) ...[ '--print-flow-graph-optimized', - '--print-flow-graph-filter=${testFile.ilMatches.join(',')}' + '--print-flow-graph-as-json', + '--print-flow-graph-filter=@pragma', ], if (_enableAsserts) '--enable_asserts', ...filterVmOptions(vmOptions), diff --git a/pkg/test_runner/lib/src/test_file.dart b/pkg/test_runner/lib/src/test_file.dart index 8b61d68b117b..3c71e5b40853 100644 --- a/pkg/test_runner/lib/src/test_file.dart +++ b/pkg/test_runner/lib/src/test_file.dart @@ -211,10 +211,7 @@ 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 []; + final isVmIntermediateLanguageTest = filePath.endsWith('_il_test.dart'); // VM options. var vmOptions = >[]; @@ -341,7 +338,7 @@ class TestFile extends _TestFileBase { sharedObjects: sharedObjects, otherResources: otherResources, experiments: experiments, - ilMatches: ilMatches); + isVmIntermediateLanguageTest: isVmIntermediateLanguageTest); } /// A special fake test file for representing a VM unit test written in C++. @@ -363,7 +360,7 @@ class TestFile extends _TestFileBase { sharedObjects = [], otherResources = [], experiments = [], - ilMatches = [], + isVmIntermediateLanguageTest = false, super(null, null, []); TestFile._(Path suiteDirectory, Path path, List expectedErrors, @@ -384,7 +381,7 @@ class TestFile extends _TestFileBase { this.sharedObjects, this.otherResources, this.experiments, - this.ilMatches = const []}) + this.isVmIntermediateLanguageTest = false}) : super(suiteDirectory, path, expectedErrors) { assert(!isMultitest || dartOptions.isEmpty); } @@ -403,6 +400,7 @@ class TestFile extends _TestFileBase { final bool hasRuntimeError; final bool hasStaticWarning; final bool hasCrash; + final bool isVmIntermediateLanguageTest; /// The features that a test configuration must support in order to run this /// test. @@ -411,9 +409,6 @@ class TestFile extends _TestFileBase { /// requirements, the test is implicitly skipped. final List requirements; - /// List of functions which will have their IL verified (in AOT mode). - final List ilMatches; - final List sharedOptions; final List dartOptions; final List dart2jsOptions; @@ -479,6 +474,7 @@ class _MultitestFile extends _TestFileBase implements TestFile { final bool hasStaticWarning; final bool hasSyntaxError; bool get hasCrash => _origin.hasCrash; + bool get isVmIntermediateLanguageTest => _origin.isVmIntermediateLanguageTest; _MultitestFile(this._origin, Path path, this.multitestKey, List expectedErrors, @@ -493,7 +489,6 @@ class _MultitestFile extends _TestFileBase implements TestFile { String get packages => _origin.packages; List get requirements => _origin.requirements; - List get ilMatches => _origin.ilMatches; List get dart2jsOptions => _origin.dart2jsOptions; List get dartOptions => _origin.dartOptions; List get ddcOptions => _origin.ddcOptions; diff --git a/pkg/vm/bin/compare_il.dart b/pkg/vm/bin/compare_il.dart index 9d9febd24856..8196a5e30743 100644 --- a/pkg/vm/bin/compare_il.dart +++ b/pkg/vm/bin/compare_il.dart @@ -5,185 +5,157 @@ // 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:collection'; +import 'dart:convert'; import 'dart:io'; +import 'dart:mirrors'; -void main(List args) { - if (args.length != 2) { - throw 'Usage: compare_il <*_il_test.dart> '; +import 'package:collection/collection.dart'; + +import 'package:vm/testing/il_matchers.dart'; + +void main(List args) async { + getName = MirrorSystem.getName; + + if (args.length < 2 || args.length > 3) { + throw 'Usage: compare_il <*_il_test.dart> []'; } final testFile = args[0]; final ilFile = args[1]; + final renamesFile = args.length == 3 ? args[2] : null; - 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'); + final rename = _loadRenames(renamesFile); + final graphs = _loadGraphs(ilFile, rename); + final tests = await _loadTestCases(testFile); + + Map findMatchingGraphs(String name) { + final suffix = '_${rename(name)}'; + return graphs.entries.firstWhere((f) => f.key.contains(suffix)).value; + } + + for (var test in tests) { + test.run(findMatchingGraphs(test.name)); } exit(0); // Success. } -// IL instruction extracted from flow graph dump. -class Instruction { - final String raw; +class TestCase { + final String name; + final String phasesFilter; + final LibraryMirror library; - Instruction(this.raw); + late final phases = + phasesFilter.split(',').expand(_expandPhasePattern).toList(); - String get opcode { - final match = instructionPattern.firstMatch(raw)!; - final op = match.namedGroup('opcode')!; - final blockType = match.namedGroup('block_type'); + TestCase({ + required this.name, + required this.phasesFilter, + required this.library, + }); - // Handle blocks which look like "B%d[%s]". - if (blockType != null) { - return blockTypes[blockType]!; - } + void run(Map graphs) { + print('matching IL (${phases.join(', ')}) for $name'); + library.invoke(MirrorSystem.getSymbol('matchIL\$$name'), + phases.map((phase) => graphs[phase]!).toList()); + print('... ok'); + } - // Handle parallel moves specially. - if (op.startsWith('ParallelMove')) { - return 'ParallelMove'; + /// Parses phase filter components (same format as --compiler-passes flag). + static List _expandPhasePattern(String pattern) { + bool printBefore = false, printAfter = false; + switch (pattern[0]) { + case '[': + printBefore = true; + break; + case ']': + printAfter = true; + break; + case '*': + printBefore = printAfter = true; + break; } - // Handle branches. - if (op.startsWith(branchIfPrefix)) { - return 'Branch(${op.substring(branchIfPrefix.length)})'; + final phaseName = + (printBefore || printAfter) ? pattern.substring(1) : pattern; + + if (!printBefore && !printAfter) { + printAfter = true; } - // Normal instruction. - return op; + return [ + if (printBefore) 'Before $phaseName', + if (printAfter) 'After $phaseName', + ]; } - - @override - String toString() => 'Instruction($opcode)'; - - static final instructionPattern = RegExp( - r'^\s*\d+:\s+(v\d+ <- )?(?[^:[(]+(?\[[\w ]+\])?)'); - - static const blockTypes = { - '[join]': 'JoinEntry', - '[target]': 'TargetEntry', - '[graph]': 'GraphEntry', - '[function entry]': 'FunctionEntry' - }; - - static const branchIfPrefix = 'Branch if '; } -Map> _extractGraphs(String ilFile) { - final graphs = >{}; - - final reader = LineReader(ilFile); - - var instructions = []; - 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 = []; - } else { - reader.next(); +/// Extracts test cases from the given file by looking for functions +/// marked with @pragma('vm:testing:print-flow-graph', ...). +Future> _loadTestCases(String testFile) async { + final mirrorSystem = currentMirrorSystem(); + final library = + await mirrorSystem.isolate.loadUri(File(testFile).absolute.uri); + + pragma? getPragma(DeclarationMirror decl, String name) => decl.metadata + .map((m) => m.reflectee) + .whereType() + .firstWhereOrNull((p) => p.name == name); + + final cases = LinkedHashSet( + equals: (a, b) => a.name == b.name, + hashCode: (a) => a.name.hashCode, + ); + + void processDeclaration(DeclarationMirror decl) { + final p = getPragma(decl, 'vm:testing:print-flow-graph'); + if (p != null) { + final name = MirrorSystem.getName(decl.simpleName); + final added = cases.add(TestCase( + name: name, + phasesFilter: (p.options as String?) ?? 'AllocateRegisters', + library: library, + )); + if (!added) throw 'duplicate test case with name $name'; } } - return graphs; -} - -Map> _extractExpectations(String testFile) { - final expectations = >{}; - - final reader = LineReader(testFile); - - final matchILPattern = RegExp(r'^// MatchIL\[AOT\]=(?.*)$'); - final matcherPattern = RegExp(r'^// __ (?.*)$'); - - var matchers = []; - 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 = []; + for (var decl in library.declarations.values) { + if (decl is ClassMirror) { + decl.declarations.values.forEach(processDeclaration); } else { - reader.next(); + processDeclaration(decl); } } - return expectations; + return cases; } -class LineReader { - final List lines; - int lineno = 0; - - LineReader(String path) : lines = File(path).readAsLinesSync(); +Map> _loadGraphs(String ilFile, Renamer rename) { + final graphs = >{}; - String get current => lines[lineno]; + for (var graph in File(ilFile).readAsLinesSync()) { + final m = jsonDecode(graph) as Map; + graphs.putIfAbsent(m['f'], () => {})[m['p']] = + FlowGraph(m['b'], m['desc'], rename: rename); + } - bool get hasMore => lineno < lines.length; + return graphs; +} - String next() { - final curr = current; - lineno++; - return curr; +Renamer _loadRenames(String? renamesFile) { + // Load renames map if present. + if (renamesFile == null) { + return (v) => v; } - bool testNext(String expected) { - if (current == expected) { - next(); - return true; - } - return false; - } + final list = + (jsonDecode(File(renamesFile).readAsStringSync()) as List).cast(); - String? matchNext(RegExp pattern) { - final m = pattern.firstMatch(current); - return m?.namedGroup('value'); - } + final renamesMap = { + for (var i = 0; i < list.length; i += 2) list[i]: list[i + 1], + }; + + return (v) => renamesMap[v] ?? v; } diff --git a/pkg/vm/lib/testing/il_matchers.dart b/pkg/vm/lib/testing/il_matchers.dart new file mode 100644 index 000000000000..dd5eb8f56df7 --- /dev/null +++ b/pkg/vm/lib/testing/il_matchers.dart @@ -0,0 +1,430 @@ +// 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. + +/// A library to facilitate programmatic matching against flow graphs +/// collected during IL tests. See runtime/docs/infra/il_tests.md for more +/// info. + +typedef Renamer = String Function(String); + +/// Flow graph parsed from --print-flow-graph-as-json output. +class FlowGraph { + final List blocks; + final Map descriptors; + final Renamer rename; + + FlowGraph(this.blocks, Map desc, {required this.rename}) + : descriptors = { + for (var e in desc.entries) + e.key: InstructionDescriptor.fromJson(e.value) + }; + + /// Match the sequence of blocks in this flow graph against the given + /// sequence of matchers: `expected[i]` is expected to match `blocks[i]`, + /// but there can be more blocks in the graph than matchers (the suffix is + /// ignored). + /// + /// If [env] is provided it will be used as matching environment, otherwise + /// a fresh instance of [Env] will be created and used. + /// + /// This function returns the populated matching environment. + Env match(List expected, {Env? env}) { + env ??= Env(rename: rename, descriptors: descriptors); + + for (var i = 0; i < expected.length; i++) { + expected[i].match(env, blocks[i]).expectMatched('failed to match'); + } + + return env; + } +} + +class InstructionDescriptor { + final List attributes; + final Map attributeIndex; + + InstructionDescriptor.fromJson(List attrs) + : this._(attrs.map((v) => _demangle(v)).toList()); + + InstructionDescriptor._(List attrs) + : attributes = attrs.cast(), + attributeIndex = {for (var i = 0; i < attrs.length; i++) attrs[i]: i}; + + static String _demangle(String v) { + final prefixLen = v.startsWith('&') ? 1 : 0; + final suffixLen = v.endsWith('()') ? 2 : 0; + return v.substring(prefixLen, v.length - suffixLen); + } +} + +/// Matching environment. +/// +/// This is fundamentally just a name to id mapping which allows to track +/// correspondence between names given to some matchers and blocks/instructions +/// which matched those matchers. +/// +/// This object is also used to carry around auxiliary information which might +/// be needed for matching, e.g. [Renamer]. +class Env { + final Map descriptors; + final Renamer rename; + final Map nameToId = {}; + + Env({required this.rename, required this.descriptors}); + + void bind(String name, Map instrOrBlock) { + final id = instrOrBlock['v'] ?? instrOrBlock['b']; + + if (nameToId.containsKey(name) && nameToId[name] != id) { + throw 'Binding mismatch for $name: got ${nameToId[name]} and $id'; + } + + nameToId[name] = id; + } +} + +abstract class Matcher { + /// Try matching this matcher against the given value. Returns + /// [MatchStatus.matched] if match succeeded and an instance of + /// [MatchStatus.fail] otherwise. + MatchStatus match(Env e, dynamic v); +} + +class MatchStatus { + final String? message; + + const MatchStatus._matched() : message = null; + const MatchStatus.fail(String message) : message = message; + + bool get isMatch => message == null; + bool get isFail => message != null; + + static const MatchStatus matched = MatchStatus._matched(); + + void expectMatched(String s) { + if (message != null) { + throw 'Failed to match: $message'; + } + } +} + +/// Matcher which always succeeds. +class _AnyMatcher implements Matcher { + const _AnyMatcher(); + + @override + MatchStatus match(Env e, v) => MatchStatus.matched; + + @override + String toString() { + return '*'; + } +} + +/// Matcher which updates matching environment when it succeeds. +class _BoundMatcher implements Matcher { + final String name; + final Matcher nested; + + _BoundMatcher(this.name, this.nested); + + @override + MatchStatus match(Env e, dynamic v) { + final result = nested.match(e, v); + if (result.isMatch) { + e.bind(name, v); + } + return result; + } + + @override + String toString() { + return '$name <- $nested'; + } +} + +/// Matcher which matches a specified value [v]. +class _EqualsMatcher implements Matcher { + final dynamic v; + + _EqualsMatcher(this.v); + + @override + MatchStatus match(Env e, v) { + if (this.v == v) { + return MatchStatus.matched; + } + + // Some instructions refer to obfuscated names, try to rename + // the expectation and try again. + if (this.v is String && v is String && e.rename(this.v) == v) { + return MatchStatus.matched; + } + + return this.v == v + ? MatchStatus.matched + : MatchStatus.fail('expected ${this.v} got $v'); + } + + @override + String toString() => '$v'; +} + +/// Matcher which matches the value which is equivalent to the binding +/// with the given [name] in the matching environment. +/// +/// If this matcher is [binding] then it will populate the binding in the +/// matching environment if the [name] is not bound yet on the first call +/// to [match]. Otherwise if the [name] is not bound when [match] is called +/// an exception will be thrown. +/// +/// Binding matchers are used when we might see the use of a value before its +/// definition (e.g. we usually use the name of the block in the `Goto` or +/// `Branch` before we see the block itself). +class _RefMatcher implements Matcher { + final String name; + final bool binding; + + _RefMatcher(this.name, {this.binding = false}); + + @override + MatchStatus match(Env e, v) { + if (e.nameToId.containsKey(name)) { + return e.nameToId[name] == v + ? MatchStatus.matched + : MatchStatus.fail( + 'expected $name to bind to ${e.nameToId[name]} but got $v'); + } + + if (!binding) { + throw UnimplementedError('Unbound reference to ${name}'); + } + + e.nameToId[name] = v; + return MatchStatus.matched; + } + + @override + String toString() { + return name; + } +} + +/// A wrapper which matches a list of matchers against a list of values. +class _ListMatcher implements Matcher { + final List expected; + + _ListMatcher(this.expected); + + @override + MatchStatus match(Env e, dynamic got) { + if (got is! List) { + return MatchStatus.fail('expected List, got ${got.runtimeType}'); + } + + if (expected.length > got.length) { + return MatchStatus.fail( + 'expected at least ${expected.length} elements got ${got.length}'); + } + + for (var i = 0; i < expected.length; i++) { + final result = expected[i].match(e, got[i]); + if (result.isFail) { + return MatchStatus.fail( + 'mismatch at index ${i}, expected ${expected[i]} ' + 'got ${got[i]}: ${result.message}'); + } + } + + if (expected.last is _AnyMatcher || expected.length == got.length) { + return MatchStatus.matched; + } + + return MatchStatus.fail( + 'expected exactly ${expected.length} elements got ${got.length}'); + } + + @override + String toString() => '[${expected.join(',')}]'; +} + +/// A matcher which matches a block of the specified [kind] and contents. +/// +/// Contents are specified as a sequence of matchers ([body]). For each of +/// those matchers a matching block is expected to contain at least one +/// instruction that matches it. Matching is done in order: first we scan +/// the block until we find the match for the first matcher in body, then +/// we continue scanning until we find the match for the second and so on. +class _BlockMatcher implements Matcher { + final String kind; + final List body; + + _BlockMatcher(this.kind, [this.body = const []]); + + @override + MatchStatus match(Env e, covariant Map block) { + if (block['o'] != '${kind}Entry') { + return MatchStatus.fail( + 'Expected block of kind ${kind} got ${block['o']} ' + 'when matching B${block['b']}'); + } + + final gotBody = [...?block['d'], ...?block['is']]; + + var matcherIndex = 0; + for (int i = 0; i < gotBody.length && matcherIndex < body.length; i++) { + if (body[matcherIndex].match(e, gotBody[i]).isMatch) { + matcherIndex++; + } + } + if (matcherIndex != body.length) { + return MatchStatus.fail('Unmatched instruction: ${body[matcherIndex]} ' + 'in block B${block['b']}'); + } + return MatchStatus.matched; + } +} + +/// A matcher for instruction's named attributes. +/// +/// Attributes are resolved to their indices through [Env.descriptors]. +class _AttributesMatcher implements Matcher { + final String op; + final Map matchers; + + _ListMatcher? impl; + + _AttributesMatcher(this.op, this.matchers); + + @override + MatchStatus match(Env e, dynamic v) { + impl ??= _ListMatcher(e.descriptors[op]!.attributes + .map((name) => matchers[name] ?? const _AnyMatcher()) + .toList()); + return impl!.match(e, v); + } +} + +/// Matcher which matches an instruction with opcode [op] and properties +/// specified in [matchers] map. +class InstructionMatcher implements Matcher { + final String op; + final Map matchers; + + InstructionMatcher( + {required String op, List? data, List? inputs}) + : this._(op: op, matchers: { + if (data != null) 'd': _ListMatcher(data), + if (inputs != null) 'i': _ListMatcher(inputs), + }); + + InstructionMatcher._({ + required this.op, + required this.matchers, + }); + + @override + MatchStatus match(Env e, covariant Map instr) { + if (instr['o'] != op) { + return MatchStatus.fail('expected instruction ${op} got ${instr['o']}'); + } + + for (var entry in matchers.entries) { + final result = entry.value.match(e, instr[entry.key]); + if (result.isFail) { + return result; + } + } + + return MatchStatus.matched; + } + + @override + String toString() { + return '$op($matchers)'; + } +} + +/// This class uses `noSuchMethod` to allow writing code like +/// +/// ``` +/// match.Op(in0, ..., inN, attr0: a0, ..., attrK: aK) +/// ``` +/// +/// This will produce an instruction matcher which matches opcode `Op` and +/// expects `in0, ..., inN` to match instructions inputs, while `a0, ...` +/// matchers are expected to match attributes with names `attr0, ...`. +class Matchers { + _BlockMatcher block(String kind, [List body = const []]) { + return _BlockMatcher(kind, List.from(body)); + } + + final _AnyMatcher any = const _AnyMatcher(); + + InstructionMatcher Goto(String dest) => + InstructionMatcher._(op: 'Goto', matchers: { + 's': _ListMatcher([_blockRef(dest)]) + }); + + InstructionMatcher Branch(InstructionMatcher compare, + {String? ifTrue, String? ifFalse}) => + InstructionMatcher._(op: 'Branch', matchers: { + 'cc': compare, + 's': _ListMatcher([ + ifTrue != null ? _blockRef(ifTrue) : any, + ifFalse != null ? _blockRef(ifFalse) : any, + ]), + }); + + @override + Object? noSuchMethod(Invocation invocation) { + final data = { + for (var e in invocation.namedArguments.entries) + getName(e.key): Matchers._toAttributeMatcher(e.value), + }; + final inputs = + invocation.positionalArguments.map(Matchers._toInputMatcher).toList(); + final op = getName(invocation.memberName); + return InstructionMatcher._(op: op, matchers: { + if (data.isNotEmpty) 'd': _AttributesMatcher(op, data), + if (inputs.isNotEmpty) 'i': _ListMatcher(inputs), + }); + } + + static Matcher _blockRef(String name) => _RefMatcher(name, binding: true); + + static Matcher _toAttributeMatcher(dynamic v) { + if (v is Matcher) { + return v; + } else { + return _EqualsMatcher(v); + } + } + + static Matcher _toInputMatcher(dynamic v) { + if (v is Matcher) { + return v; + } else if (v is String) { + return _RefMatcher(v); + } else { + throw ArgumentError.value( + v, 'v', 'Expected either a Matcher or a String (binding name)'); + } + } +} + +/// Extension which enables `'name' << matcher` syntax for creating bound +/// matchers. +extension BindingExtension on String { + Matcher operator <<(Matcher matcher) { + return _BoundMatcher(this, matcher); + } +} + +final dynamic match = Matchers(); + +/// This file should not depend on dart:mirrors because it is imported into +/// tests, which are compiled in AOT mode. So instead we let compare_il driver +/// set this field. +late String Function(Symbol) getName; diff --git a/pkg/vm/tool/compare_il b/pkg/vm/tool/compare_il index 65ad55e2ff48..4973042b0051 100755 --- a/pkg/vm/tool/compare_il +++ b/pkg/vm/tool/compare_il @@ -26,10 +26,6 @@ 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 +DART="$SDK_DIR/tools/sdks/dart-sdk/bin/dart" exec "$DART" $DART_VM_FLAGS "${SDK_DIR}/pkg/vm/bin/compare_il.dart" $@ diff --git a/runtime/docs/infra/il_tests.md b/runtime/docs/infra/il_tests.md index 8400b6e0c432..94c4500ad70b 100644 --- a/runtime/docs/infra/il_tests.md +++ b/runtime/docs/infra/il_tests.md @@ -11,76 +11,47 @@ 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: +Each IL test should contain one or more of the functions marked with a +`@pragma('vm:testing:print-flow-graph'[, 'phases filter'])`. -```dart -// MatchIL[AOT]=functionName -// comment -// __ op -// comment -// __ op -// __ op -// __ op -``` +These functions will have their IL dumped at points specified by the +_phases filter_ (if present, `]AllocateRegisters` by default), which follows +the same syntax as `--compiler-passes=` flag and dumped IL will be compared +against the expectations, which are specified programmatically using +`package:vm/testing/il_matchers.dart` helpers. A function named `foo` has +its IL expectations in the function called `matchIL$foo` in the same file. -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. +```dart +import 'package:vm/testing/il_matchers.dart'; -`// 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). +@pragma('vm:testing:print-flow-graph') +void foo() { +} -`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. +/// Expectations for [foo]. +void matchIL$foo(FlowGraph graph) { + graph.match([/* expectations */]); +} +``` -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`). +Actual matching is done by the `pkg/vm/tool/compare_il` script. ## Example ```dart -// MatchIL[AOT]=factorial -// __ GraphEntry -// __ FunctionEntry -// __ CheckStackOverflow -// __ Branch(EqualityCompare) @pragma('vm:never-inline') +@pragma('vm:testing:print-flow-graph') 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} +void matchIL$factorial(FlowGraph graph) { + // Expected a graph which starts with GraphEntry block followed by a + // FunctionEntry block. FunctionEntry block should contain a Branch() + // instruction, with EqualityCompare as a comparison. + graph.match([ + match.block('Graph'), + match.block('Function', [ + match.Branch(match.EqualityCompare(match.any, match.any, kind: '==')), + ]), + ]); } - 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. (dart:isolate-patch/isolate_patch.dart:285:32) -#2 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:187:12) -``` +``` \ No newline at end of file diff --git a/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart b/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart index 9baf000cd85e..8447554eabfc 100644 --- a/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart +++ b/runtime/tests/vm/dart/aot_prefer_equality_comparison_il_test.dart @@ -5,14 +5,21 @@ // Test that we emit EqualityCompare rather than StrictCompare+BoxInt64 // when comparing non-nullable integer to a Smi. -// MatchIL[AOT]=factorial -// __ GraphEntry -// __ FunctionEntry -// __ CheckStackOverflow -// __ Branch(EqualityCompare) +import 'package:vm/testing/il_matchers.dart'; + @pragma('vm:never-inline') +@pragma('vm:testing:print-flow-graph') int factorial(int value) => value == 1 ? value : value * factorial(value - 1); +void matchIL$factorial(FlowGraph graph) { + graph.match([ + match.block('Graph'), + match.block('Function', [ + match.Branch(match.EqualityCompare(match.any, match.any, kind: '==')), + ]), + ]); +} + void main() { print(factorial(4)); } diff --git a/runtime/tests/vm/dart_2/aot_prefer_equality_comparison_il_test.dart b/runtime/tests/vm/dart_2/aot_prefer_equality_comparison_il_test.dart index 9baf000cd85e..8447554eabfc 100644 --- a/runtime/tests/vm/dart_2/aot_prefer_equality_comparison_il_test.dart +++ b/runtime/tests/vm/dart_2/aot_prefer_equality_comparison_il_test.dart @@ -5,14 +5,21 @@ // Test that we emit EqualityCompare rather than StrictCompare+BoxInt64 // when comparing non-nullable integer to a Smi. -// MatchIL[AOT]=factorial -// __ GraphEntry -// __ FunctionEntry -// __ CheckStackOverflow -// __ Branch(EqualityCompare) +import 'package:vm/testing/il_matchers.dart'; + @pragma('vm:never-inline') +@pragma('vm:testing:print-flow-graph') int factorial(int value) => value == 1 ? value : value * factorial(value - 1); +void matchIL$factorial(FlowGraph graph) { + graph.match([ + match.block('Graph'), + match.block('Function', [ + match.Branch(match.EqualityCompare(match.any, match.any, kind: '==')), + ]), + ]); +} + void main() { print(factorial(4)); } diff --git a/runtime/vm/compiler/api/print_filter.cc b/runtime/vm/compiler/api/print_filter.cc index be9762c772e8..4abe1e940abd 100644 --- a/runtime/vm/compiler/api/print_filter.cc +++ b/runtime/vm/compiler/api/print_filter.cc @@ -7,24 +7,46 @@ #include "vm/compiler/api/print_filter.h" #include "vm/flags.h" +#if !defined(DART_PRECOMPILED_RUNTIME) +#include "vm/compiler/compiler_pass.h" #include "vm/object.h" +#endif +#include "vm/symbols.h" namespace dart { DEFINE_FLAG(charp, print_flow_graph_filter, - NULL, + nullptr, "Print only IR of functions with matching names"); namespace compiler { // Checks whether function's name matches the given filter, which is // a comma-separated list of strings. -static bool PassesFilter(const char* filter, const Function& function) { - if (filter == NULL) { +static bool PassesFilter(const char* filter, + const Function& function, + uint8_t** compiler_pass_filter) { + if (filter == nullptr) { return true; } +#if !defined(DART_PRECOMPILED_RUNTIME) + if (strcmp(filter, "@pragma") == 0) { + Object& pass_filter = Object::Handle(); + const auto has_pragma = + Library::FindPragma(dart::Thread::Current(), /*only_core=*/false, + function, Symbols::vm_testing_print_flow_graph(), + /*multiple=*/false, &pass_filter); + if (has_pragma && !pass_filter.IsNull() && + compiler_pass_filter != nullptr) { + *compiler_pass_filter = dart::CompilerPass::ParseFiltersFromPragma( + String::Cast(pass_filter).ToCString()); + } + return has_pragma; + } +#endif + char* save_ptr; // Needed for strtok_r. const char* scrubbed_name = String::Handle(function.QualifiedScrubbedName()).ToCString(); @@ -36,9 +58,9 @@ static bool PassesFilter(const char* filter, const Function& function) { strncpy(filter_buffer, filter, len); // strtok modifies arg 1. char* token = strtok_r(filter_buffer, ",", &save_ptr); bool found = false; - while (token != NULL) { - if ((strstr(function_name, token) != NULL) || - (strstr(scrubbed_name, token) != NULL)) { + while (token != nullptr) { + if ((strstr(function_name, token) != nullptr) || + (strstr(scrubbed_name, token) != nullptr)) { found = true; break; } @@ -53,15 +75,17 @@ static bool PassesFilter(const char* filter, const Function& function) { } } } - token = strtok_r(NULL, ",", &save_ptr); + token = strtok_r(nullptr, ",", &save_ptr); } delete[] filter_buffer; return found; } -bool PrintFilter::ShouldPrint(const Function& function) { - return PassesFilter(FLAG_print_flow_graph_filter, function); +bool PrintFilter::ShouldPrint(const Function& function, + uint8_t** compiler_pass_filter /* = nullptr */) { + return PassesFilter(FLAG_print_flow_graph_filter, function, + compiler_pass_filter); } } // namespace compiler diff --git a/runtime/vm/compiler/api/print_filter.h b/runtime/vm/compiler/api/print_filter.h index 604fa9c365e5..95d7a3906a32 100644 --- a/runtime/vm/compiler/api/print_filter.h +++ b/runtime/vm/compiler/api/print_filter.h @@ -19,7 +19,8 @@ namespace compiler { class PrintFilter : public AllStatic { public: - static bool ShouldPrint(const Function& function); + static bool ShouldPrint(const Function& function, + uint8_t** compiler_pass_filter = nullptr); }; } // namespace compiler diff --git a/runtime/vm/compiler/backend/flow_graph.cc b/runtime/vm/compiler/backend/flow_graph.cc index b0195987fedd..1dbf21362583 100644 --- a/runtime/vm/compiler/backend/flow_graph.cc +++ b/runtime/vm/compiler/backend/flow_graph.cc @@ -56,7 +56,10 @@ FlowGraph::FlowGraph(const ParsedFunction& parsed_function, loop_invariant_loads_(nullptr), captured_parameters_(new (zone()) BitVector(zone(), variable_count())), inlining_id_(-1), - should_print_(FlowGraphPrinter::ShouldPrint(parsed_function.function())) { + should_print_(false) { + should_print_ = FlowGraphPrinter::ShouldPrint(parsed_function.function(), + &compiler_pass_filters_); + direct_parameters_size_ = ParameterOffsetAt( function(), num_direct_parameters_, /*last_slot*/ false); DiscoverBlocks(); diff --git a/runtime/vm/compiler/backend/flow_graph.h b/runtime/vm/compiler/backend/flow_graph.h index c847babf4786..2c82c131175d 100644 --- a/runtime/vm/compiler/backend/flow_graph.h +++ b/runtime/vm/compiler/backend/flow_graph.h @@ -469,6 +469,9 @@ class FlowGraph : public ZoneAllocated { void RenameUsesDominatedByRedefinitions(); bool should_print() const { return should_print_; } + const uint8_t* compiler_pass_filters() const { + return compiler_pass_filters_; + } // // High-level utilities. @@ -632,6 +635,7 @@ class FlowGraph : public ZoneAllocated { intptr_t inlining_id_; bool should_print_; + uint8_t* compiler_pass_filters_ = nullptr; const Array* coverage_array_ = &Array::empty_array(); }; diff --git a/runtime/vm/compiler/backend/il.cc b/runtime/vm/compiler/backend/il.cc index 2ba03358f661..9434956caeb2 100644 --- a/runtime/vm/compiler/backend/il.cc +++ b/runtime/vm/compiler/backend/il.cc @@ -1207,7 +1207,7 @@ bool GraphEntryInstr::IsCompiledForOsr() const { // ==== Support for visiting flow graphs. #define DEFINE_ACCEPT(ShortName, Attrs) \ - void ShortName##Instr::Accept(FlowGraphVisitor* visitor) { \ + void ShortName##Instr::Accept(InstructionVisitor* visitor) { \ visitor->Visit##ShortName(this); \ } diff --git a/runtime/vm/compiler/backend/il.h b/runtime/vm/compiler/backend/il.h index 62c5fb4f414f..2be44dcc4f04 100644 --- a/runtime/vm/compiler/backend/il.h +++ b/runtime/vm/compiler/backend/il.h @@ -11,6 +11,7 @@ #endif // defined(DART_PRECOMPILED_RUNTIME) #include +#include #include #include "vm/allocation.h" @@ -54,6 +55,7 @@ class FlowGraphCompiler; class FlowGraphVisitor; class ForwardInstructionIterator; class Instruction; +class InstructionVisitor; class LocalVariable; class LoopInfo; class ParsedFunction; @@ -537,7 +539,7 @@ FOR_EACH_ABSTRACT_INSTRUCTION(FORWARD_DECLARATION) // Functions required in all concrete instruction classes. #define DECLARE_INSTRUCTION_NO_BACKEND(type) \ virtual Tag tag() const { return k##type; } \ - virtual void Accept(FlowGraphVisitor* visitor); \ + virtual void Accept(InstructionVisitor* visitor); \ DEFINE_INSTRUCTION_TYPE_CHECK(type) #define DECLARE_INSTRUCTION_BACKEND() \ @@ -564,9 +566,13 @@ FOR_EACH_ABSTRACT_INSTRUCTION(FORWARD_DECLARATION) #define PRINT_TO_SUPPORT virtual void PrintTo(BaseTextBuffer* f) const; #define PRINT_OPERANDS_TO_SUPPORT \ virtual void PrintOperandsTo(BaseTextBuffer* f) const; +#define DECLARE_ATTRIBUTES(...) \ + auto GetAttributes() const { return std::make_tuple(__VA_ARGS__); } \ + static auto GetAttributeNames() { return std::make_tuple(#__VA_ARGS__); } #else #define PRINT_TO_SUPPORT #define PRINT_OPERANDS_TO_SUPPORT +#define DECLARE_ATTRIBUTES(...) #endif // defined(INCLUDE_IL_PRINTER) // Together with CidRange, this represents a mapping from a range of class-ids @@ -867,7 +873,7 @@ class Instruction : public ZoneAllocated { } // Visiting support. - virtual void Accept(FlowGraphVisitor* visitor) = 0; + virtual void Accept(InstructionVisitor* visitor) = 0; Instruction* previous() const { return previous_; } void set_previous(Instruction* instr) { @@ -2566,6 +2572,7 @@ class ParameterInstr : public Definition { block_(block) {} DECLARE_INSTRUCTION(Parameter) + DECLARE_ATTRIBUTES(index()) intptr_t index() const { return index_; } intptr_t param_offset() const { return param_offset_; } @@ -3254,6 +3261,7 @@ class ComparisonInstr : public Definition { virtual TokenPosition token_pos() const { return token_pos_; } Token::Kind kind() const { return kind_; } + DECLARE_ATTRIBUTES(kind()) virtual ComparisonInstr* CopyWithNewOperands(Value* left, Value* right) = 0; @@ -6674,6 +6682,8 @@ class LoadFieldInstr : public TemplateDefinition<1, Throws> { bool IsPotentialUnboxedDartFieldLoad() const; DECLARE_INSTRUCTION(LoadField) + DECLARE_ATTRIBUTES(&slot()) + virtual CompileType ComputeType() const; virtual intptr_t DeoptimizationTarget() const { return GetDeoptId(); } @@ -9664,9 +9674,27 @@ class Environment : public ZoneAllocated { DISALLOW_COPY_AND_ASSIGN(Environment); }; +class InstructionVisitor : public ValueObject { + public: + InstructionVisitor() {} + virtual ~InstructionVisitor() {} + +// Visit functions for instruction classes, with an empty default +// implementation. +#define DECLARE_VISIT_INSTRUCTION(ShortName, Attrs) \ + virtual void Visit##ShortName(ShortName##Instr* instr) {} + + FOR_EACH_INSTRUCTION(DECLARE_VISIT_INSTRUCTION) + +#undef DECLARE_VISIT_INSTRUCTION + + private: + DISALLOW_COPY_AND_ASSIGN(InstructionVisitor); +}; + // Visitor base class to visit each instruction and computation in a flow // graph as defined by a reversed list of basic blocks. -class FlowGraphVisitor : public ValueObject { +class FlowGraphVisitor : public InstructionVisitor { public: explicit FlowGraphVisitor(const GrowableArray& block_order) : current_iterator_(NULL), block_order_(&block_order) {} @@ -9680,15 +9708,6 @@ class FlowGraphVisitor : public ValueObject { // instructions in order from the block entry to exit. virtual void VisitBlocks(); -// Visit functions for instruction classes, with an empty default -// implementation. -#define DECLARE_VISIT_INSTRUCTION(ShortName, Attrs) \ - virtual void Visit##ShortName(ShortName##Instr* instr) {} - - FOR_EACH_INSTRUCTION(DECLARE_VISIT_INSTRUCTION) - -#undef DECLARE_VISIT_INSTRUCTION - protected: void set_block_order(const GrowableArray& block_order) { block_order_ = &block_order; diff --git a/runtime/vm/compiler/backend/il_printer.cc b/runtime/vm/compiler/backend/il_printer.cc index 1c8b3f8c44e1..df13a3a573b9 100644 --- a/runtime/vm/compiler/backend/il_printer.cc +++ b/runtime/vm/compiler/backend/il_printer.cc @@ -4,6 +4,8 @@ #include "vm/compiler/backend/il_printer.h" +#include + #include "vm/compiler/api/print_filter.h" #include "vm/compiler/backend/il.h" #include "vm/compiler/backend/linearscan.h" @@ -20,19 +22,195 @@ DEFINE_FLAG(bool, false, "Calls display a unary, sorted-by count form of ICData"); DEFINE_FLAG(bool, print_environments, false, "Print SSA environments."); +DEFINE_FLAG(bool, + print_flow_graph_as_json, + false, + "Use machine readable output when printing IL graphs."); DECLARE_FLAG(bool, trace_inlining_intervals); -bool FlowGraphPrinter::ShouldPrint(const Function& function) { - return compiler::PrintFilter::ShouldPrint(function); +class IlTestPrinter : public AllStatic { + public: + static void PrintGraph(const char* phase, FlowGraph* flow_graph) { + JSONWriter writer; + writer.OpenObject(); + writer.PrintProperty("p", phase); + writer.PrintProperty("f", flow_graph->function().ToFullyQualifiedCString()); + writer.OpenArray("b"); + for (auto block : flow_graph->reverse_postorder()) { + PrintBlock(&writer, block); + } + writer.CloseArray(); + writer.OpenObject("desc"); + AttributesSerializer(&writer).WriteDescriptors(); + writer.CloseObject(); + writer.CloseObject(); + THR_Print("%s\n", writer.ToCString()); + } + + static void PrintBlock(JSONWriter* writer, BlockEntryInstr* block) { + writer->OpenObject(); + writer->PrintProperty64("b", block->block_id()); + writer->PrintProperty("o", block->DebugName()); + if (auto block_with_defs = block->AsBlockEntryWithInitialDefs()) { + if (block_with_defs->initial_definitions() != nullptr && + block_with_defs->initial_definitions()->length() > 0) { + writer->OpenArray("d"); + for (auto defn : *block_with_defs->initial_definitions()) { + if (defn->IsConstant() && !defn->HasUses()) continue; + PrintInstruction(writer, defn); + } + writer->CloseArray(); + } + } + writer->OpenArray("is"); + if (auto join = block->AsJoinEntry()) { + for (PhiIterator it(join); !it.Done(); it.Advance()) { + PrintInstruction(writer, it.Current()); + } + } + for (auto instr : block->instructions()) { + PrintInstruction(writer, instr); + } + writer->CloseArray(); + writer->CloseObject(); + } + + static void PrintInstruction(JSONWriter* writer, + Instruction* instr, + const char* name = nullptr) { + writer->OpenObject(name); + if (auto defn = instr->AsDefinition()) { + if (defn->ssa_temp_index() != -1) { + writer->PrintProperty("v", defn->ssa_temp_index()); + } + } + writer->PrintProperty("o", instr->DebugName()); + if (auto branch = instr->AsBranch()) { + PrintInstruction(writer, branch->comparison(), "cc"); + } else { + if (instr->InputCount() != 0) { + writer->OpenArray("i"); + for (intptr_t i = 0; i < instr->InputCount(); i++) { + writer->PrintValue(instr->InputAt(i)->definition()->ssa_temp_index()); + } + writer->CloseArray(); + } else if (instr->ArgumentCount() != 0 && + instr->GetPushArguments() != nullptr) { + writer->OpenArray("i"); + for (intptr_t i = 0; i < instr->ArgumentCount(); i++) { + writer->PrintValue( + instr->ArgumentValueAt(i)->definition()->ssa_temp_index()); + } + writer->CloseArray(); + } + AttributesSerializer serializer(writer); + instr->Accept(&serializer); + } + if (instr->SuccessorCount() > 0) { + writer->OpenArray("s"); + for (auto succ : instr->successors()) { + writer->PrintValue(succ->block_id()); + } + writer->CloseArray(); + } + writer->CloseObject(); + } + + template + class HasGetAttributes { + template + static std::true_type test(decltype(&U::GetAttributes)); + template + static std::false_type test(...); + + public: + static constexpr bool value = decltype(test(0))::value; + }; + + class AttributesSerializer : public InstructionVisitor { + public: + explicit AttributesSerializer(JSONWriter* writer) : writer_(writer) {} + + void WriteDescriptors() { +#define DECLARE_VISIT_INSTRUCTION(ShortName, Attrs) \ + WriteDescriptor(#ShortName); + + FOR_EACH_INSTRUCTION(DECLARE_VISIT_INSTRUCTION) + +#undef DECLARE_VISIT_INSTRUCTION + } + +#define DECLARE_VISIT_INSTRUCTION(ShortName, Attrs) \ + virtual void Visit##ShortName(ShortName##Instr* instr) { Write(instr); } + + FOR_EACH_INSTRUCTION(DECLARE_VISIT_INSTRUCTION) + +#undef DECLARE_VISIT_INSTRUCTION + + private: + void WriteAttribute(const char* value) { writer_->PrintValue(value); } + + void WriteAttribute(intptr_t value) { writer_->PrintValue(value); } + + void WriteAttribute(Token::Kind kind) { + writer_->PrintValue(Token::Str(kind)); + } + + void WriteAttribute(const Slot* slot) { writer_->PrintValue(slot->Name()); } + + template + void WriteTuple(const std::tuple& tuple) { + std::apply([&](Ts const&... elements) { WriteAttribute(elements...); }, + tuple); + } + + template ::value>> + void Write(T* instr) { + writer_->OpenArray("d"); + WriteTuple(instr->GetAttributes()); + writer_->CloseArray(); + } + + void Write(Instruction* instr) { + // Default, do nothing. + } + + template + void WriteDescriptor( + const char* name, + typename std::enable_if_t::value>* = 0) { + writer_->OpenArray(name); + WriteTuple(T::GetAttributeNames()); + writer_->CloseArray(); + } + + template + void WriteDescriptor( + const char* name, + typename std::enable_if_t::value>* = 0) {} + + JSONWriter* writer_; + }; +}; + +bool FlowGraphPrinter::ShouldPrint( + const Function& function, + uint8_t** compiler_pass_filter /* = nullptr */) { + return compiler::PrintFilter::ShouldPrint(function, compiler_pass_filter); } void FlowGraphPrinter::PrintGraph(const char* phase, FlowGraph* flow_graph) { LogBlock lb; - THR_Print("*** BEGIN CFG\n%s\n", phase); - FlowGraphPrinter printer(*flow_graph); - printer.PrintBlocks(); - THR_Print("*** END CFG\n"); + if (FLAG_print_flow_graph_as_json) { + IlTestPrinter::PrintGraph(phase, flow_graph); + } else { + THR_Print("*** BEGIN CFG\n%s\n", phase); + FlowGraphPrinter printer(*flow_graph); + printer.PrintBlocks(); + THR_Print("*** END CFG\n"); + } fflush(stdout); } @@ -1286,7 +1464,9 @@ void FlowGraphPrinter::PrintICData(const ICData& ic_data, UNREACHABLE(); } -bool FlowGraphPrinter::ShouldPrint(const Function& function) { +bool FlowGraphPrinter::ShouldPrint( + const Function& function, + uint8_t** compiler_pass_filter /* = nullptr */) { return false; } diff --git a/runtime/vm/compiler/backend/il_printer.h b/runtime/vm/compiler/backend/il_printer.h index 96268c8eabe4..3784083d067e 100644 --- a/runtime/vm/compiler/backend/il_printer.h +++ b/runtime/vm/compiler/backend/il_printer.h @@ -56,7 +56,8 @@ class FlowGraphPrinter : public ValueObject { static void PrintCidRangeData(const CallTargets& ic_data, intptr_t num_checks_to_print = kPrintAll); - static bool ShouldPrint(const Function& function); + static bool ShouldPrint(const Function& function, + uint8_t** compiler_pass_filter = nullptr); private: const Function& function_; diff --git a/runtime/vm/compiler/compiler_pass.cc b/runtime/vm/compiler/compiler_pass.cc index 0d91ec1ef177..7b15f39c897c 100644 --- a/runtime/vm/compiler/compiler_pass.cc +++ b/runtime/vm/compiler/compiler_pass.cc @@ -75,8 +75,9 @@ CompilerPassState::CompilerPassState( } CompilerPass* CompilerPass::passes_[CompilerPass::kNumPasses] = {NULL}; +uint8_t CompilerPass::flags_[CompilerPass::kNumPasses] = {0}; -DEFINE_OPTION_HANDLER(CompilerPass::ParseFilters, +DEFINE_OPTION_HANDLER(CompilerPass::ParseFiltersFromFlag, compiler_passes, "List of comma separated compilation passes flags. " "Use -Name to disable a pass, Name to print IL after it. " @@ -110,7 +111,18 @@ static const char* kCompilerPassesUsage = "\n" "List of compiler passes:\n"; -void CompilerPass::ParseFilters(const char* filter) { +void CompilerPass::ParseFiltersFromFlag(const char* filter) { + ParseFilters(filter, flags_); +} + +uint8_t* CompilerPass::ParseFiltersFromPragma(const char* filter) { + auto flags = + ThreadState::Current()->zone()->Alloc(CompilerPass::kNumPasses); + ParseFilters(filter, flags); + return flags; +} + +void CompilerPass::ParseFilters(const char* filter, uint8_t* pass_flags) { if (filter == NULL || *filter == 0) { return; } @@ -126,11 +138,7 @@ void CompilerPass::ParseFilters(const char* filter) { } // Clear all flags. - for (intptr_t i = 0; i < kNumPasses; i++) { - if (passes_[i] != NULL) { - passes_[i]->flags_ = 0; - } - } + memset(pass_flags, 0, CompilerPass::kNumPasses); for (const char *start = filter, *end = filter; *end != 0; start = (end + 1)) { @@ -144,54 +152,58 @@ void CompilerPass::ParseFilters(const char* filter) { continue; } - uint8_t flags = 0; - if (*start == '-') { - flags = kDisabled; - } else if (*start == ']') { - flags = kTraceAfter; - } else if (*start == '[') { - flags = kTraceBefore; - } else if (*start == '*') { - flags = kTraceBeforeOrAfter; - } - if (flags == 0) { - flags |= kTraceAfter; - } else { - start++; // Skip the modifier - } + ParseOneFilter(start, end, pass_flags); + } +} - size_t suffix = 0; - if (end[-1] == '+') { - if (start == (end - 1)) { - OS::PrintErr("Sticky modifier '+' should follow pass name\n"); - continue; - } - flags |= kSticky; - suffix = 1; +void CompilerPass::ParseOneFilter(const char* start, + const char* end, + uint8_t* pass_flags) { + uint8_t flags = 0; + if (*start == '-') { + flags = kDisabled; + } else if (*start == ']') { + flags = kTraceAfter; + } else if (*start == '[') { + flags = kTraceBefore; + } else if (*start == '*') { + flags = kTraceBeforeOrAfter; + } + if (flags == 0) { + flags |= kTraceAfter; + } else { + start++; // Skip the modifier + } + + size_t suffix = 0; + if (end[-1] == '+') { + if (start == (end - 1)) { + OS::PrintErr("Sticky modifier '+' should follow pass name\n"); + return; } + flags |= kSticky; + suffix = 1; + } - size_t length = (end - start) - suffix; - if (length != 0) { - char* pass_name = Utils::StrNDup(start, length); - CompilerPass* pass = FindPassByName(pass_name); - if (pass != NULL) { - pass->flags_ |= flags; - } else { - OS::PrintErr("Unknown compiler pass: %s\n", pass_name); - } - free(pass_name); - } else if (flags == kTraceBeforeOrAfter) { - for (intptr_t i = 0; i < kNumPasses; i++) { - if (passes_[i] != NULL) { - passes_[i]->flags_ = kTraceAfter; - } - } + size_t length = (end - start) - suffix; + if (length != 0) { + char* pass_name = Utils::StrNDup(start, length); + CompilerPass* pass = FindPassByName(pass_name); + if (pass != NULL) { + pass_flags[pass->id()] |= flags; + } else { + OS::PrintErr("Unknown compiler pass: %s\n", pass_name); + } + free(pass_name); + } else if (flags == kTraceBeforeOrAfter) { + for (intptr_t i = 0; i < kNumPasses; i++) { + pass_flags[i] = kTraceAfter; } } } void CompilerPass::Run(CompilerPassState* state) const { - if (IsFlagSet(kDisabled)) { + if ((flags() & kDisabled) != 0) { return; } @@ -231,8 +243,11 @@ void CompilerPass::Run(CompilerPassState* state) const { void CompilerPass::PrintGraph(CompilerPassState* state, Flag mask, intptr_t round) const { - const intptr_t current_flags = flags() | state->sticky_flags; FlowGraph* flow_graph = state->flow_graph(); + const uint8_t* graph_flags = flow_graph->compiler_pass_filters(); + const uint8_t current_flags = + (graph_flags != nullptr ? graph_flags[id()] : flags()) | + state->sticky_flags; if ((FLAG_print_flow_graph || FLAG_print_flow_graph_optimized) && flow_graph->should_print() && ((current_flags & mask) != 0)) { diff --git a/runtime/vm/compiler/compiler_pass.h b/runtime/vm/compiler/compiler_pass.h index 9afa960c34ca..01a719492046 100644 --- a/runtime/vm/compiler/compiler_pass.h +++ b/runtime/vm/compiler/compiler_pass.h @@ -115,13 +115,15 @@ class CompilerPass { static constexpr intptr_t kNumPasses = 0 COMPILER_PASS_LIST(ADD_ONE); #undef ADD_ONE - CompilerPass(Id id, const char* name) : id_(id), name_(name), flags_(0) { + CompilerPass(Id id, const char* name) : id_(id), name_(name) { ASSERT(passes_[id] == NULL); passes_[id] = this; // By default print the final flow-graph after the register allocation. if (id == kAllocateRegisters) { - flags_ = kTraceAfter; + flags_[id] = kTraceAfter; + } else { + flags_[id] = 0; } } virtual ~CompilerPass() {} @@ -136,15 +138,18 @@ class CompilerPass { void Run(CompilerPassState* state) const; - intptr_t flags() const { return flags_; } + uint8_t flags() const { return flags_[id()]; } const char* name() const { return name_; } Id id() const { return id_; } - bool IsFlagSet(Flag flag) const { return (flags() & flag) != 0; } - static CompilerPass* Get(Id id) { return passes_[id]; } - static void ParseFilters(const char* filter); + static void ParseFiltersFromFlag(const char* filter); + static uint8_t* ParseFiltersFromPragma(const char* filter); + static void ParseFilters(const char* filter, uint8_t* flags); + static void ParseOneFilter(const char* start, + const char* end, + uint8_t* flags); enum PipelineMode { kJIT, kAOT }; @@ -196,10 +201,10 @@ class CompilerPass { void PrintGraph(CompilerPassState* state, Flag mask, intptr_t round) const; static CompilerPass* passes_[]; + static uint8_t flags_[]; Id id_; const char* name_; - intptr_t flags_; }; } // namespace dart diff --git a/runtime/vm/symbols.h b/runtime/vm/symbols.h index ee81b9993122..50640ad1025e 100644 --- a/runtime/vm/symbols.h +++ b/runtime/vm/symbols.h @@ -470,7 +470,8 @@ class ObjectPointerVisitor; V(vm_trace_entrypoints, "vm:testing.unsafe.trace-entrypoints-fn") \ V(vm_ffi_struct_fields, "vm:ffi:struct-fields") \ V(vm_unsafe_no_interrupts, "vm:unsafe:no-interrupts") \ - V(vm_external_name, "vm:external-name") + V(vm_external_name, "vm:external-name") \ + V(vm_testing_print_flow_graph, "vm:testing:print-flow-graph") // Contains a list of frequently used strings in a canonicalized form. This // list is kept in the vm_isolate in order to share the copy across isolates