SE-0267 — `where` clauses on contextually generic declarations

Slava's point is the real sticking point to making this ABI-compatible.

X Unconstrained extension
(or original decl)
Constrained extension
Unconstrained method Foo.bar() (Foo where T: Hashable).bar()
Constrained method Foo.(bar() where T.Comparable) (proposed) (Foo where T: Hashable).(bar() where T: Comparable)

Canonicalizing the bottom-left cell to be the same format as the top-right isn't so bad, but the bottom-right cell already exists, and is already distinct from the top cell. We might have been able to canonicalize that too, but now it's too late, and so I'm not sure it's worth making a special case for the bottom-left case that won't apply to the bottom-right.

2 Likes

Obviously this is entirely an Apple policy decision, but it might not necessarily be too late. Let's say, for the sake of argument, that all forms are canonicalised to the bottom-right as of the next compiler version unless there is a method annotation to use the 5.0 mangling. If a library author producing a binary-stable library (probably Apple) compiles with the new compiler they would receive a warning that they need to specify the mangling through the method annotation to avoid an ABI break.

For anyone producing new binary-stable libraries after the new compiler version and any existing clients of the old libraries nothing would change; existing clients can assume that a library built using an older compiler version uses the non-canonical manglings.

To be clear, the bottom right is the problem. The canonicalization has to be to the bottom left or top right even when written as one of the other three quadrants with constraints in them.

I feel like it'd be a pretty nasty move to have released Xcode support for binary frameworks in Xcode 11 and then immediately break it. Is it worth it to make this a short-term issue rather than a long-term one, though? Maybe.

(Note that we don't have to break binary frameworks built with Xcode 11, but purveyors of such frameworks would need to update their code for Xcode 12 or whatever.)

2 Likes

Why is the bottom right cell the problem and not the bottom left? Are there non-cosmetic issues with mangling the bottom left like the bottom right instead of the other way around?

It seems to me like we could version breaking ABI rules like this in a way that we only take advantage of them if you target newer compiler or runtime versions (and only ever have). That would be another axis of complexity to have to reason about, though.

A more straightforward approach to these sorts of ABI fixes might be to just implement them behind an ABI version flag. That way other platforms that don't have ABI constraints can benefit right away, as can new platforms that might eventually be introduced that can take an ABI break.

2 Likes

The whole bottom row represents a second way to write things already expressible by the top-right cell. We didn't bother to do that for the bottom-right row, so we already have this problem today:

extension Foo where T: Hashable {
  func bar<U>(_: U) where T: Comparable {}
}

extension Foo where T: Hashable, T: Comparable {
  func bar<U>(_: U) {} // could be equivalent to above, but isn't
}

SE-0267 exposes the problem in a lot more places, and while there's an obvious way to canonicalize the newly-allowed declarations to an existing syntax (bottom-left cell -> top-right cell*), we have both the top-right and bottom-right cells in existence today. So the options would be:

  1. Moving a constraint between the extension and the member is always breaking (as proposed).
  2. Moving a constraint between the extension and the member is okay if the member is not itself generic (still ABI compatible but a much more subtle rule).
  3. Moving a constraint between the extension and the member is always okay (ABI-breaking for instances of the bottom-right cell without extra annotations, in order to canonicalize them to the top cell).

* We can't canonicalize to the bottom-right cell because it's not obvious which constraints go on the extension and which go on the function. If it's "all of them that can go on the extension", that's the top-right cell. We could in theory canonicalize to the bottom-left cell, but that'd be extra breaking since it's the one cell that isn't possible today, and it loses some nice properties for partial names.

6 Likes

I suppose, based on our experience with opaque result types, that any new mangling will make this feature not back-deployable by default.

The proposal does not mention a requirement for newer runtime, which means this should still be back-deployable.

Only new type manglings need new runtime support, though the Swift 5.1 has additional fallbacks that can allow for the compiler to avoid needing to emit new manglings when targeting older OSes. Our existing mangling scheme ought to be able to cover naming symbols using this feature.

This is what Slava originally said on this matter:

Though I don't see how the lexical structure breaks when we exchange constraints between the extension and the member.

Does it matter at all to the demangler where the constraints are placed?


Yes, that will be allowed too since you can already constrain the extension instead.

I think you misunderstand, so let me be more clear with a code example.

I want to be able to write this:

extension Sequence {
    func toSet() -> Set<Element> where Element: Hashable {
        return Set(self)
    }
}

Instead of this:

extension Sequence where Element: Hashable {
    func toSet() -> Set<Element> {
        return Set(self)
    }
}

It isn't clear from the proposal whether that is possible. It is clear that you cannot use constraints on protocol requirements. What about symbols declared in extensions?

There is a good reason to disallow this in the context of protocol requirements. It would be confusing to have a requirement with such constraints. What would that even mean in a case where Self or an associated type does not meet the constraints?

On the other hand, the meaning is quite clear in the code I have written above. The only reason I can imagine for disallowing the former is implementation difficulty. Ideally if we're going to lift this limitation we would do it everywhere it is sensible to do so.

I did mention it in a generalized form in the introduction note, but apparently it should be made more eye-catching:

Only declarations that already support a generic parameter list and being constrained via a conditional extension fall under this enhancement.

Unless the constraint is on a protocol requirement (we haven't modelled those yet), there is no misunderstanding :slight_smile:.

P.S There are some tests for protocol extensions too.
https://github.com/apple/swift/pull/23489/files#diff-f058e6035325de0d3eeeb9cf08946e69

Just out of curiosity, why do you want the former rather than the latter?

I think the proposal itself answers this question quite nicely, just in the context of concrete types. I want to be able to group declarations as I desire, and not be forced to put them in separate extensions because of a language limitation.

4 Likes

"Demangling" isn't the only client of mangled names, but in this particular case the concern is about canonicalization. Canonicalization means that given a set of constraints they're always printed in the same way, even if we got them differently.

1 Like

Right, adding code is perfectly fine. Moving constraints between extensions and members is also fine with the rare exception of doing so to public symbols in a library with a stable ABI.

-1/2 as written. I could be convinced that this is a good idea, but for now it makes me uncomfortable for several reasons:

  • Swift tries hard to provide exactly one correct way to do something syntactically. This introduces an ambiguity of which way is best from a style point of view, especially in cases where you have a single method in a constrained extension.
    • When we previously moved the preferred location of function where clauses from the generic parameter definitions to the end of the function declaration, we also introduced a diagnostic pointing people to the new location.
    • At this point we are likely too far down the road of source stability to push people onto this new form.
    • Even if we wanted to definitively say that where clauses should always be included on the function instead of the extension, we couldn’t because properties, etc. would be excluded (at least for now).
    • In my mind this creates an uncomfortable style inconsistency. This additionally means that if swift-format eventually decides to pick one flavor over the other for consistency sake, the choice of which way is best is rather subjective.
  • Even though we already have two sets of where clauses in relevant examples (one on the extension and one on the function), they currently have different semantic meanings, which makes the original “artificial” restriction make sense. Constraints on the extension always reference the type being constrained and the impact is what additional members or conformance that type is given. Constraints on functions always reference parameter types of that function and never impact the member’s availability on a type (until specific argument types are considered).
    • Implementing the premise of this proposal would eliminate this distinction. In my mind, this creates an uncomfortable semantic inconsistency that would make it more difficult to reason about a particular method declaration. In order to determine if a given method will be available on a specific type, I need to manually parse the where clause to see if it references any outer generic parameters.

However, I like the main idea behind this proposal because:

  • Grouping related methods is often desirable. That said, pragma marks are already a reasonable way of grouping related extensions.
  • This change would actually benefit readability greatly in the case of a method declared in the original object declaration that depends only on outer generic parameters. I’m just concerned because it might hurt readability in cases where the where clause of a generic method references both inner and outer generic parameters.

I would be open to a solution that somehow could reconcile the first list with the second.

4 Likes

I can't say that this is my impression at all. Or it is not having a lot of success, if that is indeed a goal. e.g. you're free to choose between func f<T: Equatable>(…) and func f<T>(…) where T: Equatable according to personal taste, amongst many other examples. I personally think it's fine to provide some stylistic choice, and to tailor your style to whatever reads better in context.

5 Likes

One other thought on this: would it be possible to emit members of the bottom-right cell under both the original and canonicalised manglings if the deployment target predates the canonicalisation change? That would trade some code size for an easier long-term deprecation path.

1 Like

That's potentially doable, since it's just functions, initializers, and subscripts we're talking about. @Slava_Pestov, what do you think?

Terms of Service

Privacy Policy

Cookie Policy