Hey Allan, It is very exciting on my end to hear that this is an idea that has been under active consideration. Made my day!
My opinion on this is that a runtime version of this functionality would be not necessarily a separate feature, but an adaptation that could work hand-in-hand with this pitch. Semantically, it almost makes sense that it could be determined by where the # symbol is located. That already implicitly determines whether something is a runtime check or a compiler check. So, at a high level, it might look like this:
if #hasSymbol(...) {
// This gets compiled in, and allows you to use the symbol
} else {
// This still gets compiled in, but is only called if the symbol is unavailable during runtime
}
Now addressing the auto-complete or structure of the symbol being passed in, I'm assuming there would be more than one format depending on the level of ambiguity the symbol requires to reference.
For example, let's first look at specifically the way(s) it could be passed into the compiler directive.
If the module the symbol is defined in is importable, the autocomplete could supply the names of the symbols (regardless of availability) for the developer to use. By default that would result in something like this:
#if hasSymbol(Module.some.ambiguous.symbol)
// Names are hard, autocomplete would help in this case
#endif
If the module was unable to be imported though, there would be no autocomplete. That would suck, especially if the developer gets the name wrong, but there is usually adequate information about the symbol that should lead the developer along the right path.
In another circumstance though, you're trying to check if you can access a function symbol. This can be tricky because of overloads. An example of a tricky situation would be something like this:
func someFunction() -> Void
func someFunction(_ parameter: Any) -> Void
These two can be disambiguated from each other by using the same syntax that #selector uses.
// func someFunction() -> Void
#if hasSymbol(module.someFunction)
#endif
// func someFunction(_ parameter: Any) -> Void
#if hasSymbol(Module.someFunction(_:))
#endif
Unfortunately, though, we can be met with two functions with the same signature that only differs in the return value:
func someFunction() -> Int
func someFunction() -> String
The only way to disambiguate the use of these functions is to determine them from their use context which would be unavailable to us as a compiler directive.
This is where the second possible alternative way to pass a symbol into hasSymbol comes in handy. By passing the symbol in as a mangled name, all the type information is able to be used for disambiguation:
// func someFunction() -> String
#if hasSymbol("$s6Module12someFunctionSSyF")
#endif
An alternative to this is passing in the return type as a second parameter, but for implementation purposes that might not be the best idea. Nevertheless, it's an option to consider:
#if hasSymbol(Module.someFunction, String)
#endif
To someone reading this without knowing what the parameters are, it may not be initially clear why "String" is passed in as a second argument.
For the runtime counterpart, this unclear use of a type can be clarified nicely. Here's an example of the what it may look like for #hasSymbol to disambiguate a function:
if #hasSymbol(Module.someFunction, withType: (() -> String).self) {}
For unapplied functions, this could still be used because the type information can now be passed into the check. Inside the if scope, the function could then be accessed as if it was available to the developer the entire time without needing it to be explicitly passed into the scope.