Compiling types with the same name for different runtimes?

I'm pretty sure there's no way to achieve this… but maybe there is?

I have some type that requires macOS 14:

@available(macOS 14.0.0, *)
struct S<each T> {
  init(_: repeat each T) {
    
  }
}

If a user is running the application on an earlier version of macOS, I am attempting to build a type of the same name:

@available(macOS, introduced: 10.0.0, obsoleted: 14.0.0)
struct S<T> {
       ` Invalid redeclaration of 'S'
  init(_: T) {
    
  }
}

Which leads to a compiler error.

I'm not sure there's any way to make this work with two different types from the top level namespace that are compiled together but are "unique" across runtimes.

The ideal callsite would be something like this for macOS 14:

let _ = S(1)
let _ = S(1, 2)
let _ = S(1, 2, 3)

With users on older systems running the "legacy" version:

let _ = S(1)
let _ = S(2)
let _ = S(3)

I don't think this is possible using the existing tools to express availability. AFAIK my option at this point is to explicitly name these types differently (S1 and S2)… with the product engineer then needing to choose the appropriate type with some logic at runtime.

Any other ideas or case studies about this situation? Two types with the same name coexisting at compile time but independent across runtimes?

https://nshipster.com/available/

A possible solution would be to use compiler control statements. The downside to using them is they might not give you the exact control you want and can be tedious to use if you need a lot of them, but you can still achieve it doing some research and testing.

1 Like

Here's the best I can think of:

struct S {
  private struct Mac10Impl { ... }

  @available(macOS 14, *)
  private struct Mac14Impl { ... }

  private enum Storage {
    case mac10(Mac10Impl)

    @available(macOS 14, *)
    case mac14(Mac14Impl)
  }

  private var storage: Storage

  init() {
    if #available(macOS 14, *) {
      self.storage = .mac14(...)
    } else {
      self.storage = .mac10(...)
    }
  }
}
1 Like

Unfortunately this only works if your deployment target is at least macOS 14. Try this instead, you'll see an error:

@available(macOS 200, *)
struct S { }

enum Storage {
  @available(macOS 200, *)
  case foo(S)
}
error: enum cases with associated values cannot be marked potentially unavailable with '@available'

There's no mechanism to perform dynamic layout based on availability at runtime; enum cases and stored properties cannot involve a less-available type than the enum or struct itself.

2 Likes

As a workaround, would you be able to wrap the conditionally-available payload in an Any, and cast it to S in conditionally-available accessors?

2 Likes

That's an interesting idea. It might be even cleaner to define a new protocol that S (and only S) conforms to, and then you can avoid the cast and just store an any P instead.

1 Like

Hmm… so we're thinking something like a Box wrapper? An abstraction indirection layer that wraps an existential type that can be resolved at runtime?

struct Box {
  @available(macOS 14.0.0, *)
  init<each T>(_: repeat each T) {
    
  }
  
  init<T>(_: T) {
    
  }
}

@available(macOS 14.0.0, *)
func f() {
  let _ = Box(1)
  let _ = Box(1, 2)
  let _ = Box(1, 2, 3)
}

let _ = Box(1)
let _ = Box(2)
let _ = Box(3)

This compiles with no errors. Maybe my question at that point might be if the user is running on macOS 14 and both constructors are available… is there a stable and deterministic pattern to know which constructor is chosen by the compiler when T is just one element? Can I expect the compiler to treat T as a "parameter pack of one"?

And then there is also:

struct S {
  init(_: Any...) {
    
  }
}

let _ = S(1)
let _ = S(1, 2)
let _ = S(1, 2, 3)

Which can just forget about parameter packs and modern variadic types in the name of supporting legacy platforms without additional plumbing implementation.

I believe [CSRanking] Augment overload ranking to account for variadic generics by xedin · Pull Request #67435 · swiftlang/swift · GitHub made it so the compiler should pick the non-variadic overload in the case that both choices are viable, yeah.

1 Like