blob: d45f291824e724017a7bf0d30fb8a7f6402d290b [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.
import 'package:kernel/ast.dart' as ir;
import '../common.dart';
import '../constants/values.dart' show BoolConstantValue;
import '../elements/entities.dart';
import '../elements/types.dart';
import '../ir/static_type.dart' show ClassRelation;
import '../world.dart';
import 'abstract_value_domain.dart';
import 'type_graph_nodes.dart';
/// Strategy for creating type information from members and parameters and type
/// information for nodes.
abstract class TypeSystemStrategy {
/// Creates [MemberTypeInformation] for [member].
MemberTypeInformation createMemberTypeInformation(
AbstractValueDomain abstractValueDomain, MemberEntity member);
/// Creates [ParameterTypeInformation] for [parameter].
ParameterTypeInformation createParameterTypeInformation(
AbstractValueDomain abstractValueDomain,
Local parameter,
TypeSystem types);
/// Calls [f] for each parameter in [function].
void forEachParameter(FunctionEntity function, void f(Local parameter));
/// Returns whether [node] is valid as a general phi node.
bool checkPhiNode(ir.Node node);
/// Returns whether [node] is valid as a loop phi node.
bool checkLoopPhiNode(ir.Node node);
/// Returns whether [node] is valid as a list allocation node.
bool checkListNode(ir.Node node);
/// Returns whether [node] is valid as a set allocation node.
bool checkSetNode(ir.Node node);
/// Returns whether [node] is valid as a map allocation node.
bool checkMapNode(ir.Node node);
/// Returns whether [cls] is valid as a type mask base class.
bool checkClassEntity(ClassEntity cls);
}
/// The class [SimpleInferrerVisitor] will use when working on types.
class TypeSystem {
final JClosedWorld _closedWorld;
final TypeSystemStrategy strategy;
/// [parameterTypeInformations] and [memberTypeInformations] ordered by
/// creation time. This is used as the inference enqueueing order.
final List<TypeInformation> _orderedTypeInformations = <TypeInformation>[];
/// [ParameterTypeInformation]s for parameters.
final Map<Local, ParameterTypeInformation> parameterTypeInformations =
Map<Local, ParameterTypeInformation>();
/// [MemberTypeInformation]s for members.
final Map<MemberEntity, MemberTypeInformation> memberTypeInformations =
Map<MemberEntity, MemberTypeInformation>();
/// [ListTypeInformation] for allocated lists.
final Map<ir.TreeNode, ListTypeInformation> allocatedLists =
Map<ir.TreeNode, ListTypeInformation>();
/// [SetTypeInformation] for allocated Sets.
final Map<ir.TreeNode, SetTypeInformation> allocatedSets =
Map<ir.TreeNode, SetTypeInformation>();
/// [MapTypeInformation] for allocated Maps.
final Map<ir.TreeNode, TypeInformation> allocatedMaps =
Map<ir.TreeNode, TypeInformation>();
/// Closures found during the analysis.
final Set<TypeInformation> allocatedClosures = Set<TypeInformation>();
/// Cache of [ConcreteTypeInformation].
final Map<AbstractValue, TypeInformation> concreteTypes =
Map<AbstractValue, TypeInformation>();
/// Cache of some primitive constant types.
final Map<Object, TypeInformation> primitiveConstantTypes = {};
/// List of [TypeInformation]s for calls inside method bodies.
final List<CallSiteTypeInformation> allocatedCalls =
<CallSiteTypeInformation>[];
/// List of [TypeInformation]s allocated inside method bodies (narrowing,
/// phis, and containers).
final List<TypeInformation> allocatedTypes = <TypeInformation>[];
/// [parameterTypeInformations] and [memberTypeInformations] ordered by
/// creation time. This is used as the inference enqueueing order.
Iterable<TypeInformation> get orderedTypeInformations =>
_orderedTypeInformations;
Iterable<TypeInformation> get allTypes => [
parameterTypeInformations.values,
memberTypeInformations.values,
allocatedLists.values,
allocatedSets.values,
allocatedMaps.values,
allocatedClosures,
concreteTypes.values,
primitiveConstantTypes.values,
allocatedCalls,
allocatedTypes,
].expand((x) => x);
TypeSystem(this._closedWorld, this.strategy) {
nonNullEmptyType = getConcreteTypeFor(_abstractValueDomain.emptyType);
}
AbstractValueDomain get _abstractValueDomain =>
_closedWorld.abstractValueDomain;
/// Used to group [TypeInformation] nodes by the element that triggered their
/// creation.
MemberTypeInformation _currentMember = null;
MemberTypeInformation get currentMember => _currentMember;
void withMember(MemberEntity element, void action()) {
assert(_currentMember == null,
failedAt(element, "Already constructing graph for $_currentMember."));
_currentMember = getInferredTypeOfMember(element);
action();
_currentMember = null;
}
TypeInformation nullTypeCache;
TypeInformation get nullType {
if (nullTypeCache != null) return nullTypeCache;
return nullTypeCache = getConcreteTypeFor(_abstractValueDomain.nullType);
}
TypeInformation intTypeCache;
TypeInformation get intType {
if (intTypeCache != null) return intTypeCache;
return intTypeCache = getConcreteTypeFor(_abstractValueDomain.intType);
}
TypeInformation uint32TypeCache;
TypeInformation get uint32Type {
if (uint32TypeCache != null) return uint32TypeCache;
return uint32TypeCache =
getConcreteTypeFor(_abstractValueDomain.uint32Type);
}
TypeInformation uint31TypeCache;
TypeInformation get uint31Type {
if (uint31TypeCache != null) return uint31TypeCache;
return uint31TypeCache =
getConcreteTypeFor(_abstractValueDomain.uint31Type);
}
TypeInformation positiveIntTypeCache;
TypeInformation get positiveIntType {
if (positiveIntTypeCache != null) return positiveIntTypeCache;
return positiveIntTypeCache =
getConcreteTypeFor(_abstractValueDomain.positiveIntType);
}
TypeInformation numTypeCache;
TypeInformation get numType {
if (numTypeCache != null) return numTypeCache;
return numTypeCache = getConcreteTypeFor(_abstractValueDomain.numType);
}
TypeInformation boolTypeCache;
TypeInformation get boolType {
if (boolTypeCache != null) return boolTypeCache;
return boolTypeCache = getConcreteTypeFor(_abstractValueDomain.boolType);
}
TypeInformation functionTypeCache;
TypeInformation get functionType {
if (functionTypeCache != null) return functionTypeCache;
return functionTypeCache =
getConcreteTypeFor(_abstractValueDomain.functionType);
}
TypeInformation listTypeCache;
TypeInformation get listType {
if (listTypeCache != null) return listTypeCache;
return listTypeCache = getConcreteTypeFor(_abstractValueDomain.listType);
}
TypeInformation constListTypeCache;
TypeInformation get constListType {
if (constListTypeCache != null) return constListTypeCache;
return constListTypeCache =
getConcreteTypeFor(_abstractValueDomain.constListType);
}
TypeInformation fixedListTypeCache;
TypeInformation get fixedListType {
if (fixedListTypeCache != null) return fixedListTypeCache;
return fixedListTypeCache =
getConcreteTypeFor(_abstractValueDomain.fixedListType);
}
TypeInformation growableListTypeCache;
TypeInformation get growableListType {
if (growableListTypeCache != null) return growableListTypeCache;
return growableListTypeCache =
getConcreteTypeFor(_abstractValueDomain.growableListType);
}
TypeInformation _mutableArrayType;
TypeInformation get mutableArrayType => _mutableArrayType ??=
getConcreteTypeFor(_abstractValueDomain.mutableArrayType);
TypeInformation setTypeCache;
TypeInformation get setType =>
setTypeCache ??= getConcreteTypeFor(_abstractValueDomain.setType);
TypeInformation constSetTypeCache;
TypeInformation get constSetType => constSetTypeCache ??=
getConcreteTypeFor(_abstractValueDomain.constSetType);
TypeInformation mapTypeCache;
TypeInformation get mapType {
if (mapTypeCache != null) return mapTypeCache;
return mapTypeCache = getConcreteTypeFor(_abstractValueDomain.mapType);
}
TypeInformation constMapTypeCache;
TypeInformation get constMapType {
if (constMapTypeCache != null) return constMapTypeCache;
return constMapTypeCache =
getConcreteTypeFor(_abstractValueDomain.constMapType);
}
TypeInformation stringTypeCache;
TypeInformation get stringType {
if (stringTypeCache != null) return stringTypeCache;
return stringTypeCache =
getConcreteTypeFor(_abstractValueDomain.stringType);
}
TypeInformation typeTypeCache;
TypeInformation get typeType {
if (typeTypeCache != null) return typeTypeCache;
return typeTypeCache = getConcreteTypeFor(_abstractValueDomain.typeType);
}
TypeInformation dynamicTypeCache;
TypeInformation get dynamicType {
if (dynamicTypeCache != null) return dynamicTypeCache;
return dynamicTypeCache =
getConcreteTypeFor(_abstractValueDomain.dynamicType);
}
TypeInformation asyncFutureTypeCache;
// Subtype of Future returned by async methods.
TypeInformation get asyncFutureType {
if (asyncFutureTypeCache != null) return asyncFutureTypeCache;
return asyncFutureTypeCache =
getConcreteTypeFor(_abstractValueDomain.asyncFutureType);
}
TypeInformation syncStarIterableTypeCache;
TypeInformation get syncStarIterableType {
if (syncStarIterableTypeCache != null) return syncStarIterableTypeCache;
return syncStarIterableTypeCache =
getConcreteTypeFor(_abstractValueDomain.syncStarIterableType);
}
TypeInformation asyncStarStreamTypeCache;
TypeInformation get asyncStarStreamType {
if (asyncStarStreamTypeCache != null) return asyncStarStreamTypeCache;
return asyncStarStreamTypeCache =
getConcreteTypeFor(_abstractValueDomain.asyncStarStreamType);
}
TypeInformation _lateSentinelType;
TypeInformation get lateSentinelType => _lateSentinelType ??=
getConcreteTypeFor(_abstractValueDomain.lateSentinelType);
TypeInformation nonNullEmptyType;
TypeInformation stringLiteralType(String value) {
return StringLiteralTypeInformation(
_abstractValueDomain, value, _abstractValueDomain.stringType);
}
TypeInformation boolLiteralType(bool value) {
return primitiveConstantTypes[value] ??= _boolLiteralType(value);
}
TypeInformation _boolLiteralType(bool value) {
AbstractValue abstractValue = _abstractValueDomain
.computeAbstractValueForConstant(BoolConstantValue(value));
return BoolLiteralTypeInformation(
_abstractValueDomain, value, abstractValue);
}
bool isLiteralTrue(TypeInformation info) {
return info is BoolLiteralTypeInformation && info.value == true;
}
bool isLiteralFalse(TypeInformation info) {
return info is BoolLiteralTypeInformation && info.value == false;
}
/// Returns the least upper bound between [firstType] and
/// [secondType].
TypeInformation computeLUB(
TypeInformation firstType, TypeInformation secondType) {
if (firstType == null) return secondType;
if (firstType == secondType) return firstType;
if (firstType == nonNullEmptyType) return secondType;
if (secondType == nonNullEmptyType) return firstType;
if (firstType == dynamicType || secondType == dynamicType) {
return dynamicType;
}
return getConcreteTypeFor(
_abstractValueDomain.union(firstType.type, secondType.type));
}
/// Returns `true` if `selector` should be updated to reflect the new
/// `receiverType`.
bool selectorNeedsUpdate(TypeInformation info, AbstractValue mask) {
return info.type != mask;
}
bool _isNonNullNarrow(TypeInformation type) =>
type is NarrowTypeInformation &&
_abstractValueDomain.isNull(type.typeAnnotation).isDefinitelyFalse;
/// Returns the intersection between [type] and [annotation].
///
/// [isCast] indicates whether narrowing comes from a cast or parameter check
/// rather than an 'is' test. (In legacy semantics these differ on whether
/// `null` is accepted).
///
/// If [excludeNull] is true, the intersection excludes `null` even if the
/// Dart type implies `null`.
///
/// [narrowType] will not exclude the late sentinel value by default, only if
/// [excludeLateSentinel] is `true`.
TypeInformation narrowType(TypeInformation type, DartType annotation,
{bool isCast = true,
bool excludeNull = false,
bool excludeLateSentinel = false}) {
// Avoid refining an input with an exact type. It we are almost always
// adding a narrowing to a subtype of the same class or a superclass.
if (_abstractValueDomain.isExact(type.type).isDefinitelyTrue) return type;
AbstractValueWithPrecision narrowing =
_abstractValueDomain.createFromStaticType(annotation,
classRelation: ClassRelation.subtype, nullable: isCast);
AbstractValue abstractValue = narrowing.abstractValue;
if (excludeNull) {
abstractValue = _abstractValueDomain.excludeNull(abstractValue);
}
if (!excludeLateSentinel) {
abstractValue = _abstractValueDomain.includeLateSentinel(abstractValue);
}
if (_abstractValueDomain.containsAll(abstractValue).isPotentiallyTrue) {
// Top, or non-nullable Top.
if (_abstractValueDomain.isNull(abstractValue).isPotentiallyTrue) {
return type;
}
// If the input is already narrowed to be not-null, there is no value
// in adding another narrowing node.
if (_isNonNullNarrow(type)) return type;
}
TypeInformation newType =
NarrowTypeInformation(_abstractValueDomain, type, abstractValue);
allocatedTypes.add(newType);
return newType;
}
ParameterTypeInformation getInferredTypeOfParameter(Local parameter) {
return parameterTypeInformations.putIfAbsent(parameter, () {
ParameterTypeInformation typeInformation =
strategy.createParameterTypeInformation(
_abstractValueDomain, parameter, this);
_orderedTypeInformations.add(typeInformation);
return typeInformation;
});
}
void forEachParameterType(
void f(Local parameter, ParameterTypeInformation typeInformation)) {
parameterTypeInformations.forEach(f);
}
MemberTypeInformation getInferredTypeOfMember(MemberEntity member) {
assert(!member.isAbstract,
failedAt(member, "Unexpected abstract member $member."));
return memberTypeInformations[member] ??= _getInferredTypeOfMember(member);
}
void forEachMemberType(
void f(MemberEntity member, MemberTypeInformation typeInformation)) {
memberTypeInformations.forEach(f);
}
MemberTypeInformation _getInferredTypeOfMember(MemberEntity member) {
MemberTypeInformation typeInformation =
strategy.createMemberTypeInformation(_abstractValueDomain, member);
_orderedTypeInformations.add(typeInformation);
return typeInformation;
}
/// Returns the internal inferrer representation for [mask].
ConcreteTypeInformation getConcreteTypeFor(AbstractValue mask) {
assert(mask != null);
return concreteTypes.putIfAbsent(mask, () {
return ConcreteTypeInformation(mask);
});
}
String getInferredSignatureOfMethod(FunctionEntity function) {
ElementTypeInformation info = getInferredTypeOfMember(function);
var res = "";
strategy.forEachParameter(function, (Local parameter) {
TypeInformation type = getInferredTypeOfParameter(parameter);
res += "${res.isEmpty ? '(' : ', '}${type.type} ${parameter.name}";
});
res += ") -> ${info.type}";
return res;
}
TypeInformation nonNullSubtype(ClassEntity cls) {
assert(strategy.checkClassEntity(cls));
return getConcreteTypeFor(_abstractValueDomain.createNonNullSubtype(cls));
}
TypeInformation nonNullSubclass(ClassEntity cls) {
assert(strategy.checkClassEntity(cls));
return getConcreteTypeFor(_abstractValueDomain.createNonNullSubclass(cls));
}
TypeInformation nonNullExact(ClassEntity cls) {
assert(strategy.checkClassEntity(cls));
return getConcreteTypeFor(_abstractValueDomain.createNonNullExact(cls));
}
TypeInformation nonNullEmpty() {
return nonNullEmptyType;
}
bool isNull(TypeInformation type) {
return type == nullType;
}
TypeInformation allocateList(
TypeInformation type, ir.TreeNode node, MemberEntity enclosing,
[TypeInformation elementType, int length]) {
assert(strategy.checkListNode(node));
ClassEntity typedDataClass = _closedWorld.commonElements.typedDataClass;
bool isTypedArray = typedDataClass != null &&
_closedWorld.classHierarchy.isInstantiated(typedDataClass) &&
_abstractValueDomain
.isInstanceOfOrNull(type.type, typedDataClass)
.isDefinitelyTrue;
bool isConst = (type.type == _abstractValueDomain.constListType);
bool isFixed = (type.type == _abstractValueDomain.fixedListType) ||
isConst ||
isTypedArray;
bool isElementInferred = isConst || isTypedArray;
int inferredLength = isFixed ? length : null;
AbstractValue elementTypeMask =
isElementInferred ? elementType.type : dynamicType.type;
AbstractValue mask = _abstractValueDomain.createContainerValue(
type.type, node, enclosing, elementTypeMask, inferredLength);
ElementInContainerTypeInformation element =
ElementInContainerTypeInformation(
_abstractValueDomain, currentMember, elementType);
element.inferred = isElementInferred;
allocatedTypes.add(element);
return allocatedLists[node] = ListTypeInformation(
_abstractValueDomain, currentMember, mask, element, length);
}
/// Creates a [TypeInformation] object either for the closurization of a
/// static or top-level method [element] used as a function constant or for
/// the synthesized 'call' method [element] created for a local function.
TypeInformation allocateClosure(FunctionEntity element) {
TypeInformation result =
ClosureTypeInformation(_abstractValueDomain, currentMember, element);
allocatedClosures.add(result);
return result;
}
TypeInformation allocateSet(
TypeInformation type, ir.TreeNode node, MemberEntity enclosing,
[TypeInformation elementType]) {
assert(strategy.checkSetNode(node));
bool isConst = type.type == _abstractValueDomain.constSetType;
AbstractValue elementTypeMask =
isConst ? elementType.type : dynamicType.type;
AbstractValue mask = _abstractValueDomain.createSetValue(
type.type, node, enclosing, elementTypeMask);
ElementInSetTypeInformation element = ElementInSetTypeInformation(
_abstractValueDomain, currentMember, elementType);
element.inferred = isConst;
allocatedTypes.add(element);
return allocatedSets[node] =
SetTypeInformation(currentMember, mask, element);
}
TypeInformation allocateMap(
ConcreteTypeInformation type, ir.TreeNode node, MemberEntity element,
[List<TypeInformation> keyTypes, List<TypeInformation> valueTypes]) {
assert(strategy.checkMapNode(node));
assert(keyTypes.length == valueTypes.length);
bool isFixed = (type.type == _abstractValueDomain.constMapType);
TypeInformation keyType, valueType;
for (int i = 0; i < keyTypes.length; ++i) {
TypeInformation type = keyTypes[i];
keyType = keyType == null
? allocatePhi(null, null, type, isTry: false)
: addPhiInput(null, keyType, type);
type = valueTypes[i];
valueType = valueType == null
? allocatePhi(null, null, type, isTry: false)
: addPhiInput(null, valueType, type);
}
keyType =
keyType == null ? nonNullEmpty() : simplifyPhi(null, null, keyType);
valueType =
valueType == null ? nonNullEmpty() : simplifyPhi(null, null, valueType);
AbstractValue keyTypeMask, valueTypeMask;
if (isFixed) {
keyTypeMask = keyType.type;
valueTypeMask = valueType.type;
} else {
keyTypeMask = valueTypeMask = dynamicType.type;
}
AbstractValue mask = _abstractValueDomain.createMapValue(
type.type, node, element, keyTypeMask, valueTypeMask);
TypeInformation keyTypeInfo =
KeyInMapTypeInformation(_abstractValueDomain, currentMember, keyType);
TypeInformation valueTypeInfo = ValueInMapTypeInformation(
_abstractValueDomain, currentMember, valueType);
allocatedTypes.add(keyTypeInfo);
allocatedTypes.add(valueTypeInfo);
MapTypeInformation map =
MapTypeInformation(currentMember, mask, keyTypeInfo, valueTypeInfo);
for (int i = 0; i < keyTypes.length; ++i) {
TypeInformation newType = map.addEntryInput(
_abstractValueDomain, keyTypes[i], valueTypes[i], true);
if (newType != null) allocatedTypes.add(newType);
}
// Shortcut: If we already have a first approximation of the key/value type,
// start propagating it early.
if (isFixed) map.markAsInferred();
allocatedMaps[node] = map;
return map;
}
AbstractValue newTypedSelector(TypeInformation info, AbstractValue mask) {
// Only type the selector if [info] is concrete, because the other
// kinds of [TypeInformation] have the empty type at this point of
// analysis.
return info.isConcrete ? info.type : mask;
}
/// Returns a new type that unions [firstInput] and [secondInput].
TypeInformation allocateDiamondPhi(
TypeInformation firstInput, TypeInformation secondInput) {
PhiElementTypeInformation result = PhiElementTypeInformation(
_abstractValueDomain, currentMember, null, null,
isTry: false);
result.addInput(firstInput);
result.addInput(secondInput);
allocatedTypes.add(result);
return result;
}
PhiElementTypeInformation _addPhi(
ir.Node node, Local variable, TypeInformation inputType, bool isTry) {
PhiElementTypeInformation result = PhiElementTypeInformation(
_abstractValueDomain, currentMember, node, variable,
isTry: isTry);
allocatedTypes.add(result);
result.addInput(inputType);
return result;
}
/// Returns a new type for holding the potential types of [element].
/// [inputType] is the first incoming type of the phi.
PhiElementTypeInformation allocatePhi(
ir.Node node, Local variable, TypeInformation inputType,
{bool isTry}) {
assert(strategy.checkPhiNode(node));
// Check if [inputType] is a phi for a local updated in
// the try/catch block [node]. If it is, no need to allocate a new
// phi.
if (inputType is PhiElementTypeInformation &&
inputType.branchNode == node &&
inputType.isTry) {
return inputType;
}
return _addPhi(node, variable, inputType, isTry);
}
/// Returns a new type for holding the potential types of [element].
/// [inputType] is the first incoming type of the phi. [allocateLoopPhi]
/// only differs from [allocatePhi] in that it allows the underlying
/// implementation of [TypeSystem] to differentiate Phi nodes due to loops
/// from other merging uses.
PhiElementTypeInformation allocateLoopPhi(
ir.Node node, Local variable, TypeInformation inputType,
{bool isTry}) {
assert(strategy.checkLoopPhiNode(node));
return _addPhi(node, variable, inputType, isTry);
}
/// Simplies the phi representing [element] and of the type
/// [phiType]. For example, if this phi has one incoming input, an
/// implementation of this method could just return that incoming
/// input type.
TypeInformation simplifyPhi(
ir.Node node, Local variable, PhiElementTypeInformation phiType) {
assert(phiType.branchNode == node);
if (phiType.inputs.length == 1) return phiType.inputs.first;
return phiType;
}
/// Adds [newType] as an input of [phiType].
PhiElementTypeInformation addPhiInput(Local variable,
PhiElementTypeInformation phiType, TypeInformation newType) {
phiType.addInput(newType);
return phiType;
}
AbstractValue computeTypeMask(Iterable<TypeInformation> assignments) {
return joinTypeMasks(assignments.map((e) => e.type));
}
AbstractValue joinTypeMasks(Iterable<AbstractValue> masks) {
var topType = _abstractValueDomain.internalTopType;
// Optimization: we are iterating over masks twice, but because `masks` is a
// mapped iterable, we save the intermediate results to avoid computing them
// again.
var list = [];
bool isTopIgnoringFlags = false;
bool mayBeNull = false;
bool mayBeLateSentinel = false;
for (AbstractValue mask in masks) {
// Don't do any work on computing unions if we know that after all that
// work the result will be `dynamic`.
// TODO(sigmund): change to `mask == internalTopType` so we can continue
// to track the non-nullable and late sentinel bits.
if (_abstractValueDomain.containsAll(mask).isPotentiallyTrue) {
isTopIgnoringFlags = true;
}
if (_abstractValueDomain.isNull(mask).isPotentiallyTrue) {
mayBeNull = true;
}
if (_abstractValueDomain.isLateSentinel(mask).isPotentiallyTrue) {
mayBeLateSentinel = true;
}
if (isTopIgnoringFlags && mayBeNull && mayBeLateSentinel) return topType;
list.add(mask);
}
AbstractValue newType = null;
for (AbstractValue mask in list) {
newType =
newType == null ? mask : _abstractValueDomain.union(newType, mask);
// Likewise - stop early if we already reach dynamic.
if (_abstractValueDomain.containsAll(newType).isPotentiallyTrue) {
isTopIgnoringFlags = true;
}
if (_abstractValueDomain.isNull(newType).isPotentiallyTrue) {
mayBeNull = true;
}
if (_abstractValueDomain.isLateSentinel(newType).isPotentiallyTrue) {
mayBeLateSentinel = true;
}
if (isTopIgnoringFlags && mayBeNull && mayBeLateSentinel) return topType;
}
return newType ?? _abstractValueDomain.emptyType;
}
}