SwiftIfConfig library is replacing the compiler's #if handling

Hi all,

Recently, I've been on a quest to reimplement how the Swift compiler handles #if directives. The first major piece of this is the SwiftIfConfig library, which is part of the swift-syntax package that I introduced a few months ago in this forum thread.

Since then, I've managed to replace all of the #if handling in the compiler with the new library:

We're in the home stretch. The next major task is to remove IfConfigDecl from the C++ semantic AST. We no longer want to model #ifs at all in the C++ semantic AST, because doing so is what prevents us from being able to expand #if usage to other parts of the grammar easily. It's also a nice simplification for the compiler, allowing us to eliminate log for dealing with inactive #if regions in a lot of places.

So, I went ahead and turned off the generation of IfConfigDecl in the compiler, and uncovered the next set of issues based on the compiler's test suite. Here they are!

Suppressing warnings based on what's in inactive code

Consider this code:

func f() {
  let x = 10
#if DEBUG
  print(x)
#endif
}

The variable x is never used. Without the #if DEBUG block, the compiler would emit a warning "initialization of immutable value 'x' was never used". However, the compiler looks into the inactive #if DEBUG block and sees that x was mentioned, so it suppresses the warning.

There's a similar thing for try and throw occurrences within an inactive #if:

func f() {
  do {
#if DEBUG
    try g()
#endif
  } catch {
    // ... dead code ...
  }
}

Normally, the compiler would warn about an unreachable catch, but that warning is suppressed when there's a try or throw within an inactive #if.

I think we want to preserve this behavior. The way I'm planning to do that is to have the warning go look for any inactive #if clauses in the current scope (using the "configured regions" API mentioned earlier) and look for any tokens that refer to the variable name (for the unused variable warnings) or are try/throw (for the thrown-error one).

Anachronistic SourceKit queries

There are a number of SourceKit queries that do syntactic things based on the C++ IfConfigDecl. These have effectively all been replaced with swift-syntax (and swift-format), which does a better job with unparsed code (e.g., code behind an inactive #if compiler(>=6.1)) and is generally better for IDE usage. These are the tests from the Swift test suite that start failing because they test handling of inactive #if regions based on IfConfigDecl:

Swift(macosx-arm64) :: SourceKit/SyntaxMapData/syntaxmap.swift
Swift(macosx-arm64) :: SourceKit/CodeExpand/code-expand.swift
Swift(macosx-arm64) :: SourceKit/CodeFormat/indent-pound-if.swift
Swift(macosx-arm64) :: SourceKit/DocumentStructure/structure.swift
Swift(macosx-arm64) :: IDE/coloring_configs.swift
Swift(macosx-arm64) :: IDE/coloring_unclosed_hash_if.swift

The main SourceKit clients no longer use these SourceKit queries, so I propose to break their current handling of #if in the upcoming Swift release (the next one to branch from main) and deprecate the queries, then remove the queries entirely in the following Swift release.

Remove the swift-indent frontend mode

Until I did this experiment, I hadn't realized that it's possible to ask the Swift compiler frontend to indent the given source file. It uses the C++ semantic AST and a bunch of heuristics, and predates swift-syntax and swift-format by years. We don't actually ship this as a tool anywhere, so I think we can outright remove it now without anyone being affected. If that's not possible, we can do the same staged deprecated/removal I'm proposing for SourceKit.

Given how far we've come with SwiftIfConfig, we're really close to getting this part of the compiler onto a nice, new swift-syntax library and simplifying the main compiler code base.

Doug

46 Likes

I think that this can be done pretty quickly. There is very little value from this tool today, especially with swift-format being available. The implementation is also quite limited as it was never really given the needed attention. I would need to check if we still include it in the Windows toolchain, but would be more than happy to see it removed.

1 Like

It's worth to mention that this is currently broken for postfix #if expressions. .e.g

func f() -> Foo {
  let x = 10 // warning: ... 'x' was never used...
  return foo
     #if FLAG
     .modifier(x)
     #endif
}

This is because AST doesn't preserve the inactive regions as IfConfigDecl.

This should fix the current postfix #if issue, and can be future proof for all the potential #if support expansions (e.g. array/dictionary literals) :raised_hands:

2 Likes

Great point!

Yes. Improvements into where #if can be used have effectively been blocked on the C++ IfConfigDecl design.

Doug

My only interaction with IfConfigDecl has been running into situations where it’s special cased. I’m very happy to hear it’s going away!

1 Like

I took a stab at re-implementing this functionality in terms of swift-syntax and SwiftIfConfig. The pull request is here and, as expected, suppresses warnings for "unparsed" code (such as code in #if compiler(>=7.0)) correctly.

Doug

3 Likes

Using the Swift 6.0 compiler in Xcode 16 beta 6 (16A5230g), I've noticed that the following warning isn't suppressed:

#if false
  @available(System 0.0.1, *)  // warning: unrecognized platform name 'System'
#endif

But it's suppressed when first checking for a previous compiler version:

#if compiler(>=5.8) && false
  @available(System 0.0.1, *)  // OK
#endif

I can't find any tests for the above, so I don't know if it behaves as expected.

The fact that the #if compiler(5.8) suppresses the diagnostic is intended. In the SwiftIfConfig library, it's the distinction between "inactive" (parsed but does not contribute to the program) and "unparsed" (not even parsed), where the former can emit diagnostics and the latter cannot.

Unit tests for SwiftIfConfig test the distinction between inactive and unparsed. The compiler also has tests to show that the compiler check suppresses diagnostics.

Doug

2 Likes

I've made the change in the behavior of these queries (so they ignore inactive/unparsed regions) in this pull request.

We also had some behavior that printed the contents of #if from the AST representation, although it's disabled for all of the important outputs (e.g., swift interfaces). I've removed this behavior and expect that this chance will be invisible outside the compiler test suite.

Doug

1 Like

And... here is the pull request to remove IfConfigDecl from the C++ semantic AST. All of the logic to handle #if declarations in the compiler is now implemented via the SwiftIfConfig library.

We're still carrying some of the C++ implementations of #if handling for use when the compiler is building without a host Swift compiler. We'll get to remove those once we stop supporting building a Swift toolchain without a Swift host compiler, but they are effectively dead code right now in the main host configurations.

Doug

6 Likes

Remove `swift-indent` tool by compnerd · Pull Request #76577 · swiftlang/swift · GitHub will remove the swift-indent tool as that use case is better served with swift-format. I don't think that the tool was sufficiently complete nor well maintained sufficiently to really require a deprecation period and should be relatively harmless to drop. I don't think that we were currently shipping it anywhere.

7 Likes