[vm,aot] Simple unreachable code elimination before type-flow analysis
This change adds transformation for very early cleanup of unreachable
code such as code guarded by if statements with constant conditions or
code used in assert statements when assertions are disabled.
The advantage of cleaning such code early is that type-flow analysis
won't be looking at it and TFA-based tree shaker is able to remove
more code.
flutter_gallery_total_size -0.5663% (arm), -0.5409% (arm64)
build_bench_total_size -2.533% (arm), -2.449% (arm64)
gesture_detector_total_size -4.183% (arm), -4.072% (arm64)
Change-Id: Ic764c056b6594164dc32eed4fd5679a7077ea2f9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/121500
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/frontend_server/lib/frontend_server.dart b/pkg/frontend_server/lib/frontend_server.dart
index c409d5f..cf94613 100644
--- a/pkg/frontend_server/lib/frontend_server.dart
+++ b/pkg/frontend_server/lib/frontend_server.dart
@@ -440,6 +440,7 @@
aot: options['aot'],
useGlobalTypeFlowAnalysis: options['tfa'],
environmentDefines: environmentDefines,
+ enableAsserts: options['enable-asserts'],
useProtobufTreeShaker: options['protobuf-tree-shaker']));
}
if (results.component != null) {
diff --git a/pkg/vm/lib/kernel_front_end.dart b/pkg/vm/lib/kernel_front_end.dart
index 2963159..313dd86 100644
--- a/pkg/vm/lib/kernel_front_end.dart
+++ b/pkg/vm/lib/kernel_front_end.dart
@@ -59,6 +59,8 @@
import 'transformations/obfuscation_prohibitions_annotator.dart'
as obfuscationProhibitions;
import 'transformations/call_site_annotator.dart' as call_site_annotator;
+import 'transformations/unreachable_code_elimination.dart'
+ as unreachable_code_elimination;
/// Declare options consumed by [runCompiler].
void declareCompilerOptions(ArgParser args) {
@@ -216,6 +218,7 @@
aot: aot,
useGlobalTypeFlowAnalysis: tfa,
environmentDefines: environmentDefines,
+ enableAsserts: enableAsserts,
genBytecode: genBytecode,
bytecodeOptions: bytecodeOptions,
dropAST: dropAST && !splitOutputByPackages,
@@ -284,6 +287,7 @@
{bool aot: false,
bool useGlobalTypeFlowAnalysis: false,
Map<String, String> environmentDefines,
+ bool enableAsserts: true,
bool genBytecode: false,
BytecodeOptions bytecodeOptions,
bool dropAST: false,
@@ -307,6 +311,7 @@
component,
useGlobalTypeFlowAnalysis,
environmentDefines,
+ enableAsserts,
useProtobufTreeShaker,
errorDetector);
}
@@ -361,6 +366,7 @@
Component component,
bool useGlobalTypeFlowAnalysis,
Map<String, String> environmentDefines,
+ bool enableAsserts,
bool useProtobufTreeShaker,
ErrorDetector errorDetector) async {
if (errorDetector.hasCompilationErrors) return;
@@ -375,6 +381,10 @@
// when building a platform dill file for VM/JIT case.
mixin_deduplication.transformComponent(component);
+ // Unreachable code elimination transformation should be performed
+ // before type flow analysis so TFA won't take unreachable code into account.
+ unreachable_code_elimination.transformComponent(component, enableAsserts);
+
if (useGlobalTypeFlowAnalysis) {
globalTypeFlow.transformComponent(
compilerOptions.target, coreTypes, component);
diff --git a/pkg/vm/lib/transformations/unreachable_code_elimination.dart b/pkg/vm/lib/transformations/unreachable_code_elimination.dart
new file mode 100644
index 0000000..68fbc3d
--- /dev/null
+++ b/pkg/vm/lib/transformations/unreachable_code_elimination.dart
@@ -0,0 +1,134 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:kernel/ast.dart';
+
+/// Simple unreachable code elimination: removes asserts and if statements
+/// with constant conditions. Does a very limited constant folding of
+/// logical expressions.
+Component transformComponent(Component component, bool enableAsserts) {
+ new SimpleUnreachableCodeElimination(enableAsserts).visitComponent(component);
+ return component;
+}
+
+class SimpleUnreachableCodeElimination extends Transformer {
+ final bool enableAsserts;
+
+ SimpleUnreachableCodeElimination(this.enableAsserts);
+
+ bool _isBoolConstant(Expression node) =>
+ node is BoolLiteral ||
+ (node is ConstantExpression && node.constant is BoolConstant);
+
+ bool _getBoolConstantValue(Expression node) {
+ if (node is BoolLiteral) {
+ return node.value;
+ }
+ if (node is ConstantExpression) {
+ final constant = node.constant;
+ if (constant is BoolConstant) {
+ return constant.value;
+ }
+ }
+ throw 'Expected bool constant: $node';
+ }
+
+ Expression _createBoolLiteral(bool value, int fileOffset) =>
+ new BoolLiteral(value)..fileOffset = fileOffset;
+
+ @override
+ TreeNode visitIfStatement(IfStatement node) {
+ node.transformChildren(this);
+ final condition = node.condition;
+ if (_isBoolConstant(condition)) {
+ final value = _getBoolConstantValue(condition);
+ return value ? node.then : node.otherwise;
+ }
+ return node;
+ }
+
+ @override
+ visitConditionalExpression(ConditionalExpression node) {
+ node.transformChildren(this);
+ final condition = node.condition;
+ if (_isBoolConstant(condition)) {
+ final value = _getBoolConstantValue(condition);
+ return value ? node.then : node.otherwise;
+ }
+ return node;
+ }
+
+ @override
+ TreeNode visitNot(Not node) {
+ node.transformChildren(this);
+ final operand = node.operand;
+ if (_isBoolConstant(operand)) {
+ return _createBoolLiteral(
+ !_getBoolConstantValue(operand), node.fileOffset);
+ }
+ return node;
+ }
+
+ @override
+ TreeNode visitLogicalExpression(LogicalExpression node) {
+ node.transformChildren(this);
+ final left = node.left;
+ final right = node.right;
+ final operator = node.operator;
+ if (_isBoolConstant(left)) {
+ final leftValue = _getBoolConstantValue(left);
+ if (_isBoolConstant(right)) {
+ final rightValue = _getBoolConstantValue(right);
+ if (operator == '||') {
+ return _createBoolLiteral(leftValue || rightValue, node.fileOffset);
+ } else if (operator == '&&') {
+ return _createBoolLiteral(leftValue && rightValue, node.fileOffset);
+ } else {
+ throw 'Unexpected LogicalExpression operator ${operator}: $node';
+ }
+ } else {
+ if (leftValue && operator == '||') {
+ return _createBoolLiteral(true, node.fileOffset);
+ } else if (!leftValue && operator == '&&') {
+ return _createBoolLiteral(false, node.fileOffset);
+ }
+ }
+ }
+ return node;
+ }
+
+ @override
+ visitStaticGet(StaticGet node) {
+ node.transformChildren(this);
+ final target = node.target;
+ if (target is Field && target.isConst) {
+ throw 'StaticGet from const field $target should be evaluated by front-end: $node';
+ }
+ return node;
+ }
+
+ @override
+ TreeNode visitAssertStatement(AssertStatement node) {
+ if (!enableAsserts) {
+ return null;
+ }
+ return super.visitAssertStatement(node);
+ }
+
+ @override
+ TreeNode visitAssertBlock(AssertBlock node) {
+ if (!enableAsserts) {
+ return null;
+ }
+ return super.visitAssertBlock(node);
+ }
+
+ @override
+ TreeNode visitAssertInitializer(AssertInitializer node) {
+ if (!enableAsserts) {
+ return null;
+ }
+ return super.visitAssertInitializer(node);
+ }
+}
diff --git a/pkg/vm/test/common_test_utils.dart b/pkg/vm/test/common_test_utils.dart
index da4e6d8..aa0001e 100644
--- a/pkg/vm/test/common_test_utils.dart
+++ b/pkg/vm/test/common_test_utils.dart
@@ -33,15 +33,18 @@
}
Future<Component> compileTestCaseToKernelProgram(Uri sourceUri,
- {Target target, bool enableSuperMixins: false}) async {
+ {Target target,
+ bool enableSuperMixins = false,
+ Map<String, String> environmentDefines}) async {
final platformKernel =
computePlatformBinariesLocation().resolve('vm_platform_strong.dill');
target ??= new TestingVmTarget(new TargetFlags())
..enableSuperMixins = enableSuperMixins;
+ environmentDefines ??= <String, String>{};
final options = new CompilerOptions()
..target = target
..linkedDependencies = <Uri>[platformKernel]
- ..environmentDefines = <String, String>{}
+ ..environmentDefines = environmentDefines
..onDiagnostic = (DiagnosticMessage message) {
fail("Compilation error: ${message.plainTextFormatted.join('\n')}");
};
diff --git a/pkg/vm/test/transformations/unreachable_code_elimination_test.dart b/pkg/vm/test/transformations/unreachable_code_elimination_test.dart
new file mode 100644
index 0000000..10db062
--- /dev/null
+++ b/pkg/vm/test/transformations/unreachable_code_elimination_test.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:kernel/target/targets.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/kernel.dart';
+import 'package:test/test.dart';
+import 'package:vm/transformations/unreachable_code_elimination.dart'
+ show transformComponent;
+
+import '../common_test_utils.dart';
+
+final String pkgVmDir = Platform.script.resolve('../..').toFilePath();
+
+runTestCase(Uri source) async {
+ final target = new TestingVmTarget(new TargetFlags());
+ Component component = await compileTestCaseToKernelProgram(source,
+ target: target,
+ environmentDefines: {
+ 'test.define.isTrue': 'true',
+ 'test.define.isFalse': 'false'
+ });
+
+ component = transformComponent(component, /* enableAsserts = */ false);
+
+ final actual = kernelLibraryToString(component.mainMethod.enclosingLibrary);
+
+ compareResultWithExpectationsFile(source, actual);
+}
+
+main() {
+ group('unreachable-code-elimination', () {
+ final testCasesDir = new Directory(
+ pkgVmDir + '/testcases/transformations/unreachable_code_elimination');
+
+ for (var entry in testCasesDir
+ .listSync(recursive: true, followLinks: false)
+ .reversed) {
+ if (entry.path.endsWith(".dart")) {
+ test(entry.path, () => runTestCase(entry.uri));
+ }
+ }
+ });
+}
diff --git a/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart b/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart
new file mode 100644
index 0000000..8a99d88
--- /dev/null
+++ b/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2019, 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.
+
+const bool constTrue = const bool.fromEnvironment('test.define.isTrue');
+const bool constFalse = const bool.fromEnvironment('test.define.isFalse');
+const bool constTrue2 = !constFalse;
+const bool constFalse2 = const bool.fromEnvironment('test.define.notDefined');
+
+bool foo() => null;
+
+void testSimpleConditions() {
+ if (constTrue) {
+ print('1_yes');
+ }
+ if (constFalse) {
+ print('2_no');
+ }
+ if (constTrue2) {
+ print('3_yes');
+ if (constFalse2) {
+ print('4_no');
+ }
+ }
+}
+
+void testAndConditions() {
+ if (constTrue && foo()) {
+ print('1_yes');
+ }
+ if (constFalse && foo()) {
+ print('2_no');
+ }
+ if (constTrue && constFalse) {
+ print('3_no');
+ }
+ if (constTrue && constTrue && constFalse) {
+ print('4_no');
+ }
+}
+
+void testOrConditions() {
+ if (constTrue || foo()) {
+ print('1_yes');
+ }
+ if (constFalse || foo()) {
+ print('2_yes');
+ }
+ if (constFalse || constFalse2) {
+ print('3_no');
+ }
+ if (constFalse || !constTrue || constTrue2) {
+ print('4_yes');
+ }
+}
+
+void testNotConditions() {
+ if (!constTrue) {
+ print('1_no');
+ }
+ if (!constFalse) {
+ print('2_yes');
+ }
+ if (!(!(!constTrue && foo()) || foo())) {
+ print('3_no');
+ }
+}
+
+testConditionalExpressions() {
+ print(!constFalse && constTrue ? '1_yes' : '2_no');
+ print(constFalse && foo() ? '3_no' : '4_yes ${foo()}');
+}
+
+void testAsserts() {
+ assert(foo());
+ assert(!foo(), "oops!");
+}
+
+class TestAssertInitializer {
+ TestAssertInitializer() : assert(foo()) {}
+}
+
+main(List<String> args) {
+ testSimpleConditions();
+ testAndConditions();
+ testOrConditions();
+ testNotConditions();
+ testConditionalExpressions();
+ testAsserts();
+ new TestAssertInitializer();
+}
diff --git a/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart.expect b/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart.expect
new file mode 100644
index 0000000..c6c0c63
--- /dev/null
+++ b/pkg/vm/testcases/transformations/unreachable_code_elimination/uce_testcases.dart.expect
@@ -0,0 +1,57 @@
+library #lib;
+import self as self;
+import "dart:core" as core;
+
+class TestAssertInitializer extends core::Object {
+ constructor •() → self::TestAssertInitializer*
+ : super core::Object::•() {}
+}
+static const field core::bool* constTrue = #C1;
+static const field core::bool* constFalse = #C2;
+static const field core::bool* constTrue2 = #C1;
+static const field core::bool* constFalse2 = #C2;
+static method foo() → core::bool*
+ return null;
+static method testSimpleConditions() → void {
+ {
+ core::print("1_yes");
+ }
+ {
+ core::print("3_yes");
+ }
+}
+static method testAndConditions() → void {
+ if((#C1) && self::foo()) {
+ core::print("1_yes");
+ }
+}
+static method testOrConditions() → void {
+ {
+ core::print("1_yes");
+ }
+ if((#C2) || self::foo()) {
+ core::print("2_yes");
+ }
+ {
+ core::print("4_yes");
+ }
+}
+static method testNotConditions() → void {
+ {
+ core::print("2_yes");
+ }
+}
+static method testConditionalExpressions() → dynamic {
+ core::print("1_yes");
+ core::print("4_yes ${self::foo()}");
+}
+static method testAsserts() → void {}
+static method main(core::List<core::String*>* args) → dynamic {
+ self::testSimpleConditions();
+ self::testAndConditions();
+ self::testOrConditions();
+ self::testNotConditions();
+ self::testConditionalExpressions();
+ self::testAsserts();
+ new self::TestAssertInitializer::•();
+}