`@_exported import` does not properly export custom operators

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"

You get the weird error shown above. Is this a bug?
I already filed a bug report here: [SR-13350] `@_exported import` does not properly export custom operators · Issue #55790 · apple/swift · GitHub

@jrose I've seen multiple threads in which you mentioned the @_exported attribute. You seem to know a lot about it. Any thoughts on this issue?

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.

Hi Stephen,

Thanks for mentioning that!

I decided to add a postfix operator and a prefix operator to RealModule/Real.swift to test your theory:

prefix operator √

/// Square Root operator
public prefix func √(_ radicand: Double) -> Double {
    return radicand.squareRoot()
}

postfix operator %%

/// Percent sign. Divides the operand by 100.
public postfix func %%(num: Double) -> Double {
    return num / 100
}

And both operators get properly exported! So it must be an issue specific to infix operators.

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.)

1 Like

This has plagued Fluent for a while now. You can see how we workaround it here: fluent/Exports.swift at main · vapor/fluent · GitHub

It looks like @hamishknight fixed this on master in this PR: https://github.com/apple/swift/pull/31506

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 :-)

1 Like

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.

What is the next official release that will include this patch? Swift 5.3?

Unfortunately it won't make it into 5.3, it'll be the release after.

From trunk code SWIFT_VERSION info, next release after 5.3 will be 5.4.