Pitch: Introduce custom attributes

I think you're on the right track here! Using types for attributes, and their initializers to build values, means that we can build on existing infrastructure for (e.g.) runtime inspection (use as? dynamic casting), any future static-reflection facilities (which of course have to work with user-defined types and values thereof), and simplifies the problem space.

I don't think that we should allow just any type to be used as an attribute, though: we should require that types be marked with a (built-in!) attribute to indicate that the types themselves can be used as custom attributes. There can be a small suite of such custom attributes, which will indicate what to do with those attributes. For example:

  • @staticAttribute would indicate that the attribute is used as a marker and could be used by to static tools (Sourcery, SwiftSyntax, etc.), but would not be emitted into the resulting binary. For example:

    // Sourcery module
    @staticAttribute(usage: [.struct, .class, .enum])
    struct AutoCoding { ... }
    
    // User module
    import Sourcery
    
    @AutoCoding
    class MyClass: NSObject { }
    
    @Sourcery.AutoCoding
    class MyOtherClass: NSObject { }
    
  • @runtimeAttribute could indicate that instances of the custom attribute type would be emitted into the binary and could be queried by some reflection framework. Example:

    @runtimeAttribute(usage: [.struct, .class, .enum])
    struct Versioned {
      let major: Int
      let minor: Int
    }
    
    @Versioned(major: 5, minor: 1)
    struct DataRecord { ... }
    
    // some query mechanism to get an [Any]
    for attribute in reflect(DataRecord.self).runtimeAttributes {
      if let version = attribute as? Versioned {
        // ...
      }
    }
    
  • @propertyDelegate. Over in the property delegates thread, we're talking about using attribute syntax to state that a particular property has a delegate, e.g.,

    @propertyDelegate
    struct Lazy<Value> { ... }
    
    @Lazy var x = 10
    

Custom attributes provide an overall framework to extensions without having to invent new syntax for everything, a problem that the property delegates pitch (among others) have. Rather, they stub out what a custom attribute looks like, which matches the new grammar production:

'@' type-identifier expr-paren?

Then, we can decide which built-in attributes (like the three I mention above) make a type suitable for use as an attribute.

I have a prototype implementation of custom attribute resolution in the compiler. It's part of the property delegates pull request, making @propertyDelegate types the only types that can be used for custom attributes. This suggests a way to stage custom attributes into the language: property delegates could be the first client (since there implementation is fairly far along), @staticAttribute or similar could flesh out how we express things like "which declarations can this attribute be placed on?" before we tackle something bigger such as runtime-queried attributes.

I'd suggest that we reserve all attribute names starting with a lowercase letter or an underscore for the compiler; that fits well with Swift's API design guidelines suggesting that type names start with an uppercase letter.

Thoughts?

Doug

54 Likes