Description
Currently Dart2JS has a hardcoded limit on the number of times the type of a specific construct (member, call, etc.) can be refined. This was added as a way to bound the amount of time spent refining the type of any single construct.
During the ongoing efforts to improve performance of type inference in Dart2JS, we've tried to increase (or even remove) this hardcoded limit with the goal of regaining type accuracy lost elsewhere. This uncovered that the hardcoded limit has been hiding an issue where, given the right constraints, some types do not converge to a single value.
This can be seen in sufficiently large programs that make use of the built_value
library (usually via protobufs). There is a cycle of ~6 types stemming from BuildJsonSerializers.serialize.
BuildJsonSerializers.serialize
contains a call to BuildJsonSerializers._serialize
which in turn contains a call back to BuildJsonSerializers.serialize
. This mutual recursion on its own is fine. However, it becomes an issue when many different typed objects pass through this serializer (as can be the case when protobufs are used heavily).
At some point the type used to represent the return types of these functions accumulates too many disjoint types and our representation of unions falls back to a generic Object?
flat type mask. The problem occurs when further refinement causes a type that was previously flattened to Object?
to get turned back into a union type mask representation (by collapsing of some of the disjoint submasks from exact
into subclass
). Now because these 2 methods are mutually recursive, one has the flattened Object?
type and the other has the expanded union type and their internal types are in either of these 2 states. The types then chase each other, flattening and re-expanding repeatedly.
Here is how the types propagate for a full cycle. Notice that at the same time some types are being flattened others are being expanded.
j:method(BuiltJsonSerializers.serialize)
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
InstanceInvocation(InstanceAccessKind.Instance, this.{BuiltJsonSerializers._serialize}(transformedObject, specifiedType))
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
InstanceInvocation(InstanceAccessKind.Instance, this.{BuiltJsonSerializers.serialize}(object))
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
local(BuiltJsonSerializers.serialize#result)
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
j:method(BuiltJsonSerializers._serialize)
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
local(BuiltJsonSerializers.serialize#result)
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
InstanceInvocation(InstanceAccessKind.Instance, this.{BuiltJsonSerializers._serialize}(transformedObject, specifiedType))
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
j:method(BuiltJsonSerializers.serialize)
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
local(BuiltJsonSerializers.serialize#result)
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
InstanceInvocation(InstanceAccessKind.Instance, this.{BuiltJsonSerializers.serialize}(object))
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
local(BuiltJsonSerializers.serialize#result)
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]
j:method(BuiltJsonSerializers._serialize)
[null|subclass=Object] -> Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool])
j:method(BuiltJsonSerializers.serialize)
Union(null, [exact=JSString], [subclass=JSMutableArray], [subclass=JSNumber], [subtype=bool]) -> [null|subclass=Object]