Ambiguity of same-named types from two different modules

I think I am hitting a limitation of Swift, but want to make sure I am not missing something obvious.

I have two modules with distinct names that each define a top-level type with the same name. This top-level type is itself an enum with other types of shared names nested within its namespace. For example, module Foo and module Bar both expose enum type A which exposes B inside itself so we have Foo.A.B and Bar.A.B.

I am trying to write code in a third module that depends on both Foo and Bar modules, though I am only ever importing one of the two at a time in a single file, and yet I am getting ambiguity complaints from Swift. Even more confusingly, the following code compiles:

import Foo

struct Tmp {
    let tmp: Foo.A
}

And yet the following code hits an ambiguity error:

import Foo

struct Tmp {
    let tmp: Foo.A.B
}

The error is "Ambiguous type name 'B' in 'A'". So, somehow it knows which A to use (after all, there is only one A in scope given I am only importing Foo), but it doesn't know which B to use, presumably because both Foo and Bar define A.B? It feels like a bug, but I am holding out a bit of hope that there is some way to work around it.

I've tried getting more targeted with the imports, but the following hits the same ambiguity error:

import enum Foo.A

struct Tmp {
    let tmp: A.B
}

Out of sheer desperation :slight_smile:, have you tried typealiasing it?

typealias Foo_A_B = Foo.A.B

struct Tmp {
    let tmp: Foo_A_B
}

Yeah, I should have mentioned that. The other thing I tried was a typealias but if you set the typealias to Foo.A.B then that's where you get an ambiguity error and if you set the typealias to just Foo.A then you get the type error wherever you try to use it (Foo_A.B, for example).

So this definitely shouldn’t happen but also I’m wondering what the compiler even thinks is going on. Can you share the source of one of the modules, and also check the build log to see what extra notes the compiler provides? (There’s an old format mismatch between the compiler and Xcode that means some notes get dropped.)

1 Like

Can you share the source of one of the modules [?]

Sure; I've pushed my experimentation up to GitHub - mattpolzin/OpenAPIKit at feature/195/31-to-30.

Comparing release/3_0...feature/195/31-to-30 · mattpolzin/OpenAPIKit · GitHub gives a quick look at what I've experimented with thus far. There were already two modules with largely the same type definitions (OpenAPIKit and OpenAPIKit30). Then I created a third module OpenAPIKitCompat with the intention of importing both OpenAPIKit modules and exposing functions that convert between the two versions of OpenAPI representation. After failing to just import both in the same file from the beginning, I tried creating public typealiases to disambiguate the two root-level definitions named OpenAPI. When that didn't allow me to do what I needed within the OpenAPIKitCompat module, I also created a new module to experiment with only importing OpenAPIKitCompat but hit similar ambiguity errors in what is currently the toThirty() function on that branch. The experiment with simply trying to refer to OpenAPI.Document from within one file where only one of the module's OpenAPI enums has been imported can also be seen.

[...] and also check the build log to see what extra notes the compiler provides?

I wonder if you mean a log file rather than stdout, but this is the full contents of a swift build from the CLI:

warning: Usage of /Users/matt/Library/org.swift.swiftpm/collections.json has been deprecated. Please delete it and use the new /Users/matt/Library/org.swift.swiftpm/configuration/collections.json instead.
Building for debugging...
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKitCompat/Disambiguate30.swift:13:22: error: ambiguous type name 'Document' in 'OpenAPI'
    let tmp: OpenAPI.Document
             ~~~~~~~ ^
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKit30/Document/Document.swift:48:19: note: found candidate with type 'OpenAPI.Document'
    public struct Document: Equatable, CodableVendorExtendable {
                  ^
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKit/Document/Document.swift:48:19: note: found candidate with type 'OpenAPI.Document'
    public struct Document: Equatable, CodableVendorExtendable {
                  ^
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKitCompat/Disambiguate30.swift:13:22: error: ambiguous type name 'Document' in 'OpenAPI'
    let tmp: OpenAPI.Document
             ~~~~~~~ ^
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKit30/Document/Document.swift:48:19: note: found candidate with type 'OpenAPI.Document'
    public struct Document: Equatable, CodableVendorExtendable {
                  ^
/Users/matt/staging/OpenAPIKit/Sources/OpenAPIKit/Document/Document.swift:48:19: note: found candidate with type 'OpenAPI.Document'
    public struct Document: Equatable, CodableVendorExtendable {
                  ^
[2/2] Emitting module OpenAPIKitCompat

Aha! Foo.A and Bar.A are both typealiases of Baz.A! That makes sense, then: if Foo has an extension that adds A.B, and Bar also has an extension that adds A.B, then even though the Bs are from separate modules, the As aren't. And paths are resolved in order, i.e. (Foo.A).B and (Bar.A).B. The "Foo" or "Bar" source is discarded after that.

The most general fix to this would require something like A.Foo::B and A.Bar::B, which I know has been discussed before but I can't find the thread. In your case you might be able to split up Baz (OpenAPIKitCore) into separate modules as well, or if that's not desirable you could manually make Foo.A and Bar.A to include everything from Baz.A instead of being a typealias.

1 Like

Of course! Thank you for pointing that out. Was easy for me to look past since what I wanted was a distinct type that just happened to live in an extension of a shared type.

I think I have a path forward now.

2 Likes