Skip to content

Optimize Condition Resolution #6279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 5, 2017

Conversation

CodaFi
Copy link
Contributor

@CodaFi CodaFi commented Dec 14, 2016

A bit of cleanup and a few optimizations around the recently-landed condition resolution stuff.

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 14, 2016

@swift-ci please smoke test.

/// later by Condition Resolution.
unsigned HasBeenResolved : 1;
};
enum { NumIfConfigDeclBits = NumDeclBits + 3 };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-by-one. Wonderful.

@@ -1291,6 +1291,12 @@ ASTContext::getModule(ArrayRef<std::pair<Identifier, SourceLoc>> ModulePath) {
return M;

auto moduleID = ModulePath[0];
// Record this load request. If we've seen it before and couldn't produce a
// loaded module it must have failed to load before.
if (!RequestedModuleLoads.insert(moduleID.first).second) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to just use the existing LoadedModules map for this?

Copy link
Contributor Author

@CodaFi CodaFi Dec 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does ModuleDecl * have a sensible sentinel value?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifier isn't the one that needs the sentinel; it's a map from Identifier to ModuleDecl *.

Copy link
Contributor Author

@CodaFi CodaFi Dec 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. Well, that would go through a pointer's dense map info so... I'm not entirely sure what would work here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llvm::MapVector<Identifier, ModuleDecl*> LoadedModules;

If moduleID.first is in the map, but the value is nullptr, then it must have failed to load. (This would require changing some of the other places that use LoadedModules too.)

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 0d0dc31 to 5f6e545 Compare December 15, 2016 21:19
@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 15, 2016

I've withdrawn the parts of this pull request that deal with optimizing the module loading behavior. It turns out there's some trickiness there around loading modulemap-based modules. I'll try to take a crack at it later.

@swift-ci please smoke test.

jrose-apple
jrose-apple previously approved these changes Dec 15, 2016
return
Context.getModule({ { argumentIdent, UDRE->getLoc() } }) != nullptr;

auto *M = Context.getModule({ { argumentIdent, UDRE->getLoc() } });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In retrospect this seems terrifying; loading a module has side effects even when it isn't imported. (Extensions still get hooked up, AnyObject dynamic lookup still finds things, operators still become visible.) All of those things are bugs, but we should probably put a canImport operation on ModuleLoader. (That'd be cheaper anyway if you didn't actually end up loading the thing.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. To what extent do we need to load a module to figure out if it is "importable?" It doesn't seem enough to search for it on disk, and it might depend on further modules that fail to load themselves.

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 15, 2016

OK, so there are three major cases to consider for the definition of 'Importable' here.

  • Clang Importer:

Modules still have to be imported by the CompilerInstance to check for importability, but the effects of the load can be hidden by specifying clang::Module::Hidden.

  • SerializedModuleLoader

Modules get run through (required) validation to report rich errors about failures. "Importable" here probably means "passes this validation", but that can be expensive for repeated requests.

  • SourceLoader

The hard one. Invokes the typechecker to get diagnostics in the dependent file, so probably need to add a new flag to the typechecker for a "dry run" or something to just see if the thing passes. Though, we might be able to get by with not running the type checker and just validating that we can load the file at all.

The subtext here is: does "I can import this" really mean "I can import this without error"?

@slavapestov
Copy link
Contributor

@CodaFi Since you're working on this area, a few compiler_crashers are failing with various IfConfigStmt-related assertions. Might be an easy fix for you.

@jrose-apple
Copy link
Contributor

FWIW, we don't actually care about SourceLoader. If we ever design a Python-like module system that lets us load from source, we'll build it from scratch to do some kind of subprocess compilation, not continue to make SourceLoader limp along.

For the ClangImporter, there may be Clang APIs we can use to determine if a module exists and can be imported without actually loading it, even as hidden. For SerializedModuleLoader, I think we can just do enough to detect that the module exists; this mechanism isn't supposed to be used for fallbacks like "oh, one of my dependencies is missing".

@jrose-apple
Copy link
Contributor

At least, that's my understanding. @erica, without asking you to go read all the kinds of module loading we support, what behavior would you expect for a module that happens to exist on disk, but would fail some time in the loading process? It seems like checking for this in canImport would allow things to silently fail without anyone noticing.

@slavapestov
Copy link
Contributor

@CodaFi Do you mind adding an entry to CHANGELOG.md to note that SE-0075 is now implemented?

@erica
Copy link
Contributor

erica commented Dec 19, 2016

Here from tweet (http://twitter.com/CodaFi_/status/810709572956647424)

Putting kids to bed. My interpretation is if if the compiler emits "No such module (name)", it fails.

Email notifications from github seem borked and intermittent right now. Don't know why. If you don't see me respond to a name ping, please email me directly. @CodaFi @jrose-apple. I caught the tweet by accident.

I'll stick around for the next 30 minutes if you need me.

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 19, 2016

Thanks for taking the time.

Currently, that diagnostic is emitted late in the module loading phase. To take a fairly uncommon example in practice, but one relevant to something like the Swift Standard Library: What should the result checking a version of the Swift module built for an older compiler (currently something we diagnose as an error) be?

@erica
Copy link
Contributor

erica commented Dec 19, 2016

@CodaFi What I believe you're asking is: how should if canImport(module) act in the presence of an invalid module. Logic suggests that a module that cannot be imported should fail this test.

Stepping back. The discussion for this proposal grew from platform differentiation, specifically for allowing module-dependencies to direct code. On iOS, watchOS, tvOS, you can use UIKit to perform identical tasks. Not all tasks but there's a basic core UIKit whose APIs are guaranteed to work across all three platforms. This test enables you to differentiate this code from code that run with Cocoa. Using modules lets you establish an API contract rather than testing for platform qualities.

You write:

What should the result checking a version of the Swift module built for an older compiler (currently something we diagnose as an error) be?

If you cannot import it for any reason, the API is unavailable, and the code should be excluded.

  • Will the module link, even with an older compiler?
  • Do modules have an inherent version number? (like SPM specifies?)
  • Is it necessary to specify the earliest acceptable version of Swift used to compile the module moving forward? e.g. #if canImport(ModuleName, swift: >=3.2), for some point where a 3.2 module would work with Swift 3.3?

@erica
Copy link
Contributor

erica commented Dec 19, 2016

@jrose-apple @CodaFi

Ideally, a test for canImport that cannot import for any reason other than the module does not exist natively on that platform should emit a warning or error.

You can work around this in an extremely ugly way using Swift features that doesn't exist:

  1. Test whether any conditional compilation that mentions canImport reaches #endif without any branches firing and emit a warning if that happens. If the conditions and the branches are not exhaustive, then there is a likely bug.

  2. Allow developer-defined warnings to be emitted a la LLVM #warning/#error preprocessor directives. Developers add #warning/#error in an exhaustive else branch to override the compiler's default warning or to silence that warning because this is intentional design, with code that is guarded elsewhere.

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 5f6e545 to 9c8683a Compare December 19, 2016 07:36
@karwa
Copy link
Contributor

karwa commented Dec 19, 2016

I don't think it's right to require #canImport to be exhaustive (not sure if that's what you're saying). What about if you define some protocols in your library, and you want to expose some conformances via extensions to Foundation types, without requiring all supported platforms to support Foundation? e.g:

protocol MyProtocol {}

#if canImport(Foundation)
    // On platforms which don't support Foundation, this conformance isn't meaningful
    import Foundation
    extension Data: MyProtocol {}
#endif

I think that's a reasonably common use-case. You shouldn't have to have an 'else' block and manually silence a compiler warning.

@jrose-apple
Copy link
Contributor

jrose-apple commented Dec 19, 2016

Ideally, a test for canImport that cannot import for any reason other than the module does not exist natively on that platform should emit a warning or error.

Hm. I agree with this in theory—you shouldn't be asking the question if the subsequent import is going to break—but at the same time I'd still like to not do all the work of loading a module just to be sure the error occurs. I guess it's rare enough that you'd want to check for the presence of a module but not load it that we don't need to worry for now, as long as the error still gets emitted.

I agree with @karwa that canImport should not require #else.

@erica
Copy link
Contributor

erica commented Dec 19, 2016

@jrose-apple @karwa @CodaFi

Got to sleep on this overnight.

Hm. I agree with this in theory—you shouldn't be asking the question if the subsequent import is going to break—but at the same time I'd still like to not do all the work of loading a module just to be sure the error occurs. I guess it's rare enough that you'd want to check for the presence of a module but not load it that we don't need to worry for now, as long as the error still gets emitted.

If I understand this correctly, whether the import is successful or not, the test is for "would import RoseKit work, ignoring the suitability of RoseKit to link against the rest of the code"

The scenario I want to specifically avoid is this:

#if canImport(RoseKit)
    // something that isn't included because RoseKit was compiled for Swift 1.3
#endif

This should not silently exclude code because RoseKit is compiled for the wrong version of Swift. Something has to complain, I don't know where. If Swift "fails" here, I'd prefer it to fail by including the code and incurring failures later in the compilation process rather than excluding it and having that code be omitted without notice.

I'm thinking that so long as RoseKit is recognized as an available module, no matter whether it's reasonable to import it or not, the test will succeed if you have to allow/disallow earlier in the process than the point at which you decide whether the module is valid or invalid.

  • If the code is dependent on RoseKit, there are secondary mechanisms to catch the error.
  • If the code is not dependent on RoseKit (poor style, white shoes after labor day), then the mere presence of the module should be sufficient to allow it to compile early in the process.

Again, you guys all know the implementation details better than I do.

@jrose-apple
Copy link
Contributor

Ah, yes, it definitely shouldn't silently skip the body of the #if. I would expect it to either emit an error and then skip the body (the good case), or silently process the body because we didn't get far enough into the import to detect an error. I think that means we're on the same page.

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 19, 2016

Fantastic, that should speed things up quite a bit. I'll update this pull request soon.

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 9c8683a to 1ae4597 Compare December 19, 2016 20:32
@CodaFi CodaFi requested a review from jrose-apple December 19, 2016 20:33

// Now load the top-level module, so that we can check if the submodule
// exists without triggering a fatal error.
return (result != nullptr);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure of whether this can all be replaced by

Impl.Instance->getPreprocessor().getHeaderSearchInfo().lookupModule(path)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely seems interesting. @benlangmuir?

If not, I think we need to factor this out in some way to share code. Also, does this do the right thing for submodules?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canImport does not work with submodules at the moment. I can make it work and see what happens.

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch 2 times, most recently from 08f9beb to 3b374c4 Compare December 19, 2016 22:57
@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 19, 2016

@swift-ci please smoke test.

@CodaFi
Copy link
Contributor Author

CodaFi commented Dec 22, 2016

@jrose-apple Is this still good to go?

/// \brief Check whether the module with a given name can be imported without
/// importing it.
///
/// Note that if errors occur during the module loading process this check may
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not quite the condition we're going for. It's more "even if this check succeeds, errors may still occur if the module is eventually loaded", right?


// Now load the top-level module, so that we can check if the submodule
// exists without triggering a fatal error.
return (result != nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely seems interesting. @benlangmuir?

If not, I think we need to factor this out in some way to share code. Also, does this do the right thing for submodules?

return false;
}
}
return (moduleInputBuffer != nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth avoiding the construction of the llvm::MemoryBuffers. Maybe we should be passing pointers instead of references?

class UIKitView : UIView {} // expected-error {{use of undeclared type 'UIView'}}
#endif

#if canImport(CoreGraphics)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have some tests for Swift modules that aren't overlays and for Objective-C modules that don't have overlays, because I don't trust overlays to do normal things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still applies.

@jrose-apple jrose-apple dismissed their stale review December 22, 2016 23:57

A bit more detailed feedback for the more lazy approach.

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 3b374c4 to 14f5251 Compare December 23, 2016 18:25
@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch 2 times, most recently from 0c7297c to 25f2cc5 Compare December 31, 2016 05:18
bool ClangImporter::canImportModule(std::pair<Identifier, SourceLoc> moduleID) {
// Look up the top-level module to see if it exists.
// FIXME: This only works with top-level modules. To support submodules, this
// should be replaced by checking the result of `loadModule`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the correct planned fix, but since we're only allowing users to check top-level modules for now it seems like an acceptable limitation. Maybe just delete the "To support submodules" part?

Copy link
Contributor

@jrose-apple jrose-apple left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all remaining comments are nitpicks. :-)

// should be replaced by checking the result of `loadModule`.
auto &clangHeaderSearch = Impl.getClangPreprocessor().getHeaderSearchInfo();
clang::Module *clangModule =
clangHeaderSearch.lookupModule(moduleID.first.str());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looking for confirmation from @benlangmuir that this is an acceptable way to check for the existence of a module without loading it.

// Try to open the module file first. If we fail, don't even look for the
// module documentation file.
Scratch.clear();
llvm::sys::path::append(Scratch, DirName, ModuleFilename);
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> ModuleOrErr =
if (ModuleBuffer && ModuleDocBuffer) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Let's flip this around and make it an early return.

if (ModuleDocOrErr)
*ModuleDocBuffer = std::move(ModuleDocOrErr.get());
} else {
return llvm::sys::fs::access(StringRef(Scratch.data(), Scratch.size()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's shorthand for this: llvm::sys::fs::exists

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to get a std::error_code return, not a boolean one.

// First see if we find it in the registered memory buffers.
if (!MemoryBuffers.empty()) {
// FIXME: Right now this works only with access paths of length 1.
// Once submodules are designed, this needs to support suffix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not assume anything about how submodules might work.

class UIKitView : UIView {} // expected-error {{use of undeclared type 'UIView'}}
#endif

#if canImport(CoreGraphics)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still applies.

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 25f2cc5 to 7c72f85 Compare January 4, 2017 01:51
@CodaFi
Copy link
Contributor Author

CodaFi commented Jan 4, 2017

There. Barring comment by @benlangmuir

@swift-ci please smoke test.

@CodaFi CodaFi requested a review from benlangmuir January 4, 2017 01:52
@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 7c72f85 to 5721a19 Compare January 4, 2017 03:32
@CodaFi
Copy link
Contributor Author

CodaFi commented Jan 4, 2017

@swift-ci please smoke test.

// FIXME: This only works with top-level modules.
auto &clangHeaderSearch = Impl.getClangPreprocessor().getHeaderSearchInfo();
clang::Module *clangModule =
clangHeaderSearch.lookupModule(moduleID.first.str());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late comment, but this will only tell you that the module map was found and parsed. The module may still be unavailable for a variety of reasons (e.g. it has an unfulfilled "requires" declaration). You should also call the following method from clang to check that the module makes sense to import:

  /// \brief Determine whether this module is available for use within the
  /// current translation unit.
  ///
  /// \param LangOpts The language options used for the current
  /// translation unit.
  ///
  /// \param Target The target options used for the current translation unit.
  ///
  /// \param Req If this module is unavailable, this parameter
  /// will be set to one of the requirements that is not met for use of
  /// this module.
  bool isAvailable(const LangOptions &LangOpts, 
                   const TargetInfo &Target,
                   Requirement &Req,
                   UnresolvedHeaderDirective &MissingHeader) const;

I think you can just ignore the out parameters (Req and MissingHeader), since you're not trying to diagnose why the module isn't available.

Copy link
Contributor

@benlangmuir benlangmuir Jan 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you can test this by adding a clang module whose module map contains e.g.:

module Foo {
  requires missing
}

which should cause canImport(Foo) to fail. If you want a case where it succeeds, "requires swift" should do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@CodaFi CodaFi force-pushed the linker-loader-soldier-spy branch from 5721a19 to 399c029 Compare January 5, 2017 08:38
The module loaders now have API to check whether a given module can be
imported without importing the referenced module.  This provides a
significant speed boost to condition resolution and no longer
introduces symbols from the referenced module into the current context
without the user explicitly requesting it.

The definition of ‘canImport’ does not necessarily mean that a full
import without error is possible, merely that the path to the import is
visible to the compiler and the module is loadable in some form or
another.

Note that this means this check is insufficient to guarantee that you
are on one platform or another.  For those kinds of checks, use
‘os(OSNAME)’.
@CodaFi
Copy link
Contributor Author

CodaFi commented Jan 5, 2017

@swift-ci please smoke test.

@benlangmuir
Copy link
Contributor

LGTM

@CodaFi
Copy link
Contributor Author

CodaFi commented Jan 5, 2017

⛵️

@CodaFi CodaFi merged commit dfa41d6 into swiftlang:master Jan 5, 2017
@CodaFi CodaFi deleted the linker-loader-soldier-spy branch January 5, 2017 19:08
CodaFi added a commit to CodaFi/swift that referenced this pull request Jan 6, 2017
This reverts the contents of swiftlang#5778 and replaces it with a far simpler
implementation of condition resolution along with canImport.  When
combined with the optimizations in swiftlang#6279 we get the best of both worlds
with a performance win and a simpler implementation.
ematejska pushed a commit to ematejska/swift that referenced this pull request Jan 11, 2017
This reverts the contents of swiftlang#5778 and replaces it with a far simpler
implementation of condition resolution along with canImport.  When
combined with the optimizations in swiftlang#6279 we get the best of both worlds
with a performance win and a simpler implementation.
benrimmington added a commit to swiftlang/swift-evolution that referenced this pull request Sep 3, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants