Suppose you have a library with two targets: "Numerics" and "ReadModule". "RealModule" has a public function named "realModuleTest" inside of it. "Numerics" has a single file with the following line:
@_exported import RealModule
When you import Numerics into your project, you get access to "realModuleTest". This, as far as I understand, is the whole point of @_exported import.
However, when you define a custom operator inside of "RealModule", it does not get properly exported to "Numerics". Here is the source file for the "RealModule" target:
// RealModule/Real.swift
import Foundation
precedencegroup ExponentiativePrecedence {
associativity: right
higherThan: MultiplicationPrecedence
}
infix operator ** : ExponentiativePrecedence
public func ** (base: Double, power: Double) -> Double {
return pow(base, power)
}
public func realModuleTest() {
print("this function is defined in the real module")
}
Here's the link to the package on github:
Try creating a command-line project, add the above package as a dependency, and add the following to main.swift
// main.swift
import Foundation
import Numerics
let x = 2.0
let y = 3.0
realModuleTest() // works as expected
print(x ** y) // error: "Operator is not a known binary operator"
I imagine the response might be that the underscore means we can't rely on its functionality, but we use @_exported import in the Composable Architecture to automatically import a module that provides a prefix operator:
In practice the prefix operator resolves just fine without having to import CasePaths downstream. So maybe this is an issue specific to infix operators?
Edit: The prefix operator is/, so it overlaps with an existing infix operator and maybe that's also why it works for us.
Definitely a bug. I'm surprised because at one point we had the opposite bug, where operators would be exported even when you didn't use @_exported (which, yes, is not officially supported yet, but I'd be surprised if it changed very much when folks get around to finalizing it). The reason for that is because operator lookup goes through a different path than normal name lookup (or at least it used to), and while there was a reason for that in the early Swift compiler architecture (which had more discrete passes) there's not really a good reason now. Someone should make OperatorDecls a kind of ValueDecl and remove all the custom operator lookup code.
(Operator decls aren't values, but neither are modules; it's just how the compiler models normal name lookup. Clang separates "NamedDecls" from ValueDecls for this reason.)
However, it is behind a flag, -enable-new-operator-lookup. I wonder if we can make it on by default if the source compat suite passes, though.
As an aside, this part about modules always bothered me. Not only are they values in Swift, but they're also TypeDecls. At one point I remember I wanted to change that but you convinced me otherwise, and I don't remember why :-)
The memory I have is that named lookup takes a TypeDecl as a base rather than a type, and so before Doug made Module into ModuleDecl everything was a DeclOrModule union. (And it's probably easier for ModuleType to be a type if Module is a TypeDecl.)
Maybe nowadays untangling the two will be easier since lookupQualified() got split up into two entry points, one for type members and one for module members. We still have the Type-based entry point that gets called in various places, though.
As @Slava_Pestov says, this was recently fixed on master. Although the new lookup behavior is behind a staging flag, it will always be used if there's only a single lookup candidate (which I'm hoping should cover most cases that haven't already worked around this issue). Your example should therefore compile on master – you can try out a master snapshot to verify.