NCGenerics: add availability checking

Not all runtimes can correctly operate with types that use noncopyable
generics. When the generic argument of a type is noncopyable, old
runtimes can't recognize that to correctly check conformances that may
be conditional on those arguments being Copyable, etc.

resolves rdar://126239335
This commit is contained in:
Kavon Farvardin
2024-07-02 14:49:46 -07:00
parent 5c57745c1d
commit 04454054b7
7 changed files with 234 additions and 49 deletions

View File

@@ -6737,6 +6737,16 @@ ERROR(availability_isolated_any_only_version_newer, none,
"%0 %1 or newer",
(StringRef, llvm::VersionTuple))
ERROR(availability_copyable_generics_casting_only_version_newer, none,
"runtime support for casting types with noncopyable generic arguments "
"is only available in %0 %1 or newer",
(StringRef, llvm::VersionTuple))
ERROR(availability_escapable_generics_casting_only_version_newer, none,
"runtime support for casting types with nonescapable generic arguments "
"is only available in %0 %1 or newer",
(StringRef, llvm::VersionTuple))
ERROR(availability_typed_throws_only_version_newer, none,
"runtime support for typed throws function types is only available in "
"%0 %1 or newer",

View File

@@ -71,6 +71,8 @@ FEATURE(SwiftExceptionPersonality, (6, 0))
// Metadata support for @isolated(any) function types
FEATURE(IsolatedAny, (6, 0))
FEATURE(NoncopyableGenerics, (6, 0))
FEATURE(TaskExecutor, FUTURE)
FEATURE(Differentiation, FUTURE)
FEATURE(InitRawStructMetadata, FUTURE)

View File

@@ -3032,6 +3032,83 @@ static bool diagnoseTypedThrowsAvailability(
ReferenceDC);
}
/// Make sure the generic arguments conform to all known invertible protocols.
/// Runtimes prior to NoncopyableGenerics do not check if any of the
/// generic arguments conform to Copyable/Escapable during dynamic casts.
/// But a dynamic cast *needs* to check if the generic arguments conform,
/// to determine if the cast should be permitted at all. For example:
///
/// struct X<T> {}
/// extension X: P where T: Y {}
///
/// func f<Y: ~Copyable>(...) {
/// let x: X<Y> = ...
/// _ = x as? any P // <- cast should fail
/// }
///
/// The dynamic cast here must fail because Y does not conform to Copyable,
/// thus X<Y> doesn't conform to P!
///
/// \param boundTy The generic type with its generic arguments.
/// \returns the invertible protocol for which a conformance is missing in
/// one of the generic arguments, or none if all are present for
/// every generic argument.
static std::optional<InvertibleProtocolKind> checkGenericArgsForInvertibleReqs(
BoundGenericType *boundTy) {
for (auto arg : boundTy->getGenericArgs()) {
for (auto ip : InvertibleProtocolSet::allKnown()) {
switch (ip) {
case InvertibleProtocolKind::Copyable:
if (arg->isNoncopyable())
return ip;
break;
case InvertibleProtocolKind::Escapable:
if (!arg->isEscapable())
return ip;
}
}
}
return std::nullopt;
}
/// Older runtimes won't check for required invertible protocol conformances
/// at runtime during a cast.
///
/// \param srcType the source or initial type of the cast
/// \param refLoc source location of the cast
/// \param refDC decl context in which the cast occurs
/// \return true if diagnosed
static bool checkInverseGenericsCastingAvailability(Type srcType,
SourceRange refLoc,
const DeclContext *refDC) {
if (!srcType) return false;
auto type = srcType->getCanonicalType();
if (auto boundTy = dyn_cast<BoundGenericType>(type)) {
if (auto missing = checkGenericArgsForInvertibleReqs(boundTy)) {
std::optional<Diag<StringRef, llvm::VersionTuple>> diag;
switch (*missing) {
case InvertibleProtocolKind::Copyable:
diag =
diag::availability_copyable_generics_casting_only_version_newer;
break;
case InvertibleProtocolKind::Escapable:
diag =
diag::availability_escapable_generics_casting_only_version_newer;
break;
}
// Enforce the availability restriction.
return TypeChecker::checkAvailability(
refLoc,
refDC->getASTContext().getNoncopyableGenericsAvailability(),
*diag,
refDC);
}
}
}
static bool checkTypeMetadataAvailabilityInternal(CanType type,
SourceRange refLoc,
const DeclContext *refDC) {
@@ -3075,7 +3152,13 @@ static bool checkTypeMetadataAvailabilityForConverted(Type refType,
// there.
if (type.isAnyExistentialType()) return false;
return checkTypeMetadataAvailabilityInternal(type, refLoc, refDC);
if (checkTypeMetadataAvailabilityInternal(type, refLoc, refDC))
return true;
if (checkInverseGenericsCastingAvailability(type, refLoc, refDC))
return true;
return false;
}
namespace {
@@ -3476,6 +3559,9 @@ public:
if (auto EE = dyn_cast<ErasureExpr>(E)) {
checkTypeMetadataAvailability(EE->getSubExpr()->getType(),
EE->getLoc(), Where.getDeclContext());
checkInverseGenericsCastingAvailability(EE->getSubExpr()->getType(),
EE->getLoc(),
Where.getDeclContext());
for (ProtocolConformanceRef C : EE->getConformances()) {
diagnoseConformanceAvailability(E->getLoc(), C, Where, Type(), Type(),

View File

@@ -0,0 +1,129 @@
// RUN: %target-typecheck-verify-swift \
// RUN: -debug-diagnostic-names -target arm64-apple-macos14.4 \
// RUN: -enable-experimental-feature NonescapableTypes
// REQUIRES: OS=macosx || OS=ios || OS=tvos || OS=watchOS || OS=xros
protocol P {}
struct NCG<T: ~Copyable> {}
extension NCG: P where T: Copyable {} // expected-note 2{{requirement_implied_by_conditional_conformance}}
struct NEG<T: ~Escapable> {}
extension NEG: P {} // expected-note {{requirement_implied_by_conditional_conformance}}
struct All {}
struct NoCopy: ~Copyable {}
struct NoEscape: ~Escapable {}
/// MARK: dynamic casts are gated by availability. Older runtimes don't check
/// for conformance to Copyable so they'll give bogus results.
// expected-note@+1 8{{availability_add_attribute}}
func dyn_cast_errors<T: ~Copyable, V: ~Escapable>(
_ generic: NCG<T>, _ concrete: NCG<NoCopy>,
_ genericEsc: NEG<V>, _ concreteEsc: NEG<NoEscape>) {
_ = concrete as? any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = generic as? any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = concrete is any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = generic is any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = concrete as! any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = generic as! any P // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = genericEsc as? any P // expected-error {{availability_escapable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = concreteEsc is any P // expected-error {{availability_escapable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
}
@available(SwiftStdlib 6.0, *)
func FIXED_dyn_cast_errors<T: ~Copyable, V: ~Escapable>(
_ generic: NCG<T>, _ concrete: NCG<NoCopy>,
_ genericEsc: NEG<V>, _ concreteEsc: NEG<NoEscape>) {
_ = concrete as? any P
_ = generic as? any P
_ = concrete is any P
_ = generic is any P
_ = concrete as! any P
_ = generic as! any P
_ = genericEsc as? any P
_ = concreteEsc is any P
}
func noAvailabilityNeeded<T>(_ generic: NCG<T>, _ concrete: NCG<All>) {
_ = concrete as? any P // expected-warning {{conditional_downcast_coercion}}
_ = generic as? any P // expected-warning {{conditional_downcast_coercion}}
_ = concrete is any P // expected-warning {{isa_is_always_true}}
_ = generic is any P // expected-warning {{isa_is_always_true}}
_ = concrete as! any P // expected-warning {{forced_downcast_coercion}}
_ = generic as! any P // expected-warning {{forced_downcast_coercion}}
_ = concrete as any P
_ = generic as any P
_ = concrete as Any
_ = generic as Any
}
func expected_checked_cast_errors<T: ~Copyable>(_ generic: NCG<T>, _ concrete: NCG<NoCopy>,
_ concreteEsc: NEG<NoEscape>) {
_ = concrete as any P // expected-error {{type_does_not_conform_decl_owner}}
_ = generic as any P // expected-error {{type_does_not_conform_decl_owner}}
_ = concreteEsc as any P // expected-error {{type_does_not_conform_decl_owner}}
}
/// MARK: existential erasure requires availability, because later dynamic casts
/// of that erased type will not correctly check for Copyable generic args.
func eraseImplicit(_ a: Any) {}
// expected-note@+1 9{{availability_add_attribute}}
func erasure_cast_disallowed<T: ~Copyable>(_ generic: NCG<T>, _ concrete: NCG<NoCopy>, _ concreteEsc: NEG<NoEscape>) -> Any {
_ = concrete as Any // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = concreteEsc as Any // expected-error {{availability_escapable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_ = generic as Any // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
let _: Any = concrete // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
let _: Any = generic // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
let _: Any = { concreteEsc }() // expected-error {{availability_escapable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
eraseImplicit(concrete) // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
eraseImplicit(generic) // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
return concrete // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
}
struct Box<Wrapped: ~Copyable>: ~Copyable { // expected-note {{availability_add_attribute}}
private let _pointer: UnsafeMutablePointer<Wrapped>
init(_ element: consuming Wrapped) { // expected-note {{availability_add_attribute}}
_pointer = .allocate(capacity: 1)
print("allocating",_pointer) // expected-error {{availability_copyable_generics_casting_only_version_newer}} // expected-note {{availability_guard_with_version_check}}
_pointer.initialize(to: element)
}
}
/// MARK: misc. operations that are permitted
public protocol Debatable: ~Copyable {}
public func asExistential(_ t: consuming any Debatable & ~Copyable) {}
public func hello<T: Debatable & ~Copyable>(_ t: consuming T) {
asExistential(t)
}
extension UnsafeMutableRawPointer {
public func blahInitializeMemory<T: ~Copyable>(
as type: T.Type, from source: UnsafeMutablePointer<T>, count: Int
) {
_ = UnsafeMutableRawPointer(source + count)
}
}

View File

@@ -1,5 +1,5 @@
// RUN: %target-run-simple-swift(-Xfrontend -sil-verify-all) | %FileCheck %s
// RUN: %target-run-simple-swift(-O -Xfrontend -sil-verify-all) | %FileCheck %s
// RUN: %target-run-simple-swift(-Xfrontend -sil-verify-all -Xfrontend -disable-availability-checking) | %FileCheck %s
// RUN: %target-run-simple-swift(-O -Xfrontend -sil-verify-all -Xfrontend -disable-availability-checking) | %FileCheck %s
// REQUIRES: executable_test

View File

@@ -1,4 +1,4 @@
// RUN: %target-swift-frontend -parse-as-library -O -emit-sil -verify %s
// RUN: %target-swift-frontend -parse-as-library -O -emit-sil -verify %s -disable-availability-checking
extension List {
var peek: Element {
@@ -13,30 +13,8 @@ extension List {
}
}
struct MyPointer<Wrapped: ~Copyable>: Copyable {
var v: UnsafeMutablePointer<Int>
static func allocate(capacity: Int) -> Self {
fatalError()
}
func initialize(to: consuming Wrapped) {
}
func deinitialize(count: Int) {
}
func deallocate() {
}
func move() -> Wrapped {
fatalError()
}
var pointee: Wrapped {
_read { fatalError() }
}
}
struct Box<Wrapped: ~Copyable>: ~Copyable {
private let _pointer: MyPointer<Wrapped>
private let _pointer: UnsafeMutablePointer<Wrapped>
init(_ element: consuming Wrapped) {
_pointer = .allocate(capacity: 1)

View File

@@ -1,27 +1,7 @@
// RUN: %target-swift-frontend -emit-sil -verify %s
// TODO: Remove this and just use the real `UnsafeMutablePointer` when
// noncopyable type support has been upstreamed.
struct MyPointer<Wrapped: ~Copyable>: Copyable {
var v: UnsafeMutablePointer<Int>
static func allocate(capacity: Int) -> Self {
fatalError()
}
func initialize(to: consuming Wrapped) {
}
func deinitialize(count: Int) {
}
func deallocate() {
}
func move() -> Wrapped {
fatalError()
}
}
// RUN: %target-swift-frontend -emit-sil -verify %s -disable-availability-checking
struct Box<Wrapped: ~Copyable>: ~Copyable {
private let _pointer: MyPointer<Wrapped>
private let _pointer: UnsafeMutablePointer<Wrapped>
init(_ element: consuming Wrapped) {
_pointer = .allocate(capacity: 1)