diff --git a/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift b/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift index 5d01fee11..5063e1c57 100644 --- a/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift +++ b/Sources/OpenSwiftUICore/Data/Combine/ObservedObject.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: Blocked by addTreeValueSlow +// Status: Complete // ID: C212C242BFEB175E53A59438AB276A7C (SwiftUICore) import OpenAttributeGraphShims @@ -222,7 +222,13 @@ extension ObservedObject { invalidation: WeakAttribute(attribute) ) buffer.append(box, fieldOffset: fieldOffset) - // TODO: addTreeValueSlow + addTreeValue( + attribute, + as: ObjectType.self, + at: fieldOffset, + in: Value.self, + flags: .observedObjectSignal + ) } } diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift index d26aa1196..12a206212 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift @@ -2,20 +2,21 @@ // DynamicProperty.swift // OpenSwiftUICore // -// Audited for iOS 18.0 +// Audited for 6.5.4 // Status: Complete // ID: 49D2A32E637CD497C6DE29B8E060A506 (SwiftUI) // ID: A4C1D658B3717A3062FEFC91A812D6EB (SwiftUICore) package import OpenAttributeGraphShims -// MARK: - DynamicProperty +// MARK: - DynamicProperty [6.5.4] /// An interface for a stored variable that updates an external property of a /// view. /// /// The view gives values to these properties prior to recomputing the view's /// ``View/body-swift.property``. +@available(OpenSwiftUI_v1_0, *) public protocol DynamicProperty { /// Creates an instance of the dynamic `View` property /// represented by `self`. @@ -28,8 +29,9 @@ public protocol DynamicProperty { /// Describes the static behaviors of the property type. Returns /// a raw integer value from DynamicPropertyBehaviors. + @available(OpenSwiftUI_v3_0, *) static var _propertyBehaviors: UInt32 { get } - + /// Updates the underlying value of the stored value. /// /// OpenSwiftUI calls this function before rendering a view's @@ -38,7 +40,7 @@ public protocol DynamicProperty { mutating func update() } -// MARK: - DynamicPropertyBehaviors +// MARK: - DynamicPropertyBehaviors [6.5.4] package struct DynamicPropertyBehaviors: OptionSet { package let rawValue: UInt32 @@ -52,34 +54,49 @@ package struct DynamicPropertyBehaviors: OptionSet { } } -// MARK: - DynamicPropertyBox +// MARK: - DynamicPropertyBox [6.5.4] package protocol DynamicPropertyBox: DynamicProperty { associatedtype Property: DynamicProperty + mutating func destroy() + mutating func reset() + mutating func update(property: inout Property, phase: ViewPhase) -> Bool + func getState(type: Value.Type) -> Binding? } -// MARK: - Default implementation for DynamicPropertyBox +// MARK: - Default implementation for DynamicPropertyBox [6.5.4] extension DynamicPropertyBox { - package func destroy() {} - package func reset() {} - package func getState(type: S.Type = S.self) -> Binding? { nil } + package func destroy() { + _openSwiftUIEmptyStub() + } + + package func reset() { + _openSwiftUIEmptyStub() + } + + package func getState(type: S.Type = S.self) -> Binding? { + nil + } } -// MARK: - Default implementation for DynamicProperty +// MARK: - Default implementation for DynamicProperty [6.5.4] private struct EmbeddedDynamicPropertyBox: DynamicPropertyBox { + typealias Property = Value + func update(property: inout Property, phase _: ViewPhase) -> Bool { property.update() return false } } +@available(OpenSwiftUI_v1_0, *) extension DynamicProperty { public static func _makeProperty( in buffer: inout _DynamicPropertyBuffer, @@ -114,22 +131,26 @@ extension DynamicProperty { ) } - public mutating func update() {} - + public mutating func update() { + _openSwiftUIEmptyStub() + } + + @available(OpenSwiftUI_v3_0, *) public static var _propertyBehaviors: UInt32 { 0 } } -// MARK: - DynamicPropertyCache [2021] +// MARK: - DynamicPropertyCache [6.5.4] package struct DynamicPropertyCache { package struct Fields { - var layout: Layout - package var behaviors: DynamicPropertyBehaviors - enum Layout { case product([Field]) - case sum(Any.Type, [TaggedFields]) + case sum(any Any.Type, [TaggedFields]) } + + var layout: Layout + + package var behaviors: DynamicPropertyBehaviors init(layout: Layout) { var behaviors: UInt32 = 0 @@ -148,6 +169,17 @@ package struct DynamicPropertyCache { self.layout = layout self.behaviors = .init(rawValue: behaviors) } + + package func name(at offset: Int) -> String? { + _name(at: offset).flatMap { String(cString: $0, encoding: .utf8) } + } + + package func _name(at offset: Int) -> UnsafePointer? { + guard case let .product(fields) = layout else { + return nil + } + return fields.first(where: { $0.offset == offset })?.name + } } struct Field { @@ -163,55 +195,123 @@ package struct DynamicPropertyCache { private static var cache = MutableBox([ObjectIdentifier: Fields]()) - package static func fields(of type: Any.Type) -> Fields { - if let fields = cache.wrappedValue[ObjectIdentifier(type)] { - return fields - } - let fields: Fields - let typeID = Metadata(type) - switch typeID.kind { - case .enum, .optional: - var taggedFields: [TaggedFields] = [] - _ = typeID.forEachField(options: [.continueAfterUnknownField, .enumerateEnumCases]) { name, offset, fieldType in - var fields: [Field] = [] - let tupleType = TupleType(fieldType) - for index in tupleType.indices { - guard let dynamicPropertyType = tupleType.type(at: index) as? DynamicProperty.Type else { - break + package static func fields(of type: any Any.Type) -> Fields { + let identifier = ObjectIdentifier(type) + guard let fields = cache.wrappedValue[identifier] else { + var fields: Fields + let typeID = Metadata(type) + switch typeID.kind { + case .enum, .optional: + var taggedFields: [TaggedFields] = [] + _ = typeID.forEachField(options: [.continueAfterUnknownField, .enumerateEnumCases]) { name, offset, fieldType in + var fields: [Field] = [] + let tupleType = TupleType(fieldType) + for index in tupleType.indices { + guard let dynamicPropertyType = tupleType.type(at: index) as? DynamicProperty.Type else { + continue + } + let offset = tupleType.elementOffset(at: index) + let field = Field(type: dynamicPropertyType, offset: offset, name: name) + fields.append(field) } - let offset = tupleType.elementOffset(at: index) - let field = Field(type: dynamicPropertyType, offset: offset, name: name) - fields.append(field) - } - if !fields.isEmpty { - let taggedField = TaggedFields(tag: offset, fields: fields) - taggedFields.append(taggedField) + if !fields.isEmpty { + let taggedField = TaggedFields(tag: offset, fields: fields) + taggedFields.append(taggedField) + } + return true } - return true - } - fields = Fields(layout: .sum(type, taggedFields)) - case .struct, .tuple: - var fieldArray: [Field] = [] - _ = typeID.forEachField(options: [.continueAfterUnknownField]) { name, offset, fieldType in - guard let dynamicPropertyType = fieldType as? DynamicProperty.Type else { + fields = Fields(layout: .sum(type, taggedFields)) + case .struct, .tuple: + var fieldArray: [Field] = [] + _ = typeID.forEachField(options: [.continueAfterUnknownField]) { name, offset, fieldType in + guard let dynamicPropertyType = fieldType as? DynamicProperty.Type else { + return true + } + let field = Field(type: dynamicPropertyType, offset: offset, name: name) + fieldArray.append(field) return true } - let field = Field(type: dynamicPropertyType, offset: offset, name: name) - fieldArray.append(field) - return true + fields = Fields(layout: .product(fieldArray)) + default: + fields = Fields(layout: .product([])) } - fields = Fields(layout: .product(fieldArray)) - default: - fields = Fields(layout: .product([])) - } - if fields.behaviors.contains(.init(rawValue: 3)) { - Log.runtimeIssues("%s is marked async, but contains properties that require the main thread.", ["\(type)"]) + if fields.behaviors.contains([.allowsAsync, .requiresMainThread]) { + Log.runtimeIssues( + "%s is marked async, but contains properties that require the main thread.", + ["\(type)"] + ) + fields.behaviors.subtract(.allowsAsync) + } + cache.wrappedValue[identifier] = fields + return fields } - cache.wrappedValue[ObjectIdentifier(type)] = fields return fields } } +// MARK: - DynamicProperty + TreeValue [6.5.4] + +extension DynamicProperty { + @inline(__always) + package static func addTreeValue( + _ value: Attribute, + at fieldOffset: Int, + in container: any Any.Type, + flags: TreeValueFlags = .init() + ) { + guard Subgraph.shouldRecordTree else { + return + } + addTreeValueSlow( + value.identifier, + as: T.self, + in: container, + fieldOffset: fieldOffset, + flags: flags + ) + } + + @inline(__always) + package static func addTreeValue( + _ value: Attribute, + as: U.Type, + at fieldOffset: Int, + in container: any Any.Type, + flags: TreeValueFlags = .init() + ) { + guard Subgraph.shouldRecordTree else { + return + } + addTreeValueSlow( + value.identifier, + as: U.self, + in: container, + fieldOffset: fieldOffset, + flags: flags + ) + } + + @inline(never) + package static func addTreeValueSlow( + _ value: AnyAttribute, + as type: T.Type, + in container: any Any.Type, + fieldOffset: Int, + flags: TreeValueFlags = .init() + ) { + let containerFields = DynamicPropertyCache.fields(of: container) + "".withCString { unknownStringPtr in + Subgraph.addTreeValue( + Attribute(identifier: value), + forKey: containerFields._name(at: fieldOffset) ?? unknownStringPtr, + flags: flags.rawValue + ) + } + } +} + +// MARK: - BodyAccessor [6.5.4] + package protocol BodyAccessor { associatedtype Container associatedtype Body @@ -225,14 +325,14 @@ extension BodyAccessor { fields: DynamicPropertyCache.Fields ) -> (_GraphValue, _DynamicPropertyBuffer?) { guard Body.self != Never.self else { - preconditionFailure("\(Body.self) may not have Body == Never") + preconditionFailure("\(Container.self) may not have Body == Never") } - return withUnsafeMutablePointer(to: &inputs) { inputsPointer in - func project(flags _: Flags.Type) -> (_GraphValue, _DynamicPropertyBuffer?) { + return withUnsafePointer(to: inputs) { pointer in + func project(flags _: Flags.Type) -> (_GraphValue, _DynamicPropertyBuffer?) where Flags: RuleThreadFlags { let buffer = _DynamicPropertyBuffer( fields: fields, container: container, - inputs: &inputsPointer.pointee + inputs: &inputs ) if buffer._count == 0 { buffer.destroy() @@ -245,7 +345,7 @@ extension BodyAccessor { let body = DynamicBody( accessor: self, container: container.value, - phase: inputsPointer.pointee.phase, + phase: pointer.pointee.phase, links: buffer, resetSeed: 0 ) @@ -271,7 +371,7 @@ extension BodyAccessor { } } -// MARK: - BodyAccessorRule +// MARK: - BodyAccessorRule [6.5.4] package protocol BodyAccessorRule { static var container: Any.Type { get } @@ -280,9 +380,7 @@ package protocol BodyAccessorRule { static func metaProperties(as: T.Type, attribute: AnyAttribute) -> [(String, AnyAttribute)] } -// TO BE AUDITED - -// MARK: - RuleThreadFlags +// MARK: - RuleThreadFlags [6.5.4] private protocol RuleThreadFlags { static var value: _AttributeType.Flags { get } @@ -296,7 +394,7 @@ private struct MainThreadFlags: RuleThreadFlags { static var value: _AttributeType.Flags { .mainThread } } -// MARK: - StaticBody +// MARK: - StaticBody [6.5.4] private struct StaticBody { let accessor: Accessor @@ -311,7 +409,6 @@ private struct StaticBody extension StaticBody: StatefulRule { typealias Value = Accessor.Body - // Audited with 6.5.4 func updateValue() { withObservation { accessor.updateBody(of: container, changed: true) @@ -348,7 +445,7 @@ extension StaticBody: CustomStringConvertible { var description: String { "\(Accessor.Body.self)" } } -// MARK: - DynamicBody +// MARK: - DynamicBody [6.5.4] private struct DynamicBody { let accessor: Accessor @@ -375,7 +472,6 @@ private struct DynamicBody extension DynamicBody: StatefulRule { typealias Value = Accessor.Body - // Audited with 6.5.4 mutating func updateValue() { if resetSeed != phase.resetSeed { links.reset() diff --git a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift index 2d3677c4f..f83225579 100644 --- a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift @@ -178,18 +178,22 @@ package struct TreeElementFlags: OptionSet { // MARK: - TreeValueFlags [6.5.4] +// FIXME: This should not be an OptionSet. package struct TreeValueFlags: OptionSet { package let rawValue: UInt32 package init(rawValue: UInt32) { - _openSwiftUIUnimplementedFailure() + self.rawValue = rawValue } - // FIXME? package static let stateSignal: TreeValueFlags = .init(rawValue: 1) + package static let environmentObjectSignal: TreeValueFlags = .init(rawValue: 2) + package static let observedObjectSignal: TreeValueFlags = .init(rawValue: 3) + package static let appStorageSignal: TreeValueFlags = .init(rawValue: 4) + package static let sceneStorageSignal: TreeValueFlags = .init(rawValue: 5) } diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift index 7cab37612..1ea674edd 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyCacheTests.swift @@ -1,51 +1,173 @@ // // DynamicPropertyCacheTests.swift -// -// -// Created by Kyle on 2024/1/24. -// +// OpenSwiftUICoreTests -import XCTest -@testable import OpenSwiftUICore import OpenAttributeGraphShims +@testable import OpenSwiftUICore +import Testing + +@Suite(.enabled(if: attributeGraphEnabled)) // FIXME: Change to swiftToolchainSupported when we implement forEachField +struct DynamicPropertyCacheTests { + @Test + func size() { + #expect(MemoryLayout.size == 24) + #expect(MemoryLayout.size == 17) + #expect(MemoryLayout.size == 24) + #expect(MemoryLayout.size == 4) + } + + struct NormalP: DynamicProperty { + var value: Int + } + + struct AsyncP: DynamicProperty { + var value: Int + + static var _propertyBehaviors: UInt32 { + DynamicPropertyBehaviors.allowsAsync.rawValue + } + } + + struct MainP: DynamicProperty { + var value: Int + + static var _propertyBehaviors: UInt32 { + DynamicPropertyBehaviors.requiresMainThread.rawValue + } + } + + enum E { + case normal(NormalP) + case value(Int) + case async(Double, AsyncP) + case main(MainP, MainP) + } + + @Test + func enumFields() { + let fieldsE = DynamicPropertyCache.fields(of: E.self) + #expect( + fieldsE.behaviors == [.requiresMainThread], + "mix async and main properties will trigger an issue and remove async behavior" + ) + #expect(fieldsE.name(at: 0) == nil, "Only product fields support name lookup via offset") + guard case let .sum(type, taggedFields) = fieldsE.layout else { + Issue.record("layout should be sum type for enum") + return + } + #expect(type == E.self) + #expect(taggedFields.count == 3) + let normal = taggedFields[0] + #expect(normal.tag == 0) + #expect(normal.fields.count == 1) + #expect(normal.fields[0].type == NormalP.self) + #expect(normal.fields[0].offset == 0) + #expect(normal.fields[0].name.map { String(cString: $0) } == "normal") + + let async = taggedFields[1] + #expect(async.tag == 2) + #expect(async.fields.count == 1) + #expect(async.fields[0].type == AsyncP.self) + #expect(async.fields[0].offset == 8) + #expect(async.fields[0].name.map { String(cString: $0) } == "async") + + let main = taggedFields[2] + #expect(main.tag == 3) + #expect(main.fields.count == 2) + #expect(main.fields[0].type == MainP.self) + #expect(main.fields[0].offset == 0) + #expect(main.fields[0].name.map { String(cString: $0) } == "main") + #expect(main.fields[1].type == MainP.self) + #expect(main.fields[1].offset == 8) + #expect(main.fields[1].name.map { String(cString: $0) } == "main") + } + + @Test + func optionalFields() { + let optionalP = DynamicPropertyCache.fields(of: Optional.self) + #expect(optionalP.behaviors == []) + #expect(optionalP.name(at: 0) == nil, "Only product fields support name lookup via offset") + guard case let .sum(typeP, taggedFieldsP) = optionalP.layout else { + Issue.record("layout should be sum type for optional") + return + } + #expect(typeP == Optional.self) + #expect(taggedFieldsP.count == 1) + let normalP = taggedFieldsP[0] + #expect(normalP.tag == 0) + #expect(normalP.fields.count == 1) + #expect(normalP.fields[0].type == NormalP.self) + #expect(normalP.fields[0].offset == 0) + #expect(normalP.fields[0].name.map { String(cString: $0) } == "some") -final class DynamicPropertyCacheTests: XCTestCase { - func testExample() throws { - let a = MemoryLayout.size - print(a) - let b = MemoryLayout.size - print(b) - let c = MemoryLayout.size - print(c) - let d = MemoryLayout.size - print(d) - - - let t = Metadata(DynamicPropertyCache.Fields?.self) - let result = t.forEachField(options: [.enumerateEnumCases]) { name, index, type in - let s = String(cString: name) - print("\(s) \(index) \(type)") - return true - } - var f: DynamicPropertyCache.Fields? = nil - f = DynamicPropertyCache.Fields(layout: .product(.init())) - f!.behaviors = .init(rawValue: 0x7) - withUnsafeBytes(of: &f) { pointer in - print(pointer.baseAddress!) - } - f = nil - withUnsafeBytes(of: &f) { pointer in - print(pointer.baseAddress!) - } - f = DynamicPropertyCache.Fields(layout: .sum(Int.self, [])) - f!.behaviors = .init(rawValue: 0x7) - withUnsafeBytes(of: &f) { pointer in - print(pointer.baseAddress!) - } - f = nil - withUnsafeBytes(of: &f) { pointer in - print(pointer.baseAddress!) - } - print(result) + let optionalE = DynamicPropertyCache.fields(of: Optional.self) + #expect(optionalE.behaviors == []) + guard case let .sum(typeE, taggedFieldsE) = optionalE.layout else { + Issue.record("layout should be sum type for optional") + return + } + #expect(typeE == Optional.self) + #expect(taggedFieldsE.isEmpty) + } + + @Test + func structFields() { + struct S { + var p1: Int + var p2: NormalP + var p3: AsyncP + } + let fieldsS = DynamicPropertyCache.fields(of: S.self) + #expect(fieldsS.behaviors == [.allowsAsync]) + #expect(fieldsS.name(at: 0) == nil) + #expect(fieldsS.name(at: 8) == "p2") + #expect(fieldsS.name(at: 9) == nil) + #expect(fieldsS.name(at: 16) == "p3") + guard case let .product(fields) = fieldsS.layout else { + Issue.record("layout should be product type for struct") + return + } + #expect(fields.count == 2) + #expect(fields[0].type == NormalP.self) + #expect(fields[0].offset == 8) + #expect(fields[0].name.map { String(cString: $0) } == "p2") + #expect(fields[1].type == AsyncP.self) + #expect(fields[1].offset == 16) + #expect(fields[1].name.map { String(cString: $0) } == "p3") + } + + // FIXME: Figure this out when OAG implements forEachField + @Test + func tupleFields() { + let tupleFields = DynamicPropertyCache.fields(of: (n: NormalP, m: MainP).self) + print(tupleFields) + #expect(tupleFields.behaviors == []) + guard case let .product(fields) = tupleFields.layout else { + Issue.record("layout should be product type for tuple") + return + } + #expect(fields.isEmpty) + } + + @Test + func classFields() { + class C { + var p1: Int + var p2: NormalP + var p3: AsyncP + + init(p1: Int, p2: NormalP, p3: AsyncP) { + self.p1 = p1 + self.p2 = p2 + self.p3 = p3 + } + } + let fieldsC = DynamicPropertyCache.fields(of: C.self) + #expect(fieldsC.behaviors == []) + guard case let .product(fields) = fieldsC.layout else { + Issue.record("layout should be product type for class") + return + } + #expect(fields.isEmpty) } } diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyTests.swift index 040c44981..771d0a716 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyTests.swift @@ -1,11 +1,8 @@ // // DynamicPropertyTests.swift -// -// -// Created by Kyle on 2023/11/4. -// +// OpenSwiftUICoreTests -@testable import OpenSwiftUICore +import OpenSwiftUICore import Testing struct DynamicPropertyTests {