Hi all,
After working on hasFeature
a bit, I decided that I'd also like to solve a similar problem for attributes and modifiers, which we tend to add regularly and cause a bit of trouble when adopting new attributes/modifiers while supporting older compilers. Here's a pitch!
Introduction
Over time, Swift has introduced a number of new attributes and modifiers to communicate additional information in source code. Existing code can then be updated to take advantage of these new constructs to improve its behavior, providing more expressive capabilities, better compile-time checking, better performance, and so on.
However, adopting a new attribute or modifier in existing source code means that source code won't compile with an older compiler. Conditional compilation can be used to address this problem, but the result is verbose and unsatisfactory. For example, one could use #if
to check the compiler version to determine whether to use the @preconcurrency
attribute:
#if compiler(>=5.6)
@preconcurrency protocol P: Sendable {
func f()
func g()
}
#else
protocol P: Sendable {
func f()
func g()
}
#endif
This is unsatisfactory for at least two reasons. First, it's a lot of code duplication, because the entire protocol P
needs to be duplicated just to provide the attribute. Second, the Swift 5.6 compiler is the first to contain the @preconcurrency
attribute, but that is somewhat incidental and not self-documenting: the attribute could have been enabled by a compiler flag or partway through the development of Swift 5.6, making that check incorrect. Although these are small issues, they make adopting new attributes and modifiers in existing code harder than it needs to be.
Proposed solution
I propose three related changes to make it easier to adopt new attributes and modifiers in existing code:
- Allow
#if
checks to surround attributes and modifiers wherever they appear, eliminating the need to clone a declaration just to adopt a new attribute or modifier. - Add a conditional directive
hasAttribute(AttributeName)
that evalutestrue
when the compiler has support for the attribute with the nameAttributeName
in the current language mode. - Add a conditional directive
hasModifier(ModifierName)
that evalutestrue
when the compiler has support for the modifier with the nameModifierName
in the current language mode.
The first two of these can be combined to make the initial example less repetitive and more descriptive:
#if hasAttribute(preconcurrency)
@preconcurrency
#endif
protocol P: Sendable {
func f()
func g()
}
Similarly, hasModifier
can benefit code that relies on modifiers, e.g.,
#if hasModifier(distributed)
distributed
#endif
actor MyActor { // distributed when we can be, otherwise a local actor
// ...
}
Detailed design
The design of these features is relatively straightforward, but there are a few details to cover.
Grammar changes
The current production for an attribute list:
attributes â attribute attributes[opt]
will be augmented with an additional production for a conditional attribute:
attributes â conditional-compilation-attributes attributes[opt]
conditional-compilation-attributes â if-directive-attributes elseif-directive-attributes[opt] else-directive-attributes[opt] endif-directive
if-directive-attributes â if-directive compilation-condition attributes[opt]
elseif-directive-attributes â elseif-directive-attributes elseif-directive-attributes[opt]
elseif-directive-attributes â elseif-directive compilation-condition attributes[opt]
else-directive-attributes â else-directive attributes[opt]
i.e., within an attribute list one can have a conditional clause #if...#endif
that wraps another attribute list.
The same applies to declaration-modifiers
, adding a production for conditional compilation:
declaration-modifiers â conditional-compilation-modifiers declaration-modifiers[opt]
conditional-compilation-modifiers â if-directive-modifiers elseif-directive-modifiers[opt] else-directive-modifiers[opt] endif-directive
if-directive-modifiers â if-directive compilation-condition declaration-modifiers[opt]
elseif-directive-modifiers â elseif-directive-modifiers elseif-directive-modifiers[opt]
elseif-directive-modifiers â elseif-directive compilation-condition declaration-modifiers[opt]
else-directive-modifiers â else-directive declaration-modifiers[opt]
hasAttribute
only considers attributes that are part of the language
A number of Swift language features, including property wrappers, result builders, and global actors, all introduce forms of custom attributes. For example, a type MyWrapper
that has been marked with the @propertyWrapper
attribute (and meets the other requirements for a property wrapper type) can be used with the attribute syntax @MyWrapper
. While the built-in attribute that enables the feature will be recognized by hasAttribute
(e.g., hasAttribute(propertyWrapper)
will evaluate true
), the custom attribute will not (e.g., hasAttribute(MyWrapper)
will evaluate false
).
Parsing the conditionally-compiled branches not taken
Due to support for custom attributes, attributes have a very general grammar that should suffice for any new attributes we introduce into Swift:
attribute â @ attribute-name attribute-argument-clause[opt]
attribute-name â identifier
attribute-argument-clause â ( balanced-tokens[opt] )
Therefore, a conditionally-compiled branch based on #if hasAttribute(UnknownAttributeName)
can still be parsed by an existing compiler, even though it will not be applied to the declaration because it isn't understood:
#if hasAttribute(UnknownAttributeName)
@UnknownAttributeName(something we do not understand) // okay, we parse this but don't reject it
#endif
func f()
Modifiers are different, because there is neither a notion of custom modifiers nor is there an identifying token like @
. Therefore, we treat #if hasModifier(UnknownModifierName)
more like the #if compiler(>=7.2)
directive, where the contents of the branch not taken is not processed at all:
#if hasModifier(UnknownModifierName)
it is okay to have total gibberish here
#endif
func g()
What do folks think?
Doug