Hi everyone,
We would like to propose improvements to the ClangImporter to provide more detailed diagnostics when an entity cannot be imported from C or Objective-C. As it stands, when the ClangImporter fails to import an entity (function, type, macro, etc.) either entirely or partially, failure is largely silent. The current Swift compilation errors present largely as if the entity was never defined at all.
We are proposing adding two different kinds of signals to provide more context to the users:
- Warnings for the different constructs from headers that fail to import.
- Errors for when those constructs are used in Swift code by generating stubs marked unavailable in place of the not imported entities.
For the time being, we propose to hide this functionality behind a new opt-in flag.
Proof of Concept
Here GitHub - NuriAmari/swift at diagnostics-pitch we have prepared a small proof of concept that centers around a couple of builtin C types with no Swift representation. As it stands, if such a type is used in a C function signature, the function will not be imported by the ClangImporter. Similarly, if an unsupported type is used in a field declaration, the ClangImporter will import the struct, but it will appear as if the field was never declared.
Demonstration
Below we present a minimal example demonstrating the improvements provided by the proof of concept and the kinds of improvements we would like to continue making. Here are the files involved, all in the same directory.
module.map
module complex {
header "complex.h"
export *
}
complex.h
struct MyStruct {
float _Complex x;
int y;
};
double _Complex myFunc();
demo.swift
import complex
let _ = myFunc()
demo2.swift
import complex
var s : MyStruct
s.x = 5.0
For the first demonstration, we show the current behaviour before enabling the new diagnostics:
swift-frontend -typecheck -I . demo.swift
demo.swift:3:9: error: cannot find 'myFunc' in scope
let _ = myFunc();
^~~~~~
As mentioned, the function simply can’t be accessed, though the user hasn’t really made any mistakes. After diagnostics are enabled, we get more helpful information:
swift-frontend -typecheck -enable-experimental-clang-importer-diagnostics -I . demo.swift
/Users/nuriamari/git/swift-project/demo/complex.h:6:1: warning: function 'myFunc' not imported: type 'Complex' is unavailable in Swift
double _Complex myFunc();
^
demo.swift:3:9: error: 'myFunc()' is unavailable: type 'Complex' is unavailable in Swift
let _ = myFunc();
^~~~~~
complex.myFunc:2:13: note: 'myFunc()' has been explicitly marked unavailable here
public func myFunc() -> Any
^
Though not shown in this example, the proof of concept also handles invalid parameter types, not just return types.
We provide similar improvements for struct fields:
swift-frontend -typecheck -I . demo2.swift
demo2.swift:4:3: error: value of type 'MyStruct' has no member 'x'
s.x = 5.0
~ ^
Again, the user is left wondering what happened to the x field. After enabling diagnostics, the situation is a little more clear:
swift-frontend -typecheck -enable-experimental-clang-importer-diagnostics -I . demo2.swift
/Users/nuriamari/git/swift-project/demo/complex.h:2:20: warning: field 'x' not imported: type 'Complex' is unavailable in Swift
float _Complex x;
^
demo2.swift:4:3: error: 'x' is unavailable: 'Complex' is unavailable in Swift
s.x = 5.0
^
complex.MyStruct:3:16: note: 'x' has been explicitly marked unavailable here
public var x: Any
^
Admittedly, these examples are quite niche, but they serve to communicate the type of improvements we wish to make.
General Design
The proof of concept above contains two main flows that we propose be repeated where needed to add support for more diagnostics. A pull request has been opened here to highlight the changes: Clang Importer Diagnostics Pitch by NuriAmari · Pull Request #39655 · apple/swift · GitHub.
Diagnostic Reporting
The first flow is responsible for recording diagnostics and surfacing them to the correct code locations. Many methods within the ClangImporter are used to transform a particular Clang AST node and return a pointer to the corresponding Swift AST Node. In the case of import failure, nullptr is returned, leaving little room for communicating diagnostics. In the proof of concept, https://github.com/NuriAmari/swift/blob/diagnostics-pitch/lib/ClangImporter/ImporterImpl.h#L322 we have created a new wrapper structure to include both the Swift AST Node and any diagnostics that may have been produced. We have applied this new return type in a few locations, but would like feedback on the approach as a whole.
We have observed that other additional return values are communicated using by-ref parameters, for example when importing a method the async convention or the error type are returned this way. This option was briefly explored, but found it to be less appealing.
Stub Generation
To construct stubs for unimported components, we have tried as much as possible to reuse the existing AST generation code. As shown in the proof of concept swift/ImportType.cpp at diagnostics-pitch · NuriAmari/swift · GitHub when a small portion of the AST fails to import, we attempt to replace it with a version using Any as early as possible. For example, if a function fails to import due to an unsupported return type, we replace the return type with Any and allow the function generation to proceed as normal otherwise. Before the enclosing AST node is finalized, we check to see if diagnostics were reported, even for a “successful” import, and mark the node unavailable accordingly. We recognize some care must be taken in ensuring that such a replacement does not result in an erroneously imported unsupported entity, but we think reusing the existing code is worth this risk.
Diagnostics + Repeating Name Lookups
While attempting to understand the ClangImporter and how diagnostics are reported, we have come to realize that declaration lookups are often performed repeatedly in a series of increasingly broad searches: swift/PreCheckExpr.cpp at main · apple/swift · GitHub . The extra calls into the ClangImporter result in diagnostics (both new and existing) to be reported either repeatedly or at inappropriate times, particularly for the broader searches (ie. typo search). Thus far, we’ve been able to avoid additional lookups by providing a stub on the first search, but in our investigation of C macros, this hasn’t worked as well. If anyone has experience with this issue, or any suggestions as to a fix, we would appreciate any input.
Feedback / Future Direction
We would appreciate any feedback from those more familiar with the ClangImporter on our general approach. In particular, input from @zoecarver, @egor.zhdan (active in the Clang Importer code), @jrose (listed as the owner of the Clang Importer), and @michael_ilseman, @Douglas_Gregor (worked in the diagnostic code of the Clang Importer) would be wonderful. Also, feel free to bring anyone you think might be interested into the discussion. If the patterns presented in the proof of concept are reasonable, we plan to expand to other hopefully more impactful unimported C/Objective-C constructs. In the immediate future, we plan to tackle the following C constructs:
- Macros. Only mostly trivial macros are supported currently, which sometimes creates problems importing some headers that rely on macro expansion to work.
- Forward declared classes. Clang Importer supports some cases of forward declared classes, but when the forward declared class is not defined in the same module, functionality that uses it is not imported. Even importing both modules from Swift will not solve the problem.
That being said, we are actively seeking out more silently unavailable constructs that could use diagnostics. If anyone is aware of more examples, we would greatly appreciate any input and think compilation of such a list would be beneficial, even if we are not able to address every issue.
In some cases, it is complicated to distinguish between what is a bug in the Clang Importer and what is a limitation of the translation mechanisms. We are looking for the latter. If you have examples of the former, we encourage you to report them to https://bugs.swift.org.