Description
Description
It appears that the Swift optimizer interferes with resolution of key paths for _enclosingInstance
in certain situations.
Reproduction
Consider an ObservableObject
whose property is modified indirectly:
public struct CoreServer {
public var specifiers: [String]
}
public class Container<Value>: ObservableObject {
@Published
public var selection: [Value] = []
@PublishedComputed<Value?, Container>(get: { $0.selection.first }, set: { $0.selection = $1.flatMap { [$0] } ?? [] })
public var selected: Value?
}
let source = Container<CoreServer>()
source.selected?.specifiers = []
(PublishedComputed
is a custom property wrapper which uses _enclosingInstance
, see below)
This works fine when the optimizer is off (-Onone
) but when optimizing (-O
/ -Os
), crashes with:
Swift/KeyPath.swift:2838: Fatal error: could not demangle keypath type from '��O�
(lldb) bt
warning: Test was compiled with optimization - stepping may behave oddly; variables may not be available.* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: could not demangle keypath type from '��O�
frame #0: 0x000000019fc45038 libswiftCore.dylib`_swift_runtime_on_report
frame #1: 0x000000019fd21454 libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 208
frame #2: 0x000000019f8b1390 libswiftCore.dylib`closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 104
frame #3: 0x000000019f8b05d8 libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 256
frame #4: 0x000000019f9fc1cc libswiftCore.dylib`Swift._resolveKeyPathGenericArgReference(_: Swift.UnsafeRawPointer, genericEnvironment: Swift.Optional<Swift.UnsafeRawPointer>, arguments: Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.UnsafeRawPointer + 1112
frame #5: 0x000000019f9fc554 libswiftCore.dylib`generic specialization <Swift.GetKeyPathClassAndInstanceSizeFromPattern> of Swift._walkKeyPathPattern<τ_0_0 where τ_0_0: Swift.KeyPathPatternVisitor>(_: Swift.UnsafeRawPointer, walker: inout τ_0_0) -> () + 124
frame #6: 0x000000019f9fb9c0 libswiftCore.dylib`Swift._getKeyPathClassAndInstanceSizeFromPattern(Swift.UnsafeRawPointer, Swift.UnsafeRawPointer) -> (keyPathClass: Swift.AnyKeyPath.Type, rootType: Any.Type, size: Swift.Int, alignmentMask: Swift.Int) + 72
frame #7: 0x000000019f9fb790 libswiftCore.dylib`Swift._swift_getKeyPath(pattern: Swift.UnsafeMutableRawPointer, arguments: Swift.UnsafeRawPointer) -> Swift.UnsafeRawPointer + 132
frame #8: 0x0000000100abe088 Test`Container.selected.modify() at <stdin>:0 [opt]
* frame #9: 0x0000000100abedc4 Test`closure #1 in ContentView.body.getter() at ContentView.swift:33:37 [opt]
(Note: the runtime for these backtraces is iOS 18.1.1, 22B91)
Furthermore, when the type of the Container
is an existential, the problem becomes further obfuscated:
public protocol Server {
var specifiers: [String] { get set }
}
public struct CoreServer: Server {
public var specifiers: [String]
}
let source = Container<Server>()
source.selected?.specifiers = []
This time, we do not observe the Fatal error
assertion, indicating the source of the issue. We just crash, attempting to resolve the metadata:
(lldb) bt
warning: Test was compiled with optimization - stepping may behave oddly; variables may not be available.* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x000000019fc5b37c libswiftCore.dylib`swift::TargetMetadata<swift::InProcess>::isCanonicalStaticallySpecializedGenericMetadata() const + 280
frame #1: 0x000000019fc73b58 libswiftCore.dylib`areAllTransitiveMetadataComplete_cheap(swift::TargetMetadata<swift::InProcess> const*)::$_0::operator()(swift::TargetMetadata<swift::InProcess> const*) const + 24
frame #2: 0x000000019fc73a54 libswiftCore.dylib`areAllTransitiveMetadataComplete_cheap(swift::TargetMetadata<swift::InProcess> const*) + 788
frame #3: 0x000000019fc5e0d4 libswiftCore.dylib`_swift_getGenericMetadata(swift::MetadataRequest, void const* const*, swift::TargetTypeContextDescriptor<swift::InProcess> const*) + 2580
frame #4: 0x000000019fc2865c libswiftCore.dylib`__swift_instantiateCanonicalPrespecializedGenericMetadata + 40
frame #5: 0x000000010249f7f8 Test`type metadata completion function for Container at <compiler-generated>:0 [opt]
frame #6: 0x000000019fc70e30 libswiftCore.dylib`swift::MetadataCacheEntryBase<(anonymous namespace)::GenericCacheEntry, void const*>::doInitialization(swift::MetadataWaitQueue::Worker&, swift::MetadataRequest) + 212
frame #7: 0x000000019fc5e108 libswiftCore.dylib`_swift_getGenericMetadata(swift::MetadataRequest, void const* const*, swift::TargetTypeContextDescriptor<swift::InProcess> const*) + 2632
frame #8: 0x000000010249d838 Test`__swift_instantiateGenericMetadata at <compiler-generated>:0 [opt]
frame #9: 0x000000019fc93c4c libswiftCore.dylib`(anonymous namespace)::DecodedMetadataBuilder::createBoundGenericType(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::MetadataOrPack>, swift::MetadataOrPack) const + 708
frame #10: 0x000000019fc90364 libswiftCore.dylib`swift::Demangle::__runtime::TypeDecoder<(anonymous namespace)::DecodedMetadataBuilder>::decodeMangledType(swift::Demangle::__runtime::Node*, unsigned int, bool) + 9904
frame #11: 0x000000019fc8b070 libswiftCore.dylib`swift_getTypeByMangledNodeImpl(swift::MetadataRequest, swift::Demangle::__runtime::Demangler&, swift::Demangle::__runtime::Node*, void const* const*, std::__1::function<void const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>) + 892
frame #12: 0x000000019fc8ac14 libswiftCore.dylib`swift_getTypeByMangledNode + 836
frame #13: 0x000000019fc8b6dc libswiftCore.dylib`swift_getTypeByMangledNameImpl(swift::MetadataRequest, __swift::__runtime::llvm::StringRef, void const* const*, std::__1::function<void const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>) + 1196
frame #14: 0x000000019fc8519c libswiftCore.dylib`swift_getTypeByMangledName + 836
frame #15: 0x000000019fc84c78 libswiftCore.dylib`swift_getTypeByMangledNameInEnvironment + 180
frame #16: 0x000000019f9fbe7c libswiftCore.dylib`Swift._resolveKeyPathGenericArgReference(_: Swift.UnsafeRawPointer, genericEnvironment: Swift.Optional<Swift.UnsafeRawPointer>, arguments: Swift.Optional<Swift.UnsafeRawPointer>) -> Swift.UnsafeRawPointer + 264
frame #17: 0x000000019f9fc554 libswiftCore.dylib`generic specialization <Swift.GetKeyPathClassAndInstanceSizeFromPattern> of Swift._walkKeyPathPattern<τ_0_0 where τ_0_0: Swift.KeyPathPatternVisitor>(_: Swift.UnsafeRawPointer, walker: inout τ_0_0) -> () + 124
frame #18: 0x000000019f9fb9c0 libswiftCore.dylib`Swift._getKeyPathClassAndInstanceSizeFromPattern(Swift.UnsafeRawPointer, Swift.UnsafeRawPointer) -> (keyPathClass: Swift.AnyKeyPath.Type, rootType: Any.Type, size: Swift.Int, alignmentMask: Swift.Int) + 72
frame #19: 0x000000019f9fb790 libswiftCore.dylib`Swift._swift_getKeyPath(pattern: Swift.UnsafeMutableRawPointer, arguments: Swift.UnsafeRawPointer) -> Swift.UnsafeRawPointer + 132
frame #20: 0x000000010249e0cc Test`Container.selected.modify() at <stdin>:0 [opt]
* frame #21: 0x000000010249eef8 Test`closure #1 in ContentView.body.getter() at ContentView.swift:33:37 [opt]
As mentioned, these examples rely on a property wrapper which observes the enclosing instance:
/// Variant of @Published for use with computed properties.
///
/// Exposes a publisher on computed properties that can be used to monitor the computed property's value as the enclosing object changes.
@propertyWrapper
public struct PublishedComputed<Value, T: ObservableObject> {
public static subscript(
_enclosingInstance observed: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
)
-> Value {
get {
observed[keyPath: storageKeyPath].get(observed)
}
set {
// swiftlint:disable:next force_cast
(observed.objectWillChange as! ObservableObjectPublisher).send()
observed[keyPath: storageKeyPath].set(observed, newValue)
}
}
public private(set) static subscript(
_enclosingInstance observed: T,
projected wrappedKeyPath: ReferenceWritableKeyPath<T, CurrentValue<Value, Never>>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
)
-> CurrentValue<Value, Never> {
get {
// Publish both the initial value (Just) as well as future changes to the object (objectWillChange).
// Debounce the changes since they are published before the updated value is available from the object (will-change).
CurrentValue(
Just(observed[keyPath: storageKeyPath].get(observed))
.merge(with: observed.objectWillChange.latest(on: .main).map { _ in observed[keyPath: storageKeyPath].get(observed) })
)
}
// swiftlint:disable:next unused_setter_value
set { fatalError() }
}
public init(get: @escaping (T) -> Value, set: @escaping (T, Value) -> Void) {
self.get = get
self.set = set
}
@available(*, unavailable, message: "@PublishedComputed can only be used in object types.")
public var wrappedValue: Value {
get { fatalError() }
// swiftlint:disable:next unused_setter_value - https://github.com/realm/SwiftLint/issues/3863
set { fatalError() }
}
@available(*, unavailable, message: "@PublishedComputed can only be used in object types.")
public var projectedValue: CurrentValue<Value, Never> {
get { fatalError() }
// swiftlint:disable:next unused_setter_value - https://github.com/realm/SwiftLint/issues/3863
set { fatalError() }
}
// - Private
private var get: (T) -> Value, set: (T, Value) -> Void
}
Expected behavior
The code should have the same runtime effect regardless of whether -O
or -Onone
are enabled.
Environment
$ swiftc -version
swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0
Runtime: iOS 18.1.1, 22B91, iPhone 16 Pro
Additional information
No response