104 lines
6.8 KiB
Markdown
104 lines
6.8 KiB
Markdown
|
|
# Summaries in Dart analyzer
|
|||
|
|
|
|||
|
|
The purpose of this document is to provide a high-level overview of the summaries linking, storage format, and reading
|
|||
|
|
in Dart analyzer.
|
|||
|
|
|
|||
|
|
## Design Considerations
|
|||
|
|
|
|||
|
|
There are a couple of considerations to keep in mind when discussing the design.
|
|||
|
|
|
|||
|
|
**We want the linking to be done using AST.** Default values, constructor initializers, and field initializers should be
|
|||
|
|
stored in their resolved state, with elements and type, because we need them to perform constant evaluation.
|
|||
|
|
|
|||
|
|
**We want separation between AST and resolution.** Files change rarely, usually the user changes just one file, and this
|
|||
|
|
affects resolution of this file and a multiple other files. But only the resolution, not AST. So, we want to keep pieces
|
|||
|
|
that are not affected by the change.
|
|||
|
|
|
|||
|
|
**We want to read only as much as necessary to do resolution.** Libraries often have many classes, but when we resolve a
|
|||
|
|
file, we don’t need all these classes, only those that are actually referenced. So, we want the format to support
|
|||
|
|
loading individual classes, functions, etc.
|
|||
|
|
|
|||
|
|
## High level view
|
|||
|
|
|
|||
|
|
When the analyzer needs to resolve a file, it works in the following way:
|
|||
|
|
|
|||
|
|
1. Find the library cycle that contains this file, and library cycles of its dependencies, down to SDK.
|
|||
|
|
|
|||
|
|
2. Link these library cycles from SDK up to the target cycle.
|
|||
|
|
|
|||
|
|
3. Add linked bundles to `LinkedElementFactory`.
|
|||
|
|
|
|||
|
|
4. When we need a specific element, we call `LinkedElementFactory.elementOfReference`. It will request parent elements,
|
|||
|
|
and eventually load the containing library, the containing class, method, etc.
|
|||
|
|
|
|||
|
|
## Lazy loading
|
|||
|
|
|
|||
|
|
We try to load only libraries that are necessary. When a `PackageBundleReader` is created, it decodes only the header of
|
|||
|
|
the bundle - with the list of library URIs contained in the bundle, and creates `LibraryReader`s.
|
|||
|
|
|
|||
|
|
When `LinkedElementFactory.createLibraryElementForReading` is invoked, the corresponding `LibraryReader` is asked to
|
|||
|
|
load units, which are `UnitReader`s. Each `UnitReader` keeps track of the location of its AST and resolution portions
|
|||
|
|
in `astReader` and `resolutionReader`. When `UnitReader` is created, it reads the index of top-level declarations, and
|
|||
|
|
fills `Reference` subtree. For each `Reference` we set its `nodeAccessor`, a pointer to the `_UnitMemberReader`, which
|
|||
|
|
can be used to load the corresponding AST node. This is a way to present the index of the unit to `LinkedElementFactory`
|
|||
|
|
.
|
|||
|
|
|
|||
|
|
To support lazy loading the package bundle has the index of libraries, the library has the index of units, the unit has
|
|||
|
|
the index of top-level declarations, the class / extension / mixin has the index of members.
|
|||
|
|
|
|||
|
|
Any time when we need the element for a `Reference`, we call `elementOfReference` of `LinkedElementFactory`, which
|
|||
|
|
asks `Reference.nodeAccessor` for the AST node, and then creates the corresponding `Element` wrapper around it, and
|
|||
|
|
stores into `Reference.element`. For example for `ClassDeclaration` it will create `ClassElement`. No resolution is
|
|||
|
|
required yet.
|
|||
|
|
|
|||
|
|
When `Reference.element` is already set, we just return it, we have already done loading AST node and creating the
|
|||
|
|
element for this `Reference`.
|
|||
|
|
|
|||
|
|
When we link libraries, we don’t use `LinkedElementFactory` to read nodes and create elements, because we already have
|
|||
|
|
full ASTs, and we can create elements for all nodes in advance, and put them into corresponding `Reference` children.
|
|||
|
|
|
|||
|
|
We say that we need the element for a `Reference` because resolution stores elements as such references - a pair of the
|
|||
|
|
name, and the parent reference. So, we may have an empty `Reference` first, without `element` and `nodeAccessor`, then
|
|||
|
|
when `elementOfReference` is invoked, it will fill both for a `Reference`. But its siblings will stay unfilled.
|
|||
|
|
|
|||
|
|
When we ask an `Element` anything that requires resolution, we apply resolution to the whole AST node of the element.
|
|||
|
|
For example, when we ask `ClassElement` for `supertype`, we apply resolution to the `ClassDeclaration` header (but not
|
|||
|
|
to any member of the `ClassDeclaration`) - supertype, mixins, interfaces. We do this by calling `applyResolution`
|
|||
|
|
of `AstLinkedContext`. We get `AstLinkedContext` from the node, which implements `HasAstLinkedContext`.
|
|||
|
|
|
|||
|
|
`AstBinaryWriter` writes two streams of information at once - the AST itself, and resolution. In the future we might
|
|||
|
|
split writing AST and resolution into separate writers.
|
|||
|
|
|
|||
|
|
Resolution information is just a sequence of elements and types, which is stored when we visit the resolved AST
|
|||
|
|
in `AstBinaryWriter`. We use the helper `_ResolutionSink` to encode elements and types into bytes.
|
|||
|
|
|
|||
|
|
Resolution is applied to unresolved AST using `ApplyResolutionVisitor`, which visits AST nodes in the same sequence
|
|||
|
|
as `AstBinaryWriter`, and takes either elements or types from the same (untyped, unmarked in any way) stream of
|
|||
|
|
resolution bytes. `LinkedResolutionReader` corresponds to `_ResolutionSink` - it decodes elements and types from bytes.
|
|||
|
|
|
|||
|
|
Each raw element is represented by an integer, an index in the reference table. This table is collected during writing
|
|||
|
|
in `_BundleWriterReferences`, and stored by `BundleWriterResolution` during `BundleWriter.finish()`. During
|
|||
|
|
loading `BundleReader` creates `_ReferenceReader`, which lazily converts names and parent references into `Reference`
|
|||
|
|
instances in the given `LinkedElementFactory` (from which we just take `rootReference`, not actually any elements). Once
|
|||
|
|
we have `Reference` for the element that we need to decode, we actually ask `LinkedResolutionReader` for the element,
|
|||
|
|
see above.
|
|||
|
|
|
|||
|
|
In addition to raw elements, there are “members” (which is not the best name) - which might be `Element`s which we want
|
|||
|
|
to convert to legacy because they are declared in a null safe library, but we want their legacy types; or actually
|
|||
|
|
members of a class with type parameters and with some `Substitution` applied to them; or both.
|
|||
|
|
|
|||
|
|
Strings are encoded as integers, during writing using `StringIndexer`, and during loading using `_StringTable`. We
|
|||
|
|
use `SummaryDataReader` to load primitive types, and also strings.
|
|||
|
|
|
|||
|
|
## Known limitations
|
|||
|
|
|
|||
|
|
Currently `LibraryScope` and its basis `_LibraryImportScope` and `PrefixScope` - they all work by asking all elements
|
|||
|
|
from the imported libraries. Which means that we load all top-level nodes of these libraries (and all libraries that
|
|||
|
|
they export). Fortunately we don’t apply resolution to these elements until we try to access some property of these
|
|||
|
|
elements, e.g. the return type of a getter. But still, we probably don’t actually use all the imported elements, and we
|
|||
|
|
potentially could avoid loading all these AST nodes. A solution could be to work with `Reference`s instead.
|
|||
|
|
|
|||
|
|
Similarly `InheritanceManager3` builds the whole interface of a class, and loads all members of the class and all
|
|||
|
|
members of all its superinterfaces. But again, we might only call a few methods, and might not need any superinterfaces.
|
|||
|
|
A solution might be to fill class interfaces on demand.
|