Synthesized member types and type checking

Hi type checking experts,

I have a question about synthesized member types and type checking.

Currently in Swift, the only precedent for synthesizing member types in derived conformances is the CodingKeys private enum. However, as part of differentiable programming in Swift, we have implemented derived conformances for the Differentiable protocol, synthesizing TangentVector member structs.

Here's what it looks like:

struct Foo<T: Differentiable, U: Differentiable>: @memberwise Differentiable {
    var x: T
    var y: U

    // The compiler synthesizes:
    // struct TangentVector: Differentiable, AdditiveArithmetic {
    //     var x: T.TangentVector
    //     var y: U.TangentVector
    // }
}

Currently, extending synthesized types isn't supported:

foo.swift:12:15: error: 'TangentVector' is not a member type of 'Foo'
extension Foo.TangentVector {}
          ~~~ ^

I'm curious if supporting this is feasible and what changes are necessary.

My guess: I imagine member lookup, type witness resolution, etc. will somehow need knowledge of "member types that can be synthesized" and member type synthesis must triggered on first request.

Currently, derived conformances are triggered when type-checking protocol conformances, which apparently happens later than two use cases for us: type-checking attributes and resolving extensions (like the example above).

cc @Slava_Pestov

2 Likes

Extension binding happens very early, before any other type checking can take place, and we try to strictly limit how much other work extension binding can trigger. This is for two reasons:

  • when building a module with multiple files, each frontend job has to bind all extensions in all source files, in order for name lookup to work correctly.

  • if extension binding could trigger other work (eg, conformance checking) which in turn performs name lookup it would be undesirable since then we might attempt name lookup without all extensions having been bound yet.

However, it would be possible to defer binding extensions of synthesized types until those types are lazily forced into existence. This would be better than triggering all that work up front.

Thanks for the quick response!

I wonder if you have a clear sense of how to "defer binding extensions of synthesized types (only)"? That seems to require detecting whether a type is synthesizable, which I'm not sure how to implement. Any pointers would be appreciated.

The extension binding code already uses an iterative worklist to correctly handle extensions of nested types that are themselves defined in extensions. Once we stop making forward progress we diagnose all remaining types on the worklist as missing. Instead we could keep it around and try draining the worklist every time a new synthesized type comes into existence.

I see! Could you please point me to the extension binding logic/iterative worklist?
For reference, the error above occurs in diagnoseUnknownType in TypeCheckType.cpp:

foo.swift:12:15: error: 'TangentVector' is not a member type of 'Foo'
extension Foo.TangentVector {}
          ~~~ ^

You can find the code in TypeChecker.cpp, bindExtensions().

I think you'll need to hook the re-processing of the extension list into a lower level, perhaps in lib/AST/NameLookup.cpp (perhaps it could call back into the type checker via the LazyResolver interface).

1 Like