Skip to content

SILGen: Ensure Foundation is publicly imported before referencing NSError in code gen #79487

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
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/SILGen/SILGenConvert.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -656,9 +656,10 @@ ManagedValue SILGenFunction::emitExistentialErasure(
// If we're erasing to the 'Error' type, we might be able to get an NSError
// representation more efficiently.
auto &ctx = getASTContext();
auto *nsErrorDecl = ctx.getNSErrorDecl();
if (ctx.LangOpts.EnableObjCInterop && conformances.size() == 1 &&
conformances[0].getRequirement() == ctx.getErrorDecl() &&
ctx.getNSErrorDecl()) {
nsErrorDecl && referenceAllowed(nsErrorDecl)) {
// If the concrete type is NSError or a subclass thereof, just erase it
// directly.
auto nsErrorType = ctx.getNSErrorType()->getCanonicalType();
Expand Down
30 changes: 30 additions & 0 deletions lib/SILGen/SILGenFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "swift/AST/FileUnit.h"
#include "swift/AST/GenericEnvironment.h"
#include "swift/AST/Initializer.h"
#include "swift/AST/ImportCache.h"
#include "swift/AST/ParameterList.h"
#include "swift/AST/PropertyWrappers.h"
#include "swift/AST/SourceFile.h"
Expand Down Expand Up @@ -210,6 +211,35 @@ DeclName SILGenModule::getMagicFunctionName(SILDeclRef ref) {
llvm_unreachable("Unhandled SILDeclRefKind in switch.");
}

bool SILGenFunction::referenceAllowed(ValueDecl *decl) {
// Use in any non-fragile functions.
if (FunctionDC->getResilienceExpansion() == ResilienceExpansion::Maximal)
return true;

// Allow same-module references.
auto *targetMod = decl->getDeclContext()->getParentModule();
auto *thisMod = FunctionDC->getParentModule();
if (thisMod == targetMod)
return true;

ModuleDecl::ImportFilter filter = {
ModuleDecl::ImportFilterKind::Exported,
ModuleDecl::ImportFilterKind::Default,
ModuleDecl::ImportFilterKind::SPIOnly};
if (thisMod->getResilienceStrategy() != ResilienceStrategy::Resilient)
filter |= ModuleDecl::ImportFilterKind::InternalOrBelow;

// Look through public module local imports and their reexports.
llvm::SmallVector<ImportedModule, 8> imports;
thisMod->getImportedModules(imports, filter);
auto &importCache = getASTContext().getImportCache();
for (auto &import : imports) {
if (importCache.isImportedBy(targetMod, import.importedModule))
return true;
}
return false;
}

SILDebugLocation SILGenFunction::getSILDebugLocation(
SILBuilder &B, SILLocation Loc,
std::optional<SILLocation> CurDebugLocOverride, bool ForMetaInstruction) {
Expand Down
9 changes: 9 additions & 0 deletions lib/SILGen/SILGenFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,15 @@ class LLVM_LIBRARY_VISIBILITY SILGenFunction
bool isEmittingTopLevelCode() { return IsEmittingTopLevelCode; }
void stopEmittingTopLevelCode() { IsEmittingTopLevelCode = false; }

/// Can the generated code reference \c decl safely?
///
/// Checks that the module defining \c decl is as visible to clients as the
/// code referencing it, preventing an inlinable function to reference an
/// implementation-only dependency and similar. This applies similar checks
/// as the exportability checker does to source code for decls referenced by
/// generated code.
bool referenceAllowed(ValueDecl *decl);

std::optional<SILAccessEnforcement>
getStaticEnforcement(VarDecl *var = nullptr);
std::optional<SILAccessEnforcement>
Expand Down
55 changes: 55 additions & 0 deletions test/SILGen/objc_error_fast_path.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// RUN: %empty-directory(%t)
// RUN: split-file %s %t --leading-lines
// REQUIRES: objc_interop
// REQUIRES: executable_test

/// Optimization active: public import
// RUN: %target-build-swift-dylib(%t/%target-library-name(Lib)) \
// RUN: %t/optimized.swift -module-name Lib \
// RUN: -emit-module -emit-module-path %t/Lib.swiftmodule \
// RUN: -swift-version 5 -enable-library-evolution -O
// RUN: %target-build-swift %t/main.swift -o %t/main -I%t -lLib -L%t -O
// RUN: %target-codesign %t/main
// RUN: %target-run %t/main | %FileCheck %s

/// Ensure the client has the optimization we're testing here.
// RUN: %target-swift-frontend -typecheck -emit-silgen %t/Lib.swiftmodule > %t/Lib.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/Lib.sil %s
// CHECK-OPTIMIZED: _getErrorEmbeddedNSError

/// No optimization: internal import from resilient module
// RUN: %target-build-swift-dylib(%t/%target-library-name(Lib)) \
// RUN: %t/non-optimized.swift -module-name Lib \
// RUN: -emit-module -emit-module-path %t/Lib.swiftmodule \
// RUN: -swift-version 5 -enable-library-evolution -O
// RUN: %target-build-swift %t/main.swift -o %t/main -I%t -lLib -L%t -O
// RUN: %target-codesign %t/main
// RUN: %target-run %t/main | %FileCheck %s

/// Ensure the client doesn't have the optimization we're testing here.
// RUN: %target-swift-frontend -typecheck -emit-silgen %t/main.swift -I%t -O > %t/Lib.sil
// RUN: %FileCheck --check-prefix CHECK-NOT-OPTIMIZED --input-file %t/Lib.sil %s
// CHECK-NOT-OPTIMIZED-NOT: _getErrorEmbeddedNSError

//--- optimized.swift
public import Foundation

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- non-optimized.swift
internal import Foundation

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- main.swift
import Lib
import Foundation

let err: NSError = NSError(domain: "Not found", code: 404)
let errOut: Error = foo(e: err)
print(errOut.localizedDescription)
// CHECK: Not found error 404.
159 changes: 159 additions & 0 deletions test/Serialization/generated-ref-to-nserror.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// RUN: %empty-directory(%t)
// RUN: split-file %s %t --leading-lines
// REQUIRES: objc_interop

/// Build a minimal version of Foundation defining only NSError
// RUN: %target-swift-frontend -emit-module %t/Foundation.swift \
// RUN: -o %t/Foundation.swiftmodule
// RUN: %target-swift-frontend -emit-module %t/FoundationExporter.swift -I%t \
// RUN: -o %t/FoundationExporter.swiftmodule

/// Enabled existential to NSError optimization
// CHECK-OPTIMIZED: $NSError

/// Optimize Foundation itself
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/Foundation.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s

/// Public import or non-resilient modules
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/inlinable-public.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/inlinable-public.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/inlinable-internal.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s

/// Foundation is imported from a different file
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t -module-name main \
// RUN: %t/inlinable-not-imported-fileA.swift \
// RUN: %t/inlinable-not-imported-fileB.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t -module-name main \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/inlinable-not-imported-fileA.swift \
// RUN: %t/inlinable-not-imported-fileB.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s

/// Foundation is imported via a transitive dependency
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t -module-name main \
// RUN: %t/inlinable-imported-transitive.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s

/// Any non-inlinable uses
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/non-inlinable-public.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/non-inlinable-ioi.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/non-inlinable-internal.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t -module-name main \
// RUN: %t/non-inlinable-not-imported-fileA.swift \
// RUN: %t/non-inlinable-not-imported-fileB.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t -module-name main \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/non-inlinable-not-imported-fileA.swift \
// RUN: %t/non-inlinable-not-imported-fileB.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-OPTIMIZED --input-file %t/main.sil %s

/// Disabled existential to NSError optimization
// CHECK-NOT-OPTIMIZED-NOT: $NSError

/// Implementation-only import
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: %t/inlinable-ioi.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-NOT-OPTIMIZED --input-file %t/main.sil %s
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/inlinable-ioi.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-NOT-OPTIMIZED --input-file %t/main.sil %s

/// Internal import from resilient module
// RUN: %target-swift-frontend -emit-module -emit-sil -I%t \
// RUN: -swift-version 5 -enable-library-evolution \
// RUN: %t/inlinable-internal.swift > %t/main.sil
// RUN: %FileCheck --check-prefix CHECK-NOT-OPTIMIZED --input-file %t/main.sil %s

//--- Foundation.swift

class NSError {}

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- FoundationExporter.swift

@_exported import Foundation

//--- inlinable-public.swift
public import Foundation

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- inlinable-ioi.swift
@_implementationOnly import Foundation

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- inlinable-internal.swift
internal import Foundation

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- inlinable-not-imported-fileA.swift
@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}
//--- inlinable-not-imported-fileB.swift
import Foundation

//--- inlinable-imported-transitive.swift
public import FoundationExporter

@inlinable public func foo<E: Error>(e: E) -> Error {
return e
}

//--- non-inlinable-public.swift
public import Foundation

public func foo<E: Error>(e: E) -> Error {
return e
}

//--- non-inlinable-ioi.swift
@_implementationOnly import Foundation

public func foo<E: Error>(e: E) -> Error {
return e
}

//--- non-inlinable-internal.swift
internal import Foundation

public func foo<E: Error>(e: E) -> Error {
return e
}

//--- non-inlinable-not-imported-fileA.swift
public func foo<E: Error>(e: E) -> Error {
return e
}
//--- non-inlinable-not-imported-fileB.swift
import Foundation