Pitch: Static Custom Attributes (Round #2)

Hello community,

After the feedback on the first pitch of Custom Attributes, @Vinicius_Vendramini, @Douglas_Gregor and me joined forces to come up with this second iteration of the proposal that concentrates on Static Custom Attributes using structs.

We welcome any further feedback before moving on to implementation.

The following link will contain the up-to-date proposal:

11 Likes

Neat!

@staticAttribute(declarations: [.struct, .class], scopes: [.module])
struct AutoCoding {}

...

/// Represents a declaration scope the attribute can be annotated to.
public enum Scope: CaseIterable {
        
  /// Global scope declaration.
  case global

  /// Local scope declaration (within a function, method, etc...)
  case local

  /// Static member of a type.
  case `static`
 
  /// Instance member of a type.
  case instance
}

The example earlier used .module as a scope. Is that a typo?

@staticAttribute(declarations: [.variable], scopes: [.instance])
public struct IBOutlet {}

Would this conflict with a property delegate declaration? Can static attributes start as just attributes and later become delegates while preserving source compatibility? Same, preserving ABI compatibility?

Awesome to see this moving forward!

I’m unsure of the choice to make staticAttribute itself a user-defined staticAttribute in the standard library. This is cute, but it violates the principle that lowercase attributes are reserved for the compiler and that types use uppercase names. That said, I suppose it’s ok to expand lowercase attribute names to attributes defined in the standard library that are implemented with compiler magic.

Is there a reason you chose to exclude enums from being attributes? I would like to see them supported, both with initializer syntax as well as @MyEnum.myCase(myAssociatedValue) syntax. Some attributes will work best as enums and it would be unfortunate to have to wrap them in a struct just to satisfy the language.

The list of declarations is missing associated values. I use Sourcery annotations on individual associated values. This is crucial to support coding policies for associated values.

It also looks like accessor attributes can be applied to both getters and setters. Should get and set accessors be represented differently? Should we have propertyGetAccessor, propertySetAccessor, subscriptGetAccessor and subscriptSetAccessor?

I’m a bit confused by the omission of nested types from the list of scopes. Are these (was well as nested typealias considered to be in static scope? I think this is the intent based on the examples but it is not totally obvious from the text of the proposal.

I don’t understand this example at all:

Protocol

@GlobalProtocolAttribute
class GlobalProtocol {}

func foo() {
    @LocalProtocolAttribute
    class LocalProtocol {}
}

Should this be protocol GlobalProtocol {} with the local example removed (since protocols can’t be declared locally)?

1 Like

Yes it is. I fixed the proposal and it consistently mentions global now.

As property delegates declare custom attributes (just not Static Custom Attributes), they share the same namespace. That's the only way I can see them conflicting. I see no source compatibility issue migrating from a static custom attribute (which only lives at compile time) to a property delegate. Again, as Static Custom Attributes don't exist past compile time, they have no ABI impact. @Douglas_Gregor, does this answer sound correct to you?

I just had a quick glance at the proposal and I‘m a little confused. I like the idea that staticAttribute will only go on types, this aligns well with propertyDelegate. What I don‘t like are the examples of compiler attributes that are now part of stdlib for two reasons. This makes them eventually ABI locked which would be a huge pain in the butt, and second that these types are allowed to ignore the general rule of thumb that all types start with a capital letter.

I also view this recursive declaration as confusing:

@staticAttribute(declarations: [.struct])
public struct staticAttribute

You also need to fix quite a few examples that start with @Static* as I feel that there are a few copy paste mistakes.

1 Like

In the proposal, we gave the reasoning:

They are not supported on enums as those don't have natural initializers that can be used for the creation pattern.

The @MyEnum.myCase(myAssociatedValue) does look very different from the pattern that structs allow us.

You mean enum associated values?

I'm not against it, but I'm not entirely sure it's worth being this granular.

Yes, the proposal suggests that nested types have a static scope. Would have imagined something different?

That was a mistake in the proposal text. I fixed it. Thanks!

I'm not sure I understand. Static Custom Attributes can annotate more than just type declarations.

Static Custom Attributes don't have any ABI: they only exist at compile time.

Concerning the name casing, we have many attributes defined in the compiler which start with a lowercase letter. To keep source compatibility, we have to keep those with a lowercase name when transforming them into static custom attributes in the Standard Library. But all user-defined attributes MUST start with a upper-case letter.

I'm fairly sure there are no mistakes left. Which ones were you thinking about?

I meant that @staticAttribute itself goes on types (in your proposal it's restricted to a struct for now), not an actual type that can act as a static attribute.

One question about the restriction:

  • Isn't @propertyDelegate already like @staticAttribute(/* with some predefined constraints */)?
    Property delegate types definitely will have ABI if they cross module boundary.

How so? If you want your attributes be available across module boundary I'm pretty sure these will have ABI and if they would live in the stdlib they will be ABI locked. If in Swift 6 we had and static attribute @something as public struct something, which would have compiler support for its behavior and if this behavior needs a fix in Swift 7, then this fix won't be backwards compatible. (If I'm saying nonsense please feel free to correct me.)

I see it similarly to KeyPath's behavior. Right now if you call value[keyPath: ...] = newValue it will call the getter of value before it accesses the setter which is a bug (see this thread). If this bug is fixed in the next Swift release it will still remain on an OS that is ABI locked, which means that you never can just write value[keyPath: ...] = newValue if you targeting that OS where KeyPath has this bug. The compiler attributes should remain as compiler attributes and not become stdlib citizens.

I'm fine for user-attributes but why would we want to convert (some) compiler attributes to be like custom static attributes? This does not make sense to me. There are some attributes that are baked into the language because there was previously no other way and Apple frameworks required them, but these attributes are already written in upper camel case fashion (@IBOutlet). I think we should only move these attributes into their dedicated modules, but leave compiler attributes as they are now. I don't see any gain form @Swift.nonobjc other than breaking the rules custom attributes want to establish with this proposal.

Also one thing about IBOutlet as a custom attribute. You said that these won't have any ABI and would only exist at compile time. In that particular case I highly doubt that fact because I think @IBoutlet should be a property delegate which will be used as a baking storage for outlets and probably behave like DelayedMutable. This would not work if it was just annotated with @staticAttribute(declarations: [.variable], scopes: [.instance]).

Sorry this was my mistake, I got distracted with the assumption that anything annotated with @Static* must have a static keyword applied, which is not true for nested types. No mistakes there.


Don't get me wrong, I like the overall direction, but I'm not convinced by the current state of the proposal.

I'm pretty excited about this proposal.

@staticAttribute(declarations: [.class])
public struct objcMembers {}

@staticAttribute(declarations: [.class], scopes: [.global])
public struct NSApplicationMain {}

@staticAttribute(declarations: [.variable, .function], scopes: [.instance])
public struct NSManaged {}

@staticAttribute(declarations: [.function], scopes: [.instance])
public struct IBAction {}

@staticAttribute(declarations: [.class, .extension], scopes: [.global])
public struct IBDesignable {}

@staticAttribute(declarations: [.variable], scopes: [.instance])
public struct IBInspectable {}

@staticAttribute(declarations: [.variable], scopes: [.instance])
public struct IBOutlet {}

@staticAttribute(declarations: [.class], scopes: [.global])
public struct UIApplicationMain {}

@staticAttribute(declarations: [.variable], scopes: [.instance])
public struct GKInspectable {}

Off the top of my head, I believe all of these imply that the declaration they're applied to is @objc. Would it make sense to add an impliesObjC: Bool = false parameter on staticAttribute.init(…)?

Also, some of these further validate the declarations they're applied to; for instance, @IBOutlets must be Optional and @IBActions must return Void and have certain parameter signatures. Do we still plan to implement this validation with bespoke code in the compiler, or do we think we can generalize it somehow?

(Maybe you could use declarations inside the struct to control the validation rules—"the declaration you apply this to must have the same shape as one of the members of this type".)

1 Like

One other thing I forgot to ask. Is there any valid case for this today?

func foo(@ParameterAttribute parameter: Int) {}

And what about attributes on argument type? I don't see any case for that in the proposal.

func bar(_: @escaping () -> Void)

I think these are all examples of how current compiler-implemented attributes could have been written using this feature (showing the generality of the approach, justifying all the different options, etc) and not a proposal to change the implementation. That would be a decision for the Swift developers and not a matter for Swift evolution, at least where it is possible without breaking source or ABI compatibility.

That's where the proposals differ: property delegates have an ABI impact because they generate backing storage. Static custom attributes don't exist at all post-compilation. They exist only to be scanned using compiler tools like SwiftSyntax and SourceKit. What you're thinking of is what we call @runtimeAttribute which will be stored in the module metadata and be accessible at runtime. That will come in a future proposal.

@Vinicius_Vendramini @Douglas_Gregor Perhaps we should rename to @compileTimeAttribute

As the attributes in this proposal only exist at compile time, you're only dependent on the version of the compiler you use to "scan" them.

It's always a win to give the language capabilities which allows simplifying the compiler by moving functionality into the Standard Library. For exemple, it's a goal to one day be able to implement Equatable, Hashable, and perhaps Codable code synthesis in a future hygenic macro system.

For what it's worth, attributes like @objc and @available can't be migrated to static attributes in the Standard Library because their options can't be represented by valid Swift.

You're right, @IBOutlet might make more sense as a Property Delegate.

1 Like

That seems like a very specific property to have available on all custom attributes, even if it has a default value. Perhaps the compiler can just know which attributes imply @objc.

This design space seems interesting. Could you elaborate or provide examples?

Okay thank you for clarifying these things to me. One follow up question though. How can I expose a type with my library and also make it act like a staticProperty? From what I could observe in your reply the current state is that types are kind of exposed but they cannot be used in any ways other than as an attribute.

I find this reasoning specious. It’s going to lead to @MyWrapperStruct(.myCase(myAssociatedValue)). This is clunky for users and really annoying for implementers of an attribute that is best expressed as an enum. Yes, initializing enum values usually looks a bit different than initializing struct values. This is not a problem.

I think there should be much stronger motivation for banning enums if you really think it’s necessary.

Yes. For example:

enum Foo {
    case bar(/* sourcery: codingPolicy */ Int)
}
1 Like

The reason I called these declarations in particular is that it looked to me like you had pretty thoroughly covered everything else so it looks like an oversight. IMO there probably shouldn’t be an arbitrary line of granularity. Why not just support all declarations we have in Swift?

How about creating one initializer for each case? It’s nicer to the users, even if worse for the implementer.

@MyWrapperStruct(myCase: myAssociatedValue)

I sympathize with wanting the proposal to support enums too, and I actually started down that path. But I was convinced otherwise to simplify the creation syntax of attributes and to have them map more closely to current compiler attributes.

Concerning associated values, I was thinking of having them be supported by the .parameter declaration. What do you think?

We mapped to the declaration kinds in the compiler, and the compiler does not see those accessors as different ‘kinds’, just accessors with different names. Can you confirm that @Douglas_Gregor ?

One question regarding both custom attributes and property delegates.

Is it permitted to initialize them using the explicit call to init(...)?

@MyAttribe.init(...)

I think if this is possible you should be able to create static members on the attribute type which will return the same type. That however implies that we should allow enums from the beginning.

@staticAttrinbute(...)
struct MyType {
  static func foo(value: Int) -> MyType
}

// Usage:
@MyType.foo(value: 42)

I don't think this is a good direction. It is boilerplate for implementers and obfuscates what is actually going on for users.

I don't think this simplifies anything. It only obfuscates. I am not suggesting you require the use of enums. Most attributes will probably be structs but it is unnecessary and will inevitably be frustrating to limit attributes to structs.

One of the enormous benefits of using types for attributes is that it is immediately straightforward to everyone. If I have an attribute that is naturally modeled as an enum please let me make that clear to users of the attribute (and avoid a bunch of boilerplate in my own code).

I don't think this makes sense at all. I don't want my serialization attributes applied to parameters. Associated values are represented as parameters in the case initializers but they are fundamentally different.

4 Likes

Things that only live at compile time are relevant to source compatibility. If a library declared Foo as a public @staticAttribute for its users to annotate their code with, then later want to also make Foo a property delegate, how would they do that? Can they annotate it with both, would they need to declare a source-break, or what? Using one type as both sounds a little awkward to me at this point, but I'm just wondering what the interaction is.

2 Likes