Removing File Scope Restriction for Import Statements

Hello Swift-Evolution

I wanted to inquire as to why import statements are only allowed at file scope? Is there any particular reason or is it legacy? If it was just because that's how it was done before then I would like to float the idea of removing that restriction.

My primary motivation came from this talk [youtube link] the specific feature is talked about from 18:18 - 35:23. If you don't want to watch then here's the language reference and a short summary.

TL;dr

Allow declarations like this to decouple dependencies from files.

struct Vector {
  import Darwin
  ...
}

func square(_ x: Float) -> Float {
  import func Darwin.powf
  return powf(x, 2)
}

My guess is that it's mostly because of how modules are implemented in the Swift compiler. It's become the Swift standard to just import entire modules rather than importing parts of them i.e import MyModule vs import struct MyModule.MyStruct

One big benefit of scoped imports in D is that you can put the import inside of templates. The import is then only performed when instantiating the template when the client code uses it. Since not all templates are instantiated all the time, the compiler has less things to import, and the runtime has less modules to initialize at startup.

None of that applies to Swift where generics are not templates instantiated on demand and where modules don't have static initializers. There are other reasons to want scoped imports, but many benefits of this feature in D will not apply to Swift.

I figured many performance benefits wouldn't apply, but still, the better modularity seems like a big plus. Especially if it would not be too difficult to lift the restriction (I imagine it wouldn't?). Other than that are there any downsides to doing this?

One downside to lifting the restriction is that the Swift compiler might have to parse more code to find all of the imports in a given module. Finding all of the imports up-front is one way to help discover dependencies of a module, and is part of providing a more efficient parallel build.

  • Doug
3 Likes

Does this mean that the Swift compiler eagerly links to dependencies whether or not anything from a module is actually called? Would you not have parse code that actually uses a module before deciding to link to it?

If import statements were more narrowly scoped would it not make it easier to find a modules usage?

I’ll admit this is not that pressing for Swift currently. However, I do see having many unassociated imports at the top of a file becoming a problem if something like SPM becomes popular with many small modules.

The linker itself will only link against a library when we've used a symbol from that library. I'm talking about the compiler itself, which will need to load the Swift "module" so that name lookup can determine whether one of the names that appear in the Swift code refers to an entity in that module.

  • Doug

Ah okay, thanks for the clarifying my misunderstanding.

When compiling a file incrementally, the compiler need to read all the other files in the module to find internal declarations. Doesn't the compiler skim over the function bodies in that situation? If the import was inside of the function body, the compiler could avoid reading the imported module while skimming because it only affects name lookups inside of the function.

In the best scenario, your fast-to-compile module would have functions that do not accept or return types from other modules, which would allow every import to be function-scoped, and skimming over the files in that module could be done without importing anything. That's a bit contrived though, I think.

That's a good point! There are some cases in which we should care about the bodies of functions in other source files within the same module, for example, if we want to inline the body. Presumably we could have the compiler in change of dependency scanning, so it would understand these cases.

  • Doug