Skip to content

NCGenerics: fix runtime metadata request strategy (demangling vs response) #74864

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 5 commits into from
Jul 16, 2024
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
41 changes: 35 additions & 6 deletions lib/IRGen/GenReflection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,25 @@ getRuntimeVersionThatSupportsDemanglingType(CanType type) {
// involving them.
}

/// Any nominal type that has an inverse requirement in its generic
/// signature uses NoncopyableGenerics. Since inverses are mangled into
/// symbols, a Swift 6.0+ runtime is generally needed to demangle them.
///
/// We make an exception for types in the stdlib, like Optional, since the
/// runtime should still be able to demangle them, based on the availability
/// of the type.
if (auto nominalTy = dyn_cast<NominalOrBoundGenericNominalType>(t)) {
auto *nom = nominalTy->getDecl();
if (auto sig = nom->getGenericSignature()) {
SmallVector<InverseRequirement, 2> inverses;
SmallVector<Requirement, 2> reqs;
sig->getRequirementsWithInverses(reqs, inverses);
if (!inverses.empty() && !nom->getModuleContext()->isStdlibModule()) {
return addRequirement(Swift_6_0);
}
}
}

return false;
});

Expand Down Expand Up @@ -418,21 +437,31 @@ getTypeRefImpl(IRGenModule &IGM,

bool isAlwaysNoncopyable = false;
if (contextualTy->isNoncopyable()) {
isAlwaysNoncopyable = true;

// If the contextual type has any archetypes in it, it's plausible that
// we could end up with a copyable type in some instances. Look for those.
// we could end up with a copyable type in some instances. Look for those
// so we can permit unsafe reflection of the field, by assuming it could
// be Copyable.
if (contextualTy->hasArchetype()) {
// If this is a nominal type, check whether it can ever be copyable.
if (auto nominal = contextualTy->getAnyNominal()) {
if (!nominal->canBeCopyable())
isAlwaysNoncopyable = true;
// If it's a nominal that can ever be Copyable _and_ it's defined in
// the stdlib, assume that we could end up with a Copyable type.
if (nominal->canBeCopyable()
&& nominal->getModuleContext()->isStdlibModule())
isAlwaysNoncopyable = false;
} else {
// Assume that we could end up with a copyable type somehow.
// Assume that we could end up with a Copyable type somehow.
// This allows you to reflect a 'T: ~Copyable' stored in a type.
isAlwaysNoncopyable = false;
}
} else {
isAlwaysNoncopyable = true;
}
}

// The getTypeRefByFunction strategy will emit a forward-compatible runtime
// check to see if the runtime can safely reflect such fields. Otherwise,
// the field will be artificially hidden to reflectors.
if (isAlwaysNoncopyable) {
IGM.IRGen.noteUseOfTypeMetadata(type);
return getTypeRefByFunction(IGM, sig, type);
Expand Down
123 changes: 111 additions & 12 deletions test/IRGen/noncopyable_field_descriptors.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-ir -o - %s -module-name test \
// RUN: %swift-frontend -emit-ir -o - %s -module-name test \
// RUN: -enable-experimental-feature NonescapableTypes \
// RUN: -parse-as-library \
// RUN: -enable-library-evolution \
// RUN: > %t/test.irgen
// RUN: -target %target-cpu-apple-macosx15 \
// RUN: > %t/test_new.irgen

// RUN: %FileCheck %s < %t/test.irgen
// RUN: %swift-frontend -emit-ir -o - %s -module-name test \
// RUN: -enable-experimental-feature NonescapableTypes \
// RUN: -parse-as-library \
// RUN: -enable-library-evolution \
// RUN: -target %target-cpu-apple-macosx14 \
// RUN: > %t/test_old.irgen

// RUN: %FileCheck --check-prefix=NEW %s < %t/test_new.irgen
// RUN: %FileCheck --check-prefix=OLD %s < %t/test_old.irgen

// rdar://124401253
// REQUIRES: OS=macosx || OS=linux || OS=windows-msvc
// REQUIRES: OS=macosx
// UNSUPPORTED: CPU=arm64e

@frozen
Expand All @@ -28,14 +37,104 @@ public enum NeverCopyable<Wrapped: ~Copyable>: ~Copyable {
@frozen
public struct NonCopyable: ~Copyable { }

// CHECK: @"$s4test1CCMF" =
// CHECK-SAME: @"symbolic _____yxG 4test21ConditionallyCopyableOAARi_zrlE"
// CHECK-SAME: @"get_type_metadata Ri_zl4test21ConditionallyCopyableOyAA03NonC0VG.3"
// CHECK-SAME: @"symbolic _____yxG 4test21ConditionallyCopyableOAARi_zrlE"
// CHECK-SAME: @"get_type_metadata Ri_zl4test21ConditionallyCopyableOyAA03NonC0VG.3"
public class C<T: ~Copyable> {
// HINT: when debugging this test, you can look for an `i32 2` field in the
// 'MF' constant as a separator that precedes each field descriptor.

// NEW: @"$s4test8CC_TestsCMF" =
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOyxG.3"
// NEW-SAME: @"symbolic _____yq_G 4test21ConditionallyCopyableOAARi_zrlE"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOyAA03NonC0VG.4"
// NEW-SAME: @"symbolic _____ySSG 4test21ConditionallyCopyableOAARi_zrlE"

// OLD: @"$s4test8CC_TestsCMF" =
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOyxG.3"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOyq_G.4"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOyAA03NonC0VG.5"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test21ConditionallyCopyableOySSG.6"
public class CC_Tests<NCG: ~Copyable, T> {
var ccNCG: ConditionallyCopyable<NCG> = .none
var ccT: ConditionallyCopyable<T> = .none
var ccNC: ConditionallyCopyable<NonCopyable> = .none
var ncT: ConditionallyCopyable<T> = .none
var ncNC: ConditionallyCopyable<NonCopyable> = .none
var ccC: ConditionallyCopyable<String> = .none
}


/// For the "never copyable" fields, we expect to always go through the
/// type metadata accessor strategy, which is designed to hide these
/// fields until a future runtime says they're safe to reflect.

// NEW: @"$s4test8NC_TestsCMF" =
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyxG.5"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyq_G.6"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyAA03NonC0VG.7"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOySSG.8"

// OLD: @"$s4test8NC_TestsCMF" =
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyxG.7"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyq_G.8"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOyAA03NonC0VG.9"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test13NeverCopyableOySSG.10"
public class NC_Tests<NCG: ~Copyable, T> {
var ncNCG: NeverCopyable<NCG> = .none
var ncT: NeverCopyable<T> = .none
var ncNC: NeverCopyable<NonCopyable> = .none
var ncC: NeverCopyable<String> = .none
}


// NEW: @"$s4test17StdlibTypes_TestsCMF" =
// NEW-SAME: @"symbolic xSg"
// NEW-SAME: @"symbolic q_Sg"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test11NonCopyableVSg.9"
// NEW-SAME: @"symbolic SSSg"
// NEW-SAME: @"symbolic SPyxG"
// NEW-SAME: @"symbolic SPyq_G"
// NEW-SAME: @"symbolic SPy_____G 4test11NonCopyableV"
// NEW-SAME: @"symbolic SPySSG"

// OLD: @"$s4test17StdlibTypes_TestsCMF" =
// OLD-SAME: @"symbolic xSg"
// OLD-SAME: @"symbolic q_Sg"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test11NonCopyableVSg.11"
// OLD-SAME: @"symbolic SSSg"
// OLD-SAME: @"symbolic SPyxG"
// OLD-SAME: @"symbolic SPyq_G"
// OLD-SAME: @"symbolic SPy_____G 4test11NonCopyableV"
// OLD-SAME: @"symbolic SPySSG"
public class StdlibTypes_Tests<NCG: ~Copyable, T> {
var optNCG: Optional<NCG> = .none
var optT: Optional<T> = .none
var optNC: Optional<NonCopyable> = .none
var optC: Optional<String> = .none

var upNCG: UnsafePointer<NCG> = .init(bitPattern: 16)!
var upT: UnsafePointer<T> = .init(bitPattern: 32)!
var upNC: UnsafePointer<NonCopyable> = .init(bitPattern: 64)!
var upC: UnsafePointer<String> = .init(bitPattern: 128)!
}


// NEW: @"$s4test19PlainlyStored_TestsCMF" =
// NEW-SAME: @"symbolic x"
// NEW-SAME: @"symbolic q_"
// NEW-SAME: @"get_type_metadata Ri_zr0_l4test11NonCopyableV.10"
// NEW-SAME: @"symbolic SS"

// OLD: @"$s4test19PlainlyStored_TestsCMF" =
// OLD-SAME: @"symbolic x"
// OLD-SAME: @"symbolic q_"
// OLD-SAME: @"get_type_metadata Ri_zr0_l4test11NonCopyableV.12"
// OLD-SAME: @"symbolic SS"
public class PlainlyStored_Tests<NCG: ~Copyable, T> {
var ncg: NCG
var t: T
var concreteNC: NonCopyable
var str: String

public init(_ ncg: consuming NCG, _ t: T) {
self.ncg = ncg
self.t = t
self.concreteNC = NonCopyable()
self.str = ""
}
}
128 changes: 128 additions & 0 deletions test/IRGen/noncopyable_metadata_requests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// RUN: %empty-directory(%t)
// RUN: %swift-frontend %s -target %target-cpu-apple-macosx15 -module-name main -emit-ir -o %t/new.ir
// RUN: %swift-frontend %s -target %target-cpu-apple-macosx14 -module-name main -emit-ir -o %t/old.ir

// RUN: %FileCheck %s --check-prefix=NEW < %t/new.ir
// RUN: %FileCheck %s --check-prefix=OLD < %t/old.ir

// Check that we add extra type metadata accessors for types with generic
// parameters that have an inverse. These are used instead of using demangling
// cache variables since old runtimes cannot synthesize type metadata based on
// the new mangling.

// RUN: %target-build-swift %s -o %t/test_mangling
// RUN: %target-run %t/test_mangling | %FileCheck %s

// REQUIRES: OS=macosx
// REQUIRES: executable_test


// This type's generic parameter is noncopyable, so older runtimes can't
// demangle the type's name to build the metadata.
struct Foo<T: ~Copyable>: ~Copyable {
mutating func bar(_ i: Int) { print("Foo.bar(\(i))") }
}

func test() {
var foo = Foo<Int>()
foo.bar(1)
}
test()
// CHECK: Foo.bar(1)

// NEW: define hidden swiftcc void @"$s4main4testyyF"()
// NEW: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main3FooVySiGMD")
// NEW: }

// OLD: define hidden swiftcc void @"$s4main4testyyF"()
// OLD: call swiftcc %swift.metadata_response @"$s4main3FooVySiGMa"(i64 0)
// OLD: }

struct NC: ~Copyable {}

// NEW: define hidden swiftcc void @"$s4main10testWithNCyyF"()
// NEW: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main3FooVyAA2NCVGMD")
// NEW: }

// OLD: define hidden swiftcc void @"$s4main10testWithNCyyF"()
// OLD: call swiftcc %swift.metadata_response @"$s4main3FooVyAA2NCVGMa"
// OLD: }
func testWithNC() {
var foo = Foo<NC>()
foo.bar(2)
}
testWithNC()
// CHECK: Foo.bar(2)


// NEW: define hidden swiftcc void @"$s4main17testWithNCGenericyyxnRi_zlF"
// NEW: call swiftcc %swift.metadata_response @"$s4main3FooVMa"
// NEW: }

// OLD: define hidden swiftcc void @"$s4main17testWithNCGenericyyxnRi_zlF"
// OLD: call swiftcc %swift.metadata_response @"$s4main3FooVMa"
// OLD: }
func testWithNCGeneric<T: ~Copyable>(_ t: consuming T) {
var foo = Foo<T>()
foo.bar(3)
}
testWithNCGeneric(Foo<NC>())
// CHECK: Foo.bar(3)


// This type does not need a Swift 6.0 runtime, despite being noncopyable,
// because it doesn't have a noncopyable generic parameter.
struct JustNoncopyable<T>: ~Copyable {
mutating func bar() { print("JustNoncopyable.bar") }
}

func testNonGeneric() {
var ng = JustNoncopyable<Int>()
ng.bar()
}
testNonGeneric()
// CHECK: JustNoncopyable.bar

// NEW: define hidden swiftcc void @"$s4main14testNonGenericyyF"()
// NEW: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main15JustNoncopyableVySiGMD")
// NEW: }

// OLD: define hidden swiftcc void @"$s4main14testNonGenericyyF"()
// OLD: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main15JustNoncopyableVySiGMD")
// OLD: }


/// Check that Optional still uses `__swift_instantiateConcreteTypeFromMangledName`
/// even when calling a method available to a noncopyable Optional.
extension Optional where Wrapped: ~Copyable {
mutating func bar(_ i: Int) { print("Optional.bar(\(i))") }
}

// NEW: define hidden swiftcc void @"$s4main20testCopyableOptionalyyF"()
// NEW: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$sSiSgMD")
// NEW: }

// OLD: define hidden swiftcc void @"$s4main20testCopyableOptionalyyF"()
// OLD: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$sSiSgMD")
// OLD: }
func testCopyableOptional() {
var opt = Optional<Int>(94103)
opt.bar(1)
}
testCopyableOptional()
// CHECK: Optional.bar(1)


// NEW: define hidden swiftcc void @"$s4main23testNOTCopyableOptionalyyF"()
// NEW: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main2NCVSgMD")
// NEW: }

// OLD: define hidden swiftcc void @"$s4main23testNOTCopyableOptionalyyF"()
// OLD: call ptr @__swift_instantiateConcreteTypeFromMangledName(ptr @"$s4main2NCVSgMD")
// OLD: }
func testNOTCopyableOptional() {
var opt = Optional<NC>(NC())
opt.bar(2)
}
testNOTCopyableOptional()
// CHECK: Optional.bar(2)
57 changes: 57 additions & 0 deletions test/Interpreter/moveonly_reflection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// RUN: %empty-directory(%t)

// RUN: %target-build-swift -D CRASH %s -o %t/crash
// RUN: %target-codesign %t/crash

// RUN: %target-build-swift %s -o %t/exe
// RUN: %target-codesign %t/exe

// RUN: %target-run %t/exe | %FileCheck %s
// RUN: %target-run not --crash %t/crash

// REQUIRES: executable_test

// Simulators and devices don't appear to have the 'not' binary in their PATH
// to handle tests that intentionally crash such as this.
// UNSUPPORTED: DARWIN_SIMULATOR={{.*}}
// UNSUPPORTED: remote_run || device_run

enum Maybe<Wrapped: ~Copyable>: ~Copyable {
case some(Wrapped)
case none
}
extension Maybe: Copyable where Wrapped: Copyable {}

struct NC: ~Copyable {
let data: Int
}

class Protected<T: ~Copyable> {
var field: Maybe<T>
init(_ t: consuming T) {
self.field = .some(t)
}
}

class Dangerous<T: ~Copyable> {
var field: Optional<T>
init(_ t: consuming T) {
self.field = .some(t)
}
}

func printFields<T: Copyable>(_ val: T) {
let mirror = Mirror.init(reflecting: val)
mirror.children.forEach { print($0.label ?? "", $0.value) }
}

defer { test() }
func test() {
#if CRASH
printFields(Dangerous(NC(data: 22)))
#else
printFields(Protected("oreo")) // CHECK: field ()
printFields(Protected(NC(data: 11))) // CHECK: field ()
printFields(Dangerous("spots")) // CHECK: field Optional("spots")
#endif
}