33.7. Implementation of the Abuild Interface System

Up to this point, we have pretended that when abuild builds an item, it recursively reads the interface files of all its dependencies. Although this is the effect of what the interface system does, it is not exactly what happens. In this section, we will explain what really happens.

Internally, abuild implements an Interface object and an InterfaceParser object. Each InterfaceParser instance contains one Interface object. We use one InterfaceParser instance to load each Abuild.interface file and all of its after-build files. The scope of reset and reset-all statements is the InterfaceParser instance.

Internally, an Interface object maintains a list of variables, each of which has a declaration and a list of assignments. Each declaration and assignment is marked with the file location (file name, line number, and column number) at which it appeared. Additionally, assignment information includes any flag that the assignment may be conditional upon. Abuild does not actually maintain the value of a variable. It only maintains the list of assignments. Values of variables are computed on the fly as they are needed. For list variables, all assignment statements are maintained. For scalar variables, we store first all fallback assignments in the opposite of the order in which they appeared (with later fallback assignments being pushed onto the beginning of the history), the one normal assignment (as more than one normal assignment is an error), and then all override assignments in the order in which they appear (with later assignments added to the end of the history). When we perform a reset operation on an interface variable, we do not store the reset operation (other than to record that it happened for purposes of showing it in the --dump-interfaces output). Rather, we actually clear out the variable's assignment history. We discuss this further momentarily.

When a build item or another Interface object attempts to retrieve the value of a variable, abuild determines what flags, if any, are in effect and filters out any assignments that are connected with flags that are not set. Then, for list variables, the results of each remaining assignment are appended or prepended to the list, depending upon whether the list was declared as append or prepend. For scalar variables, only the last item in the assignment history is used. In this way, if there were only fallback assignments, the first fallback assignment would be at the end of the list. If there were any override assignments, the last override assignment would be at the end of the list. If there were only normal assignments, the normal assignment would be there. It is important that we maintain all of this information because we might filter out some assignments based on flags. We discuss this in more depth below.

One Interface object may import other Interface objects. When one Interface object imports another, the object merges the imported object's variable history with its own. Any declarations or assignments that are exactly duplicated (that is, they have the same file location as a previously seen operation) are ignored. This is important since we may import the same interface file through more than one path.

The import process is the only part of the interface system implementation that is affected by the scope of a variable (whether the variable is a normal recursive variable or was declared non-recursive or local). Specifically, when importing an interface, if the variable was declared as local, the declaration and assignments are both ignored by the import process. If the variable was declared as non-recursive, the declaration is always imported, but only assignments that were made in the item that owns the interface are actually imported. For example, suppose A imports B's interface which in turn imports C's interface. In this case, A would not see the affect of any assignments to non-recursive variables that were made in C since it does not directly import C's interface. It would also not see declarations or local variable assignments to any local variables in either B or C.

There is a subtle aspect of how reset works in connection with loading interfaces as a result of the fact that a reset actually clears the assignment history of a variable at the time of the reset operation rather than storing the reset as part of the history. For example, suppose you have interfaces Q and R and that R imports Q, Q assigns to variable A, and R resets variable A. If interface S imports just R, it will not see Q's assignment to A because that assignment is not part of R. On the other hand, if S imports both Q and R in any order, it will see Q's assignment to A. If the reset operation were actually part of the assignment history rather than being a local operation, then whether or not S saw Q's assignment to A would be dependent upon the order in which S loaded Q and R. For items that are not in each other's dependency chains, the order is not deterministic. This could cause very strange side effects: if one build item depended on other, it could sometimes not see all of that item's interface because of some third item that did a reset. Note also that abuild uses a single interface parser to load a given interface file and any after-build files, so a reset in an after-build actually does effectively remove the effect of any assignments to that variable in the file that loads it. Since a reset in an after-build file is not visible to the item itself, this is a useful construct for clearing interface variables that a build item means to set for its own use but not for its dependencies. For an example of this construct, see Section 27.1, “Opaque Wrapper Example”.

When a variable assignment is prefixed by a flag statement, the assignment entry that goes into the variable's assignment history is associated with the name of the build item and the flag. When a variable value is retrieved, abuild filters out any assignments that are marked with a flag that is not set. This makes it possible for abuild to store exactly one representation of each interface object rather than having to keep track of different instances for each possible combination of flags. It also makes it possible for different build items to actually see different results for the same interface objects depending upon what flags they are requesting.

Abuild only turns on interface flags when it retrieves variable values for export into the automatically generated file used by the back end (the dynamic output file, first introduced in Section 17.1, “Abuild Interface Functionality Overview”). It does not have any flags set when it references variables inside of other Abuild.interface files. For example, if A does this:

declare X string
declare Y string
X = v1
flag f1 override X = v2
Y = $(X)

the value of Y will always be v1 in every build item's dynamic output file regardless of whether or not that build item sets the f1 flag in its dependency on A. This is because that is the value that X had at the time when Y was assigned since the flag was not in effect during the parsing of the interface file. The value of X in the dynamic output files will be dependent upon whether the flag is in effect for the dependency on A because abuild does set flags before generating the dynamic output files. This makes sense when you consider that abuild reads each Abuild.interface file once for each platform and that values of variables are not computed until they are needed.