-
Notifications
You must be signed in to change notification settings - Fork 197
Description
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 (
swift-foundation/Sources/FoundationEssentials/Predicate/KeyPath+Inspection.swift
Lines 50 to 88 in 2f4f5b8
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))") | |
} | |
} | |
} |
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.