Skip to content

Bug: #Predicate macro incorrectly fails for ‘final’ classes #1173

Open
@bdkjones

Description

@bdkjones

Summary

I've discovered that Foundation's #Predicate fails when a class is marked as final. Here's a very simple test case:

final class Master {
    var id: UUID = UUID()
}

let u = UUID()
let p = #Predicate<Master>{ $0.id == u }

That expands to:

Foundation.Predicate<Master>({
    PredicateExpressions.build_Equal(
        lhs: PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_Arg($0),
            keyPath: \.id
        ),
        rhs: PredicateExpressions.build_Arg(u)
    )
})

It compiles correctly, but at runtime crashes on the build_KeyPath line: Thread 1: Fatal error: Predicate does not support keypaths with multiple components

This is obviously not correct; there is only one component in the KeyPath. If you remove final from the class declaration, it works fine.

I see where this check exists in Foundation (

func _validateForPredicateUsage(restrictArguments: Bool = false) {
var ptr = unsafeBitCast(self, to: UnsafeRawPointer.self)
ptr = ptr.advanced(by: Self.WORD_SIZE * 3) // skip isa, type metadata, and KVC string pointers
let header = ptr.load(as: UInt32.self)
ptr = ptr.advanced(by: Self.WORD_SIZE)
let firstComponentHeader = ptr.load(as: UInt32.self)
switch firstComponentHeader._keyPathComponentHeader_kind {
case 1: // struct/tuple/self stored property
fallthrough
case 3: // class stored property
// Key paths to stored properties are only single-component if MemoryLayout.offset(of:) returns an offset
func project<T>(_: T.Type) -> Bool {
_keyPathOffset(T.self, self) == nil
}
if _openExistential(Self.rootType, do: project) {
fatalError("Predicate does not support keypaths with multiple components")
}
case 2: // computed
var componentWords = 3
if firstComponentHeader._keyPathComponentHeader_computedIsSettable {
componentWords += 1
}
if firstComponentHeader._keyPathComponentHeader_computedHasArguments {
if restrictArguments {
fatalError("Predicate does not support keypaths with arguments")
}
let capturesSize = ptr.advanced(by: Self.WORD_SIZE * componentWords).load(as: UInt.self)
componentWords += 2 + (Int(capturesSize) / Self.WORD_SIZE)
}
if header._keyPathHeader_bufferSize > (Self.WORD_SIZE * componentWords) {
fatalError("Predicate does not support keypaths with multiple components")
}
case 4: // optional chain
fatalError("Predicate does not support keypaths with optional chaining/unwrapping")
default: // unknown keypath component
fatalError("Predicate does not support this type of keypath (\(firstComponentHeader._keyPathComponentHeader_kind))")
}
}
}
) but I don't see why final should affect whether a UUID property has an offset.

Any Value

The behavior is not limited to UUID. I can reproduce it with a String, Int, and other values.

Speculation

I can’t find any information on how the presence of final affects the memory layout of a class such that offset(of:) would fail. I thought maybe the issue affects only types that are capable of storing their value without allocation (i.e. tagged pointers), but again I can’t see how final would affect that.

Intermittent

IF a class has a certain combination of properties, this issue does not always manifest. For example, Predicates ran just fine here:

final class Master
{
    var title: String = “”
    var numbers: [Int] = []
    var isEnabled: Bool = false
    var percentage: Double = 57.5
}

But add var id: UUID = UUID() and that fails until final is removed. Given the offset(of:) check, I figured the issue might have something to do with the exact way properties are aligned and packed into the storage for the class. But that’s just a guess.

Environment

Xcode 16.2.

Since #Predicate isn’t available in Playgrounds, I created a new project from the “command line application” template in Xcode. I changed no build settings.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions