[Pitch] Lifting restriction on explicit specialization in function calls

Ah, I confirmed that this bug was somehow introduced in Swift 5.8.
Swift 3, 4, and 5.7 definitely deny such code.

On the other hand, Swift 6.2-dev(DEVELOPMENT-SNAPSHOT-2025-02-28-a) detects all of such explicitly specialized functions, while the compiler emits only warnings not errors. I'm not sure why...
EDIT: swift#76141 downgraded such errors to warnings temporarily in Swift 5 mode.


Anyway, I want to reiterate that we'd better consider which overloaded function should be chosen in regard to this syntax.

If we need a new syntax anyway, can we get completely unapplied references instead of the curried ones?

How are multiple parameter packs to be handled?


Types don't have the problem because they're limited to one parameter pack.

struct S<T, each U, V> { }
_ = S<Bool, Int, String>() // Compiles

Functions require entupling all but the last parameter unless you use labels.

No labels:

func f<each T, each U, each V>(
  _: (repeat (each T).Type),
  _: (repeat (each U).Type),
  _: repeat (each V).Type
) { }
f(Bool.self, (Int.self, Int.self), String.self)

Labels:

func f<each T, each U, each V>(
  T: repeat (each T).Type,
  U: repeat (each U).Type,
  V: repeat (each V).Type
) { }
f(T: Bool.self, U: Int.self, Int.self, V: String.self)

I think there are four options?

  1. Have the labeling be optional, as with e.g. tuple labels
let tuple: (t: Bool, u: Int) = (true, 1)
f<Bool, U: Int, V: String>() // No `T:` there.
  1. Treat the placeholder names like subscript* argument labels, requiring the name being present twice.
func f<T each T, U each U, V each V>() { }
f<T: Bool, U: Int, V: String>()

* This sort of thing, I mean:

enum E {
  static subscript(label label: some Any) -> Void { }
}
E[label: "value"]
  1. Continue to require metatype arguments for all but one of the pack arguments.
func f<each T, each U, each V>(
  U: repeat (each U).Type,
  V: repeat (each V).Type
)
f<Bool, _, _>(U: Int.self, Int.self, V: String.self)

(I loathe this option.)

  1. Disallow explicit specialization for functions that use multiple parameter packs.
1 Like

This is one of those unfortunate situations where compiler bugs meant we accepted invalid code, and this invalid code then made its way into developer projects, so to maintain source compatibility once the bug was fixed we downgraded the error to a warning…

1 Like

Edit: Actually, I remembered that we do already accept explicit specialization for parameter packs specifically in 6.1+, see this godbolt link: Compiler Explorer

But with multiple sets of parameter packs, explicit specialization fails as expected since it's ambiguous.

/tmp/test.swift:8:13: error: generic type 'f' specialized with mismatched type parameter pack
 1 | func f<each T, each U>(
   |      `- note: generic type 'f()' declared here
 2 |     
 3 | ) -> (repeat each T, repeat each U) {
   :
 6 | 
 7 | func x() {
 8 |     let g = f<Int, Bool, Bool>()
   |             `- error: generic type 'f' specialized with mismatched type parameter pack
 9 | }
10 | 

I still suspect labeled generic arguments will have to be part of this if we pursue lifting the restriction.

(also the diagnostics says "generic type 'f'" which is mainly an indication that we'll need to audit the diagnostics around specialization to make sure we don't make assumptions like this.)

That's certainly not intentional behavior either. I have no idea why that works.

1 Like

From what I can tell there's an explicit carve-out: swift/lib/Sema/CSSimplify.cpp at main · swiftlang/swift · GitHub

  // FIXME: We could support explicit function specialization.
  if (openedGenericParams.empty() ||
      (isa<AbstractFunctionDecl>(decl) && !hasParameterPack)) {
    return recordFix(AllowFunctionSpecialization::create(
               *this, decl, getConstraintLocator(locator)))
               ? SolutionKind::Error
               : SolutionKind::Solved;
  }

Still seems unintentional, @gregtitus was this intentionally allowed? I didn't see any discussion about this carve out in the PR review: [Sema] Add specialization constraints for func and variable types, then diagnose w/fixes. by gregomni · Pull Request #74909 · swiftlang/swift · GitHub

2 Likes

I suspect the isParameterPack check was added as a quick workaround because the diagnostic path was crashing with parameter packs; but this had the effect of accepting the construct and just making it work, which surely was unintentional.

However, this all suggests that implementing your feature should be straightforward, since all of the pieces are apparently there already.

Another edge case to consider is generic subscripts. Should this work?

struct S {
  subscript<T>(_: T) -> Int { fatalError() }
}

let s = S()
s<Int>[123]  // ?
1 Like

My inclination here would be that

s<Int>[123]

would not resolve immediately since it would imply instantiating s with generic params, rather than the subscript decl, but

s.subscript<Int>[123]

should be something we support.

Still seems unintentional, @gregtitus was this intentionally allowed?

Totally unintentional! That FIXME was pre-existing, and I want to say that also the carve-out for excluding parameter packs from creating an error at that location was pre-existing? I think I moved the logic around but that test was already there.

The purpose of that PR was to move these types of errors into type checking instead of being a kind of vague, broad post-pass check, which allowed for improving the messages. And removing the post-pass check apparently let things through that were previously disallowed.

1 Like

Is there real ambiguity here or just parsing grossness? Not sure how square brackets could immediately follow such an instantiation.

Yes. Sometimes it won't be redundant, when the return type is not inferable.

let container = try decoder.container<CodingKeys>()
let property = container<Property>[.property]
self.property = property

That's longhand for

property = try decoder.container<CodingKeys>()[.property]
extensions
extension Decoder {
  func container<Key: CodingKey>() throws -> KeyedDecodingContainer<Key> {
    try container(keyedBy: Key.self)
  }
}

extension KeyedDecodingContainer {
  subscript<Decodable: Swift.Decodable>(key: Key) -> Decodable {
    get throws { try decode(Decodable.self, forKey: key) }
  }
}

static subscripts are going to be the weird ones.

enum E<T> {
  static subscript<U>(_: some Any) -> (T.Type, U.Type) { (T.self, U.self) }
}
E<Bool><Int>[""]
1 Like

An expression like foo(x < y, z > [w]) could be parsed as two argument expressions involving the < and > operators. This is also true for . and (, to be fair, but the bet the current heuristic takes with those tokens is that, if the string between the angle brackets parses as a type, the interpretation as operators is far less likely than the interpretation as a generic argument list. On balance that's probably true with [ as well, since Arrays, Sets, and other collections are not typically used directly with comparison operators.

4 Likes

I agree that:

withTaskGroup<Int, Bool> { }

is less clear but what about:

let result: Bool = withTaskGroup<Int> { }

That seems to make everything clear even when reading the code down the road. Obviously, if result's type is already known, even that could be elided.

1 Like

Short response: yes please from me. I needed this today for the N+1th time