[Pitch] Expanding #if to declaration modifiers as well as attributes

Hey folks!

I noticed recently that SE-0367 - Conditional Compilation for Attributes only applies to @attributes, and not to declaration modifiers like public. I regret not catching this during review period.

Wanted to gauge interest and see if we think it'd be a good idea to extend this to declaration modifiers as well as attributes, since they're modeled similarly in the compiler anyway and more-or-less parsed together.

This would enable e.g.

#if os(macOS)
internal
#else
@available(iOS 16.4, *)
public
#endif
struct OnlyPublicForiOS { ... }

I know of at least one project that needs to do something similar to this, and that project is currently using gyb to accomplish this. I'd love to obviate the need for gyb with a small syntactic extension.

I've only started noodling on the implementation here, but it seems straightforward-ish.

Do y'all think this is something that would be desirable? Or are there Reasons™ why I should abandon pursuing this?

13 Likes

+1.

Obviously we are quite far from the power of C preprocessor still. Imagine you'd need this prefix for 100 declarations, how easy you'd express it with define, was it available:

#if os(macOS)
#define pink internal
#else
#define pink @available(iOS 16.4, *) public
#endif

pink struct OnlyPublicForiOS_1 { ... }
pink struct OnlyPublicForiOS_2 { ... }
...
pink struct OnlyPublicForiOS_100 { ... }
1 Like

Heh, I proposed this in the original pitch, and was convinced that modifiers weren't necessary or couldn't work. I recommend scanning that thread to see if any real problems came up.

Doug

2 Likes

Ah! I see that now. I think modifiers would be a nice extension of this, but I definitely agree there's dubious utility for #if hasModifier. I would want to refactor attribute/modifier parsing to allow you to surround both modifiers and attributes in the same #if, and just separately produce a parse error that @attributes must precede modifiers.

There is (are?) preceding thread(s) that discuss solving exactly this problem by a totally different solution, namely declaring public availability; this would allow one to annotate this with an unconditional macOS @available attribute without having to bracket them with compile-time conditionals on separate lines.

Besides access modifiers, I’d imagine most other declaration modifiers would be weird to enable with such a conditional compilation feature—static or lazy for instance—and I’d be wary about making this possible without a careful consideration whether it might be actively undesirable.

For this reason, and because we have been unable to come up with a systematic or overarching vision as to what should be supported with conditional complication given that we actively do not want a C-style preprocessor, suggest to me that maybe the approach focused on availability is the more judiciously tailored one.

The other issue is this type's dependency on one particular framework that is not exposed to clients on one platform, but is exposed to clients on another. We have to conditionally mark this type internal on one platform, it's not sufficient to mark it public-but-unavailable.

1 Like

I’m not sure I understand. Why is the latter not sufficient?

Because it is a subclass of a class that comes from a dependency that is exposed to clients on certain platforms, but not exposed to clients on other platforms.

Sorry, I'm entirely too dense to understand here. It is perfectly fine for a public type (even actually available ones) to be a subclass of an internal type, no?

I have just been wondering what to do about this exact problem. I am preparing a swift-collections change to make it easier to build the code in a single-module configuration.

The package comes with an internal module that is shared across its many products, containing common helper functions that I didn't want to duplicate all over.

In the current configuration, these helpers are defined public; I avoid them polluting client namespaces by putting them in a separate module that they won't import.

Unfortunately, in the new build configuration, I have to put everything in a single module, which would leak these internals into clients. So I'd rather have them declared internal in this variant.

1 Like

It is fine, if the type is internal and part of this module. However, not a type that comes from a dependency that is not available to clients.

// Dependency (available on iOS/tvOS/watchOS, but not exposed to macOS clients)

open class DependencyClass { ... }

MyModule (Dependency is visible to iOS/tvOS/watchOS, but is an internal implementation detail on macOS)

#if os(macOS)
@_implementationOnly import Dependency
#else
import Dependency
#endif

@available(iOS 16.0, *)
@available(tvOS 16.0, *)
@available(watchOS 9.0, *)
#if os(macOS)
@available(macOS 13.0, *)
internal
#else
@available(macOS 13.0, unavailable)
open
#endif
class MyClass: DependencyClass 
1 Like

How does this work with context-sensitive keywords?

func test(`final`: Int) {
#if os(macOS)
  final
#endif
  class Inner {}
}

This code is stupid, but valid today with a different meaning.

2 Likes

Quick and dirty idea of taking C-preprocessor to the next level:

var pink: Meta.List<Meta.DeclarationModifier> {
	#if os(macOS)
	return Meta.List(internal)
	#else
	return Meta.List(@available(iOS 16.4, *), public)
	#endif
}

var nonisolated_and_pink: Meta.List<Meta.DeclarationModifier> {
    Meta.List(nonisolated, pink)
}

pink struct OnlyPublicForiOS { ... }

And then, if we can not only add new grammar rules, but extend/modify existing grammar rules... the sky is the limit.

Definitely wildly out of scope for this pitch :smile: