diff --git a/Package.resolved b/Package.resolved index 27b0692e..f10a664f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5e3702487c06d4bad355311803d0cebe543527e4de91cb6857a25375e22f85a9", + "originHash" : "b9fba48e8675bc3d76b8bb419a70ff1739d473df3240c29e20a4d2bfc78dd1c0", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index 86c26847..21e2a6c6 100644 --- a/Package.swift +++ b/Package.swift @@ -163,6 +163,7 @@ let openSwiftUICoreTestTarget = Target.testTarget( dependencies: [ "OpenSwiftUI", // NOTE: For the Glue link logic only, do not call `import OpenSwiftUI` in this target "OpenSwiftUICore", + "OpenSwiftUITestsSupport", .product(name: "Numerics", package: "swift-numerics"), ], exclude: ["README.md"], @@ -193,6 +194,14 @@ let openSwiftUITarget = Target.target( swiftSettings: sharedSwiftSettings ) +let openSwiftUITestsSupportTarget = Target.target( + name: "OpenSwiftUITestsSupport", + dependencies: [ + "OpenSwiftUI", + ], + swiftSettings: sharedSwiftSettings +) + let openSwiftUIExtensionTarget = Target.target( name: "OpenSwiftUIExtension", dependencies: [ @@ -205,6 +214,7 @@ let openSwiftUITestTarget = Target.testTarget( name: "OpenSwiftUITests", dependencies: [ "OpenSwiftUI", + "OpenSwiftUITestsSupport", ], exclude: ["README.md"], swiftSettings: sharedSwiftSettings @@ -213,6 +223,7 @@ let openSwiftUITestTarget = Target.testTarget( let openSwiftUICompatibilityTestTarget = Target.testTarget( name: "OpenSwiftUICompatibilityTests", dependencies: [ + "OpenSwiftUITestsSupport", .product(name: "Numerics", package: "swift-numerics"), ], exclude: ["README.md"], @@ -238,6 +249,7 @@ let openSwiftUIBridgeTestTarget = Target.testTarget( name: "OpenSwiftUIBridgeTests", dependencies: [ "OpenSwiftUIBridge", + "OpenSwiftUITestsSupport", ], exclude: ["README.md"], sources: ["BridgeableTests.swift", bridgeFramework], @@ -246,8 +258,8 @@ let openSwiftUIBridgeTestTarget = Target.testTarget( // MARK: - OpenSwiftUISymbolDualTests Target -let openSwiftUISymbolDualTestsHelperTarget = Target.target( - name: "OpenSwiftUISymbolDualTestsHelper", +let openSwiftUISymbolDualTestsSupportTarget = Target.target( + name: "OpenSwiftUISymbolDualTestsSupport", dependencies: [ .product(name: "SymbolLocator", package: "SymbolLocator"), ], @@ -262,7 +274,8 @@ let openSwiftUISymbolDualTestsTarget = Target.testTarget( name: "OpenSwiftUISymbolDualTests", dependencies: [ "OpenSwiftUI", - .target(name: "OpenSwiftUISymbolDualTestsHelper"), + "OpenSwiftUITestsSupport", + "OpenSwiftUISymbolDualTestsSupport", ], exclude: ["README.md"], swiftSettings: sharedSwiftSettings @@ -323,6 +336,7 @@ let package = Package( cOpenSwiftUITarget, openSwiftUITarget, + openSwiftUITestsSupportTarget, openSwiftUIExtensionTarget, openSwiftUITestTarget, openSwiftUICompatibilityTestTarget, @@ -330,7 +344,7 @@ let package = Package( openSwiftUIBridgeTarget, openSwiftUIBridgeTestTarget, - openSwiftUISymbolDualTestsHelperTarget, + openSwiftUISymbolDualTestsSupportTarget, openSwiftUISymbolDualTestsTarget ] ) diff --git a/Sources/OpenSwiftUICore/Data/Other/BloomFilter.swift b/Sources/OpenSwiftUICore/Data/Other/BloomFilter.swift index 0bd1f38d..7e967f2e 100644 --- a/Sources/OpenSwiftUICore/Data/Other/BloomFilter.swift +++ b/Sources/OpenSwiftUICore/Data/Other/BloomFilter.swift @@ -94,10 +94,4 @@ package struct BloomFilter: Equatable { let bit2 = 1 &<< (value &>> 0x4) self.value = bit0 | bit1 | bit2 } - - // FIXME: - @inline(__always) - package func match(_ filter: BloomFilter) -> Bool { - (value & filter.value) == value - } } diff --git a/Sources/OpenSwiftUICore/Data/Property/DerivedPropertyKey.swift b/Sources/OpenSwiftUICore/Data/Property/DerivedPropertyKey.swift deleted file mode 100644 index 7b81c9ec..00000000 --- a/Sources/OpenSwiftUICore/Data/Property/DerivedPropertyKey.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// DerivedPropertyKey.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: Complete - -package protocol DerivedPropertyKey { - associatedtype Value: Equatable - static func value(in: PropertyList) -> Value -} diff --git a/Sources/OpenSwiftUICore/Data/Property/PropertyKey.swift b/Sources/OpenSwiftUICore/Data/Property/PropertyKey.swift deleted file mode 100644 index 11a221e3..00000000 --- a/Sources/OpenSwiftUICore/Data/Property/PropertyKey.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// PropertyKey.swift -// OpenSwiftUICore -// -// Audited for iOS 18.0 -// Status: Complete - -import OpenGraphShims - -package protocol PropertyKey { - associatedtype Value - static var defaultValue: Value { get } - static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool -} - -extension PropertyKey where Value: Equatable { - package static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool { - lhs == rhs - } -} - -extension PropertyKey { - package static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool { - compareValues(lhs, rhs) - } -} diff --git a/Sources/OpenSwiftUICore/Data/Property/PropertyList.swift b/Sources/OpenSwiftUICore/Data/Property/PropertyList.swift deleted file mode 100644 index b0e6554b..00000000 --- a/Sources/OpenSwiftUICore/Data/Property/PropertyList.swift +++ /dev/null @@ -1,511 +0,0 @@ -// -// PropertyList.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Blocked by merge & WIP in 2024 -// ID: 2B32D570B0B3D2A55DA9D4BFC1584D20 (RELEASE_2021) -// ID: D64CE6C88E7413721C59A34C0C940F2C (RELEASE_2024) - -import OpenSwiftUI_SPI -import OpenGraphShims - -// MARK: - PropertyList - -/// A mutable container of key-value pairs -@usableFromInline -@frozen -package struct PropertyList: CustomStringConvertible { - // FIXME: should be internal - @usableFromInline - package var elements: Element? - - @inlinable - package init() { - elements = nil - } - - package init(data: AnyObject?) { - guard let data else { - elements = nil - return - } - elements = (data as! Element) - } - - @inlinable - package var data: AnyObject? { - elements - } - - @inlinable - package var isEmpty: Bool { - elements === nil - } - - package var id: UniqueID { - elements?.id ?? .invalid - } - - package mutating func override(with other: PropertyList) { - if let element = elements { - // TO BE AUDITED in 2024 - elements = element.byPrepending(other.elements) - } else { - elements = other.elements - } - } - - // TO BE AUDITED in 2024 BEGIN - - package subscript(key: K.Type) -> K.Value where K: PropertyKey { - get { - withExtendedLifetime(key) { - guard let result = find(elements.map { .passUnretained($0) }, key: key) else { - return K.defaultValue - } - return result.takeUnretainedValue().value - } - } - set { - if let result = find(elements.map { .passUnretained($0) }, key: key) { - guard !compareValues( - newValue, - result.takeUnretainedValue().value - ) else { - return - } - } - elements = TypedElement(value: newValue, before: nil, after: elements) - } - } - - package subscript(key: K.Type) -> K.Value where K: DerivedPropertyKey { - // preconditionFailure("TODO") - K.value(in: self) - } - - @usableFromInline - package var description: String { - var description = "[" - var shouldAddSeparator = false - elements?.forEach { element, stop in - let element = element.takeUnretainedValue() - if shouldAddSeparator { - description.append(", ") - } else { - shouldAddSeparator = true - } - description.append(element.description) - } - description.append("]") - return description - } - - func forEach(keyType: Key.Type, _ body: (Key.Value, inout Swift.Bool) -> Void) { - guard let elements else { - return - } - elements.forEach { element, stop in - let element = element.takeUnretainedValue() - guard element.keyType == Key.self else { - return - } - body((element as! TypedElement).value, &stop) - } - } - - func mayNotBeEqual(to: PropertyList) -> Bool { - let equalResult: Bool - if let elements { - var ignoredTypes = Set() - equalResult = elements.isEqual(to: to.elements, ignoredTypes: &ignoredTypes) - } else { - equalResult = to.elements == nil - } - return !equalResult - } - - mutating func merge(_ plist: PropertyList) { - // preconditionFailure("TODO") - } -} - -@available(*, unavailable) -extension PropertyList: Sendable {} - -// MARK: - PropertyList Help functions - -private func find( - _ element: Unmanaged?, - key: Key.Type, - keyFilter: BloomFilter = BloomFilter(type: Key.self) -) -> Unmanaged>? { - guard var element else { - return nil - } - repeat { - guard keyFilter.match(element.map(\.keyFilter)) else { - return nil - } - if let before = element.map(\.before), - let result = find(before, key: key, keyFilter: keyFilter) { - return result - } - if element.map(\.keyType) == Key.self { - return element.map { $0 as? TypedElement } - } - guard let after = element.map(\.after) else { - break - } - element = after - } while(true) - return nil -} - -// MARK: - PropertyList.Element - -extension PropertyList { - @usableFromInline - package class Element: CustomStringConvertible { - let keyType: Any.Type - let before: Element? - let after: Element? - let length: Int - let keyFilter: BloomFilter - let id = UniqueID() - - init(keyType: Any.Type, before: Element?, after: Element?) { - self.keyType = keyType - self.before = before - self.after = after - var length = 1 - var keyFilter = BloomFilter(type: keyType) - if let before { - length += before.length - keyFilter.value |= before.keyFilter.value - } - if let after { - length += after.length - keyFilter.value |= after.keyFilter.value - } - self.length = length - self.keyFilter = keyFilter - } - - @usableFromInline - package var description: String { preconditionFailure("") } - - func matches(_: Element, ignoredTypes _: inout Set) -> Bool { - preconditionFailure("") - } - - func hasMatchingValue(in _: Unmanaged?) -> Bool { - preconditionFailure("") - } - - func copy(before _: Element?, after _: Element?) -> Element { - preconditionFailure("") - } - - final package func byPrepending(_ element: Element?) -> Element { - guard let element else { - return self - } - return if before != nil { - TypedElement(value: EmptyKey.defaultValue, before: element, after: self) - } else { - copy(before: element, after: after) - } - } - - final package func isEqual(to element: Element?, ignoredTypes: inout Set) -> Bool { - guard let element else { - return false - } - guard length == element.length else { - return false - } - guard self !== element else { - return true - } - guard matches(element, ignoredTypes: &ignoredTypes) else { - return false - } - var element1 = self - var element2 = element - repeat { - if let before1 = element1.before { - guard before1.isEqual(to: element2.before, ignoredTypes: &ignoredTypes) else { - return false - } - } else { - guard element2.before == nil else { - return false - } - } - if let after1 = element1.after { - guard let after2 = element2.after else { - return false - } - guard after1 !== after2 else { - return true - } - guard after1.isEqual(to: after2, ignoredTypes: &ignoredTypes) else { - return false - } - element1 = after1 - element2 = after2 - } else { - return element.after == nil - } - } while(true) - } - - final func forEach(_ body: ( - _ element: Unmanaged, - _ stop: inout Bool - ) -> Void) { - var element = self - var stop = false - repeat { - if let before = element.before { - before.forEach(body) - } - body(.passUnretained(element), &stop) - guard !stop else { break } - guard let after = element.after else { - break - } - element = after - } while(true) - } - } -} - -// MARK: - TypedElement - -private class TypedElement: PropertyList.Element { - var value: Key.Value - - init(value: Key.Value, before: PropertyList.Element?, after: PropertyList.Element?) { - self.value = value - super.init(keyType: Key.self, before: before, after: after) - } - - override var description: String { - "\(Key.self) = \(value)" - } - - override func matches(_ element: PropertyList.Element, ignoredTypes: inout Set) -> Bool { - guard let typedElement = element as? TypedElement else { - return false - } - guard !ignoredTypes.contains(ObjectIdentifier(Key.self)) else { - return true - } - guard compareValues(value, typedElement.value) else { - return false - } - ignoredTypes.insert(ObjectIdentifier(Key.self)) - return true - } - - override func copy(before: PropertyList.Element?, after: PropertyList.Element?) -> PropertyList.Element { - TypedElement(value: value, before: before, after: after) - } -} - -// MARK: - PropertyList.Tracker - -extension PropertyList { - package class Tracker { - @AtomicBox - private var data: TrackerData - - package init() { - _data = AtomicBox(wrappedValue: .init( - plistID: .invalid, - values: [:], - derivedValues: [:], - invalidValues: [], - unrecordedDependencies: false - )) - } - - package func value(_ plist: PropertyList, for keyType: Key.Type) -> Key.Value { - $data.access { data in - guard match(data: data, plist: plist) else { - data.unrecordedDependencies = true - return plist[keyType] - } - if let trackedValue = data.values[ObjectIdentifier(Key.self)] { - return trackedValue.unwrap() - } else { - let value = plist[keyType] - let trackedValue = TrackedValue(value: value) - data.values[ObjectIdentifier(Key.self)] = trackedValue - return value - } - } - } - - package func derivedValue(_ plist: PropertyList, for keyType: Key.Type) -> Key.Value { - preconditionFailure("TODO") - } - - package func initializeValues(from: PropertyList) { - data.plistID = from.elements?.id ?? .invalid - } - - package func invalidateValue(for keyType: Key.Type, from: PropertyList, to: PropertyList) { - $data.access { data in - guard let id = match(data: data, from: from, to: to) else { - return - } - let removedValue = data.values.removeValue(forKey: ObjectIdentifier(Key.self)) - if let removedValue { - data.invalidValues.append(removedValue) - } - move(&data.derivedValues, to: &data.invalidValues) - data.plistID = id - } - } - - package func invalidateAllValues(from: PropertyList, to: PropertyList) { - $data.access { data in - guard let id = match(data: data, from: from, to: to) else { - return - } - move(&data.values, to: &data.invalidValues) - move(&data.derivedValues, to: &data.invalidValues) - data.plistID = id - } - } - - package func reset() { - $data.access { data in - data.plistID = .invalid - data.values.removeAll(keepingCapacity: true) - data.derivedValues.removeAll(keepingCapacity: true) - data.invalidValues.removeAll(keepingCapacity: true) - data.unrecordedDependencies = false - } - } - - package func hasDifferentUsedValues(_ plist: PropertyList) -> Bool { - let data = data - guard !data.unrecordedDependencies else { - return true - } - guard match(data: data, plist: plist) else { - return false - } - guard compare(data.values, against: plist) else { - return true - } - guard compare(data.derivedValues, against: plist) else { - return true - } - for invalidValue in data.invalidValues { - guard invalidValue.hasMatchingValue(in: plist) else { - return true - } - } - return false - } - } -} - -// MARK: - PropertyList.Tracker Helper functions - -@inline(__always) -private func match(data: TrackerData, plist: PropertyList) -> Bool { - if let elements = plist.elements, - data.plistID == elements.id { - true - } else if plist.elements == nil, data.plistID == .invalid { - true - } else { - false - } -} - -@inline(__always) -private func match(data: TrackerData, from: PropertyList, to: PropertyList) -> UniqueID? { - if let fromElement = from.elements, - fromElement.id == data.plistID { - if let toElement = to.elements { - toElement.id != data.plistID ? toElement.id : nil - } else { - data.plistID != .invalid ? .invalid : nil - } - } else if from.elements == nil, - data.plistID == .invalid, - let toElement = to.elements, - toElement.id != data.plistID { - toElement.id - } else { - nil - } -} - -private func move(_ values: inout [ObjectIdentifier: any AnyTrackedValue], to invalidValues: inout [any AnyTrackedValue]) { - guard !values.isEmpty else { return } - invalidValues.append(contentsOf: values.values) - values.removeAll(keepingCapacity: true) -} - -private func compare(_ values: [ObjectIdentifier: any AnyTrackedValue], against plist: PropertyList) -> Bool { - for (_, value) in values { - guard value.hasMatchingValue(in: plist) else { - return false - } - } - return true -} - -// MARK: - TrackerData - -private struct TrackerData { - var plistID: UniqueID - var values: [ObjectIdentifier: any AnyTrackedValue] - var derivedValues: [ObjectIdentifier: any AnyTrackedValue] - var invalidValues: [AnyTrackedValue] - var unrecordedDependencies: Bool -} - -// MARK: - AnyTrackedValue - -private protocol AnyTrackedValue { - func unwrap() -> Value - func hasMatchingValue(in: PropertyList) -> Bool -} - -private struct TrackedValue: AnyTrackedValue { - var value: Key.Value - - func unwrap() -> Value { - value as! Value - } - - func hasMatchingValue(in plist: PropertyList) -> Bool { - compareValues(value, plist[Key.self]) - } -} - -private struct DerivedValue: AnyTrackedValue { - var value: Key.Value - - func unwrap() -> Value { - value as! Value - } - - func hasMatchingValue(in plist: PropertyList) -> Bool { - value == Key.value(in: plist) - } -} - -private struct EmptyKey: PropertyKey { - static var defaultValue: Void { () } -} diff --git a/Sources/OpenSwiftUICore/Data/PropertyList.swift b/Sources/OpenSwiftUICore/Data/PropertyList.swift new file mode 100644 index 00000000..192a7e99 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/PropertyList.swift @@ -0,0 +1,974 @@ +// +// PropertyList.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete +// ID: 2B32D570B0B3D2A55DA9D4BFC1584D20 (SwiftUI) +// ID: D64CE6C88E7413721C59A34C0C940F2C (SwiftUICore) + +import OpenSwiftUI_SPI +import OpenGraphShims + +// MARK: - PropertyKey + +/// A protocol that defines a key for use in a PropertyList. +/// +/// Types conforming to PropertyKey serve as strongly-typed keys with associated values +/// that can be stored and retrieved from a PropertyList. +package protocol PropertyKey { + /// The type of value associated with this key. + associatedtype Value + + /// The default value to return when no value is found for this key in a PropertyList. + static var defaultValue: Value { get } + + /// Compares two values of this key's type for equality. + /// + /// - Parameters: + /// - lhs: The first value to compare. + /// - rhs: The second value to compare. + /// - Returns: `true` if the values are equal, otherwise `false`. + static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool +} + +extension PropertyKey where Value: Equatable { + package static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool { + lhs == rhs + } +} + +extension PropertyKey { + package static func valuesEqual(_ lhs: Value, _ rhs: Value) -> Bool { + compareValues(lhs, rhs) + } +} + +// MARK: - DerivedPropertyKey + +/// A protocol that defines a key for a value derived from other values in a PropertyList. +/// +/// Types conforming to DerivedPropertyKey can compute their values based on +/// other values stored in a PropertyList. +package protocol DerivedPropertyKey { + /// The type of value associated with this key. + associatedtype Value: Equatable + + /// Computes the derived value from the provided PropertyList. + /// + /// - Parameter list: The PropertyList from which to derive the value. + /// - Returns: The derived value. + static func value(in: PropertyList) -> Value +} + +// MARK: - PropertyKeyLookup + +/// A protocol that defines a relationship between two property keys. +/// +/// Types conforming to PropertyKeyLookup provide a way to look up a primary value +/// using a secondary value from a PropertyList. +package protocol PropertyKeyLookup { + /// The primary key type used for the lookup result. + associatedtype Primary: PropertyKey + + /// The secondary key type used for the lookup source. + associatedtype Secondary: PropertyKey + + /// Looks up a primary value from a secondary value. + /// + /// - Parameter secondaryValue: The secondary value to use for lookup. + /// - Returns: The primary value if found, or `nil` if not found. + static func lookup(in: Secondary.Value) -> Primary.Value? +} + +// MARK: - PropertyList + +/// A mutable container of key-value pairs. +/// +/// PropertyList provides a type-safe way to store and retrieve values using keys +/// that conform to `PropertyKey`. It efficiently manages the storage of multiple +/// property values and supports value overriding and merging operations. +@usableFromInline +@frozen +package struct PropertyList: CustomStringConvertible { + @usableFromInline + var elements: Element? + + /// Creates an empty PropertyList. + @inlinable + package init() { + elements = nil + } + + /// Creates a PropertyList with the specified data. + /// + /// - Parameter data: The data object to initialize the PropertyList with. + package init(data: AnyObject?) { + guard let data else { + return + } + elements = (data as! Element) + } + + /// The underlying data object of the PropertyList. + @inlinable + package var data: AnyObject? { elements } + + /// Returns a Boolean value indicating whether the PropertyList is empty. + /// + /// - Returns: `true` if the PropertyList contains no elements, otherwise `false`. + @inlinable + package var isEmpty: Bool { + elements === nil + } + + /// The unique identifier for this PropertyList. + /// + /// - Returns: A UniqueID that identifies this PropertyList, or `.invalid` if empty. + package var id: UniqueID { + elements?.id ?? .invalid + } + + /// Overrides the current PropertyList with values from another PropertyList. + /// + /// This method gives priority to values from the other PropertyList when both + /// PropertyLists contain values for the same key. + /// + /// - Parameter other: The PropertyList to override with. + package mutating func override(with other: PropertyList) { + let newElements: Element? + if let elements { + if let otherElements = other.elements { + if elements.before != nil { + newElements = TypedElement(value: (), before: otherElements, after: elements) + } else { + newElements = elements.copy(before: otherElements, after: elements.after) + } + } else { + newElements = elements + } + } else { + newElements = other.elements + } + elements = newElements + } + + /// Gets or sets the value for the specified key type. + /// + /// When getting a value, returns the stored value for the key, or the key's default value + /// if no value is stored. When setting a value, stores the new value only if it's different + /// from the current value. + /// + /// - Parameter key: The key type to get or set the value for. + /// - Returns: The value associated with the key. + package subscript(key: K.Type) -> K.Value where K: PropertyKey { + get { + withExtendedLifetime(elements) { + guard let result = find( + elements.map { .passUnretained($0) }, + key: key + ) else { + return K.defaultValue + } + return result.takeUnretainedValue().value + } + } + set { + guard let result = find( + elements.map { .passUnretained($0) }, + key: key + ), K.valuesEqual(newValue, result.takeUnretainedValue().value) + else { + prependValue(newValue, for: key) + return + } + } + } + + /// Gets the derived value for the specified key type. + /// + /// - Parameter key: The derived key type to get the value for. + /// - Returns: The derived value associated with the key. + package subscript(key: K.Type) -> K.Value where K: DerivedPropertyKey { + K.value(in: self) + } + + /// Gets a value using a secondary lookup. + /// + /// Searches for a value using the specified lookup handler to convert between + /// primary and secondary keys. + /// + /// - Parameter key: The lookup handler type to use for the search. + /// - Returns: The primary value if found, or the primary key's default value if not found. + package func valueWithSecondaryLookup(_ key: L.Type) -> L.Primary.Value where L: PropertyKeyLookup { + withExtendedLifetime(elements) { + guard let result = findValueWithSecondaryLookup( + elements.map { .passUnretained($0) }, + secondaryLookupHandler: key, + filter: BloomFilter(type: L.Primary.self), + secondaryFilter: BloomFilter(type: L.Secondary.self) + ) else { + return L.Primary.defaultValue + } + return result + } + } + + /// Prepends a value to the PropertyList for the specified key. + /// + /// - Parameters: + /// - value: The value to prepend. + /// - key: The key type to associate with the value. + package mutating func prependValue(_ value: K.Value, for key: K.Type) where K: PropertyKey { + elements = TypedElement(value: value, before: nil, after: elements) + } + + /// Checks if this PropertyList might not be equal to another PropertyList. + /// + /// - Parameter other: The PropertyList to compare with. + /// - Returns: `true` if the PropertyLists might not be equal, otherwise `false`. + package func mayNotBeEqual(to other: PropertyList) -> Bool { + var ignoredTypes = [ObjectIdentifier]() + return mayNotBeEqual(to: other, ignoredTypes: &ignoredTypes) + } + + /// Checks if this PropertyList might not be equal to another PropertyList, ignoring specified types. + /// + /// - Parameters: + /// - other: The PropertyList to compare with. + /// - ignoredTypes: Types to ignore during comparison. + /// - Returns: `true` if the PropertyLists might not be equal, otherwise `false`. + package func mayNotBeEqual(to other: PropertyList, ignoredTypes: inout [ObjectIdentifier]) -> Bool { + guard let elements, + let otherElements = other.elements else { + return !isEmpty || !other.isEmpty + } + return !compareLists( + .passUnretained(elements), + .passUnretained(otherElements), + ignoredTypes: &ignoredTypes + ) + } + + /// Sets this PropertyList to be the same as another PropertyList. + /// + /// - Parameter other: The PropertyList to set from. + @_transparent + package mutating func set(_ other: PropertyList) { + guard other.elements !== elements else { + return + } + elements = other.elements + } + + /// A textual representation of the PropertyList. + @usableFromInline + package var description: String { + var description = "[" + var index = 0 + if let elements { + elements.forEach(filter: BloomFilter()) { element, stop in + if index != 0 { + description.append(", ") + } + description.append(element.takeUnretainedValue().description) + index += 1 + } + } + description.append("]") + return description + } + + /// Iterates through all values of a specific key type in the PropertyList. + /// + /// - Parameters: + /// - keyType: The key type to filter values by. + /// - body: A closure to execute for each matching value. Set the second parameter to `true` to stop iteration. + package func forEach(keyType: K.Type, _ body: (K.Value, inout Bool) -> Void) where K: PropertyKey { + guard let elements else { + return + } + elements.forEach(filter: BloomFilter(type: K.self)) { element, stop in + guard element.takeUnretainedValue().keyType == K.self else { + return + } + body(Unmanaged>.fromOpaque(element.toOpaque()).takeUnretainedValue().value, &stop) + } + } + + /// Merges another PropertyList into this PropertyList. + /// + /// - Parameter other: The PropertyList to merge from. + package mutating func merge(_ other: PropertyList) { + guard let selfElements = self.elements else { + elements = other.elements + return + } + guard let otherElements = other.elements else { + return + } + guard elements !== otherElements else { + return + } + var copyCount = 0 + var currentSelfElement = selfElements + var currentOptionalSelfElement: Element? = elements + var currentOtherElement = otherElements + var currentOptinoalOtherElement: Element? = otherElements + repeat { + if currentOtherElement.length >= currentSelfElement.length { + copyCount += 1 + currentOptinoalOtherElement = currentOtherElement.after + if let after = currentOtherElement.after { + currentOtherElement = after + continue + } else { + break + } + } else { + currentOptionalSelfElement = currentSelfElement.after + if let after = currentSelfElement.after { + currentSelfElement = after + continue + } else { + break + } + } + } while currentSelfElement !== currentOtherElement + guard let currentOptionalSelfElement, + let currentOptinoalOtherElement, + currentOptionalSelfElement === currentOptinoalOtherElement + else { + override(with: other) + return + } + guard currentOptionalSelfElement !== otherElements else { + return + } + guard currentOptionalSelfElement !== selfElements else { + elements = otherElements + return + } + guard copyCount != 0 else { + return + } + withUnsafeTuple(of: TupleType(Element.self), count: copyCount) { tuple in + let pointer = tuple.address(as: Element.self) + var current: Element! = otherElements + for index in 0 ..< copyCount { + pointer[index] = current + current = current.after + } + for index in 0 ..< copyCount { + let element = pointer[copyCount - index - 1] + elements = element.copy(before: element.before, after: elements) + } + } + } + + /// Creates a new PropertyList by merging this PropertyList with another. + /// + /// - Parameter other: The PropertyList to merge with. + /// - Returns: A new PropertyList containing values from both PropertyLists. + package func merging(_ other: PropertyList) -> PropertyList { + var value = self + value.merge(other) + return value + } + + /// Extracts a value of the specified type from an Element. + /// + /// - Parameters: + /// - type: The type to extract the value as. + /// - element: The Element to extract the value from. + /// - Returns: The extracted value. + package static func value(as _: T.Type, from element: Element) -> T { + element.value(as: T.self) + } +} + +@available(*, unavailable) +extension PropertyList: Sendable {} + +// MARK: - PropertyList Helper Functions + +private func find( + _ element: Unmanaged?, + key: Key.Type +) -> Unmanaged>? where Key: PropertyKey { + find1(element, key: key, filter: BloomFilter(type: key)) +} + +private func find1( + _ element: Unmanaged?, + key: Key.Type, + filter: BloomFilter +) -> Unmanaged>? where Key: PropertyKey { + guard let element else { + return nil + } + var currentElement = element.takeUnretainedValue() + repeat { + guard currentElement.skipFilter.mayContain(filter) else { + if currentElement.skip != nil { + continue + } else { + return nil + } + } + if let before = currentElement.before { + let result = find1(.passUnretained(before), key: key, filter: filter) + if let result { return result } + } + if currentElement.keyType == Key.self { + return .fromOpaque(Unmanaged.passUnretained(currentElement).toOpaque()) + } + guard let after = currentElement.after else { + return nil + } + currentElement = after + } while true +} + +private func findValueWithSecondaryLookup( + _ element: Unmanaged?, + secondaryLookupHandler: Lookup.Type, + filter: BloomFilter, + secondaryFilter: BloomFilter +) -> Lookup.Primary.Value? where Lookup: PropertyKeyLookup { + guard let element else { + return nil + } + var currentElement = element.takeUnretainedValue() + repeat { + let skipFilter = currentElement.skipFilter + guard skipFilter.mayContain(filter) || skipFilter.mayContain(secondaryFilter) else { + if currentElement.skip != nil { + continue + } else { + return nil + } + } + if let before = currentElement.before { + let result = findValueWithSecondaryLookup( + .passUnretained(before), + secondaryLookupHandler: secondaryLookupHandler, + filter: filter, + secondaryFilter: secondaryFilter + ) + if let result { return result } + } + let keyType = currentElement.keyType + if keyType == Lookup.Primary.self { + let element: Unmanaged> = .fromOpaque(Unmanaged.passUnretained(currentElement).toOpaque()) + return element.takeUnretainedValue().value + } else if keyType == Lookup.Secondary.self { + let element: Unmanaged> = .fromOpaque(Unmanaged.passUnretained(currentElement).toOpaque()) + if let value = Lookup.lookup(in: element.takeUnretainedValue().value) { + return value + } + } + guard let after = currentElement.after else { + return nil + } + currentElement = after + } while true +} + +private func compareLists( + _ lhs: Unmanaged, + _ rhs: Unmanaged, + ignoredTypes: inout [ObjectIdentifier] +) -> Bool { + let lhsElement = lhs.takeUnretainedValue() + let rhsElement = rhs.takeUnretainedValue() + guard lhsElement.length == rhsElement.length else { + return false + } + guard lhsElement !== rhsElement else { + return true + } + guard lhsElement.matches(rhsElement, ignoredTypes: &ignoredTypes) else { + return false + } + var currentLhsElement = lhsElement + var currentRhsElement = rhsElement + repeat { + let lhsBefore = currentLhsElement.before + let rhsBefore = currentRhsElement.before + if let lhsBefore, let rhsBefore { + if !compareLists(.passUnretained(lhsBefore), .passUnretained(rhsBefore), ignoredTypes: &ignoredTypes) { + return false + } + } else if lhsBefore != nil || rhsBefore != nil { + return false + } + let lhsAfter = currentLhsElement.after + let rhsAfter = currentRhsElement.after + guard let lhsAfter, let rhsAfter else { + return lhsAfter == nil && rhsAfter == nil + } + if lhsAfter === rhsAfter { + return true + } + currentLhsElement = lhsAfter + currentRhsElement = rhsAfter + } while true +} + +// MARK: - PropertyList.Tracker + +extension PropertyList { + /// A class that tracks property accesses and detects changes in a PropertyList. + /// + /// Tracker is used to efficiently determine when relevant values in a PropertyList + /// have changed, which can help avoid unnecessary updates in UI system. + @usableFromInline + package class Tracker { + @AtomicBox + private var data: TrackerData + + /// Creates a new Tracker instance. + package init() { + _data = AtomicBox(wrappedValue: .init( + plistID: .invalid, + values: [:], + derivedValues: [:], + invalidValues: [], + unrecordedDependencies: false + )) + } + + /// Resets the tracker to its initial state. + /// + /// This removes all tracked values and dependencies. + final package func reset() { + $data.access { data in + data.plistID = .invalid + data.values.removeAll(keepingCapacity: true) + data.derivedValues.removeAll(keepingCapacity: true) + data.invalidValues.removeAll(keepingCapacity: true) + data.unrecordedDependencies = false + } + } + + /// Gets a tracked value for the specified key from the PropertyList. + /// + /// - Parameters: + /// - plist: The PropertyList to get the value from. + /// - key: The key type to get the value for. + /// - Returns: The value associated with the key. + final package func value(_ plist: PropertyList, for key: K.Type) -> K.Value where K: PropertyKey { + $data.access { data in + guard data.plistID == plist.id else { + data.unrecordedDependencies = true + return plist[K.self] + } + let keyID = ObjectIdentifier(K.self) + guard let trackedValue = data.values[keyID] else { + let value = plist[K.self] + let trackedValue = TrackedValue(value: value) + data.values[keyID] = trackedValue + return value + } + return trackedValue.unwrap() + } + } + + /// Gets a tracked value using a secondary lookup from the PropertyList. + /// + /// - Parameters: + /// - plist: The PropertyList to get the value from. + /// - secondaryLookupHandler: The lookup handler type to use. + /// - Returns: The primary value if found. + final package func valueWithSecondaryLookup(_ plist: PropertyList, secondaryLookupHandler: Lookup.Type) -> Lookup.Primary.Value where Lookup: PropertyKeyLookup { + $data.access { data in + guard data.plistID == plist.id else { + data.unrecordedDependencies = true + return plist.valueWithSecondaryLookup(secondaryLookupHandler) + } + let keyID = ObjectIdentifier(Lookup.Primary.self) + guard let trackedValue = data.values[keyID] else { + let value = plist.valueWithSecondaryLookup(secondaryLookupHandler) + let trackedValue = SecondaryLookupTrackedValue(value: value) + data.values[keyID] = trackedValue + return value + } + return trackedValue.unwrap() + } + } + + /// Gets a tracked derived value for the specified key from the PropertyList. + /// + /// - Parameters: + /// - plist: The PropertyList to get the derived value from. + /// - key: The derived key type to get the value for. + /// - Returns: The derived value associated with the key. + final package func derivedValue(_ plist: PropertyList, for key: K.Type) -> K.Value where K: DerivedPropertyKey { + $data.access { data in + guard data.plistID == plist.id else { + data.unrecordedDependencies = true + return K.value(in: plist) + } + let keyID = ObjectIdentifier(K.self) + guard let trackedValue = data.derivedValues[keyID] else { + let value = K.value(in: plist) + let derivedValue = DerivedValue(value: value) + data.derivedValues[keyID] = derivedValue + return value + } + return trackedValue.unwrap() + } + } + + /// Initializes the tracker with values from a PropertyList. + /// + /// - Parameter plist: The PropertyList to initialize values from. + final package func initializeValues(from plist: PropertyList) { + data.plistID = plist.id + } + + /// Invalidates the tracked value for a specific key when moving from one PropertyList to another. + /// + /// - Parameters: + /// - key: The key type whose value should be invalidated. + /// - oldPlist: The original PropertyList. + /// - newPlist: The new PropertyList. + final package func invalidateValue(for key: K.Type, from oldPlist: PropertyList, to newPlist: PropertyList) where K: PropertyKey { + $data.access { data in + guard data.plistID == oldPlist.id, data.plistID != newPlist.id else { + return + } + let removedValue = data.values.removeValue(forKey: ObjectIdentifier(K.self)) + if let removedValue { + data.invalidValues.append(removedValue) + } + move(&data.derivedValues, to: &data.invalidValues) + data.plistID = newPlist.id + } + } + + /// Invalidates all tracked values when moving from one PropertyList to another. + /// + /// - Parameters: + /// - oldPlist: The original PropertyList. + /// - newPlist: The new PropertyList. + final package func invalidateAllValues(from oldPlist: PropertyList, to newPlist: PropertyList) { + $data.access { data in + guard data.plistID == oldPlist.id, data.plistID != newPlist.id else { + return + } + move(&data.values, to: &data.invalidValues) + move(&data.derivedValues, to: &data.invalidValues) + data.plistID = newPlist.id + } + } + + /// Checks if the PropertyList has different values for any of the tracked keys. + /// + /// - Parameter plist: The PropertyList to check against. + /// - Returns: `true` if any tracked value differs, otherwise `false`. + final package func hasDifferentUsedValues(_ plist: PropertyList) -> Bool { + let data = data + guard !data.unrecordedDependencies else { + return true + } + guard data.plistID != plist.id else { + return false + } + guard compare(data.values, against: plist) else { + return true + } + guard compare(data.derivedValues, against: plist) else { + return true + } + for invalidValue in data.invalidValues { + guard invalidValue.hasMatchingValue(in: plist) else { + return true + } + continue + } + return false + } + + /// Combines the tracked values from another Tracker into this one. + /// + /// - Parameter other: The Tracker to merge values from. + final package func formUnion(_ other: Tracker) { + data.formUnion(other.data) + } + } +} + +@available(*, unavailable) +extension PropertyList.Tracker: Sendable {} + +// MARK: - PropertyList.Tracker Helper Functions + +@inline(never) +private func move(_ source: inout [ObjectIdentifier: any AnyTrackedValue], to destination: inout [any AnyTrackedValue]) { + guard !source.isEmpty else { return } + destination.append(contentsOf: source.values) + source.removeAll(keepingCapacity: true) +} + +@inline(never) +private func compare(_ values: [ObjectIdentifier: any AnyTrackedValue], against plist: PropertyList) -> Bool { + for (_, value) in values { + guard value.hasMatchingValue(in: plist) else { + return false + } + } + return true +} + +// MARK: - TrackerData + +private struct TrackerData { + var plistID: UniqueID + var values: [ObjectIdentifier: any AnyTrackedValue] + var derivedValues: [ObjectIdentifier: any AnyTrackedValue] + var invalidValues: [any AnyTrackedValue] + var unrecordedDependencies: Bool + + mutating func formUnion(_ other: TrackerData) { + guard other.plistID != .invalid && plistID != other.plistID else { + return + } + if plistID == .invalid { + plistID = other.plistID + values = other.values + derivedValues = other.derivedValues + invalidValues = other.invalidValues + unrecordedDependencies = other.unrecordedDependencies + } else { + plistID = other.plistID + values.merge(other.values) { first, _ in first } + derivedValues.merge(other.derivedValues) { first, _ in first } + invalidValues.append(contentsOf: other.invalidValues) + unrecordedDependencies = unrecordedDependencies || other.unrecordedDependencies + } + } +} + +// MARK: - AnyTrackedValue + +private protocol AnyTrackedValue { + func unwrap() -> Value + func hasMatchingValue(in: PropertyList) -> Bool +} + +// MARK: - TrackedValue + +private struct TrackedValue: AnyTrackedValue where Key: PropertyKey { + var value: Key.Value + + func unwrap() -> Value { + unsafeBitCast(value, to: Value.self) + } + + func hasMatchingValue(in plist: PropertyList) -> Bool { + Key.valuesEqual(value, plist[Key.self]) + } +} + +// MARK: - DerivedValue + +private struct DerivedValue: AnyTrackedValue where Key: DerivedPropertyKey { + var value: Key.Value + + func unwrap() -> Value { + unsafeBitCast(value, to: Value.self) + } + + func hasMatchingValue(in plist: PropertyList) -> Bool { + value == Key.value(in: plist) + } +} + +private struct SecondaryLookupTrackedValue: AnyTrackedValue where Lookup: PropertyKeyLookup { + var value: Lookup.Primary.Value + + init(value: Lookup.Primary.Value) { + self.value = value + } + + func unwrap() -> Value { + unsafeBitCast(value, to: Value.self) + } + + func hasMatchingValue(in plist: PropertyList) -> Bool { + Lookup.Primary.valuesEqual(value, plist.valueWithSecondaryLookup(Lookup.self)) + } +} + +// MARK: - PropertyList.Element + +extension PropertyList { + /// A base class for elements stored in a PropertyList. + /// + /// Element provides the foundation for type-safe storage of key-value pairs + /// in a PropertyList, with support for efficient traversal and comparison. + @usableFromInline + package class Element: CustomStringConvertible { + let keyType: any Any.Type + let before: Element? + var after: Element? + var skip: Unmanaged? + let length: UInt32 + let skipCount: UInt32 + let skipFilter: BloomFilter + let id = UniqueID() + + fileprivate init(keyType: any Any.Type, before: Element?, after: Element?) { + self.keyType = keyType + self.before = before + self.after = after + + var filter = BloomFilter(type: keyType) + if let before { + var length = before.length + 1 + if let after { length += after.length } + self.length = length + self.skipCount = 0 + filter.value = .max + self.skipFilter = filter + self.skip = .passUnretained(self) + } else { + if let after { + let length = after.length + 1 + if after.skipCount > 15 { + self.length = length + self.skipCount = 1 + self.skipFilter = filter + self.skip = .passUnretained(after) + } else { + self.length = length + self.skipCount = after.skipCount &+ 1 + self.skipFilter = after.skipFilter.union(filter) + self.skip = after.skip + } + } else { + self.length = 1 + self.skipCount = 1 + self.skipFilter = filter + self.skip = nil + } + } + } + + /// Executes a closure for each element that matches the filter. + /// + /// - Parameters: + /// - filter: A bloom filter to quickly skip elements that don't match. + /// - body: A closure to execute for each matching element. Set the second parameter to `true` to stop iteration. + /// - Returns: `false` if iteration was stopped, otherwise `true`. + @discardableResult + final func forEach( + filter: BloomFilter, + _ body: (Unmanaged, inout Bool) -> Void + ) -> Bool { + var currentElement = self + var stop = false + repeat { + guard currentElement.skipFilter.mayContain(filter) else { + if currentElement.skip != nil { + continue + } else { + return true + } + } + if let before = currentElement.before { + let result = before.forEach(filter: filter, body) + stop = !result + guard result else { return false } + } + _ = body(.passUnretained(currentElement), &stop) + guard !stop else { return false } + guard let after = currentElement.after else { + return true + } + currentElement = after + } while true + } + + /// A textual representation of the element. + @usableFromInline + package var description: String { preconditionFailure("") } + + /// Checks if this element matches another element, ignoring specified types. + /// + /// - Parameters: + /// - other: The element to compare with. + /// - ignoredTypes: Types to ignore during comparison. + /// - Returns: `true` if the elements match, otherwise `false`. + func matches(_ other: Element, ignoredTypes: inout [ObjectIdentifier]) -> Bool { + preconditionFailure("") + } + + /// Creates a copy of this element with the specified before and after elements. + /// + /// - Parameters: + /// - before: The element to link before this one. + /// - after: The element to link after this one. + /// - Returns: A new element with the same value but different links. + func copy(before: Element?, after: Element?) -> Element { + preconditionFailure("") + } + + /// Extracts a value of the specified type from this element. + /// + /// - Parameter type: The type to extract the value as. + /// - Returns: The extracted value. + func value(as type: T.Type) -> T { + preconditionFailure("") + } + } +} + +@available(*, unavailable) +extension PropertyList.Element: Sendable {} + +// MARK: - TypedElement + +private class TypedElement: PropertyList.Element where Key: PropertyKey { + let value: Key.Value + + init(value: Key.Value, before: PropertyList.Element?, after: PropertyList.Element?) { + self.value = value + super.init(keyType: Key.self, before: before, after: after) + } + + override var description: String { + "\(Key.self) = \(value)" + } + + override func matches(_ other: PropertyList.Element, ignoredTypes: inout [ObjectIdentifier]) -> Bool { + guard other.keyType == keyType else { + return false + } + let keyID = ObjectIdentifier(Key.self) + guard !ignoredTypes.contains(keyID) else { + return true + } + guard Key.valuesEqual(value, other.value(as: Key.Value.self)) else { + return false + } + ignoredTypes.append(keyID) + return true + } + + override func copy(before: PropertyList.Element?, after: PropertyList.Element?) -> PropertyList.Element { + TypedElement(value: value, before: before, after: after) + } + + override func value(as type: T.Type) -> T { + unsafeBitCast(value, to: type) + } +} + +// MARK: - EmptyKey + +private struct EmptyKey: PropertyKey { + static var defaultValue: Void { () } +} diff --git a/Sources/OpenSwiftUICore/Data/State/ObservableObjectLocation.swift b/Sources/OpenSwiftUICore/Data/State/ObservableObjectLocation.swift index aa3dabf8..b85fb33a 100644 --- a/Sources/OpenSwiftUICore/Data/State/ObservableObjectLocation.swift +++ b/Sources/OpenSwiftUICore/Data/State/ObservableObjectLocation.swift @@ -26,7 +26,8 @@ struct ObservableObjectLocation: Location where Root: ObservableObj let element = _threadTransactionData().map { Unmanaged.fromOpaque($0).takeRetainedValue() } let newElement: PropertyList.Element? if let element = transaction.plist.elements { - newElement = element.byPrepending(element) + // newElement = element.byPrepending(element) + newElement = nil } else { newElement = element } diff --git a/Sources/OpenSwiftUICore/Data/Update.swift b/Sources/OpenSwiftUICore/Data/Update.swift index a4dbfda5..509cc4ed 100644 --- a/Sources/OpenSwiftUICore/Data/Update.swift +++ b/Sources/OpenSwiftUICore/Data/Update.swift @@ -173,7 +173,7 @@ package enum Update { } } } - } while(!Update.actions.isEmpty) + } while !Update.actions.isEmpty } package static func dispatchImmediately(_ body: () -> T) -> T { diff --git a/Sources/OpenSwiftUICore/Semantic/SemanticFeature.swift b/Sources/OpenSwiftUICore/Semantic/SemanticFeature.swift index 98252807..52c94323 100644 --- a/Sources/OpenSwiftUICore/Semantic/SemanticFeature.swift +++ b/Sources/OpenSwiftUICore/Semantic/SemanticFeature.swift @@ -8,7 +8,7 @@ import OpenSwiftUI_SPI /// Protocol for features that are conditionally available based on semantic versions. -package protocol SemanticFeature { +package protocol SemanticFeature: Feature { /// The semantic version in which this feature was introduced static var introduced: Semantics { get } diff --git a/Sources/OpenSwiftUICore/Util/RunLoopUtils.swift b/Sources/OpenSwiftUICore/Util/RunLoopUtils.swift index 6e8d312a..b689c047 100644 --- a/Sources/OpenSwiftUICore/Util/RunLoopUtils.swift +++ b/Sources/OpenSwiftUICore/Util/RunLoopUtils.swift @@ -58,7 +58,7 @@ extension RunLoop { } package static func flushObservers() { - while(!observerActions.isEmpty) { + while !observerActions.isEmpty { let actions = observerActions observerActions = [] Update.begin() diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 52b4ec86..312542eb 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -238,7 +238,7 @@ extension ViewRendererHost { guard shouldContinue else { break } - } while(true) + } while true } var nextTime = viewGraph.nextUpdate.views.time if updateDisplayList { diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c new file mode 100644 index 00000000..13771756 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c @@ -0,0 +1,43 @@ +// +// PropertyListTestsStub.c +// OpenSwiftUISymbolDualTestsHelper + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +// MARK: - PropertyList + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListInit, SwiftUI, $s7SwiftUI12PropertyListVACycfC); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListInitWithData, SwiftUI, $s7SwiftUI12PropertyListV4dataACyXlSg_tcfC); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListOverride, SwiftUI, $s7SwiftUI12PropertyListV8override4withyAC_tF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListSubscriptWithPropertyKeyGetter, SwiftUI, $s7SwiftUI12PropertyListVy5ValueQzxmcAA0C3KeyRzluig); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListSubscriptWithPropertyKeySetter, SwiftUI, $s7SwiftUI12PropertyListVy5ValueQzxmcAA0C3KeyRzluis); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListSubscriptWithDerivedPropertyKeyGetter, SwiftUI, $s7SwiftUI12PropertyListVy5ValueQzxmcAA07DerivedC3KeyRzluig); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListValueWithSecondaryLookup, SwiftUI, $s7SwiftUI12PropertyListV24valueWithSecondaryLookupy7Primary_5ValueQZxmAA0c3KeyH0RzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListPrependValue, SwiftUI, $s7SwiftUI12PropertyListV12prependValue_3fory0F0Qz_xmtAA0C3KeyRzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListMayNotBeEqual, SwiftUI, $s7SwiftUI12PropertyListV13mayNotBeEqual2toSbAC_tF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListMayNotBeEqualIgnoredTypes, SwiftUI, $s7SwiftUI12PropertyListV13mayNotBeEqual2to12ignoredTypesSbAC_SaySOGtF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListSet, SwiftUI, $s7SwiftUI12PropertyListV3setyyACF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListDescription, SwiftUI, $s7SwiftUI12PropertyListV11descriptionSSvg); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListForEach, SwiftUI, $s7SwiftUI12PropertyListV7forEach7keyType_yxm_y5ValueQz_SbztXEtAA0C3KeyRzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListMerge, SwiftUI, $s7SwiftUI12PropertyListV5mergeyyACF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListMerging, SwiftUI, $s7SwiftUI12PropertyListV7mergingyA2CF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListValue, SwiftUI, $s7SwiftUI12PropertyListV5value2as4fromxxm_AC7ElementCtlFZ); + +// MARK: - PropertyList.Tracker + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerInit, SwiftUI, $s7SwiftUI12PropertyListV7TrackerCAEycfc); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerReset, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC5resetyyF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerValue, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC5value_3for5ValueQzAC_xmtAA0C3KeyRzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerValueWithSecondaryLookup, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC24valueWithSecondaryLookup_09secondaryI7Handler7Primary_5ValueQZAC_xmtAA0c3KeyI0RzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerDerivedValue, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC12derivedValue_3for0G0QzAC_xmtAA07DerivedC3KeyRzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerInitializeValues, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC16initializeValues4fromyAC_tF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerInvalidateValue, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC15invalidateValue3for4from2toyxm_A2CtAA0C3KeyRzlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerInvalidateAllValues, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC19invalidateAllValues4from2toyAC_ACtF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerHasDifferentUsedValues, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC22hasDifferentUsedValuesySbACF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_PropertyListTrackerFormUnion, SwiftUI, $s7SwiftUI12PropertyListV7TrackerC9formUnionyyAEF); + +#endif diff --git a/Sources/OpenSwiftUISymbolDualTestsHelper/Extension/CGSize+ExtensionTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Extension/CGSize+ExtensionTestsStub.c similarity index 86% rename from Sources/OpenSwiftUISymbolDualTestsHelper/Extension/CGSize+ExtensionTestsStub.c rename to Sources/OpenSwiftUISymbolDualTestsSupport/Extension/CGSize+ExtensionTestsStub.c index 7ac7c729..4322de97 100644 --- a/Sources/OpenSwiftUISymbolDualTestsHelper/Extension/CGSize+ExtensionTestsStub.c +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Extension/CGSize+ExtensionTestsStub.c @@ -1,6 +1,6 @@ // // CGSize+ExtensionTestsStub.c -// OpenSwiftUISymbolDualTestsHelper +// OpenSwiftUISymbolDualTestsSupport #include "OpenSwiftUIBase.h" diff --git a/Sources/OpenSwiftUITestsSupport/Data/PropertyList+Test.swift b/Sources/OpenSwiftUITestsSupport/Data/PropertyList+Test.swift new file mode 100644 index 00000000..6d8c7eab --- /dev/null +++ b/Sources/OpenSwiftUITestsSupport/Data/PropertyList+Test.swift @@ -0,0 +1,43 @@ +// +// PropertyList+Test.swift +// OpenSwiftUITestsSupport + +package import OpenSwiftUI + +package struct BoolKey: PropertyKey { + package static let defaultValue = false +} + +package struct IntKey: PropertyKey { + package static let defaultValue = 0 +} + +package struct StringKey: PropertyKey { + package static var defaultValue: String { "" } +} + +package struct DerivedIntPlus2Key: DerivedPropertyKey { + package static func value(in plist: PropertyList) -> Int { + plist[IntKey.self] + 2 + } +} + +package struct DerivedStringKey: DerivedPropertyKey { + package static func value(in plist: PropertyList) -> String { + "d:" + plist[StringKey.self] + } +} + +package struct StringFromIntLookup: PropertyKeyLookup { + package struct Primary: PropertyKey { + package static var defaultValue: String { "" } + } + + package struct Secondary: PropertyKey { + package static var defaultValue: Int { 0 } + } + + package static func lookup(in value: Int) -> String? { + value == Secondary.defaultValue ? nil : "\(value)" + } +} diff --git a/Tests/OpenSwiftUICoreTests/Data/Property/PropertyListTests.swift b/Tests/OpenSwiftUICoreTests/Data/Property/PropertyListTests.swift deleted file mode 100644 index cba485de..00000000 --- a/Tests/OpenSwiftUICoreTests/Data/Property/PropertyListTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// PropertyListTests.swift -// OpenSwiftUICoreTests - -import OpenSwiftUICore -import Testing - -struct PropertyListTests { - struct BoolKey: PropertyKey { - static let defaultValue = false - } - - struct IntKey: PropertyKey { - static let defaultValue = 0 - } - - @Test - func description() throws { - var plist = PropertyList() - #expect(plist.description == "[]") - - var bool = plist[BoolKey.self] - #expect(bool == BoolKey.defaultValue) - #expect(plist.description == "[]") - - plist[BoolKey.self] = bool - #expect(plist.description == "[\(BoolKey.self) = \(bool)]") - - plist[BoolKey.self] = !bool - bool = plist[BoolKey.self] - #expect(bool == !BoolKey.defaultValue) - #expect(plist.description == "[\(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") - - let value = 1 - plist[IntKey.self] = value - #expect(plist.description == "[\(IntKey.self) = \(value), \(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") - } -} diff --git a/Tests/OpenSwiftUICoreTests/Data/PropertyListTests.swift b/Tests/OpenSwiftUICoreTests/Data/PropertyListTests.swift new file mode 100644 index 00000000..89e622da --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Data/PropertyListTests.swift @@ -0,0 +1,237 @@ +// +// PropertyListTests.swift +// OpenSwiftUICoreTests + +import OpenSwiftUICore +import OpenSwiftUITestsSupport +import Testing + +struct PropertyListTests { + @Test + func override() { + // Test basic override functionality + var basePlist = PropertyList() + basePlist[IntKey.self] = 10 + basePlist[BoolKey.self] = true + basePlist[StringKey.self] = "base" + #expect(basePlist.description == #""" + [StringKey = base, BoolKey = true, IntKey = 10] + """#) + + var overridePlist = PropertyList() + overridePlist[IntKey.self] = 20 + overridePlist[StringKey.self] = "override" + #expect(overridePlist.description == #""" + [StringKey = override, IntKey = 20] + """#) + + // Override basePlist with overridePlist + basePlist.override(with: overridePlist) + #expect(basePlist[IntKey.self] == 20) + #expect(basePlist[BoolKey.self] == true) + #expect(basePlist[StringKey.self] == "override") + #expect(basePlist.description == #""" + [StringKey = override, IntKey = 20, StringKey = base, BoolKey = true, IntKey = 10] + """#) + + // Test empty override list + var emptyPlist = PropertyList() + var fullPlist = PropertyList() + fullPlist[IntKey.self] = 42 + #expect(fullPlist.description == #""" + [IntKey = 42] + """#) + + fullPlist.override(with: emptyPlist) + #expect(fullPlist[IntKey.self] == 42) + #expect(fullPlist.description == #""" + [IntKey = 42] + """#) + + emptyPlist.override(with: fullPlist) + #expect(emptyPlist[IntKey.self] == 42) + #expect(emptyPlist.description == #""" + [IntKey = 42] + """#) + + // Test chained overrides + var plist1 = PropertyList() + plist1[IntKey.self] = 1 + + var plist2 = PropertyList() + plist2[IntKey.self] = 2 + plist2[StringKey.self] = "two" + + var plist3 = PropertyList() + plist3[IntKey.self] = 3 + plist3[BoolKey.self] = true + + // Chain multiple overrides + plist1.override(with: plist2) + plist1.override(with: plist3) + + // Latest override should take precedence + #expect(plist1[IntKey.self] == 3) + #expect(plist1[StringKey.self] == "two") + #expect(plist1[BoolKey.self] == true) + #expect(plist1.description == #""" + [BoolKey = true, IntKey = 3, EmptyKey = (), StringKey = two, IntKey = 2, IntKey = 1] + """#) + + // Test derived values after override + var derivedBasePlist = PropertyList() + derivedBasePlist[IntKey.self] = 5 + + var derivedOverridePlist = PropertyList() + derivedOverridePlist[IntKey.self] = 10 + derivedBasePlist.override(with: derivedOverridePlist) + #expect(derivedBasePlist[DerivedIntPlus2Key.self] == 12) + #expect(derivedBasePlist.description == #""" + [IntKey = 10, IntKey = 5] + """#) + } + + @Test + func valueWithSecondaryLookup() { + var plist = PropertyList() + #expect(plist.valueWithSecondaryLookup(StringFromIntLookup.self) == StringFromIntLookup.Primary.defaultValue) + plist[StringFromIntLookup.Primary.self] = "AA" + #expect(plist.valueWithSecondaryLookup(StringFromIntLookup.self) == "AA") + plist[StringFromIntLookup.Secondary.self] = 42 + #expect(plist.valueWithSecondaryLookup(StringFromIntLookup.self) == "42") + plist[StringFromIntLookup.Primary.self] = "BB" + #expect(plist.valueWithSecondaryLookup(StringFromIntLookup.self) == "BB") + } + + @Test + func description() { + var plist = PropertyList() + #expect(plist.description == "[]") + + var bool = plist[BoolKey.self] + #expect(bool == BoolKey.defaultValue) + #expect(plist.description == "[]") + + plist[BoolKey.self] = bool + #expect(plist.description == "[\(BoolKey.self) = \(bool)]") + + plist[BoolKey.self] = !bool + bool = plist[BoolKey.self] + #expect(bool == !BoolKey.defaultValue) + #expect(plist.description == "[\(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") + + let value = 1 + plist[IntKey.self] = value + #expect(plist.description == "[\(IntKey.self) = \(value), \(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") + } + + @Test + func merging() { + var plist1 = PropertyList() + plist1[IntKey.self] = 42 + var plist2 = PropertyList() + plist2[StringKey.self] = "Hello" + + let plist3 = plist1.merging(plist2) + let plist4 = plist2.merging(plist1) + + #expect(plist3.description == #""" + [StringKey = Hello, IntKey = 42] + """#) + #expect(plist4.description == #""" + [IntKey = 42, StringKey = Hello] + """#) + } +} + +struct PropertyListTrackerTests { + @Test + func invalidateValue() { + let tracker = PropertyList.Tracker() + + var plist = PropertyList() + plist[IntKey.self] = 42 + plist[StringKey.self] = "original" + tracker.initializeValues(from: plist) + + #expect(tracker.value(plist, for: IntKey.self) == 42) + #expect(tracker.derivedValue(plist, for: DerivedIntPlus2Key.self) == 44) + #expect(tracker.value(plist, for: StringKey.self) == "original") + #expect(tracker.derivedValue(plist, for: DerivedStringKey.self) == "d:original") + #expect(!tracker.hasDifferentUsedValues(plist)) + + var newPlist = PropertyList() + newPlist[IntKey.self] = 100 + newPlist[StringKey.self] = "modified" + #expect(tracker.hasDifferentUsedValues(newPlist)) + + tracker.invalidateValue(for: IntKey.self, from: plist, to: newPlist) + #expect(tracker.value(newPlist, for: IntKey.self) == 100) + #expect(tracker.derivedValue(newPlist, for: DerivedIntPlus2Key.self) == 102) + #expect(tracker.value(newPlist, for: StringKey.self) == "original") + #expect(tracker.derivedValue(newPlist, for: DerivedStringKey.self) == "d:modified") + } + + @Test + func invalidateAllValues() { + let tracker = PropertyList.Tracker() + + var plist = PropertyList() + plist[IntKey.self] = 42 + plist[StringKey.self] = "original" + plist[StringFromIntLookup.Secondary.self] = 23 + tracker.initializeValues(from: plist) + + #expect(tracker.value(plist, for: IntKey.self) == 42) + #expect(tracker.derivedValue(plist, for: DerivedIntPlus2Key.self) == 44) + #expect(tracker.value(plist, for: StringKey.self) == "original") + #expect(tracker.valueWithSecondaryLookup(plist, secondaryLookupHandler: StringFromIntLookup.self) == "23") + #expect(!tracker.hasDifferentUsedValues(plist)) + + var newPlist = PropertyList() + newPlist[IntKey.self] = 100 + newPlist[StringKey.self] = "modified" + newPlist[StringFromIntLookup.Secondary.self] = 200 + + #expect(tracker.hasDifferentUsedValues(newPlist)) + + tracker.invalidateAllValues(from: plist, to: newPlist) + #expect(tracker.value(newPlist, for: IntKey.self) == 100) + #expect(tracker.derivedValue(newPlist, for: DerivedIntPlus2Key.self) == 102) + #expect(tracker.value(newPlist, for: StringKey.self) == "modified") + #expect(tracker.valueWithSecondaryLookup(newPlist, secondaryLookupHandler: StringFromIntLookup.self) == "200") + } + + @Test + func formUnion() { + let tracker1 = PropertyList.Tracker() + var plist1 = PropertyList() + plist1[IntKey.self] = 23 + plist1[BoolKey.self] = true + tracker1.initializeValues(from: plist1) + #expect(tracker1.value(plist1, for: IntKey.self) == 23) + #expect(tracker1.value(plist1, for: BoolKey.self) == true) + + let tracker2 = PropertyList.Tracker() + var plist2 = PropertyList() + plist2[IntKey.self] = 42 + plist2[StringKey.self] = "2" + tracker2.initializeValues(from: plist2) + #expect(tracker2.value(plist2, for: IntKey.self) == 42) + #expect(tracker2.value(plist2, for: StringKey.self) == "2") + + tracker1.formUnion(tracker2) + #expect(tracker1.value(plist2, for: IntKey.self) == 23) + #expect(tracker1.value(plist2, for: BoolKey.self) == true) + #expect(tracker1.value(plist2, for: StringKey.self) == "2") + + #expect(tracker1.value(plist1, for: IntKey.self) == 23) + #expect(tracker1.value(plist1, for: BoolKey.self) == true) + #expect(tracker1.value(plist1, for: StringKey.self) == "") + + plist1[IntKey.self] = 24 + plist2[IntKey.self] = 25 + #expect(tracker1.value(plist1, for: IntKey.self) == 24) + #expect(tracker1.value(plist2, for: IntKey.self) == 25) + } +} diff --git a/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift b/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift new file mode 100644 index 00000000..b0a3366f --- /dev/null +++ b/Tests/OpenSwiftUISymbolDualTests/Data/PropertyListTests.swift @@ -0,0 +1,339 @@ +// +// PropertyListTests.swift +// OpenSwiftUISymbolDualTests + +#if canImport(SwiftUI, _underlyingVersion: 6.0.87) +import Testing +import SwiftUI +import OpenSwiftUI +import OpenSwiftUITestsSupport + +#if compiler(>=6.1) // https://github.com/swiftlang/swift/issues/81248 + +extension PropertyList { + @_silgen_name("OpenSwiftUITestStub_PropertyListInit") + init(swiftUI: Void) + + @_silgen_name("OpenSwiftUITestStub_PropertyListInitWithData") + init(swiftUI_data: AnyObject?) + + @_silgen_name("OpenSwiftUITestStub_PropertyListOverride") + mutating func swiftUI_override(with other: PropertyList) + + subscript(swiftUI key: K.Type) -> K.Value where K: PropertyKey { + @_silgen_name("OpenSwiftUITestStub_PropertyListSubscriptWithPropertyKeyGetter") + get + @_silgen_name("OpenSwiftUITestStub_PropertyListSubscriptWithPropertyKeySetter") + set + } + + subscript(swiftUI key: K.Type) -> K.Value where K: DerivedPropertyKey { + @_silgen_name("OpenSwiftUITestStub_PropertyListSubscriptWithDerivedPropertyKeyGetter") + get + } + + @_silgen_name("OpenSwiftUITestStub_PropertyListValueWithSecondaryLookup") + func swiftUI_valueWithSecondaryLookup(_ key: L.Type) -> L.Primary.Value where L: PropertyKeyLookup + + @_silgen_name("OpenSwiftUITestStub_PropertyListPrependValue") + mutating func swiftUI_prependValue(_ value: K.Value, for key: K.Type) where K: PropertyKey + + @_silgen_name("OpenSwiftUITestStub_PropertyListMayNotBeEqual") + func swiftUI_mayNotBeEqual(to: PropertyList) -> Bool + + @_silgen_name("OpenSwiftUITestStub_PropertyListMayNotBeEqualIgnoredTypes") + func swiftUI_mayNotBeEqual(to: PropertyList, ignoredTypes: inout [ObjectIdentifier]) -> Bool + + @_silgen_name("OpenSwiftUITestStub_PropertyListSet") + mutating func swiftUI_set(_ other: PropertyList) + + var swiftUI_description: String { + @_silgen_name("OpenSwiftUITestStub_PropertyListDescription") + get + } + + @_silgen_name("OpenSwiftUITestStub_PropertyListForEach") + func swiftUI_forEach(keyType: K.Type, _ body: (K.Value, inout Bool) -> Void) where K: PropertyKey + + @_silgen_name("OpenSwiftUITestStub_PropertyListMerge") + mutating func swiftUI_merge(_ plist: PropertyList) + + @_silgen_name("OpenSwiftUITestStub_PropertyListMerging") + func swiftUI_merging(_ other: PropertyList) -> PropertyList + + @_silgen_name("OpenSwiftUITestStub_PropertyListValue") + static func swiftUI_value(as _: T.Type, from element: Element) -> T +} + +private struct DerivedIntPlus2Key: DerivedPropertyKey { + static func value(in plist: PropertyList) -> Int { + plist[swiftUI: IntKey.self] + 2 + } +} + +struct PropertyListTests { + @Test + func override() { + // Test basic override functionality + var basePlist = PropertyList(swiftUI: ()) + basePlist[swiftUI: IntKey.self] = 10 + basePlist[swiftUI: BoolKey.self] = true + basePlist[swiftUI: StringKey.self] = "base" + #expect(basePlist.swiftUI_description == #""" + [StringKey = base, BoolKey = true, IntKey = 10] + """#) + + var overridePlist = PropertyList(swiftUI: ()) + overridePlist[swiftUI: IntKey.self] = 20 + overridePlist[swiftUI: StringKey.self] = "override" + #expect(overridePlist.swiftUI_description == #""" + [StringKey = override, IntKey = 20] + """#) + + // Override basePlist with overridePlist + basePlist.swiftUI_override(with: overridePlist) + #expect(basePlist[swiftUI: IntKey.self] == 20) + #expect(basePlist[swiftUI: BoolKey.self] == true) + #expect(basePlist[swiftUI: StringKey.self] == "override") + #expect(basePlist.swiftUI_description == #""" + [StringKey = override, IntKey = 20, StringKey = base, BoolKey = true, IntKey = 10] + """#) + + // Test empty override list + var emptyPlist = PropertyList(swiftUI: ()) + var fullPlist = PropertyList(swiftUI: ()) + fullPlist[swiftUI: IntKey.self] = 42 + #expect(fullPlist.swiftUI_description == #""" + [IntKey = 42] + """#) + + fullPlist.swiftUI_override(with: emptyPlist) + #expect(fullPlist[swiftUI: IntKey.self] == 42) + #expect(fullPlist.swiftUI_description == #""" + [IntKey = 42] + """#) + + emptyPlist.swiftUI_override(with: fullPlist) + #expect(emptyPlist[swiftUI: IntKey.self] == 42) + #expect(emptyPlist.swiftUI_description == #""" + [IntKey = 42] + """#) + + // Test chained overrides + var plist1 = PropertyList(swiftUI: ()) + plist1[swiftUI: IntKey.self] = 1 + + var plist2 = PropertyList(swiftUI: ()) + plist2[swiftUI: IntKey.self] = 2 + plist2[swiftUI: StringKey.self] = "two" + + var plist3 = PropertyList(swiftUI: ()) + plist3[swiftUI: IntKey.self] = 3 + plist3[swiftUI: BoolKey.self] = true + + // Chain multiple overrides + plist1.swiftUI_override(with: plist2) + plist1.swiftUI_override(with: plist3) + + // Latest override should take precedence + #expect(plist1[swiftUI: IntKey.self] == 3) + #expect(plist1[swiftUI: StringKey.self] == "two") + #expect(plist1[swiftUI: BoolKey.self] == true) + #expect(plist1.swiftUI_description == #""" + [BoolKey = true, IntKey = 3, EmptyKey = (), StringKey = two, IntKey = 2, IntKey = 1] + """#) + + // Test derived values after override + var derivedBasePlist = PropertyList(swiftUI: ()) + derivedBasePlist[swiftUI: IntKey.self] = 5 + + var derivedOverridePlist = PropertyList(swiftUI: ()) + derivedOverridePlist[swiftUI: IntKey.self] = 10 + derivedBasePlist.swiftUI_override(with: derivedOverridePlist) + #expect(derivedBasePlist[swiftUI: DerivedIntPlus2Key.self] == 12) + #expect(derivedBasePlist.swiftUI_description == #""" + [IntKey = 10, IntKey = 5] + """#) + } + + @Test + func valueWithSecondaryLookup() { + var plist = PropertyList(swiftUI: ()) + #expect(plist.swiftUI_valueWithSecondaryLookup(StringFromIntLookup.self) == StringFromIntLookup.Primary.defaultValue) + plist[swiftUI: StringFromIntLookup.Primary.self] = "AA" + #expect(plist.swiftUI_valueWithSecondaryLookup(StringFromIntLookup.self) == "AA") + plist[swiftUI: StringFromIntLookup.Secondary.self] = 42 + #expect(plist.swiftUI_valueWithSecondaryLookup(StringFromIntLookup.self) == "42") + plist[swiftUI: StringFromIntLookup.Primary.self] = "BB" + #expect(plist.swiftUI_valueWithSecondaryLookup(StringFromIntLookup.self) == "BB") + } + + @Test + func description() { + var plist = PropertyList(swiftUI: ()) + #expect(plist.swiftUI_description == "[]") + + var bool = plist[swiftUI: BoolKey.self] + #expect(bool == BoolKey.defaultValue) + #expect(plist.swiftUI_description == "[]") + + plist[swiftUI: BoolKey.self] = bool + #expect(plist.swiftUI_description == "[\(BoolKey.self) = \(bool)]") + + plist[swiftUI: BoolKey.self] = !bool + bool = plist[swiftUI: BoolKey.self] + #expect(bool == !BoolKey.defaultValue) + #expect(plist.swiftUI_description == "[\(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") + + let value = 1 + plist[swiftUI: IntKey.self] = value + #expect(plist.swiftUI_description == "[\(IntKey.self) = \(value), \(BoolKey.self) = \(bool), \(BoolKey.self) = \(BoolKey.defaultValue)]") + } + + @Test + func merging() { + var plist1 = PropertyList(swiftUI: ()) + plist1[swiftUI: IntKey.self] = 42 + var plist2 = PropertyList(swiftUI: ()) + plist2[swiftUI: StringKey.self] = "Hello" + + let plist3 = plist1.swiftUI_merging(plist2) + let plist4 = plist2.swiftUI_merging(plist1) + + #expect(plist3.swiftUI_description == #""" + [StringKey = Hello, IntKey = 42] + """#) + #expect(plist4.swiftUI_description == #""" + [IntKey = 42, StringKey = Hello] + """#) + } +} + +extension PropertyList.Tracker { + // FIXME: Compiler limitation + // @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerInit") + // init(swiftUI: Void) + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerReset") + func swiftUI_reset() + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerValue") + func swiftUI_value(_ plist: PropertyList, for key: K.Type) -> K.Value where K: PropertyKey + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerValueWithSecondaryLookup") + func swiftUI_valueWithSecondaryLookup(_ plist: PropertyList, secondaryLookupHandler: Lookup.Type) -> Lookup.Primary.Value where Lookup: PropertyKeyLookup + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerDerivedValue") + func swiftUI_derivedValue(_ plist: PropertyList, for key: K.Type) -> K.Value where K: DerivedPropertyKey + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerInitializeValues") + func swiftUI_initializeValues(from plist: PropertyList) + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerInvalidateValue") + func swiftUI_invalidateValue(for key: K.Type, from oldPlist: PropertyList, to newPlist: PropertyList) where K: PropertyKey + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerInvalidateAllValues") + func swiftUI_invalidateAllValues(from oldPlist: PropertyList, to newPlist: PropertyList) + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerHasDifferentUsedValues") + func swiftUI_hasDifferentUsedValues(_ plist: PropertyList) -> Bool + + @_silgen_name("OpenSwiftUITestStub_PropertyListTrackerFormUnion") + func swiftUI_formUnion(_ other: PropertyList.Tracker) +} + +struct PropertyListTrackerTests { + @Test + func invalidateValue() { + let tracker = PropertyList.Tracker() + + var plist = PropertyList(swiftUI: ()) + plist[swiftUI: IntKey.self] = 42 + plist[swiftUI: StringKey.self] = "original" + tracker.swiftUI_initializeValues(from: plist) + + #expect(tracker.swiftUI_value(plist, for: IntKey.self) == 42) + #expect(tracker.swiftUI_derivedValue(plist, for: DerivedIntPlus2Key.self) == 44) + #expect(tracker.swiftUI_value(plist, for: StringKey.self) == "original") + #expect(tracker.swiftUI_derivedValue(plist, for: DerivedStringKey.self) == "d:original") + #expect(!tracker.swiftUI_hasDifferentUsedValues(plist)) + + var newPlist = PropertyList(swiftUI: ()) + newPlist[swiftUI: IntKey.self] = 100 + newPlist[swiftUI: StringKey.self] = "modified" + #expect(tracker.swiftUI_hasDifferentUsedValues(newPlist)) + + tracker.swiftUI_invalidateValue(for: IntKey.self, from: plist, to: newPlist) + #expect(tracker.swiftUI_value(newPlist, for: IntKey.self) == 100) + #expect(tracker.swiftUI_derivedValue(newPlist, for: DerivedIntPlus2Key.self) == 102) + #expect(tracker.swiftUI_value(newPlist, for: StringKey.self) == "original") + #expect(tracker.swiftUI_derivedValue(newPlist, for: DerivedStringKey.self) == "d:modified") + } + + @Test + func invalidateAllValues() { + let tracker = PropertyList.Tracker() + + var plist = PropertyList() + plist[swiftUI: IntKey.self] = 42 + plist[swiftUI: StringKey.self] = "original" + plist[swiftUI: StringFromIntLookup.Secondary.self] = 23 + tracker.swiftUI_initializeValues(from: plist) + + #expect(tracker.swiftUI_value(plist, for: IntKey.self) == 42) + #expect(tracker.swiftUI_derivedValue(plist, for: DerivedIntPlus2Key.self) == 44) + #expect(tracker.swiftUI_value(plist, for: StringKey.self) == "original") + #expect(tracker.swiftUI_valueWithSecondaryLookup(plist, secondaryLookupHandler: StringFromIntLookup.self) == "23") + #expect(!tracker.swiftUI_hasDifferentUsedValues(plist)) + + var newPlist = PropertyList() + newPlist[swiftUI: IntKey.self] = 100 + newPlist[swiftUI: StringKey.self] = "modified" + newPlist[swiftUI: StringFromIntLookup.Secondary.self] = 200 + + #expect(tracker.swiftUI_hasDifferentUsedValues(newPlist)) + + tracker.swiftUI_invalidateAllValues(from: plist, to: newPlist) + #expect(tracker.swiftUI_value(newPlist, for: IntKey.self) == 100) + #expect(tracker.swiftUI_derivedValue(newPlist, for: DerivedIntPlus2Key.self) == 102) + #expect(tracker.swiftUI_value(newPlist, for: StringKey.self) == "modified") + #expect(tracker.swiftUI_valueWithSecondaryLookup(newPlist, secondaryLookupHandler: StringFromIntLookup.self) == "200") + } + + @Test + func formUnion() { + let tracker1 = PropertyList.Tracker() + var plist1 = PropertyList(swiftUI: ()) + plist1[swiftUI: IntKey.self] = 23 + plist1[swiftUI: BoolKey.self] = true + tracker1.swiftUI_initializeValues(from: plist1) + #expect(tracker1.swiftUI_value(plist1, for: IntKey.self) == 23) + #expect(tracker1.swiftUI_value(plist1, for: BoolKey.self) == true) + + let tracker2 = PropertyList.Tracker() + var plist2 = PropertyList(swiftUI: ()) + plist2[swiftUI: IntKey.self] = 42 + plist2[swiftUI: StringKey.self] = "2" + tracker2.swiftUI_initializeValues(from: plist2) + #expect(tracker2.swiftUI_value(plist2, for: IntKey.self) == 42) + #expect(tracker2.swiftUI_value(plist2, for: StringKey.self) == "2") + + tracker1.swiftUI_formUnion(tracker2) + #expect(tracker1.swiftUI_value(plist2, for: IntKey.self) == 23) + #expect(tracker1.swiftUI_value(plist2, for: BoolKey.self) == true) + #expect(tracker1.swiftUI_value(plist2, for: StringKey.self) == "2") + + #expect(tracker1.swiftUI_value(plist1, for: IntKey.self) == 23) + #expect(tracker1.swiftUI_value(plist1, for: BoolKey.self) == true) + #expect(tracker1.swiftUI_value(plist1, for: StringKey.self) == "") + + plist1[swiftUI: IntKey.self] = 24 + plist2[swiftUI: IntKey.self] = 25 + #expect(tracker1.swiftUI_value(plist1, for: IntKey.self) == 24) + #expect(tracker1.swiftUI_value(plist2, for: IntKey.self) == 25) + } +} + +#endif + +#endif diff --git a/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift b/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift index c8ccc667..39350b69 100644 --- a/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift +++ b/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift @@ -8,7 +8,7 @@ import SwiftUI import OpenSwiftUI extension CGSize { - var swiftUIHasZero: Bool { + var swiftUI_hasZero: Bool { @_silgen_name("OpenSwiftUITestStub_CGSizeHasZero") get } @@ -17,26 +17,23 @@ extension CGSize { #if compiler(>=6.1) // https://github.com/swiftlang/swift/issues/81248 struct CGSize_ExtensionTests { @Test( - arguments: [ - (CGSize(width: 0, height: 0), true), - (CGSize(width: 1, height: 0), true), - (CGSize(width: 0, height: 1), true), - (CGSize(width: 1, height: 1), false) - ], .enabled { if #available(iOS 18, macOS 14, *) { return true } else { return false } - } + }, + arguments: [ + (CGSize(width: 0, height: 0), true), + (CGSize(width: 1, height: 0), true), + (CGSize(width: 0, height: 1), true), + (CGSize(width: 1, height: 1), false) + ] ) func hasZero(size: CGSize, expectedResult: Bool) { - let openSwiftUIResult = size.hasZero - let swiftUIResult = size.swiftUIHasZero - #expect(openSwiftUIResult == expectedResult) - #expect(swiftUIResult == expectedResult) - #expect(openSwiftUIResult == swiftUIResult) + let result = size.swiftUI_hasZero + #expect(result == expectedResult) } } #endif diff --git a/Tests/OpenSwiftUISymbolDualTests/README.md b/Tests/OpenSwiftUISymbolDualTests/README.md index 650ced67..62c62062 100644 --- a/Tests/OpenSwiftUISymbolDualTests/README.md +++ b/Tests/OpenSwiftUISymbolDualTests/README.md @@ -11,13 +11,12 @@ import SwiftUI import OpenSwiftUI extension CGSize { - var swiftUIHasZero: Bool { + var swiftUI_hasZero: Bool { @_silgen_name("OpenSwiftUITestStub_CGSizeHasZero") get } } let size = CGSize(width: 0, height: 0) -#expect(size.hasZero) -#expect(size.swiftUIHasZero) +#expect(size.swiftUI_hasZero == true) ```