I think that 2 enhancements to declaration macros (either in this proposal, or as follow-on proposals) would enable type wrappers to be expressed entirely as declaration macros.
- The ability for declaration macros attached to types to attach declaration macros to properties inside that type + recursive macro expansion.
This is necessary because type wrappers turn all stored properties into computed properties that indirect access through a synthesized _storage property. This pitch already supports adding a new member called _storage using members: [.named("_storage")].
This pitch also already supports adding accessors by attaching a declaration macro to a stored property, so I think the cleanest way to allow a type declaration macro to turn stored properties into computed properties is to allow the macro to apply property declaration macros. Maybe a declaration macro could specify that it can annotate existing members with attributes:
@declaration(.attached, .annotatesMembers, addsMembers: [.named("_storage")]) macro makeAllPropertiesComputed
Now imagine I have another declaration macro that adds accessors to a single stored property:
@declaration(.attached, members: [.accessors]) macro makeComputed
The makeAllPropertiesComputed macro could simply attach @makeComputed to all stored properties:
@makeAllPropertiesComputed
struct S {
var x: Int
var y: Int
}
// expanded to
struct S {
private var _storage: SomeWrapperType
@makeComputed
var x: Int
@makeComputed
var y: Int
}
When the @makeComputed macros are expanded, that will add the get and set accessors that indirect access to _storage.
I believe opting out through @typeWrapperIgnored can also be implemented as a macro that does nothing in its own expansion, but it instructs the type wrapper macro to leave that particular stored property alone. This is actually pretty neat, because it allows type wrapper macros to choose whether to support opting out. If the macro does choose to support opting out, the library author can pick a domain-specific attribute name instead of @typeWrapperIgnored, which nobody is very fond of.
- A hook into definite initialization that allows declaration macros to customize initialization of stored properties.
Property wrappers and type wrappers both have special logic in definite initialization that rewrites assignment to a wrapped property to either an initialization of the backing wrapper type or a setter call depending on whether all of self is initialized. For example:
@propertyWrapper
struct Wrapper<T> {
var wrappedValue: T
init(wrappedValue: T) { ... }
}
struct Me {
@Wrapper var name: String
var age: Int
init() {
self.name = "Holly" // re-written to self._name = Wrapper(wrappedValue: "Holly")
self.age = 27
// all of 'self' is initialized now
self.name = "Holly Annesa" // this is a setter call through the computed 'name' property
}
}
The compiler calls these possibly-rewritten assignments "assign-by-wrapper". I think this operation might be generally useful for declaration macros.
My first idea for supporting this was to introduce a builtin expression macro that hooks into assign-by-wrapper. #assignByWrapper would take 3 arguments: a key-path, a value, and an initialization expression. A type wrapper macro could then re-write an initializer to change all property assignments to #assignByWrapper and provide a custom wrapper initialization. However, this doesn't really work for property-wrapper-like macros, because they cannot reach into an initializer and re-write assignments. Similarly, this doesn't work for property-wrapper-like macros attached to local variables.
My second thought is that customizing initialization could be a fundamental capability of declaration macros that are attached to properties. Such macros could opt into custom initialization and provide the re-written initialization expression through AttachedDeclarationExpansion:
public struct AttachedDeclarationExpansion {
/// The set of peer declarations introduced by this macro, which will be introduced alongside the use of the
/// macro.
public var peers: [DeclSyntax] = []
/// The set of member declarations introduced by this macro, which are nested inside
public var members: [DeclSyntax] = []
/// For a function, body for the function. If non-nil, this will replace any existing function body.
public var functionBody: CodeBlockSyntax? = nil
/// For a property, an expression for initializing that property given a value of the property type.
/// If non-nil, definite initialization will use assign-by-wrapper and re-write to initialization using
/// this expression.
public func initialization(from value: ExprSyntax) -> ExprSyntax?
public init(peers: [DeclSyntax] = [], members: [DeclSyntax] = [], functionBody: CodeBlockSyntax? = nil, initialization: ExprSyntax? = nil)
}
Aside from those two things, I think everything else in the current type wrapper pitch (and more!) is already covered by this design for declaration macros. Very cool!