Hi everyone! In my recent post about DocC we worked out how we want to present extensions to external types in DocC. The proposed solution can be found here. Unfortunately, we also found out that the symbol graph files currently do not contain all the information this solution requires and further, that the current structure might not be ideal to ensure easy processing in Swift-DocC.
I am very pleased with the result from the last thread, so I hope we can have a similarly productive discussion regarding the implementation details here!
Current State
I took the last days to check out the symbol graph generation in the Swift compiler and the output it generates.
AST
In the AST we have easy access to all the information we need. This includes specifically
- comments on extension blocks e.g.:
/// THIS COMMENT HERE
public extension SomeExternalType: SomeProtocol {
/* ... */
}
- information on the extended type (e.g. its
kind
(swift.struct
vsswift.protocol
vs ...))
Those were two areas where we weren't sure if that information would be easily accessible in the UX design phase.
Symbol Graph Files
Extensions to external types are currently excluded from the module's main symbol graph. I.e. where the main module is SwiftDocC
, public extensions to the standard library declared in SwiftDocC
are located in SwiftDocC@Swift.symbols.json
. This includes the symbol
of kind
e.g. swift.property
, swift.func
, or swift.struct
as well as relationships
of kind
memberOf
and conformsTo
.
The symbol
has a swiftExtension.extendedModule
field containing the extended module's name (e.g. "Swift"
). This also applies to extensions to local types.
Both relationships refer to the actual type declaration in the external module, e.g. String
with identifier "s:SS"
. For extensions to external types, the symbol
for this original type declaration is never present in the same file. It might be emitted as part of another symbol graph file, but e.g. for the Swift standard library it isn't emitted at all (locally).
Proposed Solution
My general idea is to adapt the symbol graph's structure in a way that best fits the structure in the UX design so that meddling with the graph in Swift-DocC is avoided. Meanwhile, the symbol graph should continue to transport raw data only. Specifically, I wouldn't want to bring any of the heuristics mentioned in the UX pitch discussion into the Swift compiler.
General Structure
The basis for my proposed solution is @ethankuster 's reply on the UX thread. You can find a diagram of my proposed graph structure below:
We introduce an alternative "extension" version for all type symbol kinds (struct
, class
, enum
, protocol
), e.g. next to swift.struct
we also introduce swift.struct.extension
. There exists one such symbol (of the respective kind) in the graph per extended external type.
The existing memberOf
and conformsTo
relationships (for extended external types) also end/start in these new symbols instead of referring to the original type-declaration's symbol.
Furthermore a new symbol swift.module.extension
is introduced. This symbol represents the external module that is publicly extended. There exists one such symbol in the graph per imported module where at least one type is publicly extended.
The swift.TYPE_KIND.extension
symbols are connected to the swift.module.extension
symbols via a new memberOf
relationship. (We could also use a different name here if we want to reserve the term "member" for type-members.)
Both these types of symbols carry little information (e.g. no comments) as they have no direct representation in the source code. The first type of symbols basically represents all usages of extension SomeExternalType
whereas the second type of symbol represents all usages of the respective import SomeModule
. I emphasize all, because there can be multiple occurrences of both in the source code! Nevertheless, we need these aggregate-symbols in order to achieve the structure that feels natural in DocC as pointed out in the UX discussion.
Adding Extension Block Comments
If we decide that we want to utilize comments above extension blocks to automatically generate more meaningful documentation on the extended type's documentation pages in Swift-DocC later on, I propose the following solution:
A new symbol of kind swift.extension
is introduced. There exists one such symbol per extension block with at least one public member. This symbol carries information such as the comments above the respective extension block.
Additional "memberOf" and "conformsTo" relationships are introduced in accordance with the members/conformance declared in the respective block. The relationships to the swift.TYPE_KIND.extension
symbols remain intact!
Finally, new contributesTo
(naming totally up for discussion) relationships are added between the swift.extension
symbols and the swift.TYPE_KIND.extension
symbol that correspond to the same extended external type.
These symbols and relationships would be inspected and transformed into synthesized additions to other symbols based on various heuristics in Swift-DocC, and finally removed from the internal symbol graph representation. The prime example for this would be aggregating comments from above extension blocks and adding them to the respective swift.TYPE_KIND.extension
symbol.
Unifying the Graphs
The existence of the swift.module.extension
symbols would also allow us to move all these symbols into the main symbol graph, e.g. they are emitted in the main SwiftDocC.symbols.json
file, not SwiftDocC@Swift.symbols.json
. This should also simplify processing the data in Swift-DocC.
Note that all these changes would be hidden behind a flag in the Swift compiler, so that this changed behavior is opt-in for other projects depending on symbol graphs!
Alternatives Considered
Synthesizing Artificial Symbols in Swift-DocC
One option would be synthesizing the swift.TYPE_KIND.extension
and swift.module.extension
symbols in Swift-DocC. This would make sense as they do not really exist in code and do not really transport any information/metadata beyond the structure they provide.
However, this structure is required in DocC in order to achieve the desired outcome from a UX perspective and therefore the symbols would have to be synthesized after importing the symbol files in Swift-DocC.
While I cannot fully grasp the architectural consequences this would have inside Swift-DocC yet, I think the biggest problem with this approach is the following:
At some point we have imported the symbol graph file in Swift-DocC. At this point the symbol graph is described using SymbolKit and it would be relatively easy to synthesize the missing nodes and relationships. However, SymbolKit is The specification and reference model for the Symbol Graph File Format. Therefore it would be odd to add symbols to SymbolKit that do not exist in the symbol graph files emitted by the Swift compiler. Furthermore, the most important type, Symbol
, is a struct
, so extending the library (in terms of altering the graph's structure) from within Swift-DocC should prove to be difficult.
Synthesizing the pages later on in Swift-DocC is quite difficult I think. This would involve re-writing large parts of the conversion logic as there would be many new rules to follow and tricky situations that just couldn't happen before, e.g. relationships where one end doesn't exist.
Do Not Unify Symbol Graph Files
An option to keep the changes to the symbol graph files rather low would be to keep extensions to external types in separate files. That would mean we'd have no swift.module.extension
symbols as this information can be deduced from what file contains a symbol.
Just synthesizing the top level documentation pages for the separate symbol graphs could be manageable inside Swift-DocC, however I'm not sure if the effort is justified. After all, the symbol graph files would still contain new symbols and relationships, which I guess is considered a breaking change. And if we already break it, we can also break it in a way that really fits our needs.
Discussion Points
Please keep in mind that I'm still rather new to this entire code-base, so I'm really dependent on your expert opinions for these large architectural decisions!
Of course, any feedback is welcome! I also want to bring up three questions to you all right at the beginning:
Firstly, what would the precise
identifier for all the X.extension
symbols be? How does the Swift compiler currently generate them? Is there some documentation I could read?
Secondly, should we add a "extensionOf" relationship between swift.TYPE_KIND.extension
and the respective original declaration symbols (even though this original declaration is not part of the same symbol graph)? I think this could cause problems in the conversion in Swift-DocC again, however it also seems right to have a reference to the original type declaration when considering the discussion about [SR-15431] Support DocC references to symbols defined in another module · Issue #208 · apple/swift-docc · GitHub in the UX thread. If I have to mess with the symbol graph file format once I guess it would be best to plan ahead a bit.
Finally, for my broader understanding: What role does Swift-DocC play in the universe of Swift symbol graph files? Is it the main reason they exist or just one of many consumers of this interface? What would us introducing a new symbol graph file format mean for the old format? Would both versions have to be maintained next to each other indefinitely, or would the old one phase out after a short(ish) period of time?