Skip to content

-O breaks KeyPath resolution #78948

Open
Open
@lhunath

Description

@lhunath

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.compilerThe Swift compiler itselfmanglingArea → compiler: Manglingoptimized onlyFlag: An issue whose reproduction requires optimized compilationrun-time crashBug → crash: Swift code crashed during executionruntimeThe Swift Runtime

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions