Pitch: Introduce custom attributes

I really like the look of this... especially as the init signature directly mirrors the attribute declaration structure... makes the variations of attributes very discoverable, which is a real problem with them right now.

In your last example, usage became a single enum value, as opposed to a list or option set, was that intentional?

Yeah, maybe an option set would make more sense here. I wanted to suggest deriving the name from the struct name and the usage from the protocol conformances, like so:

public protocol Attribute { }
public protocol VariableAttribute: Attribute { }
public protocol FunctionAttribute: Attribute { }
// ...

// Example
public struct Coding: VariableAttribute, FunctionAttribute {
    public let key: String? = nil
    public let ignored: Bool = false
}

struct Person: Decodable {
    @coding(key: "first_name")
    let firstName: String

    @coding(key: "last_name")
    let lastName: String

    @coding(ignored: true)
    let age: Int
}

(It gets a lot cleaner now that we have memberwise inits with default values from SE-242.)

1 Like

Um, for non-runtime attributes, how are they supposed to be used? How are they going to be read, let alone do action on them? Wouldn't we need constexpr support for the attribute values and for the functions that will exploit them? Or are they only for external tools, like "swiftlint," acting as an advanced substitute for comment-embedded directives?

I would think we would still need compiler-level attributes like the open/public/etc. family, since they affect the layout of items in the object code and making them work like custom attributes would require the compiler to be user-mutable.

Yeah, they would be exposed by libSyntax for use by external tools like SwiftLint, Sourcery, etc. Static and dynamic metaprogramming via compiler evaluable and reflection respectively can come later.

Nobody is talking about changing how the compiler-implemented attributes and declaration modifiers work.

1 Like

Of course, Youā€™re right. I wrote the code from my phone while not quite awake :-)

I thought about that too. But I ended up dropping the idea because the empty protocols felt odd to me.

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

Doug, I love this. Excellent way to pull together and unify these ideas into a common framework. +1!

12 Likes

I love this idea. @Vinicius_Vendramini Do you want to take your proposal in this direction? Do you want help with it? We could start small with @staticAttribute. @Douglas_Gregor Could we count on some help from you for the implementation?

1 Like

I also love this direction! I have some questions about access control on storage of property delegates but will share those on the appropriate thread. Attributes with uppercase names will take some getting used to but it makes sense to just use the name of the type itself and I really like the idea that we will be able to easily distinguish user-defined attributes from built in attributes.

@Douglas_Gregor, is it correct that the rules for attribute initialization will depend upon which kind of user-defined attribute is in use? Specifically, I am thinking about how property delegates can be directly initialized using runtime data, whereas most attributes are used with only static information. Another example is unparameterized attribute usage - for @staticAttribute it looks like the idea is that a default initializer is used whereas with @propertyDelegate an initial value initializer would be used.

Like @hartbit, I would be happy to help with the @staticAttribute proposal.

2 Likes

Yeah, sure. The property delegates PR has the basics for custom attributes, although the history there has become quite muddled. @staticAttribute needs some design particularly around the question of how we specify what kinds of declarations it can apply to.

Doug

Yes. For @propertyDelegate, it's part of the initialization of the backing storage property. For @staticAttribute, the initializer wouldn't be part of the binary so it wouldn't execute at run time, but it would be type-checked. For a @runtimeAttribute, the initializer would presumably run when someone queries the attributes for that declaration at run time.

Doug

Right.

Doug

I like this, but:

  1. I think we also need some kind of property delegate + runtime attribute combination feature where you have both per-instance and per-type data, with the accessor implementations able to access both. For instance, Lazy could be implemented with a static part to hold the initial value closure plus an instance part to hold the value or nil, giving it a similar memory representation and behavior to the lazy keyword today.

  2. Once we get up to four variants, it starts feeling like we should unify them into fewer features. For example, we might decide that there should always be a "runtime attribute"; a "static attribute" would be approximated by an empty (probably frozen) struct, and a "property delegate" would be an optional nested type inside the attribute that would be created on each instance. I'm not totally sure how to give the property delegate access to the attributeā€”pass it to a subscript used to get the value? Move the accessors to the attribute and pass self + key path to delegate?

What do you mean "the initializer wouldn't be part of the binary"? Surely the attribute type would still be compiled just like any other type, wouldn't it? It would probably be stripped out as dead code but @staticAttribute wouldn't prevent it from being used and therefore included in the binary.

Aside from runtime, I would expect @staticAttribute to interact well with @compilerEvaluable. For example, if we eventually have static reflection the attribute value would need to be initialized in order to be exposed to static metaprograms.

I mean that uses of the type as a custom attribute wouldn't not emit any code into the binary. The type is just a normal type. This ensures that using types for custom static attributes is close to free (you pay only a per-type cost, not per-use costs).

Doug

2 Likes

Gotcha, that makes sense. Do you anticipate static attributes eventually supporting the kind of interaction with @compilerEvaluable I described?

To be clear, the property delegates proposal has never supported the above. We would need some kind of extension to the ad hoc protocol for @propertyDelegate types to allow them to state what the static data looks like and feed it into accesses.

I feel pretty strongly that we don't want to unify "runtime attribute" and "static attribute". A runtime attribute implies the ability to discover the presence of the attribute at runtime. If we want to ensure that we don't pay a per-use cost for an attribute that's there only to support tools, we'd need to have our "truly static" attribute types have no instances. We can do that with an enum that has no cases (like Never), e.g.,

@customAttribute
enum TrulyStatic {
  // no cases!
}

However, that means that we can't use the @TrulyStatic(configParameter: "hi") syntactic form for static attributes. If we're going to have a future where some kind of macro or meta programming system can look at static attributes, we're really going to want the ability to have state in these attributes.

Doug

I hope so, yes. I'm hesitant to fuel too much excitement over @compilerEvaluable because it's a hard problem and I want to see it succeed in a few more infrastructure-ish places in the compiler before predicting what it'll be capable of.

Doug

You can write an initializer for an enum, even an uninhabited enum (as long as it fatalError()s), so we could still support that. Just have it check whether the attribute's type is uninhabited and, if it is, omit uses from the binary entirely.

(Perhaps as a separate future feature, we could allow you to omit the bodies of AbstractFunctionDecls with uninhabited inputs. The compiler could synthesize a fatalError()ing body or, in non-initializer cases, emit no valid body at all.)

Sure, that makes sense. Iā€™m happy to know this is a direction weā€™re working towards even if we donā€™t know exactly how it will end up yet.