Dependency Analysis

Swift's intra-module dependency analysis is based on a “provides” / “depends” system, which is ultimately trying to prove which files do not need to be rebuilt. In its simplest form, every file has a list of what it “provides” and what it “depends on”, and when a file is touched, every file that “depends on” what the first file “provides” needs to be rebuilt.

The golden rule of dependency analysis is to be conservative. Rebuilding a file when you don't have to is annoying. Not rebuilding a file when you do have to is tantamount to a debug-time miscompile!

Kinds of Dependency

There are four major kinds of dependency between files:

  • top-level: use of an unqualified name that is looked up at module scope, and definition of a name at module scope. This includes free functions, top-level type definitions, and global constants and variables.

  • nominal: use of a particular type, in any way, and declarations that change the “shape” of the type (the original definition, and extensions that add conformances). The type is identified by its mangled name.

  • member: a two-part entry that constitutes either providing a member or accessing a specific member of a type. This has some complications; see below.

  • dynamic-lookup: use of a specific member accessed through AnyObject, which has special id-like rules for member accesses, and definitions of @objc members that can be accessed this way.

The member dependency kind has a special entry where the member name is empty. This is in the “provides” set of every file that adds non-private members to a type, making it a superset of provided nominal entries. When listed as a dependency, it means that something in the file needs to be recompiled whenever any members are added to the type in question. This is currently used for cases of inheritance: superclasses and protocol conformances.

The “provides” sets for each file are computed after type-checking has completed. The “depends” sets are tracked by instrumenting lookups in the compiler. The most common kinds of lookups (qualified name lookup, unqualified name lookup, and protocol conformance checks) will already properly record dependencies, but direct lookups into a type or module will not, nor will dependencies not modeled by a query of some kind. These latter dependencies must be handled on a case-by-case basis.

Note:

The compiler currently does not track nominal dependencies separate from member dependencies. Instead, it considers every member dependency to implicitly be a nominal dependency, since adding a protocol to a type may change its members drastically.

External Dependencies

External dependencies, including imported Swift module files and Clang headers, are tracked using a special depends-external set. These dependencies refer to files that are external to the module. The Swift driver interprets this set specially and decides whether or not the cross-module dependencies have changed.

Because every Swift file in the module at least has its imports resolved, currently every file in the module has the same (complete) list of external dependencies. This means if an external dependency changes, everything in the module is rebuilt.

Complications

  • A file's “provides” set may be different before and after it is compiled -- declarations can be both added and removed, and other files may depend on the declarations that were added and the declarations that were removed. This means the dependency graph has to be updated after each file during compilation. (This is also why reusing build products is hard.)

  • If a build stops in the middle, we need to make sure the next build takes care of anything that was scheduled to be rebuilt but didn't get rebuilt this time.