[vm/aot/tfa] Tree shake write-only fields

So far tree shaking was removing fields which are not used at all.
This change improves tree shaking of fields so fields
which are only written or used as interface targets can be removed.

The following limitations apply:

* Field is not removed if there is a constant object with that field, as
  it may impact identity of constant objects which is an observable
  behavior.
* Field is not removed if it has a non-trivial initializer as it may
  have side-effects.
* Late final fields are not removed, as writing such fields may have
  side-effect.
* When field is removed, we may need to introduce an abstract getter
  or abstract setter if field is used as a target of an interface call.
  If a field was written, then setter would be non-abstract (but empty).

Fixes https://github.com/dart-lang/sdk/issues/35310

Change-Id: I79c00158b8eb658081a647c5dbdecde481fddb41
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/134204
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Alexander Markov 2020-02-06 19:31:32 +00:00 committed by commit-bot@chromium.org
parent cb6b535108
commit ff34fd8110
25 changed files with 574 additions and 46 deletions

View file

@ -185,12 +185,17 @@ class _DirectInvocation extends _Invocation {
case CallKind.PropertyGet:
assertx(args.values.length == firstParamIndex);
assertx(args.names.isEmpty);
fieldValue.isGetterUsed = true;
return fieldValue.getValue(
typeFlowAnalysis, field.isStatic ? null : args.values[0]);
case CallKind.PropertySet:
case CallKind.SetFieldInConstructor:
assertx(args.values.length == firstParamIndex + 1);
assertx(args.names.isEmpty);
if (selector.callKind == CallKind.PropertySet) {
fieldValue.isSetterUsed = true;
}
final Type setterArg = args.values[firstParamIndex];
fieldValue.setValue(
setterArg, typeFlowAnalysis, field.isStatic ? null : args.receiver);
@ -200,6 +205,7 @@ class _DirectInvocation extends _Invocation {
// Call via field.
// TODO(alexmarkov): support function types and use inferred type
// to get more precise return type.
fieldValue.isGetterUsed = true;
final receiver = fieldValue.getValue(
typeFlowAnalysis, field.isStatic ? null : args.values[0]);
if (receiver != const EmptyType()) {
@ -766,6 +772,12 @@ class _FieldValue extends _DependencyTracker {
/// Flag indicating if field initializer was executed.
bool isInitialized = false;
/// Flag indicating if field getter was executed.
bool isGetterUsed = false;
/// Flag indicating if field setter was executed.
bool isSetterUsed = false;
_FieldValue(this.field, this.typeGuardSummary, TypesBuilder typesBuilder)
: staticType = typesBuilder.fromStaticType(field.type, true) {
if (field.initializer == null && _isDefaultValueOfFieldObservable()) {
@ -1385,6 +1397,26 @@ class TypeFlowAnalysis implements EntryPointsListener, CallHandler {
return false;
}
/// Returns true if analysis found that getter corresponding to the given
/// [field] could be executed.
bool isFieldGetterUsed(Field field) {
final fieldValue = _fieldValues[field];
if (fieldValue != null) {
return fieldValue.isGetterUsed;
}
return false;
}
/// Returns true if analysis found that setter corresponding to the given
/// [field] could be executed.
bool isFieldSetterUsed(Field field) {
final fieldValue = _fieldValues[field];
if (fieldValue != null) {
return fieldValue.isSetterUsed;
}
return false;
}
bool isClassAllocated(Class c) => hierarchyCache.allocatedClasses.contains(c);
Call callSite(TreeNode node) => summaryCollector.callSites[node];

View file

@ -16,7 +16,8 @@ enum CallKind {
Method, // x.foo(..) or foo()
PropertyGet, // ... x.foo ...
PropertySet, // x.foo = ...
FieldInitializer,
FieldInitializer, // run initializer of a field
SetFieldInConstructor, // foo = ... in initializer list in a constructor
}
/// [Selector] encapsulates the way of calling (at the call site).
@ -55,6 +56,7 @@ abstract class Selector {
return member.getterType;
case CallKind.PropertySet:
case CallKind.FieldInitializer:
case CallKind.SetFieldInConstructor:
return const BottomType();
}
return null;
@ -72,7 +74,8 @@ abstract class Selector {
case CallKind.PropertySet:
return (member is Field) || ((member is Procedure) && member.isSetter);
case CallKind.FieldInitializer:
return (member is Field);
case CallKind.SetFieldInConstructor:
return member is Field;
}
return false;
}
@ -84,6 +87,7 @@ abstract class Selector {
case CallKind.PropertyGet:
return 'get ';
case CallKind.PropertySet:
case CallKind.SetFieldInConstructor:
return 'set ';
case CallKind.FieldInitializer:
return 'init ';

View file

@ -800,6 +800,7 @@ class SummaryCollector extends RecursiveVisitor<TypeExpr> {
break;
case CallKind.PropertySet:
case CallKind.SetFieldInConstructor:
args.add(new Type.nullableAny());
break;
@ -2071,8 +2072,11 @@ class SummaryCollector extends RecursiveVisitor<TypeExpr> {
TypeExpr visitFieldInitializer(FieldInitializer node) {
final value = _visit(node.value);
final args = new Args<TypeExpr>([_receiver, value]);
_makeCall(node,
new DirectSelector(node.field, callKind: CallKind.PropertySet), args);
_makeCall(
node,
new DirectSelector(node.field,
callKind: CallKind.SetFieldInConstructor),
args);
return null;
}

View file

@ -67,12 +67,15 @@ Component transformComponent(
final transformsStopWatch = new Stopwatch()..start();
new TreeShaker(component, typeFlowAnalysis).transformComponent(component);
final treeShaker = new TreeShaker(component, typeFlowAnalysis);
treeShaker.transformComponent(component);
new TFADevirtualization(component, typeFlowAnalysis, hierarchy)
new TFADevirtualization(
component, typeFlowAnalysis, hierarchy, treeShaker.fieldMorpher)
.visitComponent(component);
new AnnotateKernel(component, typeFlowAnalysis).visitComponent(component);
new AnnotateKernel(component, typeFlowAnalysis, treeShaker.fieldMorpher)
.visitComponent(component);
transformsStopWatch.stop();
@ -87,9 +90,10 @@ Component transformComponent(
/// Devirtualization based on results of type flow analysis.
class TFADevirtualization extends Devirtualization {
final TypeFlowAnalysis _typeFlowAnalysis;
final FieldMorpher fieldMorpher;
TFADevirtualization(
Component component, this._typeFlowAnalysis, ClassHierarchy hierarchy)
TFADevirtualization(Component component, this._typeFlowAnalysis,
ClassHierarchy hierarchy, this.fieldMorpher)
: super(_typeFlowAnalysis.environment.coreTypes, component, hierarchy);
@override
@ -97,7 +101,8 @@ class TFADevirtualization extends Devirtualization {
{bool setter = false}) {
final callSite = _typeFlowAnalysis.callSite(node);
if (callSite != null) {
final Member singleTarget = callSite.monomorphicTarget;
final Member singleTarget = fieldMorpher
.getMorphedMember(callSite.monomorphicTarget, isSetter: setter);
if (singleTarget != null) {
return new DirectCallMetadata(
singleTarget, callSite.isNullableReceiver);
@ -110,6 +115,7 @@ class TFADevirtualization extends Devirtualization {
/// Annotates kernel AST with metadata using results of type flow analysis.
class AnnotateKernel extends RecursiveVisitor<Null> {
final TypeFlowAnalysis _typeFlowAnalysis;
final FieldMorpher fieldMorpher;
final InferredTypeMetadataRepository _inferredTypeMetadata;
final UnreachableNodeMetadataRepository _unreachableNodeMetadata;
final ProcedureAttributesMetadataRepository _procedureAttributesMetadata;
@ -117,7 +123,7 @@ class AnnotateKernel extends RecursiveVisitor<Null> {
final Class _intClass;
Constant _nullConstant;
AnnotateKernel(Component component, this._typeFlowAnalysis)
AnnotateKernel(Component component, this._typeFlowAnalysis, this.fieldMorpher)
: _inferredTypeMetadata = new InferredTypeMetadataRepository(),
_unreachableNodeMetadata = new UnreachableNodeMetadataRepository(),
_procedureAttributesMetadata =
@ -278,7 +284,8 @@ class AnnotateKernel extends RecursiveVisitor<Null> {
// TODO(alexmarkov): figure out how to pass receiver type.
}
} else if (!member.isAbstract) {
} else if (!member.isAbstract &&
!fieldMorpher.isExtraMemberWithReachableBody(member)) {
_setUnreachable(member);
}
@ -287,14 +294,15 @@ class AnnotateKernel extends RecursiveVisitor<Null> {
// interface target, and table dispatch calls need selector IDs for all
// interface targets.
if (member.isInstanceMember) {
final original = fieldMorpher.getOriginalMember(member);
final attrs = new ProcedureAttributesMetadata(
methodOrSetterCalledDynamically:
_typeFlowAnalysis.isCalledDynamically(member),
_typeFlowAnalysis.isCalledDynamically(original),
getterCalledDynamically:
_typeFlowAnalysis.isGetterCalledDynamically(member),
hasThisUses: _typeFlowAnalysis.isCalledViaThis(member),
hasNonThisUses: _typeFlowAnalysis.isCalledNotViaThis(member),
hasTearOffUses: _typeFlowAnalysis.isTearOffTaken(member),
_typeFlowAnalysis.isGetterCalledDynamically(original),
hasThisUses: _typeFlowAnalysis.isCalledViaThis(original),
hasNonThisUses: _typeFlowAnalysis.isCalledNotViaThis(original),
hasTearOffUses: _typeFlowAnalysis.isTearOffTaken(original),
methodOrSetterSelectorId:
_tableSelectorAssigner.methodOrSetterSelectorId(member),
getterSelectorId: _tableSelectorAssigner.getterSelectorId(member));
@ -393,12 +401,14 @@ class TreeShaker {
final Set<Member> _usedMembers = new Set<Member>();
final Set<Extension> _usedExtensions = new Set<Extension>();
final Set<Typedef> _usedTypedefs = new Set<Typedef>();
FieldMorpher fieldMorpher;
_TreeShakerTypeVisitor typeVisitor;
_TreeShakerConstantVisitor constantVisitor;
_TreeShakerPass1 _pass1;
_TreeShakerPass2 _pass2;
TreeShaker(Component component, this.typeFlowAnalysis) {
fieldMorpher = new FieldMorpher(this);
typeVisitor = new _TreeShakerTypeVisitor(this);
constantVisitor = new _TreeShakerConstantVisitor(this, typeVisitor);
_pass1 = new _TreeShakerPass1(this);
@ -417,13 +427,25 @@ class TreeShaker {
bool isClassAllocated(Class c) => typeFlowAnalysis.isClassAllocated(c);
bool isMemberUsed(Member m) => _usedMembers.contains(m);
bool isExtensionUsed(Extension e) => _usedExtensions.contains(e);
bool isMemberBodyReachable(Member m) => typeFlowAnalysis.isMemberUsed(m);
bool isMemberBodyReachable(Member m) =>
typeFlowAnalysis.isMemberUsed(m) ||
fieldMorpher.isExtraMemberWithReachableBody(m);
bool isFieldInitializerReachable(Field f) =>
typeFlowAnalysis.isFieldInitializerUsed(f);
bool isFieldGetterReachable(Field f) => typeFlowAnalysis.isFieldGetterUsed(f);
bool isFieldSetterReachable(Field f) => typeFlowAnalysis.isFieldSetterUsed(f);
bool isMemberReferencedFromNativeCode(Member m) =>
typeFlowAnalysis.nativeCodeOracle.isMemberReferencedFromNativeCode(m);
bool isTypedefUsed(Typedef t) => _usedTypedefs.contains(t);
bool retainField(Field f) =>
isFieldGetterReachable(f) ||
(f.initializer != null &&
isFieldInitializerReachable(f) &&
mayHaveSideEffects(f.initializer)) ||
(f.isLate && f.isFinal) ||
isMemberReferencedFromNativeCode(f);
void addClassUsedInType(Class c) {
if (_classesUsedInType.add(c)) {
if (kPrintDebug) {
@ -459,9 +481,15 @@ class TreeShaker {
} else if (m is Procedure) {
func = m.function;
if (m.forwardingStubSuperTarget != null) {
m.forwardingStubSuperTarget = fieldMorpher.adjustInstanceCallTarget(
m.forwardingStubSuperTarget,
isSetter: m.isSetter);
addUsedMember(m.forwardingStubSuperTarget);
}
if (m.forwardingStubInterfaceTarget != null) {
m.forwardingStubInterfaceTarget = fieldMorpher
.adjustInstanceCallTarget(m.forwardingStubInterfaceTarget,
isSetter: m.isSetter);
addUsedMember(m.forwardingStubInterfaceTarget);
}
} else if (m is Constructor) {
@ -514,6 +542,86 @@ class TreeShaker {
}
}
class FieldMorpher {
final TreeShaker shaker;
final Set<Member> _extraMembersWithReachableBody = <Member>{};
final Map<Field, Member> _gettersForRemovedFields = <Field, Member>{};
final Map<Field, Member> _settersForRemovedFields = <Field, Member>{};
final Map<Member, Field> _removedFields = <Member, Field>{};
FieldMorpher(this.shaker);
Member _createAccessorForRemovedField(Field field, bool isSetter) {
assertx(!field.isStatic);
assertx(!shaker.retainField(field));
Procedure accessor;
if (isSetter) {
final isAbstract = !shaker.isFieldSetterReachable(field);
final parameter = new VariableDeclaration('value', type: field.type)
..isCovariant = field.isCovariant
..isGenericCovariantImpl = field.isGenericCovariantImpl
..fileOffset = field.fileOffset;
accessor = new Procedure(
field.name,
ProcedureKind.Setter,
new FunctionNode(null,
positionalParameters: [parameter], returnType: const VoidType())
..fileOffset = field.fileOffset,
isAbstract: isAbstract,
fileUri: field.fileUri);
if (!isAbstract) {
_extraMembersWithReachableBody.add(accessor);
}
} else {
accessor = new Procedure(field.name, ProcedureKind.Getter,
new FunctionNode(null, returnType: field.type),
isAbstract: true, fileUri: field.fileUri);
}
accessor.fileOffset = field.fileOffset;
field.enclosingClass.addMember(accessor);
_removedFields[accessor] = field;
shaker.addUsedMember(accessor);
return accessor;
}
/// Return a replacement for an instance call target.
/// If necessary, creates a getter or setter as a replacement if target is a
/// field which is going to be removed by the tree shaker.
/// This method is used during tree shaker pass 1.
Member adjustInstanceCallTarget(Member target, {bool isSetter = false}) {
if (target is Field && !shaker.retainField(target)) {
final targets =
isSetter ? _settersForRemovedFields : _gettersForRemovedFields;
return targets[target] ??=
_createAccessorForRemovedField(target, isSetter);
}
return target;
}
bool isExtraMemberWithReachableBody(Member member) =>
_extraMembersWithReachableBody.contains(member);
/// Return a member which replaced [target] in instance calls.
/// This method can be used after tree shaking to discover replacement.
Member getMorphedMember(Member target, {bool isSetter = false}) {
if (target == null) {
return null;
}
final targets =
isSetter ? _settersForRemovedFields : _gettersForRemovedFields;
return targets[target] ?? target;
}
/// Return original member which was replaced by [target] in instance calls.
/// This method can be used after tree shaking.
Member getOriginalMember(Member target) {
if (target == null) {
return null;
}
return _removedFields[target] ?? target;
}
}
/// Visits Dart types and collects all classes and typedefs used in types.
/// This visitor is used during pass 1 of tree shaking. It is a separate
/// visitor because [Transformer] does not provide a way to traverse types.
@ -564,9 +672,10 @@ class _TreeShakerTypeVisitor extends RecursiveVisitor<Null> {
/// transforms unreachable calls into 'throw' expressions.
class _TreeShakerPass1 extends Transformer {
final TreeShaker shaker;
final FieldMorpher fieldMorpher;
Procedure _unsafeCast;
_TreeShakerPass1(this.shaker);
_TreeShakerPass1(this.shaker) : fieldMorpher = shaker.fieldMorpher;
void transform(Component component) {
component.transformChildren(this);
@ -609,7 +718,9 @@ class _TreeShakerPass1 extends Transformer {
'Attempt to execute code removed by Dart AOT compiler (TFA)'));
}
for (var arg in args.reversed) {
node = new Let(new VariableDeclaration(null, initializer: arg), node);
if (mayHaveSideEffects(arg)) {
node = new Let(new VariableDeclaration(null, initializer: arg), node);
}
}
Statistics.callsDropped++;
return node;
@ -686,7 +797,7 @@ class _TreeShakerPass1 extends Transformer {
@override
TreeNode visitField(Field node) {
if (shaker.isMemberBodyReachable(node)) {
if (shaker.retainField(node)) {
if (kPrintTrace) {
tracePrint("Visiting $node");
}
@ -698,12 +809,10 @@ class _TreeShakerPass1 extends Transformer {
node.initializer = _makeUnreachableCall([])..parent = node;
}
}
} else if (shaker.isMemberReferencedFromNativeCode(node)) {
// Preserve members referenced from native code to satisfy lookups, even
// if they are not reachable. An instance member could be added via
// native code entry point but still unreachable if no instances of
// its enclosing class are allocated.
shaker.addUsedMember(node);
} else if (shaker.isFieldSetterReachable(node) && !node.isStatic) {
// Make sure setter is created to replace the field even if field is not
// used as an instance call target.
fieldMorpher.adjustInstanceCallTarget(node, isSetter: true);
}
return node;
}
@ -715,6 +824,8 @@ class _TreeShakerPass1 extends Transformer {
return _makeUnreachableCall(
_flattenArguments(node.arguments, receiver: node.receiver));
} else {
node.interfaceTarget =
fieldMorpher.adjustInstanceCallTarget(node.interfaceTarget);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -728,6 +839,8 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.receiver]);
} else {
node.interfaceTarget =
fieldMorpher.adjustInstanceCallTarget(node.interfaceTarget);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -741,6 +854,8 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.receiver, node.value]);
} else {
node.interfaceTarget = fieldMorpher
.adjustInstanceCallTarget(node.interfaceTarget, isSetter: true);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -754,6 +869,8 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall(_flattenArguments(node.arguments));
} else {
node.interfaceTarget =
fieldMorpher.adjustInstanceCallTarget(node.interfaceTarget);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -767,6 +884,8 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([]);
} else {
node.interfaceTarget =
fieldMorpher.adjustInstanceCallTarget(node.interfaceTarget);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -780,6 +899,8 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.value]);
} else {
node.interfaceTarget = fieldMorpher
.adjustInstanceCallTarget(node.interfaceTarget, isSetter: true);
if (node.interfaceTarget != null) {
shaker.addUsedMember(node.interfaceTarget);
}
@ -825,7 +946,11 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.value]);
} else {
assertx(shaker.isMemberBodyReachable(node.target), details: node.target);
final target = node.target;
assertx(shaker.isMemberBodyReachable(target), details: node);
if (target is Field && !shaker.retainField(target)) {
return node.value;
}
return node;
}
}
@ -837,7 +962,7 @@ class _TreeShakerPass1 extends Transformer {
return _makeUnreachableCall(
_flattenArguments(node.arguments, receiver: node.receiver));
} else {
assertx(shaker.isMemberBodyReachable(node.target), details: node.target);
assertx(shaker.isMemberBodyReachable(node.target), details: node);
return node;
}
}
@ -848,7 +973,10 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.receiver]);
} else {
assertx(shaker.isMemberBodyReachable(node.target), details: node.target);
final target = node.target;
assertx(shaker.isMemberBodyReachable(target), details: node);
assertx(target is! Field || shaker.isFieldGetterReachable(target),
details: node);
return node;
}
}
@ -859,7 +987,9 @@ class _TreeShakerPass1 extends Transformer {
if (_isUnreachable(node)) {
return _makeUnreachableCall([node.receiver, node.value]);
} else {
assertx(shaker.isMemberBodyReachable(node.target), details: node.target);
assertx(shaker.isMemberBodyReachable(node.target), details: node);
node.target =
fieldMorpher.adjustInstanceCallTarget(node.target, isSetter: true);
return node;
}
}
@ -902,12 +1032,20 @@ class _TreeShakerPass1 extends Transformer {
}
@override
visitFieldInitializer(FieldInitializer node) {
TreeNode visitFieldInitializer(FieldInitializer node) {
node.transformChildren(this);
if (_isUnreachable(node)) {
return _makeUnreachableInitializer([node.value]);
} else {
assertx(shaker.isMemberBodyReachable(node.field), details: node.field);
if (!shaker.retainField(node.field)) {
if (mayHaveSideEffects(node.value)) {
return LocalInitializer(
VariableDeclaration(null, initializer: node.value));
} else {
return null;
}
}
return node;
}
}
@ -1180,3 +1318,26 @@ class _TreeShakerConstantVisitor extends ConstantVisitor<Null> {
constant.type.accept(typeVisitor);
}
}
bool mayHaveSideEffects(Expression node) {
if (node is BasicLiteral ||
node is ConstantExpression ||
node is ThisExpression) {
return false;
}
if (node is VariableGet && !node.variable.isLate) {
return false;
}
if (node is StaticGet) {
final target = node.target;
if (target is Field && !target.isLate) {
final initializer = target.initializer;
if (initializer == null ||
initializer is BasicLiteral ||
initializer is ConstantExpression) {
return false;
}
}
}
return true;
}

View file

@ -10,7 +10,9 @@ import 'package:front_end/src/api_unstable/vm.dart'
CompilerOptions,
DiagnosticMessage,
computePlatformBinariesLocation,
kernelForProgram;
kernelForProgram,
parseExperimentalArguments,
parseExperimentalFlags;
import 'package:kernel/ast.dart';
import 'package:kernel/text/ast_to_text.dart' show Printer;
import 'package:kernel/binary/ast_to_binary.dart' show BinaryPrinter;
@ -35,6 +37,7 @@ class TestingVmTarget extends VmTarget {
Future<Component> compileTestCaseToKernelProgram(Uri sourceUri,
{Target target,
bool enableSuperMixins = false,
List<String> experimentalFlags,
Map<String, String> environmentDefines}) async {
final platformKernel =
computePlatformBinariesLocation().resolve('vm_platform_strong.dill');
@ -45,6 +48,11 @@ Future<Component> compileTestCaseToKernelProgram(Uri sourceUri,
..target = target
..linkedDependencies = <Uri>[platformKernel]
..environmentDefines = environmentDefines
..experimentalFlags =
parseExperimentalFlags(parseExperimentalArguments(experimentalFlags),
onError: (String message) {
throw message;
})
..onDiagnostic = (DiagnosticMessage message) {
fail("Compilation error: ${message.plainTextFormatted.join('\n')}");
};

View file

@ -18,10 +18,10 @@ import '../../common_test_utils.dart';
final String pkgVmDir = Platform.script.resolve('../../..').toFilePath();
runTestCase(Uri source) async {
runTestCase(Uri source, List<String> experimentalFlags) async {
final target = new TestingVmTarget(new TargetFlags());
Component component =
await compileTestCaseToKernelProgram(source, target: target);
Component component = await compileTestCaseToKernelProgram(source,
target: target, experimentalFlags: experimentalFlags);
final coreTypes = new CoreTypes(component);
@ -44,7 +44,10 @@ main() {
.listSync(recursive: true, followLinks: false)
.reversed) {
if (entry.path.endsWith(".dart")) {
test(entry.path, () => runTestCase(entry.uri));
final List<String> experimentalFlags = [
if (entry.path.endsWith("_nnbd.dart")) 'non-nullable',
];
test(entry.path, () => runTestCase(entry.uri, experimentalFlags));
}
}
});

View file

@ -15,7 +15,7 @@ class _Vector extends core::Object {
[@vm.procedure-attributes.metadata=getterCalledDynamically:false,hasTearOffUses:false,methodOrSetterSelectorId:1] operator []([@vm.inferred-type.metadata=!] core::int* i) → core::double*
return [@vm.direct-call.metadata=dart.typed_data::_Float64List::[]] [@vm.inferred-type.metadata=dart.core::_Double (skip check)] [@vm.direct-call.metadata=#lib::_Vector::_elements] [@vm.inferred-type.metadata=dart.typed_data::_Float64List] this.{self::_Vector::_elements}.{core::List::[]}([@vm.direct-call.metadata=dart.core::_IntegerImplementation::+] [@vm.inferred-type.metadata=int (skip check)] i.{core::num::+}([@vm.direct-call.metadata=#lib::_Vector::_offset] [@vm.inferred-type.metadata=dart.core::_Smi (value: 0)] this.{self::_Vector::_offset}));
[@vm.procedure-attributes.metadata=getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:2] operator []=([@vm.inferred-type.metadata=dart.core::_OneByteString] core::int* i, core::double* value) → void {
let dynamic #t1 = [@vm.direct-call.metadata=#lib::_Vector::_elements] [@vm.inferred-type.metadata=dart.typed_data::_Float64List] this.{self::_Vector::_elements} in let dynamic #t2 = i in let dynamic #t3 = [@vm.direct-call.metadata=#lib::_Vector::_offset] [@vm.inferred-type.metadata=dart.core::_Smi (value: 0)] this.{self::_Vector::_offset} in throw "Attempt to execute code removed by Dart AOT compiler (TFA)";
let dynamic #t1 = [@vm.direct-call.metadata=#lib::_Vector::_elements] [@vm.inferred-type.metadata=dart.typed_data::_Float64List] this.{self::_Vector::_elements} in let dynamic #t2 = [@vm.direct-call.metadata=#lib::_Vector::_offset] [@vm.inferred-type.metadata=dart.core::_Smi (value: 0)] this.{self::_Vector::_offset} in throw "Attempt to execute code removed by Dart AOT compiler (TFA)";
}
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3] operator *([@vm.inferred-type.metadata=#lib::_Vector?] self::_Vector* a) → core::double* {
core::double* result = 0.0;
@ -27,7 +27,7 @@ class _Vector extends core::Object {
[@vm.inferred-type.metadata=#lib::_Vector?]static field self::_Vector* v = new self::_Vector::•(10);
[@vm.inferred-type.metadata=dart.core::_Double?]static field core::double* x = 0.0;
static method main(core::List<core::String*>* args) → dynamic {
core::Stopwatch* timer = let final core::Stopwatch* #t4 = new core::Stopwatch::•() in let final void #t5 = [@vm.direct-call.metadata=dart.core::Stopwatch::start] [@vm.inferred-type.metadata=!? (skip check)] #t4.{core::Stopwatch::start}() in #t4;
core::Stopwatch* timer = let final core::Stopwatch* #t3 = new core::Stopwatch::•() in let final void #t4 = [@vm.direct-call.metadata=dart.core::Stopwatch::start] [@vm.inferred-type.metadata=!? (skip check)] #t3.{core::Stopwatch::start}() in #t3;
for (core::int* i = 0; [@vm.direct-call.metadata=dart.core::_IntegerImplementation::<] [@vm.inferred-type.metadata=dart.core::bool (skip check)] i.{core::num::<}(100000000); i = [@vm.direct-call.metadata=dart.core::_IntegerImplementation::+] [@vm.inferred-type.metadata=int (skip check)] i.{core::num::+}(1)) {
self::x = [@vm.direct-call.metadata=dart.core::_Double::+??] [@vm.inferred-type.metadata=dart.core::_Double (skip check)] [@vm.inferred-type.metadata=dart.core::_Double?] self::x.{core::double::+}([@vm.direct-call.metadata=#lib::_Vector::*??] [@vm.inferred-type.metadata=dart.core::_Double (skip check)] [@vm.inferred-type.metadata=#lib::_Vector?] self::v.{self::_Vector::*}([@vm.inferred-type.metadata=#lib::_Vector?] self::v));
}

View file

@ -14,7 +14,7 @@ class B extends core::Object implements self::I {
[@vm.inferred-type.metadata=#lib::B?]static field self::I* ii = new self::B::•();
static method bar([@vm.inferred-type.metadata=#lib::B?] self::I* i) → void {
if(i is self::A*) {
let dynamic #t1 = i{self::A*} in let dynamic #t2 = 42 in throw "Attempt to execute code removed by Dart AOT compiler (TFA)";
throw "Attempt to execute code removed by Dart AOT compiler (TFA)";
}
}
static method main(core::List<core::String*>* args) → dynamic {

View file

@ -2,8 +2,6 @@ library #lib;
import self as self;
import "dart:core" as core;
abstract class A extends core::Object {
}
class B extends core::Object {
constructor •() → self::B*
: super core::Object::•() {
@ -16,8 +14,7 @@ class C extends core::Object {
: super core::Object::•()
;
}
[@vm.inferred-type.metadata=dart.core::Null? (value: null)]static field self::A* field = throw "Attempt to execute code removed by Dart AOT compiler (TFA)";
static method main() → void {
self::field = null;
null;
[@vm.direct-call.metadata=#lib::C::instanceField] new self::C::•().{self::C::instanceField} = null;
}

View file

@ -0,0 +1,91 @@
// Copyright (c) 2020, 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.
// Test for tree shaking of write-only fields.
import "package:expect/expect.dart";
foo() {}
class A {
// Should be removed.
var unused1;
// Should be removed.
var unused2 = 42;
// Not removed due to a non-trivial initializer.
var unused3 = foo();
}
class B {
// Should be removed.
var unused4;
// Should be removed.
var unused5;
B(this.unused4) : unused5 = foo();
}
class C<T> {
// Should be replaced with setter.
T bar;
}
class D implements C<int> {
// Should be replaced with setter.
int bar;
}
class E {
// Should be replaced with getter.
final int bar;
E(this.bar);
}
class F implements E {
int get bar => 42;
}
class G {
// Not removed because used in a constant.
final int bazz;
const G(this.bazz);
}
class H {
// Should be replaced with setter.
int unused6;
}
class I extends H {
foo() {
super.unused6 = 3;
}
}
// Should be removed.
int unusedStatic7 = foo();
void main() {
new A();
new B('hi');
C<num> c = new D();
Expect.throws(() {
c.bar = 3.14;
});
E e = new F();
Expect.equals(42, e.bar);
Expect.isTrue(!identical(const G(1), const G(2)));
new I().foo();
unusedStatic7 = 5;
}

View file

@ -0,0 +1,68 @@
library #lib;
import self as self;
import "dart:core" as core;
import "package:expect/expect.dart" as exp;
import "package:expect/expect.dart";
class A extends core::Object {
[@vm.inferred-type.metadata=dart.core::Null? (value: null)] [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2] field dynamic unused3 = [@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::foo();
synthetic constructor •() → self::A*
: super core::Object::•()
;
}
class B extends core::Object {
constructor •([@vm.inferred-type.metadata=dart.core::_OneByteString (value: hi)] dynamic unused4) → self::B*
: dynamic #t1 = [@vm.inferred-type.metadata=dart.core::Null? (value: null)] self::foo(), super core::Object::•()
;
}
abstract class C<T extends core::Object* = dynamic> extends core::Object {
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3] abstract set bar(generic-covariant-impl self::C::T* value) → void;
}
class D extends core::Object implements self::C<core::int*> {
synthetic constructor •() → self::D*
: super core::Object::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3] set bar(generic-covariant-impl core::int* value) → void;
}
abstract class E extends core::Object {
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:4] abstract get bar() → core::int*;
}
class F extends core::Object implements self::E {
synthetic constructor •() → self::F*
: super core::Object::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:4] get bar() → core::int*
return 42;
}
class G extends core::Object /*hasConstConstructor*/ {
[@vm.inferred-type.metadata=dart.core::_Smi] [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:5,getterSelectorId:6] final field core::int* bazz;
}
abstract class H extends core::Object {
synthetic constructor •() → self::H*
: super core::Object::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:7] set unused6(core::int* value) → void;
}
class I extends self::H {
synthetic constructor •() → self::I*
: super self::H::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:8,getterSelectorId:9] method foo() → dynamic {
super.{self::H::unused6} = 3;
}
}
static method foo() → dynamic {}
static method main() → void {
new self::A::•();
new self::B::•("hi");
self::C<core::num*>* c = new self::D::•();
exp::Expect::throws<dynamic>(() → core::Null? {
[@vm.call-site-attributes.metadata=receiverType:#lib::C<dart.core::num*>*] [@vm.direct-call.metadata=#lib::D::bar] c.{self::C::bar} = 3.14;
});
self::E* e = new self::F::•();
exp::Expect::equals(42, [@vm.direct-call.metadata=#lib::F::bar] [@vm.inferred-type.metadata=dart.core::_Smi (value: 42)] e.{self::E::bar});
exp::Expect::isTrue(![@vm.inferred-type.metadata=dart.core::bool] core::identical(#C2, #C4));
[@vm.direct-call.metadata=#lib::I::foo] [@vm.inferred-type.metadata=!? (skip check)] new self::I::•().{self::I::foo}();
5;
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2020, 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.
// Test for tree shaking of write-only late fields.
// This test requires non-nullable experiment.
import "package:expect/expect.dart";
foo() {}
class A {
// Should be replaced with setter.
late int x;
use() {
x = 3;
}
}
class B {
// Should be retained.
late final int x;
use() {
x = 3;
}
}
// Should be removed.
late int staticLateA;
// Should be retained.
late final int staticLateB;
void main() {
new A().use();
new B().use();
staticLateA = 4;
staticLateB = 4;
}

View file

@ -0,0 +1,31 @@
library #lib /*isNonNullableByDefault*/;
import self as self;
import "dart:core" as core;
import "package:expect/expect.dart";
class A extends core::Object {
synthetic constructor •() → self::A
: super core::Object::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2] method use() → dynamic {
[@vm.direct-call.metadata=#lib::A::x] this.{self::A::x} = 3;
}
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3] set /*isNullableByDefault*/ x(core::int value) → void;
}
class B extends core::Object {
[@vm.unreachable.metadata=] [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3,getterSelectorId:4] late final field core::int x;
synthetic constructor •() → self::B
: super core::Object::•()
;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2] method use() → dynamic {
this.{self::B::x} = 3;
}
}
[@vm.inferred-type.metadata=dart.core::_Smi?]late static final field core::int staticLateB;
static method main() → void {
[@vm.direct-call.metadata=#lib::A::use] [@vm.inferred-type.metadata=!? (skip check)] new self::A::•().{self::A::use}();
[@vm.direct-call.metadata=#lib::B::use] [@vm.inferred-type.metadata=!? (skip check)] new self::B::•().{self::B::use}();
4;
self::staticLateB = 4;
}

View file

@ -8,7 +8,15 @@ import 'package:observatory/service_io.dart';
import 'package:unittest/unittest.dart';
import 'test_helper.dart';
var cleanBlock, copyingBlock, fullBlock, fullBlockWithChain;
// Make sure these variables are not removed by the tree shaker.
@pragma("vm:entry-point")
var cleanBlock;
@pragma("vm:entry-point")
var copyingBlock;
@pragma("vm:entry-point")
var fullBlock;
@pragma("vm:entry-point")
var fullBlockWithChain;
Function genCleanBlock() {
block(x) => x;

View file

@ -12,62 +12,86 @@ import 'test_helper.dart';
// small example from [Lenguaer & Tarjan 1979]
class R {
// All fields are marked with @pragma("vm:entry-point")
// in order to make sure they are not removed by the tree shaker
// even though they are never read.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
@pragma("vm:entry-point")
var z;
}
class A {
@pragma("vm:entry-point")
var x;
}
class B {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
@pragma("vm:entry-point")
var z;
}
class C {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class D {
@pragma("vm:entry-point")
var x;
}
class E {
@pragma("vm:entry-point")
var x;
}
class F {
@pragma("vm:entry-point")
var x;
}
class G {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class H {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class I {
@pragma("vm:entry-point")
var x;
}
class J {
@pragma("vm:entry-point")
var x;
}
class K {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class L {
@pragma("vm:entry-point")
var x;
}

View file

@ -21,62 +21,86 @@ double getDoubleWithHeapObjectTag() {
// small example from [Lenguaer & Tarjan 1979]
class R {
final double fld = getDoubleWithHeapObjectTag();
// Fields are marked with @pragma("vm:entry-point")
// in order to make sure they are not removed by the tree shaker
// even though they are never read.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
@pragma("vm:entry-point")
var z;
}
class A {
@pragma("vm:entry-point")
var x;
}
class B {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
@pragma("vm:entry-point")
var z;
}
class C {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class D {
@pragma("vm:entry-point")
var x;
}
class E {
@pragma("vm:entry-point")
var x;
}
class F {
@pragma("vm:entry-point")
var x;
}
class G {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class H {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class I {
@pragma("vm:entry-point")
var x;
}
class J {
@pragma("vm:entry-point")
var x;
}
class K {
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
class L {
@pragma("vm:entry-point")
var x;
}

View file

@ -9,7 +9,10 @@ import 'test_helper.dart';
class _TestClass {
_TestClass(this.x, this.y);
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}

View file

@ -9,10 +9,14 @@ import 'test_helper.dart';
class _TestClass {
_TestClass(this.x, this.y);
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
@pragma("vm:entry-point")
var myVar;
@pragma("vm:entry-point")

View file

@ -9,7 +9,10 @@ import 'test_helper.dart';
class _TestClass {
_TestClass();
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}

View file

@ -9,6 +9,8 @@ import 'package:unittest/unittest.dart';
import 'test_helper.dart';
class Node {
// Make sure this field is not removed by the tree shaker.
@pragma("vm:entry-point")
var edge;
}

View file

@ -9,12 +9,17 @@ import 'package:unittest/unittest.dart';
import 'test_helper.dart';
class Super {
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
var z = 1;
@pragma("vm:entry-point")
var y = 2;
}
class Sub extends Super {
@pragma("vm:entry-point")
var y = 3;
@pragma("vm:entry-point")
var x = 4;
}

View file

@ -9,7 +9,10 @@ import 'package:unittest/unittest.dart';
import 'test_helper.dart';
class Foo {
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
dynamic left;
@pragma("vm:entry-point")
dynamic right;
}

View file

@ -9,7 +9,11 @@ import 'test_helper.dart';
import 'service_test_common.dart';
class Pair {
var x, y;
// Make sure these fields are not removed by the tree shaker.
@pragma("vm:entry-point")
var x;
@pragma("vm:entry-point")
var y;
}
var p1;

View file

@ -8,7 +8,10 @@ import 'package:observatory/service_io.dart';
import 'package:unittest/unittest.dart';
import 'test_helper.dart';
// Make sure these variables are not removed by the tree shaker.
@pragma("vm:entry-point")
var regex0;
@pragma("vm:entry-point")
var regex;
void script() {

View file

@ -60,7 +60,11 @@ class D extends C with M {
}
class E {
// Make sure E.fun is not removed by the tree shaker, as this test verifies
// that an object with a tear-off in E.fun cannot be sent.
@pragma("vm:entry-point")
Function fun;
E(this.fun);
static fooFun() => 499;