Allow conditional inclusion of elements in array/dictionary literals?

Was wondering if there would be any interest in being able to do the following in Swift:

let a = [
  1,
  #if something
  2,
  #endif
  3,
]

Would this sneak under the wire as bug fix or require a full blown proposal?

17 Likes

My personal opinion is that this would not require a full blown proposal, I'd suggest driving the discussion to develop evidence that no one is obviously concerned or opposed to this though.

9 Likes

Just as well really, I don’t think I have another proposal cycle in me for the moment. Putting it another way then does anybody have any objections or can they think of any potential pitfalls/refinements?

I'm generally in favor of expanding the usability # statements, as there are a few areas where they can't be used at the moment.

2 Likes

I can see this being particularly useful for SwiftPM package manifests. So +1 from me.

let package = Package(
  ...
  targets: [
      .target(...),
      .target(name: "Clib", sources: [
         "src/common.c",
         #if os(iOS) || os(macOS) // can't do this right now
         "src/platform/darwin.c",
         #else if os(linux)
         "src/platform/linux.c",
         #endif
         ...]),
    ],
...
)
12 Likes

Yes inline conditional compilation for array/dictionary literals would be useful. I can't think of any reason why this would be harmful.

Right now I do something like this in some of my Package.swift files:
// Common dependencies
var dependencies: [(name: Target.Dependency, package: Package.Dependency)] = [
    (name: "Number1", package: .package(url: "https://github.com/Dependency/Number1", from: "1.0.0"))
]

#if os(Linux)
dependencies.append(/* Linux-only dependencies */)
#else
dependencies.append(/* Darwin-only dependencies */)
#endif

let package = Package(
    dependencies: dependencies.map({ $0.package }),
    targets: [
        .target(...
            dependencies: dependencies.map({ $0.name })
        )
    ],
)   
2 Likes

Note: Using #if in Package.swift manifest file is not the direction that we want our users to take in the long term. It forces you to write the manifest in a non-declarative manner and it won't really work when cross-compling because manifest will most likely be evaluated using the host platform.

6 Likes

Along the same lines until 4.2 you couldn't have code like this:

switch someInt {
case 1:
    // do stuff
case 2:
    // do other stuff

// This used to give a compiler error
#if os(linux)
case 3:
    // do some linux stuff
#endif
}

But now that this works (much to my happiness and surprise) it makes a lot of sense that you could use #if directives in an array or dictionary literal.

I'm all for making #if more useful in the cases where it doesn't currently work.

4 Likes

Is there a preferred way to make a Package.swift cross-compilable then? Can we have a Package.swift for each platform? That seems like it would require a lot of boilerplate though..

This is absolutely something I would use. Often, I found myself using something like:

let a = [
    1,
    condition ? 2 : nil,
    3,
].flatMap { $0 }

Or worse:

var a = [1]
if condition { a += [2] }
a += [3]

I'm not a fan of the use of #if/#end for this purpose though. I think it's too wordy, and needing both a start and end token is tedious, imo. I would prefer an approach that just used some token before or after an element.

3 Likes

@AlexanderM I think you’re proposing a different feature. What’s being described here is compile-time conditionals within array literals (which I’m +1 on; it seems like it should already work), whereas you want runtime conditionals. I don’t think your request is unreasonable, but it is a different topic, and is significantly more difficult to design.

2 Likes

I don't want to speak for Ankit, but I think that the idea is for SwiftPM to allow declaratively define conditional settings (mock syntax):

let package = Package(
  ...
  targets: [
      .target(...),
      .target(name: "Clib", sources: [
         "src/common.c",
         "src/platform/darwin.c".condition(os: [.iOS, .macOS]),
         "src/platform/linux.c".condition(os: .linux)
       ]),
    ],
...
)
4 Likes

I wonder then, how will other conditional compilation blocks work then if cross-compiling uses the host system? It is VERY common for projects that support multiple OS’s to use #if os(Linux/macOS). It would seem that all of these projects could not be cross-compiled then. I think even swift-foundation may use some of these, but it has been a while since i looked at its code.

Edit: After re-reading @Aciid’s message it seems that only the package.swift will be compiled using the host system and the rest of the code will not. Can someone confirm or deny this assumption for me?

1 Like

Right, I was talking about the Package.swift manifest file, not the actual source code.

1 Like

Yeah I was wondering that - surely if you have a cross-compilation toolchain, the swift compiler can parse the manifest in the foreign platform's context. The compiler can still run on the host platform (obviously) - it's only the binary products which cannot be executed on that platform.

AFAIK, parsing a Package.swift file does not depend on executing any binary products, so it should be totally do-able.

EDIT: Oh, no, it does execute code - like if you do package.targets.append(...). So I guess it wouldn't work :/

No, the manifests are currently parsed by executing the actual code. We could compile them for different platforms but we'd need to use simulators or emulators to execute them with the current parsing mechanism. We would also need to compile the PackageDescription library for each supported platform. Instead of all that, one possibility is to introduce some declarative APIs to allow you to conditionalize the contents of the manifest file (see @hartbit's post above).

Anyway, I think OP's suggested enhancement is orthogonal and makes sense regardless of what we end up doing for the manifest file.

3 Likes

Why it's only for array/dictional literals? What's the rationale for that?
How about tuples, calling arguments, or other comma separated lists?
Why not #if ... #endif guarded decl attributes?

#if os(macOS)
@objc
#endif
func foobar() {...

or accessors

var value: Int {
  get { ... }
#if swift(>=5.0)
  modify { yield ... }
#else
  set { ... }
#endif
}

I think the community should discuss more deeply about the policy where #if can appear. Currently, conditional block must enclose whole decls/statements (or switch-cases).

8 Likes

There is no rationale. I bought this up because I can already think of a use case for container literals which is making particular tests conditional and I’m sure there are other use cases. Having already looked at the implementation, as it happens extending the scope to include most things that are lists would not be that difficult but I don’t think we should “go to town” just yet and try to implement every conceivable place conditionals could be used unless someone paid to do this is prepared to take it on. It’s just a matter of where to draw the line. A more ambitious scope can still be tackled later.

1 Like

I'm with John on this one. Collection literals are a place where it's very clear what the #if does, whereas allowing it in, say, arbitrary expression positions might be something the community or core team decides it doesn't want because it can lead to harder-to-understand code. Let's not worry about scope-creeping this proposal right away.

3 Likes

Okay. I agreed to move forward on this.

Now I have a question in the design. Sorry for bikeshedding but the following example is legal?

[
#if FLAG
  42
#endif
]

if so, how about:

[
#if FLAG
  42
#endif

#if !FLAG
  12
#endif
]

If you think of #if .. #endif as C like pre-processing directive, this expression ends up with

[
  42

]

or

[

  12
]

So it should be legal. But I think this is error prone for developers because requirement for , relies on the flags or platform conditions.

So, to keep things simple, my proposal here is: "trailing comma is mandatory in #if block in collection literal regardless of the position in the literal"
What do you think?

6 Likes