From 879f37c699d675ea00f2655b6382e94a86e7d504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 18 Nov 2024 12:22:09 -0800 Subject: [PATCH 1/4] Sema: Test that SPI operators are only printed in the public swiftinterface This was already working as expected. Adding a test to ensure it doesn't regress. --- test/SPI/private_swiftinterface.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/SPI/private_swiftinterface.swift b/test/SPI/private_swiftinterface.swift index 83d1b83e269cc..89c00474ba856 100644 --- a/test/SPI/private_swiftinterface.swift +++ b/test/SPI/private_swiftinterface.swift @@ -29,16 +29,16 @@ /// Test the textual interfaces generated from this test. // RUN: %target-swift-frontend -typecheck %s -emit-module-interface-path %t/Main.swiftinterface -emit-private-module-interface-path %t/Main.private.swiftinterface -enable-library-evolution -swift-version 5 -I %t -module-name Main -// RUN: %FileCheck -check-prefix=CHECK-PUBLIC %s < %t/Main.swiftinterface -// RUN: %FileCheck -check-prefix=CHECK-PRIVATE %s < %t/Main.private.swiftinterface +// RUN: cat %t/Main.swiftinterface | %FileCheck -check-prefix=CHECK-PUBLIC %s +// RUN: cat %t/Main.private.swiftinterface | %FileCheck -check-prefix=CHECK-PRIVATE %s // RUN: %target-swift-frontend -typecheck-module-from-interface -I %t %t/Main.swiftinterface // RUN: %target-swift-frontend -typecheck-module-from-interface -I %t %t/Main.private.swiftinterface -module-name Main /// Both the public and private textual interfaces should have /// SPI information with `-library-level spi`. // RUN: %target-swift-frontend -typecheck %s -emit-module-interface-path %t/SPIModule.swiftinterface -emit-private-module-interface-path %t/SPIModule.private.swiftinterface -enable-library-evolution -swift-version 5 -I %t -module-name SPIModule -library-level spi -// RUN: %FileCheck -check-prefix=CHECK-PRIVATE %s < %t/SPIModule.swiftinterface -// RUN: %FileCheck -check-prefix=CHECK-PRIVATE %s < %t/SPIModule.private.swiftinterface +// RUN: cat %t/SPIModule.swiftinterface | %FileCheck -check-prefix=CHECK-PRIVATE %s +// RUN: cat %t/SPIModule.private.swiftinterface | %FileCheck -check-prefix=CHECK-PRIVATE %s // RUN: %target-swift-frontend -typecheck-module-from-interface -I %t %t/SPIModule.swiftinterface // RUN: %target-swift-frontend -typecheck-module-from-interface -I %t %t/SPIModule.private.swiftinterface -module-name SPIModule @@ -238,6 +238,11 @@ extension IOIPublicStruct : LocalPublicProto {} // CHECK-PRIVATE: @_spi(S) private var spiTypeInFrozen1 } +public struct OpType {} +@_spi(S) public func +(_ s1: OpType, _ s2: OpType) -> OpType { s1 } +// CHECK-PRIVATE: @_spi(S) public func + (s1: {{.*}}.OpType, s2: {{.*}}.OpType) -> {{.*}}.OpType +// CHECK-PUBLIC-NOT: func + + // The dummy conformance should be only in the private swiftinterface for // SPI extensions. @_spi(LocalSPI) From 8e7fdb25591fbe152d19b8b763e25586ec2fb79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Wed, 20 Nov 2024 09:55:00 -0800 Subject: [PATCH 2/4] Sema: Test that we can't use SPI operators in inlinable code --- test/SPI/local_spi_decls.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/SPI/local_spi_decls.swift b/test/SPI/local_spi_decls.swift index d9350b4b3aa46..5f36727691912 100644 --- a/test/SPI/local_spi_decls.swift +++ b/test/SPI/local_spi_decls.swift @@ -115,11 +115,20 @@ public func publicFuncWithDefaultValue(_ p: SPIClass = SPIClass()) {} // expecte @_spi(S) public func spiFuncWithDefaultValue(_ p: SPIClass = SPIClass()) {} +public struct PublicType { + public init() { } +} +@_spi(S) public func -(_ s1: PublicType, _ s2: PublicType) -> PublicType { s1 } + +public let o1 = PublicType() +public let o2 = PublicType() + @inlinable public func inlinablePublic() { spiFunc() // expected-error {{global function 'spiFunc()' cannot be used in an '@inlinable' function because it is SPI}} let _ = SPIClass() // expected-error {{class 'SPIClass' cannot be used in an '@inlinable' function because it is SPI}} // expected-error@-1 {{initializer 'init()' cannot be used in an '@inlinable' function because it is SPI}} + let _ = o1 - o2 // expected-error {{operator function '-' cannot be used in an '@inlinable' function because it is SPI}} } @_spi(S) @@ -127,8 +136,11 @@ public func inlinablePublic() { public func inlinableSPI() { spiFunc() let _ = SPIClass() + let _ = o1 - o2 } @_spi(S) func internalFunc() {} // expected-error {{internal global function cannot be declared '@_spi' because only public and open declarations can be '@_spi'}} @_spi(S) package func packageFunc() {} // expected-error {{package global function cannot be declared '@_spi' because only public and open declarations can be '@_spi'}} + +let _ = o1 - o2 From b248d2d9b4d8befdc7510cdce1f77cfbb438114e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 12 Nov 2024 09:30:05 -0800 Subject: [PATCH 3/4] Sema: Restrict use of SPI operators to files importing the SPI group Ensure that calls to SPI operators are only accepted in source files importing the corresponding SPI group. This applies the same logic to operators as we do for functions. rdar://137713966 --- lib/AST/Decl.cpp | 10 +++++++++- test/SPI/Inputs/spi_helper.swift | 7 +++++++ test/SPI/public_client.swift | 8 +++++++- test/SPI/spi_client.swift | 6 ++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/AST/Decl.cpp b/lib/AST/Decl.cpp index 7b6774729a8e0..8a38cbbb7d911 100644 --- a/lib/AST/Decl.cpp +++ b/lib/AST/Decl.cpp @@ -5536,9 +5536,17 @@ static bool checkAccess(const DeclContext *useDC, const ValueDecl *VD, return srcPkg && usePkg && usePkg->isSamePackageAs(srcPkg); } case AccessLevel::Public: - case AccessLevel::Open: + case AccessLevel::Open: { + if (VD->isOperator() && VD->isSPI()) { + const DeclContext *useFile = useDC->getModuleScopeContext(); + if (useFile->getParentModule() == sourceDC->getParentModule()) + return true; + auto *useSF = dyn_cast(useFile); + return !useSF || useSF->isImportedAsSPI(VD); + } return true; } + } llvm_unreachable("bad access level"); } diff --git a/test/SPI/Inputs/spi_helper.swift b/test/SPI/Inputs/spi_helper.swift index 506ddd83ccb37..2eef695f7efe7 100644 --- a/test/SPI/Inputs/spi_helper.swift +++ b/test/SPI/Inputs/spi_helper.swift @@ -4,6 +4,10 @@ public protocol PublicProto { associatedtype Assoc } +public struct PublicType { + public init() { } +} + public func publicFunc() { print("publicFunc") } func internalFunc() {} @@ -94,3 +98,6 @@ public enum PublicEnum { case publicCase @_spi(HelperSPI) case spiCase } + +@_spi(HelperSPI) public func -(_ s1: PublicType, _ s2: PublicType) -> PublicType { s1 } +@_spi(HelperSPI) public func +(_ s1: PublicType, _ s2: PublicType) -> PublicType { s1 } diff --git a/test/SPI/public_client.swift b/test/SPI/public_client.swift index 7c59617e4c02e..625cb8174136c 100644 --- a/test/SPI/public_client.swift +++ b/test/SPI/public_client.swift @@ -40,8 +40,14 @@ otherApiFunc() // expected-error {{cannot find 'otherApiFunc' in scope}} public func publicUseOfSPI(param: SPIClass) -> SPIClass {} // expected-error 2{{cannot find type 'SPIClass' in scope}} public func publicUseOfSPI2() -> [SPIClass] {} // expected-error {{cannot find type 'SPIClass' in scope}} +public let o1 = PublicType() +public let o2 = PublicType() + @inlinable -func inlinable() -> SPIClass { // expected-error {{cannot find type 'SPIClass' in scope}} +public func inlinable() -> SPIClass { // expected-error {{cannot find type 'SPIClass' in scope}} spiFunc() // expected-error {{cannot find 'spiFunc' in scope}} _ = SPIClass() // expected-error {{cannot find 'SPIClass' in scope}} + let _ = o1 - o2 // expected-error {{binary operator '-' cannot be applied to two 'PublicType' operands}} } + +let _ = o1 - o2 // expected-error {{binary operator '-' cannot be applied to two 'PublicType' operands}} diff --git a/test/SPI/spi_client.swift b/test/SPI/spi_client.swift index 574ff2569251f..9046af2384d36 100644 --- a/test/SPI/spi_client.swift +++ b/test/SPI/spi_client.swift @@ -61,10 +61,16 @@ public func inlinable1() -> SPIClass { // expected-error {{class 'SPIClass' cann _ = [SPIClass]() // expected-error {{class 'SPIClass' cannot be used in an '@inlinable' function because it is an SPI imported from 'SPIHelper'}} } +public let o1 = PublicType() +public let o2 = PublicType() + @_spi(S) @inlinable public func inlinable2() -> SPIClass { spiFunc() _ = SPIClass() _ = [SPIClass]() + let _ = o1 - o2 } + +let _ = o1 - o2 From a4865be7e36b06a2d4316d586e08d1ed36dd90cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 11 Nov 2025 15:23:36 -0800 Subject: [PATCH 4/4] Sema: Gate SPI operator fix behind EnforceSPIOperatorGroup This new check is source breaking. Let's gate it behind an opt-in flag for now and enable it by default on a new language mode or similar. --- include/swift/Basic/Features.def | 3 +++ lib/AST/Decl.cpp | 4 +++- lib/AST/FeatureSet.cpp | 1 + test/SPI/public_client.swift | 8 ++++++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/swift/Basic/Features.def b/include/swift/Basic/Features.def index 66d57b27aaedc..0da7bf21c9cfe 100644 --- a/include/swift/Basic/Features.def +++ b/include/swift/Basic/Features.def @@ -561,6 +561,9 @@ EXPERIMENTAL_FEATURE(CheckImplementationOnly, true) /// report references to implementation-only imported modules. EXPERIMENTAL_FEATURE(CheckImplementationOnlyStrict, false) +/// Check that use sites have the required @_spi import for operators. +EXPERIMENTAL_FEATURE(EnforceSPIOperatorGroup, true) + #undef EXPERIMENTAL_FEATURE_EXCLUDED_FROM_MODULE_INTERFACE #undef EXPERIMENTAL_FEATURE #undef UPCOMING_FEATURE diff --git a/lib/AST/Decl.cpp b/lib/AST/Decl.cpp index 8a38cbbb7d911..611edf3a5d769 100644 --- a/lib/AST/Decl.cpp +++ b/lib/AST/Decl.cpp @@ -5537,7 +5537,9 @@ static bool checkAccess(const DeclContext *useDC, const ValueDecl *VD, } case AccessLevel::Public: case AccessLevel::Open: { - if (VD->isOperator() && VD->isSPI()) { + if (VD->getASTContext().LangOpts.hasFeature( + Feature::EnforceSPIOperatorGroup) && + VD->isOperator() && VD->isSPI()) { const DeclContext *useFile = useDC->getModuleScopeContext(); if (useFile->getParentModule() == sourceDC->getParentModule()) return true; diff --git a/lib/AST/FeatureSet.cpp b/lib/AST/FeatureSet.cpp index f18edb364e3ae..cdc5f2d3472e7 100644 --- a/lib/AST/FeatureSet.cpp +++ b/lib/AST/FeatureSet.cpp @@ -146,6 +146,7 @@ UNINTERESTING_FEATURE(SameElementRequirements) UNINTERESTING_FEATURE(SendingArgsAndResults) UNINTERESTING_FEATURE(CheckImplementationOnly) UNINTERESTING_FEATURE(CheckImplementationOnlyStrict) +UNINTERESTING_FEATURE(EnforceSPIOperatorGroup) static bool findUnderscoredLifetimeAttr(Decl *decl) { auto hasUnderscoredLifetimeAttr = [](Decl *decl) { diff --git a/test/SPI/public_client.swift b/test/SPI/public_client.swift index 625cb8174136c..c9e994ab06e83 100644 --- a/test/SPI/public_client.swift +++ b/test/SPI/public_client.swift @@ -6,17 +6,21 @@ // RUN: %target-swift-frontend -emit-module %S/Inputs/spi_helper.swift -module-name SPIHelper -emit-module-path %t/SPIHelper.swiftmodule -emit-module-interface-path %t/SPIHelper.swiftinterface -emit-private-module-interface-path %t/SPIHelper.private.swiftinterface -enable-library-evolution -swift-version 5 -parse-as-library /// Reading from swiftmodule -// RUN: %target-typecheck-verify-swift -verify-ignore-unrelated -I %t -verify-ignore-unknown +// RUN: %target-typecheck-verify-swift -verify-ignore-unrelated -I %t -verify-ignore-unknown \ +// RUN: -enable-experimental-feature EnforceSPIOperatorGroup /// Reading from .private.swiftinterface // RUN: rm %t/SPIHelper.swiftmodule -// RUN: %target-typecheck-verify-swift -verify-ignore-unrelated -I %t -verify-ignore-unknown +// RUN: %target-typecheck-verify-swift -verify-ignore-unrelated -I %t -verify-ignore-unknown \ +// RUN: -enable-experimental-feature EnforceSPIOperatorGroup /// Reading from the public .swiftinterface should raise errors on missing /// declarations. // RUN: rm %t/SPIHelper.private.swiftinterface // RUN: not %target-swift-frontend -typecheck -I %t %s +// REQUIRES: swift_feature_EnforceSPIOperatorGroup + import SPIHelper // Use the public API