[cxx-interop] Use formal C++ interop mode to fix name lookup in module interfaces (#79984)

It is possible for a module interface (e.g., ModuleA) to be generated
with C++ interop disabled, and then rebuilt with C++ interop enabled
(e.g., because ModuleB, which imports ModuleA, has C++ interop enabled).

This circumstance can lead to various issues when name lookup behaves
differently depending on whether C++ interop is enabled, e.g., when
a module name is shadowed by a namespace of the same name---this only
happens in C++ because namespaces do not exist in C. Unfortunately,
naming namespaces the same as a module is a common C++ convention,
leading to many textual interfaces whose fully-qualified identifiers
(e.g., c_module.c_member) cannot be correctly resolved when C++ interop
is enabled (because c_module is shadowed by a namespace of the same
name).

This patch does two things. First, it introduces a new frontend flag,
-formal-cxx-interoperability-mode, which records the C++ interop mode
a module interface was originally compiled with. Doing so allows
subsequent consumers of that interface to interpret it according to the
formal C++ interop mode. Note that the actual "versioning" used by this
flag is very crude: "off" means disabled, and "swift-6" means enabled.
This is done to be compatible with C++ interop compat versioning scheme,
which seems to produce some invalid (but unused) version numbers. The
versioning scheme for both the formal and actual C++ interop modes
should be clarified and fixed in a subsequent patch.

The second thing this patch does is fix the module/namespace collision
issue in module interface files. It uses the formal C++ interop mode to
determine whether it should resolve C++-only decls during name lookup.
For now, the fix is very minimal and conservative: it only filters out
C++ namespaces during unqualified name lookup in an interface that was
originally generated without C++ interop. Doing so should fix the issue
while minimizing the chance for collateral breakge. More cases other
than C++ namespaces should be added in subsequent patches, with
sufficient testing and careful consideration.

rdar://144566922
This commit is contained in:
John Hui
2025-03-13 23:24:18 -07:00
committed by GitHub
parent 93f9db67f8
commit 76a1742aca
10 changed files with 235 additions and 13 deletions

View File

@@ -644,7 +644,8 @@ static void diagnoseCxxInteropCompatMode(Arg *verArg, ArgList &Args,
auto validVers = {llvm::StringRef("off"), llvm::StringRef("default"),
llvm::StringRef("swift-6"), llvm::StringRef("swift-5.9")};
auto versStr = "'" + llvm::join(validVers, "', '") + "'";
diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes, versStr);
diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes,
verArg->getSpelling(), versStr);
}
void LangOptions::setCxxInteropFromArgs(ArgList &Args,
@@ -679,6 +680,54 @@ void LangOptions::setCxxInteropFromArgs(ArgList &Args,
cxxInteropCompatVersion =
validateCxxInteropCompatibilityMode("swift-5.9").second;
}
if (Arg *A = Args.getLastArg(options::OPT_formal_cxx_interoperability_mode)) {
// Take formal version from explicitly specified formal version flag
StringRef version = A->getValue();
// FIXME: the only valid modes are 'off' and 'swift-6'; see below.
if (version == "off") {
FormalCxxInteropMode = std::nullopt;
} else if (version == "swift-6") {
FormalCxxInteropMode = {6};
} else {
Diags.diagnose(SourceLoc(), diag::error_invalid_arg_value,
A->getAsString(Args), A->getValue());
Diags.diagnose(SourceLoc(), diag::valid_cxx_interop_modes,
A->getSpelling(), "'off', 'swift-6'");
}
} else {
// In the absence of a formal mode flag, we capture it from the current
// C++ compat version (if C++ interop is enabled).
//
// FIXME: cxxInteropCompatVersion is computed based on the Swift language
// version, and is either 4, 5, 6, or 7 (even though only 5.9 and 6.* make
// any sense). For now, we don't actually care about the version, so we'll
// just use version 6 (i.e., 'swift-6') to mean that C++ interop mode is on.
if (EnableCXXInterop)
FormalCxxInteropMode = {6};
else
FormalCxxInteropMode = std::nullopt;
}
}
static std::string printFormalCxxInteropVersion(const LangOptions &Opts) {
std::string str;
llvm::raw_string_ostream OS(str);
OS << "-formal-cxx-interoperability-mode=";
// We must print a 'stable' C++ interop version here, which cannot be
// 'default' and 'upcoming-swift' (since those are relative to the current
// version, which may change in the future).
if (!Opts.FormalCxxInteropMode) {
OS << "off";
} else {
// FIXME: FormalCxxInteropMode will always be 6 (or nullopt); see above
OS << "swift-6";
}
return str;
}
static std::optional<swift::StrictConcurrency>
@@ -925,6 +974,7 @@ static bool ParseEnabledFeatureArgs(LangOptions &Opts, ArgList &Args,
static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
DiagnosticEngine &Diags,
ModuleInterfaceOptions &ModuleInterfaceOpts,
const FrontendOptions &FrontendOpts) {
using namespace options;
bool buildingFromInterface =
@@ -1478,6 +1528,9 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
Opts.ClangTargetVariant = llvm::Triple(A->getValue());
Opts.setCxxInteropFromArgs(Args, Diags);
if (!Args.hasArg(options::OPT_formal_cxx_interoperability_mode))
ModuleInterfaceOpts.PublicFlags.IgnorableFlags +=
" " + printFormalCxxInteropVersion(Opts);
Opts.EnableObjCInterop =
Args.hasFlag(OPT_enable_objc_interop, OPT_disable_objc_interop,
@@ -3893,7 +3946,8 @@ bool CompilerInvocation::parseArgs(
return true;
}
if (ParseLangArgs(LangOpts, ParsedArgs, Diags, FrontendOpts)) {
if (ParseLangArgs(LangOpts, ParsedArgs, Diags, ModuleInterfaceOpts,
FrontendOpts)) {
return true;
}