blob: 8a3acaec60b9bec1c1bff8febd06e68f92d287fe [file] [log] [blame]
// Copyright (c) 2017, 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.
library js_backend.backend.annotations;
import 'package:kernel/ast.dart' as ir;
import '../common.dart';
import '../elements/entities.dart';
import '../ir/annotations.dart';
import '../ir/util.dart';
import '../kernel/dart2js_target.dart';
import '../options.dart';
import '../serialization/serialization.dart';
import '../util/enumset.dart';
/// `@pragma('dart2js:...')` annotations understood by dart2js.
///
/// Some of these annotations are (documented
/// elsewhere)[pkg/compiler/doc/pragmas.md].
enum PragmaAnnotation {
/// Tells the optimizing compiler to not inline the annotated method.
noInline('noInline', forFunctionsOnly: true),
/// Tells the optimizing compiler to always inline the annotated method, if
/// possible.
tryInline('tryInline', forFunctionsOnly: true),
/// Annotation on a member that tells the optimizing compiler to disable
/// inlining at call sites within the member.
disableInlining('disable-inlining'),
disableFinal('disableFinal', forFunctionsOnly: true, internalOnly: true),
noElision('noElision'),
/// Tells the optimizing compiler that the annotated method cannot throw.
/// Requires @pragma('dart2js:noInline') to function correctly.
noThrows('noThrows', forFunctionsOnly: true, internalOnly: true),
/// Tells the optimizing compiler that the annotated method has no
/// side-effects. Allocations don't count as side-effects, since they can be
/// dropped without changing the semantics of the program.
///
/// Requires @pragma('dart2js:noInline') to function correctly.
noSideEffects('noSideEffects', forFunctionsOnly: true, internalOnly: true),
/// Use this as metadata on method declarations to disable closed world
/// assumptions on parameters, effectively assuming that the runtime arguments
/// could be any value. Note that the constraints due to static types still
/// apply.
assumeDynamic('assumeDynamic', forFunctionsOnly: true, internalOnly: true),
asTrust('as:trust', forFunctionsOnly: false, internalOnly: false),
asCheck('as:check', forFunctionsOnly: false, internalOnly: false),
typesTrust('types:trust', forFunctionsOnly: false, internalOnly: false),
typesCheck('types:check', forFunctionsOnly: false, internalOnly: false),
parameterTrust('parameter:trust',
forFunctionsOnly: false, internalOnly: false),
parameterCheck('parameter:check',
forFunctionsOnly: false, internalOnly: false),
downcastTrust('downcast:trust', forFunctionsOnly: false, internalOnly: false),
downcastCheck('downcast:check', forFunctionsOnly: false, internalOnly: false),
indexBoundsTrust('index-bounds:trust',
forFunctionsOnly: false, internalOnly: false),
indexBoundsCheck('index-bounds:check',
forFunctionsOnly: false, internalOnly: false),
/// Annotation for a `late` field to omit the checks on the late field. The
/// annotation is not restricted to a field since it is copied from the field
/// to the getter and setter.
// TODO(45682): Make this annotation apply to local and static late variables.
lateTrust('late:trust'),
/// Annotation for a `late` field to perform the checks on the late field. The
/// annotation is not restricted to a field since it is copied from the field
/// to the getter and setter.
// TODO(45682): Make this annotation apply to local and static late variables.
lateCheck('late:check'),
loadLibraryPriority('load-priority', hasOption: true),
resourceIdentifier('resource-identifier'),
;
final String name;
final bool forFunctionsOnly;
final bool internalOnly;
final bool hasOption;
// TODO(sra): Review [forFunctionsOnly]. Fields have implied getters and
// setters, so some annotations meant only for functions could reasonable be
// placed on a field to apply to the getter and setter.
const PragmaAnnotation(this.name,
{this.forFunctionsOnly = false,
this.internalOnly = false,
this.hasOption = true});
static const Map<PragmaAnnotation, Set<PragmaAnnotation>> implies = {
typesTrust: {parameterTrust, downcastTrust},
typesCheck: {parameterCheck, downcastCheck},
};
static const Map<PragmaAnnotation, Set<PragmaAnnotation>> excludes = {
noInline: {tryInline},
tryInline: {noInline, resourceIdentifier},
typesTrust: {typesCheck, parameterCheck, downcastCheck},
typesCheck: {typesTrust, parameterTrust, downcastTrust},
parameterTrust: {parameterCheck},
parameterCheck: {parameterTrust},
downcastTrust: {downcastCheck},
downcastCheck: {downcastTrust},
asTrust: {asCheck},
asCheck: {asTrust},
lateTrust: {lateCheck},
lateCheck: {lateTrust},
resourceIdentifier: {tryInline},
};
static const Map<PragmaAnnotation, Set<PragmaAnnotation>> requires = {
noThrows: {noInline},
noSideEffects: {noInline},
};
static final Map<String, PragmaAnnotation> lookupMap = {
for (final annotation in values) annotation.name: annotation,
// Aliases
'never-inline': noInline,
'prefer-inline': tryInline,
};
}
ir.Library _enclosingLibrary(ir.TreeNode node) {
while (true) {
if (node is ir.Library) return node;
if (node is ir.Member) return node.enclosingLibrary;
node = node.parent!;
}
}
EnumSet<PragmaAnnotation> processMemberAnnotations(
CompilerOptions options,
DiagnosticReporter reporter,
ir.Annotatable node,
List<PragmaAnnotationData> pragmaAnnotationData) {
EnumSet<PragmaAnnotation> annotations = EnumSet<PragmaAnnotation>.empty();
ir.Library library = _enclosingLibrary(node);
Uri uri = library.importUri;
bool platformAnnotationsAllowed =
options.testMode || uri.isScheme('dart') || maybeEnableNative(uri);
for (PragmaAnnotationData data in pragmaAnnotationData) {
String name = data.name;
String suffix = data.suffix;
final annotation = PragmaAnnotation.lookupMap[suffix];
if (annotation != null) {
annotations += annotation;
if (data.options != null && !annotation.hasOption) {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node),
MessageKind.GENERIC,
{'text': "@pragma('$name') annotation does not take options"});
}
if (annotation.forFunctionsOnly) {
if (node is! ir.Procedure && node is! ir.Constructor) {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
'text': "@pragma('$name') annotation is only supported "
"for methods and constructors."
});
}
}
if (annotation.internalOnly && !platformAnnotationsAllowed) {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node),
MessageKind.GENERIC,
{'text': "Unrecognized dart2js pragma @pragma('$name')"});
}
} else {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node),
MessageKind.GENERIC,
{'text': "Unknown dart2js pragma @pragma('$name')"});
}
}
Map<PragmaAnnotation, EnumSet<PragmaAnnotation>> reportedExclusions = {};
for (PragmaAnnotation annotation
in annotations.iterable(PragmaAnnotation.values)) {
Set<PragmaAnnotation>? implies = PragmaAnnotation.implies[annotation];
if (implies != null) {
for (PragmaAnnotation other in implies) {
if (annotations.contains(other)) {
reporter.reportHintMessage(
computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
'text': "@pragma('dart2js:${annotation.name}') implies "
"@pragma('dart2js:${other.name}')."
});
}
}
}
Set<PragmaAnnotation>? excludes = PragmaAnnotation.excludes[annotation];
if (excludes != null) {
for (PragmaAnnotation other in excludes) {
if (annotations.contains(other) &&
!(reportedExclusions[other]?.contains(annotation) ?? false)) {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
'text': "@pragma('dart2js:${annotation.name}') must not be used "
"with @pragma('dart2js:${other.name}')."
});
reportedExclusions.update(
annotation, (exclusions) => exclusions + other,
ifAbsent: () => EnumSet.fromValue(other));
}
}
}
Set<PragmaAnnotation>? requires = PragmaAnnotation.requires[annotation];
if (requires != null) {
for (PragmaAnnotation other in requires) {
if (!annotations.contains(other)) {
reporter.reportErrorMessage(
computeSourceSpanFromTreeNode(node), MessageKind.GENERIC, {
'text': "@pragma('dart2js:${annotation.name}') should always be "
"combined with @pragma('dart2js:${other.name}')."
});
}
}
}
}
return annotations;
}
abstract class AnnotationsData {
/// Deserializes an [AnnotationsData] object from [source].
factory AnnotationsData.readFromDataSource(
CompilerOptions options,
DiagnosticReporter reporter,
DataSourceReader source) = AnnotationsDataImpl.readFromDataSource;
/// Serializes this [AnnotationsData] to [sink].
void writeToDataSink(DataSinkWriter sink);
/// Returns `true` if [member] has an `@pragma('dart2js:assumeDynamic')`
/// annotation.
bool hasAssumeDynamic(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:noInline')` annotation.
bool hasNoInline(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:tryInline')`
/// annotation.
bool hasTryInline(MemberEntity member);
/// Returns `true` if inlining is disabled at call sites inside [member].
// TODO(49475): This should be a property of call site, but ssa/builder.dart
// does not currently always pass the call site ir.TreeNode.
bool hasDisableInlining(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:disableFinal')`
/// annotation.
bool hasDisableFinal(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:noElision')`
/// annotation.
bool hasNoElision(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:noThrows')` annotation.
bool hasNoThrows(MemberEntity member);
/// Returns `true` if [member] has a `@pragma('dart2js:noSideEffects')`
/// annotation.
bool hasNoSideEffects(MemberEntity member);
/// What the compiler should do with parameter type assertions in [member].
///
/// If [member] is `null`, the default policy is returned.
// TODO(49475): Need this be nullable?
CheckPolicy getParameterCheckPolicy(MemberEntity? member);
/// What the compiler should do with implicit downcasts in [member].
///
/// If [member] is `null`, the default policy is returned.
// TODO(49475): Need this be nullable?
CheckPolicy getImplicitDowncastCheckPolicy(MemberEntity? member);
/// What the compiler should do with a boolean value in a condition context
/// in [member] when the language specification says it is a runtime error for
/// it to be null.
///
/// If [member] is `null`, the default policy is returned.
// TODO(49475): Need this be nullable?
CheckPolicy getConditionCheckPolicy(MemberEntity? member);
/// What the compiler should do with explicit casts in [member].
///
/// If [member] is `null`, the default policy is returned.
// TODO(49475): Need this be nullable?
CheckPolicy getExplicitCastCheckPolicy(MemberEntity? member);
/// What the compiler should do with index bounds checks `[]`, `[]=` and
/// `removeLast()` operations in the body of [member].
///
/// If [member] is `null`, the default policy is returned.
// TODO(49475): Need this be nullable?
CheckPolicy getIndexBoundsCheckPolicy(MemberEntity? member);
/// What the compiler should do with late field checks at a position in the
/// body of a method. The method is usually the getter or setter for a late
/// field.
// If we change our late field lowering to happen later, [node] could be the
// [ir.Field].
CheckPolicy getLateVariableCheckPolicyAt(ir.TreeNode node);
/// The priority to load the specified library with.
///
/// This can be an arbitrary string to be interpreted by the custom deferred
/// loader.
String getLoadLibraryPriority(ir.LoadLibrary node);
/// Determines whether [member] is annotated as a resource identifier.
bool methodIsResourceIdentifier(FunctionEntity member);
}
class AnnotationsDataImpl implements AnnotationsData {
/// Tag used for identifying serialized [AnnotationsData] objects in a
/// debugging data stream.
static const String tag = 'annotations-data';
final CompilerOptions _options;
final DiagnosticReporter _reporter;
final CheckPolicy _defaultParameterCheckPolicy;
final CheckPolicy _defaultImplicitDowncastCheckPolicy;
final CheckPolicy _defaultConditionCheckPolicy;
final CheckPolicy _defaultExplicitCastCheckPolicy;
final CheckPolicy _defaultIndexBoundsCheckPolicy;
final CheckPolicy _defaultLateVariableCheckPolicy;
final bool _defaultDisableInlining;
/// Pragma annotations for members. These are captured for the K-entities and
/// translated to J-entities.
// TODO(49475): Change the queries that use [pragmaAnnotation] to use the
// DirectivesContext so that we can avoid the need for persisting annotations.
final Map<MemberEntity, EnumSet<PragmaAnnotation>> pragmaAnnotations;
/// Pragma annotation environments for annotatable places in the Kernel
/// AST. These annotations generated on demand and not precomputed or
/// persisted. This map is a cache of the pragma annotation environment and
/// its enclosing environments.
// TODO(49475): Periodically clear this map to release references to tree
// nodes.
final Map<ir.Annotatable, DirectivesContext> _nodeToContextMap = {};
/// Root annotation environment that allows similar
final DirectivesContext _root = DirectivesContext.root();
AnnotationsDataImpl(
CompilerOptions options, this._reporter, this.pragmaAnnotations)
: this._options = options,
this._defaultParameterCheckPolicy = options.defaultParameterCheckPolicy,
this._defaultImplicitDowncastCheckPolicy =
options.defaultImplicitDowncastCheckPolicy,
this._defaultConditionCheckPolicy = options.defaultConditionCheckPolicy,
this._defaultExplicitCastCheckPolicy =
options.defaultExplicitCastCheckPolicy,
this._defaultIndexBoundsCheckPolicy =
options.defaultIndexBoundsCheckPolicy,
this._defaultLateVariableCheckPolicy = CheckPolicy.checked,
this._defaultDisableInlining = options.disableInlining;
factory AnnotationsDataImpl.readFromDataSource(CompilerOptions options,
DiagnosticReporter reporter, DataSourceReader source) {
source.begin(tag);
Map<MemberEntity, EnumSet<PragmaAnnotation>> pragmaAnnotations = source
.readMemberMap((MemberEntity member) => EnumSet(source.readInt()));
source.end(tag);
return AnnotationsDataImpl(options, reporter, pragmaAnnotations);
}
@override
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeMemberMap(pragmaAnnotations,
(MemberEntity member, EnumSet<PragmaAnnotation> set) {
sink.writeInt(set.mask);
});
sink.end(tag);
}
bool _hasPragma(MemberEntity member, PragmaAnnotation annotation) {
EnumSet<PragmaAnnotation>? set = pragmaAnnotations[member];
return set != null && set.contains(annotation);
}
@override
bool hasAssumeDynamic(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.assumeDynamic);
@override
bool hasNoInline(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.noInline);
@override
bool hasTryInline(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.tryInline);
@override
bool hasDisableInlining(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.disableInlining) ||
_defaultDisableInlining;
@override
bool hasDisableFinal(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.disableFinal);
@override
bool hasNoElision(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.noElision);
@override
bool hasNoThrows(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.noThrows);
@override
bool hasNoSideEffects(MemberEntity member) =>
_hasPragma(member, PragmaAnnotation.noSideEffects);
@override
CheckPolicy getParameterCheckPolicy(MemberEntity? member) {
if (member != null) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.typesTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.typesCheck)) {
return CheckPolicy.checked;
} else if (annotations.contains(PragmaAnnotation.parameterTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.parameterCheck)) {
return CheckPolicy.checked;
}
}
}
return _defaultParameterCheckPolicy;
}
@override
CheckPolicy getImplicitDowncastCheckPolicy(MemberEntity? member) {
if (member != null) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.typesTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.typesCheck)) {
return CheckPolicy.checked;
} else if (annotations.contains(PragmaAnnotation.downcastTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.downcastCheck)) {
return CheckPolicy.checked;
}
}
}
return _defaultImplicitDowncastCheckPolicy;
}
@override
CheckPolicy getConditionCheckPolicy(MemberEntity? member) {
if (member != null) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.typesTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.typesCheck)) {
return CheckPolicy.checked;
} else if (annotations.contains(PragmaAnnotation.downcastTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.downcastCheck)) {
return CheckPolicy.checked;
}
}
}
return _defaultConditionCheckPolicy;
}
@override
CheckPolicy getExplicitCastCheckPolicy(MemberEntity? member) {
if (member != null) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.asTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.asCheck)) {
return CheckPolicy.checked;
}
}
}
return _defaultExplicitCastCheckPolicy;
}
@override
CheckPolicy getIndexBoundsCheckPolicy(MemberEntity? member) {
if (member != null) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.indexBoundsTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.indexBoundsCheck)) {
return CheckPolicy.checked;
}
}
}
return _defaultIndexBoundsCheckPolicy;
}
@override
CheckPolicy getLateVariableCheckPolicyAt(ir.TreeNode node) {
return _getLateVariableCheckPolicyAt(_findContext(node));
}
CheckPolicy _getLateVariableCheckPolicyAt(DirectivesContext? context) {
while (context != null) {
EnumSet<PragmaAnnotation> annotations = context.annotations;
if (annotations.contains(PragmaAnnotation.lateTrust)) {
return CheckPolicy.trusted;
} else if (annotations.contains(PragmaAnnotation.lateCheck)) {
return CheckPolicy.checked;
}
context = context.parent;
}
return _defaultLateVariableCheckPolicy;
}
DirectivesContext _findContext(ir.TreeNode startNode) {
final node = _getContextNode(startNode);
if (node == null) return _root;
return _nodeToContextMap[node] ??= _getContext(node);
}
ir.Annotatable? _getContextNode(ir.TreeNode startNode) {
ir.TreeNode? node = startNode;
while (node is! ir.Annotatable) {
if (node == null) return null;
node = node.parent;
}
return node;
}
DirectivesContext _getContext(ir.Annotatable node) {
return _nodeToContextMap[node] ??= _findContext(node.parent!).extend(
processMemberAnnotations(_options, _reporter, node,
computePragmaAnnotationDataFromIr(node)));
}
@override
String getLoadLibraryPriority(ir.LoadLibrary node) {
String? _getPragmaOptionForNode(ir.TreeNode node) {
ir.Annotatable? contextNode = _getContextNode(node);
if (contextNode == null) return null;
if (!_hasLoadLibraryPriority(_getContext(contextNode))) return null;
final pragmaData = computePragmaAnnotationDataFromIr(contextNode);
final annotationData = pragmaData.firstWhere((d) =>
PragmaAnnotation.lookupMap[d.suffix] ==
PragmaAnnotation.loadLibraryPriority);
final option = annotationData.options;
if (option is! ir.StringConstant) return null;
return option.value;
}
// Annotation may be on enclosing declaration or on the import.
return _getPragmaOptionForNode(node) ??
_getPragmaOptionForNode(node.import) ??
'';
}
bool _hasLoadLibraryPriority(DirectivesContext? context) {
while (context != null) {
EnumSet<PragmaAnnotation> annotations = context.annotations;
if (annotations.contains(PragmaAnnotation.loadLibraryPriority)) {
return true;
}
context = context.parent;
}
return false;
}
@override
bool methodIsResourceIdentifier(MemberEntity member) {
EnumSet<PragmaAnnotation>? annotations = pragmaAnnotations[member];
if (annotations != null) {
if (annotations.contains(PragmaAnnotation.resourceIdentifier)) {
return true;
}
}
return false;
}
}
class AnnotationsDataBuilder {
Map<MemberEntity, EnumSet<PragmaAnnotation>> pragmaAnnotations = {};
void registerPragmaAnnotations(
MemberEntity member, EnumSet<PragmaAnnotation> annotations) {
if (annotations.isNotEmpty) {
pragmaAnnotations[member] = annotations;
}
}
AnnotationsData close(CompilerOptions options, DiagnosticReporter reporter) {
return AnnotationsDataImpl(options, reporter, pragmaAnnotations);
}
}
/// A [DirectivesContext] is a chain of enclosing parent annotatable
/// scopes.
///
/// The context chain for a location always starts with a node that reflects the
/// annotations at the location so that it is possible to determine if an
/// annotation is 'on' the element. Chains for different locations that have the
/// same structure are shared. `method1` and `method2` have the same annotations
/// in scope, with no annotations on the method itself but inheriting
/// `late:trust` from the class scope. This is represented by DirectivesContext
/// [D].
///
/// Links in the context chain above the element may be compressed, so `class
/// DD` and `method4` share the chain [F] with no annotations on the element but
/// inheriting `late:check` from the enclosing library scope.
///
/// @pragma('dart2js:late:check')
/// library foo; // [B]
///
/// @pragma('dart2js:late:trust')
/// class CC { // [C]
/// method1(){} // [D]
/// method2(){} // [D]
/// @pragma('dart2js:noInline')
/// method3(){} // [E]
/// }
///
/// class DD { // [F]
/// method4(); // [F]
/// }
///
///
/// A: parent: null, pragmas: {}
///
/// B: parent: A, pragmas: {late:check}
///
/// C: parent: B, pragmas: {late:trust}
///
/// D: parent: C, pragmas: {}
/// E: parent: C, pragmas: {noInline}
///
/// F: parent: B, pragmas: {}
///
/// The root scope [A] is empty. We could remove it and start the root scope at
/// the library, but the shared root might be a good place to put a set of
/// annotations derived from the command-line.
///
/// If we ever introduce a single annotation that means something different in
/// different positions (e.g. on a class vs. on a method), we might want to make
/// the [DirectivesContext] have a 'scope-kind'.
class DirectivesContext {
final DirectivesContext? parent;
final EnumSet<PragmaAnnotation> annotations;
Map<EnumSet<PragmaAnnotation>, DirectivesContext>? _children;
DirectivesContext._(this.parent, this.annotations);
DirectivesContext.root() : this._(null, EnumSet<PragmaAnnotation>.empty());
DirectivesContext extend(EnumSet<PragmaAnnotation> annotations) {
// Shorten chains of equivalent sets of annotations.
if (this.annotations == annotations) return this;
final children = _children ??= {};
return children[annotations] ??= DirectivesContext._(this, annotations);
}
}