Idea: Support #if os(Darwin) as shorthand for os(iOS) || os(OSX) || os(watchOS) || os(tvOS)

I'd like to see #if support os(Darwin) as shorthand for any Apple platform. This is a lot shorter to type, and it's also future-compatible if Apple ever releases another Darwin-based platform. If os(Darwin) evaluates to true, then import Darwin should always work, and presumably import Foundation as well.

Without this, I think people are going to be tempted to write if !os(Linux) instead of writing out all 4 Apple platforms, and this is unfortunate because it makes the assumption that Linux is the only non-Apple platform, and that's simply not true.

1 Like

I agree that we need something here, but I really like the property we have today that all of the "os" options are mutually exclusive. (Compare: TARGET_OS_IPHONE and TARGET_OS_IOS in <TargetConditionals.h>.) I'd be happy to invent a new predicate for this, though.

Other considerations:
- Different Linux distros?
- Common ground across BSDs?
- Unixy? (vs. Windows)

And for other platform conditions:
- Architecture subtypes?
- Architecture families? ("32-bit" is a good one.)

Jordan

Agreed. I’ve used #if _runtime(_ObjC) for this, and it’s just not the right thing to do…

Guillaume Lessard

Offhand it seems a decent solution would be to have some way for users to define e.g. an “os group”

#define_os_group(uikit) iOS tvOS
#define_os_group(apple) iOS tvOS watchOS OSX

…(but with a better name).

It’s not clear offhand where such `#define*` statements would naturally reside.

As a bit of bike-shedding, perhaps `platform` instead of `os`, too.

I agree that we need something here, but I really like the property we have today that all of the "os" options are mutually exclusive. (Compare: TARGET_OS_IPHONE and TARGET_OS_IOS in <TargetConditionals.h>.) I'd be happy to invent a new predicate for this, though.

Yeah, I agree this should be a distinct predicate.

Other considerations:
- Different Linux distros?
- Common ground across BSDs?
- Unixy? (vs. Windows)

And for other platform conditions:
- Architecture subtypes?
- Architecture families? ("32-bit" is a good one.)

For appley platforms, "Mac" vs "simulator" vs "device" also comes up as a common categorization.

-Joe

I've had to reverse all my `#if os`'s because nearly all the code on iOS/watchOS/tvOS is shared, so all my code that *used* to be `#if os(iOS)` is now based on OS X because of that.

I'd very much welcome a larger vocabulary, not to mention picking up debug/release (pretty please)

-- E

Adding in the different forms of compile-time conditionals our code uses in Objective-C, the following tests would be useful:

• Is the target an Apple platform (iOS, OS X, tvOS, watchOS) or not? (Answers the question "can I import Foundation"?)

• Is the target a "common UIKit platform" (compiles on both iOS and tvOS) or not? (Answers the question "can I import UIKit"?)

• Is the target a specific platform (iOS, OS X, tvOS, watchOS, Linux, etc.)?

• Is the target in Debug or Release mode?

• Is the target a simulator environment or a physical device?

• Is the target built for running tests?

All of our cases are covered by the questions above, but as Jordan indicates, other people may also need to know more about the processor architecture.

SwiftPM passes DEBUG and NDEBUG. Ugly, but the standard.

• Is the target an Apple platform (iOS, OS X, tvOS, watchOS) or not? (Answers the question "can I import Foundation"?)

Well, Corelibs Foundation will muddy those waters.

• Is the target a "common UIKit platform" (compiles on both iOS and tvOS) or not? (Answers the question "can I import UIKit"?)

Maybe test for the presence of a particular module?

  if supports(Foundation)
  if supports(UIKit)

Or allow conditional importing of a module?

  if import UIKit
    // Only compiled if UIKit imported; all UIKit APIs usable here
  #elseif import AppKit
    // Only compiled if AppKit imported; all AppKit APIs usable here
  #endif
  // No AppKit or UIKit APIs usable here

I’ve mentioned this before, but often checking for simulator is really a just a way of dealing with an include that won’t work in the simulator. The ability to exclude code based on the presence of a module in the SDK would be most welcome. Metal is my current sticky point with Swift. It’s in the device SDK but not the sim.

SwiftPM passes DEBUG and NDEBUG. Ugly, but the standard.

I don't think it's the package manager's place to do that, since the compiler's already aware of the current build mode. IMO all the standard if flags should ideally be centralized in the language so there isn't a forever-creeping set of conventional -D flags like there are in C land. (Tradition or not, negative flags like NDEBUG should be put out to pasture—we've let go of other, less terrible C traditions.)

We specifically avoided making debug/release an if condition because we considered if to be the wrong point at which to start conditionalizing code generation for assertions. Though the final executable image's behavior is unavoidably dependent on whether asserts are enabled, we didn't want the SIL for inlineable code to be, since that would mean libraries with inlineable code would need to ship three times the amount of serialized SIL to support the right behavior in -Onone, -O, and -Ounchecked builds. Instead, the standard library has some hidden helper functions, `_isDebugAssertConfiguration`, `_isReleaseAssertConfiguration`, and `_isFastAssertConfiguration`, which are guaranteed to be constant-folded away before final code generation. This means we can compile a function like this down to SIL once:

func assert(@autoclosure _ condition: () -> Bool) {
  if _isDebugAssertConfiguration() && !condition() {
    fatalError()
  }
}

for all of the possible assert behaviors.

-Joe

• Is the target a "common UIKit platform" (compiles on both iOS and tvOS) or not? (Answers the question "can I import UIKit"?)

Maybe test for the presence of a particular module?

  if supports(Foundation)
  if supports(UIKit)

Or allow conditional importing of a module?

  if import UIKit
    // Only compiled if UIKit imported; all UIKit APIs usable here
  #elseif import AppKit
    // Only compiled if AppKit imported; all AppKit APIs usable here
  #endif
  // No AppKit or UIKit APIs usable here

Being able to test for the importability of a given module/framework at runtime would be extremely helpful.

We use several frameworks that are only available in a subset of the platforms we support, and on only certain OS versions.

To work around this problem now, we dynamically load frameworks from Obj-C only when we're running on an OS version we know is supported by the framework(s) in question.

We can't dynamically load them from Swift because if they're included in an import, the runtime tries to load it right away, leading to a crash on any unsupported platform.

The only way to selectively load dynamic frameworks at runtime is to do it via Obj-C. Some sort of check like the ones you propose should let us avoid this.

SwiftPM passes DEBUG and NDEBUG. Ugly, but the standard.

I don't think it's the package manager's place to do that, since the compiler's already aware of the current build mode.

It can be reverted.

IMO all the standard if flags should ideally be centralized in the language so there isn't a forever-creeping set of conventional -D flags like there are in C land. (Tradition or not, negative flags like NDEBUG should be put out to pasture—we've let go of other, less terrible C traditions.)

This one seemed different, since SwiftPM has explicit debug and release modes for compilation.

We specifically avoided making debug/release an if condition because we considered if to be the wrong point at which to start conditionalizing code generation for assertions. Though the final executable image's behavior is unavoidably dependent on whether asserts are enabled, we didn't want the SIL for inlineable code to be, since that would mean libraries with inlineable code would need to ship three times the amount of serialized SIL to support the right behavior in -Onone, -O, and -Ounchecked builds. Instead, the standard library has some hidden helper functions, `_isDebugAssertConfiguration`, `_isReleaseAssertConfiguration`, and `_isFastAssertConfiguration`, which are guaranteed to be constant-folded away before final code generation. This means we can compile a function like this down to SIL once:

func assert(@autoclosure _ condition: () -> Bool) {
  if _isDebugAssertConfiguration() && !condition() {
    fatalError()
  }
}

for all of the possible assert behaviors.

Makes sense, I can revert it. But this is something people will be asking for pretty regularly, so I’ll forward their requests to your email ;)

SwiftPM passes DEBUG and NDEBUG. Ugly, but the standard.

I don't think it's the package manager's place to do that, since the compiler's already aware of the current build mode. IMO all the standard if flags should ideally be centralized in the language so there isn't a forever-creeping set of conventional -D flags like there are in C land. (Tradition or not, negative flags like NDEBUG should be put out to pasture—we've let go of other, less terrible C traditions.)

Agreed!

We specifically avoided making debug/release an if condition because we considered if to be the wrong point at which to start conditionalizing code generation for assertions.

There are many reasons besides assertions where one might want to know if code is running in release mode or debug mode.

For example, we use CleanroomLogger <https://github.com/emaloney/CleanroomLogger&gt; as our in-app logging API, and we configure the logger differently in debug mode:

- We perform more verbose logging in debug mode; we don't want to ship with this since verbose logging can affect the performance of the app.

- We configure the logger to run synchronously, so that when we hit a breakpoint, the console output is up-to-date. In production code, we set up CleanroomLogger to log asynchronously, which is another way to optimize performance.

Currently, the concept of debug mode only lives at the application level, and we pass the -DDEBUG flag to indicate a debug build. Since the compiler knows whether we're building for debug or not, it would be great to not have to pass this in.

I can understand this would be tricky for binary-only frameworks, but in our case, everything we touch is open-source; we integrate as git submodules with embedded Xcode projects & compile everything ourselves. I suspect this is pretty common with pure-Swift projects. Since there isn't ABI compatibility yet, people probably aren't releasing binary-only packages with Swift.

They're welcome to start evolution threads! I'll add that, in our original grand vision for compiler flags before the reality of shipping got in the way, it would have been possible to use flags both to conditionalize compilation, similar to #if in C-family languages, but also as expressions, for conditionalization during code generation, which would allow for either:

#if config(Debug)
func debugTrap() { fatalError() }
#else
func debugTrap() { /*do nothing*/ }
#endif

or:

func assert(...) {
  if #config(Debug) {
    fatalError()
  }
}

and we would have encouraged the latter where possible, since it would allow for more sharing of build products across build configurations. #if-style conditionalization is unavoidable if you need to conditionally import frameworks that are only available on specific platforms, but the 'if #' approach allows for more efficient cross-platform, cross-configuration builds and lets the compiler more eagerly check semantics without fully depending on execution tests on every target platform.

-Joe