From c32d6e0311b44749ef5f33d9902e71e344bbfadc Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 01:38:21 +0800 Subject: [PATCH 1/4] Update DynamicPropertyBuffer implementation FIx misc issues and update to use UnsafeHeterogeneousBuffer --- .../DynamicProperty/DynamicProperty.swift | 4 +- .../DynamicPropertyBuffer.swift | 428 ++++++++++++++++-- 2 files changed, 388 insertions(+), 44 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift index 12a206212..d2b83a8d0 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift @@ -56,7 +56,7 @@ package struct DynamicPropertyBehaviors: OptionSet { // MARK: - DynamicPropertyBox [6.5.4] -package protocol DynamicPropertyBox: DynamicProperty { +package protocol DynamicPropertyBox { associatedtype Property: DynamicProperty mutating func destroy() @@ -334,7 +334,7 @@ extension BodyAccessor { container: container, inputs: &inputs ) - if buffer._count == 0 { + if buffer.isEmpty { buffer.destroy() let body = StaticBody( accessor: self, diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift index 9aff6f2d9..1ad4f80a7 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift @@ -2,26 +2,192 @@ // DynamicPropertyBuffer.swift // OpenSwiftUI // -// Audited for 3.5.2 -// Status: Complete +// Audited for 6.5.4 +// Status: Blocked by Tracing // ID: 68550FF604D39F05971FE35A26EE75B0 (SwiftUI) // ID: F3A89CF4357225EF49A7DD673FDFEE02 (SwiftUICore) import OpenAttributeGraphShims +#if OPENSWIFTUI_SUPPORT_2024_API + +// MARK: - DynamicPropertyBuffer [6.5.4] + +/// Unsafe storage type for `DynamicProperty` instances. +@available(OpenSwiftUI_v1_0, *) +public struct _DynamicPropertyBuffer { + private var contents: UnsafeHeterogeneousBuffer + + package init() { + contents = .init() + } + + package init( + fields: DynamicPropertyCache.Fields, + container: _GraphValue, + inputs: inout _GraphInputs, + baseOffset: Int + ) { + self.init() + addFields(fields, container: container, inputs: &inputs, baseOffset: baseOffset) + } + + package init( + fields: DynamicPropertyCache.Fields, + container: _GraphValue, + inputs: inout _GraphInputs + ) { + self.init(fields: fields, container: container, inputs: &inputs, baseOffset: 0) + } + + package var isEmpty: Bool { + contents.isEmpty + } + + package mutating func addFields( + _ fields: DynamicPropertyCache.Fields, + container: _GraphValue, + inputs: inout _GraphInputs, + baseOffset: Int + ) { + switch fields.layout { + case let .product(fieldArray): + for field in fieldArray { + field.type._makeProperty( + in: &self, + container: container, + fieldOffset: field.offset &+ baseOffset, + inputs: &inputs + ) + } + case let .sum(type, taggedFields): + guard !taggedFields.isEmpty else { + return + } + let box = EnumBox( + cases: taggedFields.map { taggedField in + ( + taggedField.tag, + _DynamicPropertyBuffer( + fields: DynamicPropertyCache.Fields(layout: .product(taggedField.fields)), + container: container, + inputs: &inputs, + baseOffset: 0 + ) + ) + }, + active: nil + ) + func project(type _: Enum.Type) { + contents.append(box, vtable: EnumVTable.self) + } + _openExistential(type, do: project) + } + } + + package mutating func append(_ box: T, fieldOffset: Int) where T: DynamicPropertyBox { + contents.append(box, vtable: BoxVTable.self) + } + + package func destroy() { + // TODO: trace + contents.destroy() + } + + package func reset() { + for element in contents { + element.vtable(as: BoxVTableBase.self) + .reset(elt: element) + } + } + + package func traceMountedProperties( + to graphValue: _GraphValue, + fields: DynamicPropertyCache.Fields + ) { + // TODO: trace + _openSwiftUIUnimplementedWarning() + } + + package func update(container: UnsafeMutableRawPointer, phase: ViewPhase) -> Bool { + var changed = false + for element in contents { + changed = changed || element.vtable(as: BoxVTableBase.self) + .update( + elt: element, + property: container.advanced(by: Int(element.flags.fieldOffset)), + phase: phase + ) + } + return changed + } + + package func getState(type: S.Type = S.self) -> Binding? { + for element in contents { + guard let binding = element.vtable(as: BoxVTableBase.self) + .getState(elt: element, type: type) else { + continue + } + return binding + } + return nil + + } + + package func applyChanged(to body: (Int) -> Void) { + for element in contents { + if element.flags.lastChanged { + body(Int(element.flags.fieldOffset)) + } + } + } +} + +extension UInt32 { + @inline(__always) + private static var fieldOffsetMask: UInt32 { 0x7FFF_FFFF } + + fileprivate var fieldOffset: Int32 { + Int32(bitPattern: self & Self.fieldOffsetMask) + } + + @inline(__always) + private static var lastChangedMask: UInt32 { 0x8000_0000 } + + fileprivate var lastChanged: Bool { + get { (self & Self.lastChangedMask) == Self.lastChangedMask } + set { + if newValue { + self |= Self.lastChangedMask + } else { + self &= ~Self.lastChangedMask + } + } + } +} + +@available(*, unavailable) +extension _DynamicPropertyBuffer: Sendable {} + +#else + +// MARK: - DynamicPropertyBuffer [3.5.2] + private let nullPtr: UnsafeMutableRawPointer = Unmanaged.passUnretained(unsafeBitCast(0, to: AnyObject.self)).toOpaque() +/// Unsafe storage type for `DynamicProperty` instances. +@available(OpenSwiftUI_v1_0, *) public struct _DynamicPropertyBuffer { private(set) var buf: UnsafeMutableRawPointer private(set) var size: Int32 private(set) var _count: Int32 - + package init() { buf = nullPtr size = 0 _count = 0 } - + package init( fields: DynamicPropertyCache.Fields, container: _GraphValue, @@ -32,6 +198,10 @@ public struct _DynamicPropertyBuffer { addFields(fields, container: container, inputs: &inputs, baseOffset: baseOffset) } + package var isEmpty: Bool { + _count == 0 + } + package mutating func addFields( _ fields: DynamicPropertyCache.Fields, container: _GraphValue, @@ -80,9 +250,9 @@ public struct _DynamicPropertyBuffer { _count &+= 1 } } - + package mutating func append(_ box: Box, fieldOffset: Int) { - let size = MemoryLayout<(Item, Box)>.stride + let size = (MemoryLayout.size + MemoryLayout.size + 0xf) & ~0xf let pointer = allocate(bytes: size) let item = Item(vtable: BoxVTable.self, size: size, fieldOffset: fieldOffset) pointer @@ -94,7 +264,7 @@ public struct _DynamicPropertyBuffer { .initialize(to: box) _count &+= 1 } - + package func destroy() { Swift.precondition(_count >= 0) var count = _count @@ -111,7 +281,7 @@ public struct _DynamicPropertyBuffer { buf.deallocate() } } - + package func reset() { Swift.precondition(_count >= 0) var count = _count @@ -124,7 +294,7 @@ public struct _DynamicPropertyBuffer { count &-= 1 } } - + package func getState(type: Value.Type) -> Binding? { Swift.precondition(_count >= 0) var count = _count @@ -140,7 +310,7 @@ public struct _DynamicPropertyBuffer { } return nil } - + package func update(container: UnsafeMutableRawPointer, phase: _GraphInputs.Phase) -> Bool { Swift.precondition(_count >= 0) var changed = false @@ -162,7 +332,7 @@ public struct _DynamicPropertyBuffer { } return changed } - + private mutating func allocate(bytes: Int) -> UnsafeMutableRawPointer { Swift.precondition(_count >= 0) var count = _count @@ -178,7 +348,7 @@ public struct _DynamicPropertyBuffer { allocateSlow(bytes: bytes, ptr: pointer) } } - + private mutating func allocateSlow(bytes: Int, ptr: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { let oldSize = Int(size) var allocSize = max(oldSize &* 2, 64) @@ -214,12 +384,13 @@ public struct _DynamicPropertyBuffer { size = Int32(allocSize) return allocatedBuffer.advanced(by: oldBuffer.distance(to: ptr)) } - + package func traceMountedProperties(to value: _GraphValue, fields: DynamicPropertyCache.Fields) { // TODO: Signpost related + _openSwiftUIUnimplementedWarning() } - - package func applyChanged(to body: (Int) -> Void) { + + package func applyChanged(to body: (Int) -> Void) { var index = 0 var pointer = buf while index < _count { @@ -240,17 +411,17 @@ extension _DynamicPropertyBuffer { self.size = Int32(size) self._fieldOffsetAndLastChanged = UInt32(Int32(fieldOffset)) } - + private(set) var vtable: BoxVTableBase.Type private(set) var size: Int32 private var _fieldOffsetAndLastChanged: UInt32 - + @inline(__always) private static var fieldOffsetMask: UInt32 { 0x7FFF_FFFF } var fieldOffset: Int32 { Int32(bitPattern: _fieldOffsetAndLastChanged & Item.fieldOffsetMask) } - + @inline(__always) private static var lastChangedMask: UInt32 { 0x8000_0000 } var lastChanged: Bool { @@ -266,28 +437,63 @@ extension _DynamicPropertyBuffer { } } -// MARK: - BoxVTableBase +#endif + + +#if OPENSWIFTUI_SUPPORT_2024_API + +// MARK: - BoxVTableBase [6.5.4] + +private class BoxVTableBase: UnsafeHeterogeneousBuffer.VTable { + class func reset(elt: _UnsafeHeterogeneousBuffer_Element) { + _openSwiftUIEmptyStub() + } + + class func update( + elt: _UnsafeHeterogeneousBuffer_Element, + property : UnsafeMutableRawPointer, + phase: _GraphInputs.Phase + ) -> Bool { + false + } + + class func getState( + elt: _UnsafeHeterogeneousBuffer_Element, + type: Value.Type + ) -> Binding? { + nil + } +} + +#else + + +// MARK: - BoxVTableBase [3.5.2] private class BoxVTableBase { class func moveInitialize( - ptr _: UnsafeMutableRawPointer, - from _: UnsafeMutableRawPointer + ptr: UnsafeMutableRawPointer, + from: UnsafeMutableRawPointer ) { _openSwiftUIBaseClassAbstractMethod() } - - class func deinitialize(ptr _: UnsafeMutableRawPointer) {} - class func reset(ptr _: UnsafeMutableRawPointer) {} + class func deinitialize(ptr: UnsafeMutableRawPointer) { + _openSwiftUIEmptyStub() + } + + class func reset(ptr: UnsafeMutableRawPointer) { + _openSwiftUIEmptyStub() + } class func update( - ptr _: UnsafeMutableRawPointer, - property _: UnsafeMutableRawPointer, - phase _: _GraphInputs.Phase + ptr: UnsafeMutableRawPointer, + property: UnsafeMutableRawPointer, + phase: _GraphInputs.Phase ) -> Bool { false } - + class func getState( ptr _: UnsafeMutableRawPointer, type _: Value.Type @@ -296,7 +502,59 @@ private class BoxVTableBase { } } -// MARK: - BoxVTable +#endif + +#if OPENSWIFTUI_SUPPORT_2024_API + +// MARK: - BoxVTable [6.5.4] [WIP] + +private class BoxVTable: BoxVTableBase { + override class func moveInitialize( + elt: _UnsafeHeterogeneousBuffer_Element, + from: _UnsafeHeterogeneousBuffer_Element + ) { + let fromBoxPointer = from.body(as: Box.self) + let destinationBoxPointer = elt.body(as: Box.self) + destinationBoxPointer.initialize(to: fromBoxPointer.move()) + } + + override class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + let boxPointer = elt.body(as: Box.self) + boxPointer.pointee.destroy() + boxPointer.deinitialize(count: 1) + } + + override class func reset(elt: _UnsafeHeterogeneousBuffer_Element) { + let boxPointer = elt.body(as: Box.self) + boxPointer.pointee.reset() + } + + override class func update( + elt: _UnsafeHeterogeneousBuffer_Element, + property: UnsafeMutableRawPointer, + phase: _GraphInputs.Phase + ) -> Bool { + let boxPointer = elt.body(as: Box.self) + let propertyPointer = property.assumingMemoryBound(to: Box.Property.self) + let changed = boxPointer.pointee.update(property: &propertyPointer.pointee, phase: phase) + if changed { + // TODO: OSSignpost + } + return changed + } + + override class func getState( + elt: _UnsafeHeterogeneousBuffer_Element, + type: Value.Type + ) -> Binding? { + let boxPointer = elt.body(as: Box.self) + return boxPointer.pointee.getState(type: type) + } +} + +#else + +// MARK: - BoxVTable [3.5.2] [WIP] private class BoxVTable: BoxVTableBase { override class func moveInitialize(ptr destination: UnsafeMutableRawPointer, from: UnsafeMutableRawPointer) { @@ -304,18 +562,18 @@ private class BoxVTable: BoxVTableBase { let destinationBoxPointer = destination.assumingMemoryBound(to: Box.self) destinationBoxPointer.initialize(to: fromBoxPointer.move()) } - + override class func deinitialize(ptr: UnsafeMutableRawPointer) { let boxPointer = ptr.assumingMemoryBound(to: Box.self) boxPointer.pointee.destroy() boxPointer.deinitialize(count: 1) } - + override class func reset(ptr: UnsafeMutableRawPointer) { let boxPointer = ptr.assumingMemoryBound(to: Box.self) boxPointer.pointee.reset() } - + override class func update( ptr: UnsafeMutableRawPointer, property: UnsafeMutableRawPointer, @@ -329,34 +587,112 @@ private class BoxVTable: BoxVTableBase { } return changed } - + override class func getState(ptr: UnsafeMutableRawPointer, type: Value.Type) -> Binding? { let boxPointer = ptr.assumingMemoryBound(to: Box.self) return boxPointer.pointee.getState(type: type) } } -// MARK: - EnumVTable +#endif + +// MARK: - EnumBox [6.5.4] private struct EnumBox { var cases: [(tag: Int, links: _DynamicPropertyBuffer)] - var active: (tag: Swift.Int, index: Swift.Int)? + var active: (tag: Int, index: Int)? } +#if OPENSWIFTUI_SUPPORT_2024_API + +// MARK: - EnumVTable [6.5.4] + +private final class EnumVTable: BoxVTableBase { + override class func moveInitialize( + elt: _UnsafeHeterogeneousBuffer_Element, + from: _UnsafeHeterogeneousBuffer_Element + ) { + let fromBoxPointer = from.body(as: EnumBox.self) + let destinationBoxPointer = elt.body(as: EnumBox.self) + destinationBoxPointer.initialize(to: fromBoxPointer.move()) + } + + override class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + let boxPointer = elt.body(as: EnumBox.self) + for (_, links) in boxPointer.pointee.cases { + links.destroy() + } + } + + override class func reset(elt: _UnsafeHeterogeneousBuffer_Element) { + let boxPointer = elt.body(as: EnumBox.self) + guard let (_, index) = boxPointer.pointee.active else { + return + } + boxPointer.pointee.cases[index].links.reset() + boxPointer.pointee.active = nil + } + + override class func update( + elt: _UnsafeHeterogeneousBuffer_Element, + property: UnsafeMutableRawPointer, + phase: _GraphInputs.Phase + ) -> Bool { + var changed = false + withUnsafeMutablePointerToEnumCase(of: property.assumingMemoryBound(to: Enum.self)) { tag, _, pointer in + let boxPointer = elt.body(as: EnumBox.self) + if let (activeTag, index) = boxPointer.pointee.active, activeTag != tag { + boxPointer.pointee.cases[index].links.reset() + boxPointer.pointee.active = nil + changed = true + } + if boxPointer.pointee.active == nil { + guard let matchedIndex = boxPointer.pointee.cases.firstIndex(where: { $0.tag == tag }) else { + return + } + boxPointer.pointee.active = (tag, matchedIndex) + changed = true + } + if let (_, index) = boxPointer.pointee.active { + changed = boxPointer.pointee.cases[index].links.update(container: pointer, phase: phase) + } + } + return changed + } + + override class func getState( + elt: _UnsafeHeterogeneousBuffer_Element, + type: Value.Type + ) -> Binding? { + let boxPointer = elt.body(as: EnumBox.self) + guard let (_, index) = boxPointer.pointee.active else { + return nil + } + return boxPointer.pointee.cases[index].links.getState(type: type) + } +} + +#else + +// MARK: - EnumVTable [3.5.2] + private class EnumVTable: BoxVTableBase { - override class func moveInitialize(ptr destination: UnsafeMutableRawPointer, from: UnsafeMutableRawPointer) { + override class func moveInitialize( + ptr destination: UnsafeMutableRawPointer, + from: UnsafeMutableRawPointer + ) { let fromBoxPointer = from.assumingMemoryBound(to: EnumBox.self) let destinationBoxPointer = destination.assumingMemoryBound(to: EnumBox.self) destinationBoxPointer.initialize(to: fromBoxPointer.move()) } - + override class func deinitialize(ptr: UnsafeMutableRawPointer) { let boxPointer = ptr.assumingMemoryBound(to: EnumBox.self) for (_, links) in boxPointer.pointee.cases { links.destroy() } } - + override class func reset(ptr: UnsafeMutableRawPointer) { let boxPointer = ptr.assumingMemoryBound(to: EnumBox.self) guard let (_, index) = boxPointer.pointee.active else { @@ -365,8 +701,12 @@ private class EnumVTable: BoxVTableBase { boxPointer.pointee.cases[index].links.reset() boxPointer.pointee.active = nil } - - override class func update(ptr: UnsafeMutableRawPointer, property: UnsafeMutableRawPointer, phase: _GraphInputs.Phase) -> Bool { + + override class func update( + ptr: UnsafeMutableRawPointer, + property: UnsafeMutableRawPointer, + phase: _GraphInputs.Phase + ) -> Bool { var changed = false withUnsafeMutablePointerToEnumCase(of: property.assumingMemoryBound(to: Enum.self)) { tag, _, pointer in let boxPointer = ptr.assumingMemoryBound(to: EnumBox.self) @@ -388,8 +728,11 @@ private class EnumVTable: BoxVTableBase { } return changed } - - override class func getState(ptr: UnsafeMutableRawPointer, type: Value.Type) -> Binding? { + + override class func getState( + ptr: UnsafeMutableRawPointer, + type: Value.Type + ) -> Binding? { let boxPointer = ptr.assumingMemoryBound(to: EnumBox.self) guard let (_, index) = boxPointer.pointee.active else { return nil @@ -397,3 +740,4 @@ private class EnumVTable: BoxVTableBase { return boxPointer.pointee.cases[index].links.getState(type: type) } } +#endif From e0ae7524358920991e652c6fce6dcaed21b503bd Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 01:55:05 +0800 Subject: [PATCH 2/4] Fix UnsafeHeterogeneousBuffer test case --- .../UnsafeHeterogeneousBuffer.swift | 13 +- .../UnsafeHeterogeneousBufferTestsStub.c | 20 +++ ...ertyListTestsStub.c => PropertyListStub.c} | 2 +- .../UnsafeHeterogeneousBufferTests.swift | 11 +- .../UnsafeHeterogeneousBufferDualTests.swift | 121 ++++++++++++++++++ 5 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 Sources/OpenSwiftUISymbolDualTestsSupport/Data/DynamicProperty/UnsafeHeterogeneousBufferTestsStub.c rename Sources/OpenSwiftUISymbolDualTestsSupport/Data/{PropertyListTestsStub.c => PropertyListStub.c} (99%) create mode 100644 Tests/OpenSwiftUISymbolDualTests/Data/DynamicProperty/UnsafeHeterogeneousBufferDualTests.swift diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift index 78111f76c..fd7595729 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift @@ -13,7 +13,7 @@ package struct UnsafeHeterogeneousBuffer: Collection { package typealias VTable = _UnsafeHeterogeneousBuffer_VTable package typealias Element = _UnsafeHeterogeneousBuffer_Element - + package struct Index: Equatable, Comparable { var index: Int32 var offset: Int32 @@ -136,7 +136,7 @@ package struct UnsafeHeterogeneousBuffer: Collection { @discardableResult package mutating func append(_ value: T, vtable: VTable.Type) -> Index { - let bytes = MemoryLayout.size + MemoryLayout.size + let bytes = (MemoryLayout.size + MemoryLayout.size + 0xf) & ~0xf let pointer = allocate(bytes) let element = _UnsafeHeterogeneousBuffer_Element(item: pointer.assumingMemoryBound(to: Item.self)) element.item.initialize(to: Item(vtable: vtable, size: Int32(bytes), flags: 0)) @@ -183,11 +183,16 @@ open class _UnsafeHeterogeneousBuffer_VTable { false } - open class func moveInitialize(elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element) { + open class func moveInitialize( + elt: _UnsafeHeterogeneousBuffer_Element, + from: _UnsafeHeterogeneousBuffer_Element + ) { _openSwiftUIBaseClassAbstractMethod() } - open class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + open class func deinitialize( + elt: _UnsafeHeterogeneousBuffer_Element + ) { _openSwiftUIBaseClassAbstractMethod() } } diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Data/DynamicProperty/UnsafeHeterogeneousBufferTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/DynamicProperty/UnsafeHeterogeneousBufferTestsStub.c new file mode 100644 index 000000000..81d0fae34 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/DynamicProperty/UnsafeHeterogeneousBufferTestsStub.c @@ -0,0 +1,20 @@ +// +// UnsafeHeterogeneousBufferStub.c +// OpenSwiftUISymbolDualTestsSupport + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +// MARK: - UnsafeHeterogeneousBuffer + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Init, SwiftUI, $s7SwiftUI25UnsafeHeterogeneousBufferVACycfC); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Destroy, SwiftUI, $s7SwiftUI25UnsafeHeterogeneousBufferV7destroyyyF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Append, SwiftUI, $s7SwiftUI25UnsafeHeterogeneousBufferV6append_6vtableAC5IndexVx_AA01_cdE7_VTableCmtlF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_IsEmpty, SwiftUI, $s7SwiftUI25UnsafeHeterogeneousBufferV7isEmptySbvg); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_IndexAtOffset, SwiftUI, $s7SwiftUI26UnsafeHeterogeneousBufferV7indexAtE5offsetySi_tF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Subscript, SwiftUI, $s7SwiftUI25UnsafeHeterogeneousBufferVyAA01_cdE8_ElementVAC5IndexVcig); + +#endif diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListStub.c similarity index 99% rename from Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c rename to Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListStub.c index 137717562..85c848f0c 100644 --- a/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListTestsStub.c +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Data/PropertyListStub.c @@ -1,6 +1,6 @@ // // PropertyListTestsStub.c -// OpenSwiftUISymbolDualTestsHelper +// OpenSwiftUISymbolDualTestsSupport #include "OpenSwiftUIBase.h" diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift index 0faea46c0..0eed7df9e 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift @@ -35,7 +35,7 @@ struct UnsafeHeterogeneousBufferTests { #expect(index == buffer.index(atOffset: 0)) #expect(index.index == 0) #expect(index.offset == 0) - #expect(buffer.available == 44) + #expect(buffer.available == 32) #expect(buffer.count == 1) let element = buffer[index] #expect(element.body(as: UInt32.self).pointee == 1) @@ -46,8 +46,8 @@ struct UnsafeHeterogeneousBufferTests { #expect(buffer.isEmpty == false) #expect(index == buffer.index(atOffset: 1)) #expect(index.index == 1) - #expect(index.offset == 16 + 4) - #expect(buffer.available == 20) + #expect(index.offset == 32) + #expect(buffer.available == 0) #expect(buffer.count == 2) let element = buffer[index] #expect(element.body(as: Int.self).pointee == -1) @@ -58,8 +58,8 @@ struct UnsafeHeterogeneousBufferTests { #expect(buffer.isEmpty == false) #expect(index == buffer.index(atOffset: 2)) #expect(index.index == 2) - #expect(index.offset == 16 + 4 + 16 + 8) - #expect(buffer.available == 60) + #expect(index.offset == 64) + #expect(buffer.available == 32) #expect(buffer.count == 3) let element = buffer[index] #expect(element.body(as: Double.self).pointee == Double.infinity) @@ -80,7 +80,6 @@ struct UnsafeHeterogeneousBufferTests { } } - await confirmation { confirm in var buffer = UnsafeHeterogeneousBuffer() defer { buffer.destroy() } diff --git a/Tests/OpenSwiftUISymbolDualTests/Data/DynamicProperty/UnsafeHeterogeneousBufferDualTests.swift b/Tests/OpenSwiftUISymbolDualTests/Data/DynamicProperty/UnsafeHeterogeneousBufferDualTests.swift new file mode 100644 index 000000000..faa8a96a4 --- /dev/null +++ b/Tests/OpenSwiftUISymbolDualTests/Data/DynamicProperty/UnsafeHeterogeneousBufferDualTests.swift @@ -0,0 +1,121 @@ +// +// UnsafeHeterogeneousBufferDualTests.swift +// OpenSwiftUISymbolDualTests + +#if canImport(SwiftUI, _underlyingVersion: 6.5.4) +@_spi(ForOpenSwiftUIOnly) +@testable +import OpenSwiftUICore +import OpenSwiftUITestsSupport +import Testing + +extension UnsafeHeterogeneousBuffer { + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Init") + init(swiftUI: Void) + + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Destroy") + func swiftUI_destroy() + + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Append") + mutating func swiftUI_append(_ value: T, vtable: VTable.Type) -> Index + + var swiftUI_isEmpty: Bool { + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_IsEmpty") + get + } + + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_IndexOffsetBy") + func swiftUI_index(_ index: Index, offsetBy n: Int) -> Index + + subscript(swiftUI index: Index) -> Element { + @_silgen_name("OpenSwiftUITestStub_UnsafeHeterogeneousBuffer_Subscript") + get + } +} + +struct UnsafeHeterogeneousBufferDualTests { + private final class VTable: _UnsafeHeterogeneousBuffer_VTable { + override class func hasType(_ type: T.Type) -> Bool { + Value.self == T.self + } + + override class func moveInitialize(elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element) { + let dest = elt.body(as: Value.self) + let source = from.body(as: Value.self) + dest.initialize(to: source.move()) + } + + override class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + elt.body(as: Value.self).deinitialize(count: 1) + } + } + + @Test + func structBuffer() { + var buffer = UnsafeHeterogeneousBuffer(swiftUI: ()) + defer { buffer.swiftUI_destroy() } + #expect(buffer.swiftUI_isEmpty == true) + + do { + let index = buffer.swiftUI_append(UInt32(1), vtable: VTable.self) + #expect(buffer.swiftUI_isEmpty == false) + #expect(index == buffer.index(atOffset: 0)) + #expect(index.index == 0) + #expect(index.offset == 0) + #expect(buffer.available == 32) + #expect(buffer.count == 1) + let element = buffer[swiftUI: index] + #expect(element.body(as: UInt32.self).pointee == 1) + } + + do { + let index = buffer.swiftUI_append(Int(-1), vtable: VTable.self) + #expect(buffer.swiftUI_isEmpty == false) + #expect(index == buffer.index(atOffset: 1)) + #expect(index.index == 1) + #expect(index.offset == 32) + #expect(buffer.available == 0) + #expect(buffer.count == 2) + let element = buffer[swiftUI: index] + #expect(element.body(as: Int.self).pointee == -1) + } + + do { + let index = buffer.swiftUI_append(Double.infinity, vtable: VTable.self) + #expect(buffer.swiftUI_isEmpty == false) + #expect(index == buffer.index(atOffset: 2)) + #expect(index.index == 2) + #expect(index.offset == 64) + #expect(buffer.available == 32) + #expect(buffer.count == 3) + let element = buffer[swiftUI: index] + #expect(element.body(as: Double.self).pointee == Double.infinity) + } + } + + @Test + func classBuffer() async throws { + final class DeinitBox { + let deinitBlock: () -> Void + + init(deinitBlock: @escaping () -> Void) { + self.deinitBlock = deinitBlock + } + + deinit { + deinitBlock() + } + } + + await confirmation { confirm in + var buffer = UnsafeHeterogeneousBuffer(swiftUI: ()) + defer { buffer.swiftUI_destroy() } + #expect(buffer.swiftUI_isEmpty == true) + let box = DeinitBox { confirm() } + let index = buffer.swiftUI_append(box, vtable: VTable.self) + let element = buffer[swiftUI: index] + #expect(element.body(as: DeinitBox.self).pointee === box) + } + } +} +#endif From 3bca9e2836751d6314a6543eeae189a67f8459c2 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 16:07:18 +0800 Subject: [PATCH 3/4] Fix fieldOffset issue of the new implementation --- .../DynamicPropertyBuffer.swift | 13 +++-- .../UnsafeHeterogeneousBuffer.swift | 53 ++++++++++--------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift index 1ad4f80a7..0fb75b742 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift @@ -79,14 +79,16 @@ public struct _DynamicPropertyBuffer { active: nil ) func project(type _: Enum.Type) { - contents.append(box, vtable: EnumVTable.self) + let index = contents.append(box, vtable: EnumVTable.self) + contents[index].flags.fieldOffset = Int32(baseOffset) } _openExistential(type, do: project) } } package mutating func append(_ box: T, fieldOffset: Int) where T: DynamicPropertyBox { - contents.append(box, vtable: BoxVTable.self) + let index = contents.append(box, vtable: BoxVTable.self) + contents[index].flags.fieldOffset = Int32(fieldOffset) } package func destroy() { @@ -112,12 +114,14 @@ public struct _DynamicPropertyBuffer { package func update(container: UnsafeMutableRawPointer, phase: ViewPhase) -> Bool { var changed = false for element in contents { - changed = changed || element.vtable(as: BoxVTableBase.self) + let updateResult = element.vtable(as: BoxVTableBase.self) .update( elt: element, property: container.advanced(by: Int(element.flags.fieldOffset)), phase: phase ) + element.flags.lastChanged = updateResult + changed = changed || updateResult } return changed } @@ -148,7 +152,8 @@ extension UInt32 { private static var fieldOffsetMask: UInt32 { 0x7FFF_FFFF } fileprivate var fieldOffset: Int32 { - Int32(bitPattern: self & Self.fieldOffsetMask) + get { Int32(bitPattern: self & Self.fieldOffsetMask) } + set { self = UInt32(newValue) | (self & ~Self.fieldOffsetMask) } } @inline(__always) diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift index fd7595729..fba3f6e7f 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift @@ -10,45 +10,45 @@ package struct UnsafeHeterogeneousBuffer: Collection { var buf: UnsafeMutableRawPointer! var available: Int32 var _count: Int32 - + package typealias VTable = _UnsafeHeterogeneousBuffer_VTable package typealias Element = _UnsafeHeterogeneousBuffer_Element package struct Index: Equatable, Comparable { var index: Int32 var offset: Int32 - + package static func < (lhs: Index, rhs: Index) -> Bool { lhs.index < rhs.index } - + package static func == (a: Index, b: Index) -> Bool { a.index == b.index && a.offset == b.offset } } - + package struct Item { let vtable: _UnsafeHeterogeneousBuffer_VTable.Type let size: Int32 var flags: UInt32 } - + package var count: Int { Int(_count) } package var isEmpty: Bool { _count == 0 } - + package var startIndex: Index { Index(index: 0, offset: 0) } package var endIndex: Index { Index(index: _count, offset: 0) } - + package init() { buf = nil available = 0 _count = 0 } - + private mutating func allocate(_ bytes: Int) -> UnsafeMutableRawPointer { var size = 0 for element in self { @@ -62,7 +62,7 @@ package struct UnsafeHeterogeneousBuffer: Collection { available = available - Int32(bytes) return ptr } - + private mutating func growBuffer(by size: Int, capacity: Int) { let expectedSize = size + capacity var allocSize = Swift.max(capacity &* 2, 64) @@ -83,13 +83,13 @@ package struct UnsafeHeterogeneousBuffer: Collection { count &-= 1 let newItemPointer = newBuffer.assumingMemoryBound(to: Item.self) let oldItemPointer = oldBuffer.assumingMemoryBound(to: Item.self) - + if count == 0 { itemSize = 0 } else { itemSize &+= oldItemPointer.pointee.size } - newItemPointer.initialize(to: oldItemPointer.pointee) + newItemPointer.initialize(to: oldItemPointer.pointee) oldItemPointer.pointee.vtable.moveInitialize( elt: .init(item: newItemPointer), from: .init(item: oldItemPointer) @@ -104,18 +104,18 @@ package struct UnsafeHeterogeneousBuffer: Collection { buf = allocatedBuffer available += Int32(allocSize - capacity) } - + package func destroy() { defer { buf?.deallocate() } for element in self { element.item.pointee.vtable.deinitialize(elt: element) } } - + package func formIndex(after index: inout Index) { index = self.index(after: index) } - + package func index(after index: Index) -> Index { let item = self[index].item.pointee let newIndex = index.index &+ 1 @@ -126,14 +126,15 @@ package struct UnsafeHeterogeneousBuffer: Collection { return Index(index: newIndex, offset: newOffset) } } - + package subscript(index: Index) -> Element { - .init(item: buf - .advanced(by: Int(index.offset)) - .assumingMemoryBound(to: Item.self) + Element( + item: buf + .advanced(by: Int(index.offset)) + .assumingMemoryBound(to: Item.self) ) } - + @discardableResult package mutating func append(_ value: T, vtable: VTable.Type) -> Index { let bytes = (MemoryLayout.size + MemoryLayout.size + 0xf) & ~0xf @@ -150,24 +151,24 @@ package struct UnsafeHeterogeneousBuffer: Collection { @_spi(ForOpenSwiftUIOnly) public struct _UnsafeHeterogeneousBuffer_Element { var item: UnsafeMutablePointer - + package func hasType(_ type: T.Type) -> Bool { item.pointee.vtable.hasType(type) } - + package func vtable(as type: T.Type) -> T.Type where T: _UnsafeHeterogeneousBuffer_VTable { address.assumingMemoryBound(to: Swift.type(of: type)).pointee } - + package func body(as type: T.Type) -> UnsafeMutablePointer { UnsafeMutableRawPointer(item.advanced(by: 1)).assumingMemoryBound(to: type) } - + package var flags: UInt32 { get { item.pointee.flags } nonmutating set { item.pointee.flags = newValue } } - + package var address: UnsafeRawPointer { UnsafeRawPointer(item) } @@ -182,14 +183,14 @@ open class _UnsafeHeterogeneousBuffer_VTable { open class func hasType(_ type: T.Type) -> Bool { false } - + open class func moveInitialize( elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element ) { _openSwiftUIBaseClassAbstractMethod() } - + open class func deinitialize( elt: _UnsafeHeterogeneousBuffer_Element ) { From c99142d6dee07a46b1a18863611f26872de74caf Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 16:29:29 +0800 Subject: [PATCH 4/4] Add DynamicPropertyBufferTests --- .../DynamicPropertyBufferTests.swift | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift new file mode 100644 index 000000000..af5b926ca --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift @@ -0,0 +1,362 @@ +// +// DynamicPropertyBufferTests.swift +// OpenSwiftUICoreTests +// +// Status: Created by GitHub Copilot with Claude Sonnet 4.5 + +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore +@testable import OpenSwiftUICore +import Testing + +struct DynamicPropertyBufferTests { + + // MARK: - Test Helpers + + private struct TestProperty: DynamicProperty { + var value: Int + } + + private struct TestBox: DynamicPropertyBox { + typealias Property = TestProperty + var value: Int + var updateCount: Int = 0 + + mutating func destroy() {} + + mutating func reset() { + updateCount = 0 + } + + mutating func update(property: inout TestProperty, phase: _GraphInputs.Phase) -> Bool { + updateCount += 1 + let changed = property.value != value + property.value = value + return changed + } + + func getState(type: T.Type) -> Binding? { + nil + } + } + + private struct StateProperty: DynamicProperty { + var text: String + } + + private struct StateBox: DynamicPropertyBox { + typealias Property = StateProperty + var binding: Binding + + mutating func destroy() {} + mutating func reset() {} + + mutating func update(property: inout StateProperty, phase: _GraphInputs.Phase) -> Bool { + false + } + + func getState(type: T.Type) -> Binding? { + if T.self == String.self { + return binding as? Binding + } + return nil + } + } + + private struct PhaseAwareBox: DynamicPropertyBox { + typealias Property = TestProperty + var insertedValue: Int + var removedValue: Int + + mutating func destroy() {} + mutating func reset() {} + + mutating func update(property: inout TestProperty, phase: _GraphInputs.Phase) -> Bool { + let newValue = phase.isBeingRemoved ? removedValue : insertedValue + let changed = property.value != newValue + property.value = newValue + return changed + } + + func getState(type: T.Type) -> Binding? { + nil + } + } + + // MARK: - Initialization Tests + + @Test + func emptyBuffer() { + let buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + #expect(buffer.isEmpty == true) + } + + // MARK: - Append Tests + + @Test + func appendSingleBox() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + let box = TestBox(value: 42) + buffer.append(box, fieldOffset: 0) + + #expect(buffer.isEmpty == false) + } + + @Test + func appendMultipleBoxes() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 1), fieldOffset: 0) + buffer.append(TestBox(value: 2), fieldOffset: 8) + buffer.append(TestBox(value: 3), fieldOffset: 16) + + #expect(buffer.isEmpty == false) + } + + // MARK: - Update Tests + + @Test + func updateWithNoChanges() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 42), fieldOffset: 0) + + var container = TestProperty(value: 42) + let changed = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: .init()) + } + + #expect(changed == false) + } + + @Test + func updateWithChanges() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 100), fieldOffset: 0) + + var container = TestProperty(value: 42) + let changed = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: .init()) + } + + #expect(changed == true) + #expect(container.value == 100) + } + + @Test + func updateMultipleBoxes() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + struct TestContainer { + var value1: TestProperty = TestProperty(value: 0) + var value2: TestProperty = TestProperty(value: 0) + var value3: TestProperty = TestProperty(value: 0) + } + + buffer.append(TestBox(value: 10), fieldOffset: 0) + buffer.append(TestBox(value: 20), fieldOffset: MemoryLayout.size) + buffer.append(TestBox(value: 30), fieldOffset: MemoryLayout.size * 2) + + var container = TestContainer() + let changed = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: .init()) + } + + #expect(changed == true) + #expect(container.value1.value == 10) + #expect(container.value2.value == 20) + #expect(container.value3.value == 30) + } + + @Test + func updateWithDifferentPhases() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(PhaseAwareBox(insertedValue: 100, removedValue: 200), fieldOffset: 0) + + var container = TestProperty(value: 0) + + let insertedPhase = _GraphInputs.Phase() + let changed1 = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: insertedPhase) + } + #expect(changed1 == true) + #expect(container.value == 100) + + var removedPhase = _GraphInputs.Phase() + removedPhase.isBeingRemoved = true + let changed2 = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: removedPhase) + } + #expect(changed2 == true) + #expect(container.value == 200) + } + + // MARK: - Reset Tests + + @Test + func resetBuffer() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 42, updateCount: 5), fieldOffset: 0) + + buffer.reset() + + #expect(buffer.isEmpty == false) + } + + // MARK: - GetState Tests + + @Test + func getStateNotFound() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 42), fieldOffset: 0) + + let state = buffer.getState(type: String.self) + + #expect(state == nil) + } + + @Test + func getStateFound() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + var testValue = "test" + let binding = Binding(get: { testValue }, set: { testValue = $0 }) + + buffer.append(StateBox(binding: binding), fieldOffset: 0) + + let state = buffer.getState(type: String.self) + + #expect(state != nil) + #expect(state?.wrappedValue == "test") + } + + @Test + func getStateFromMultipleBoxes() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 42), fieldOffset: 0) + + var testValue = "found" + let binding = Binding(get: { testValue }, set: { testValue = $0 }) + buffer.append(StateBox(binding: binding), fieldOffset: 8) + + let state = buffer.getState(type: String.self) + + #expect(state != nil) + #expect(state?.wrappedValue == "found") + } + + // MARK: - ApplyChanged Tests + + @Test + func applyChangedWithNoChanges() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + buffer.append(TestBox(value: 42), fieldOffset: 0) + + var container = TestProperty(value: 42) + _ = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: .init()) + } + + var calledOffsets: [Int] = [] + buffer.applyChanged { offset in + calledOffsets.append(offset) + } + + #expect(calledOffsets.isEmpty) + } + + @Test + func applyChangedWithChanges() { + var buffer = _DynamicPropertyBuffer() + defer { buffer.destroy() } + + struct TestContainer { + var value1: TestProperty = TestProperty(value: 0) + var value2: TestProperty = TestProperty(value: 20) + var value3: TestProperty = TestProperty(value: 0) + } + + buffer.append(TestBox(value: 10), fieldOffset: 0) + buffer.append(TestBox(value: 20), fieldOffset: MemoryLayout.size) + buffer.append(TestBox(value: 30), fieldOffset: MemoryLayout.size * 2) + + var container = TestContainer() + _ = withUnsafeMutablePointer(to: &container) { pointer in + buffer.update(container: UnsafeMutableRawPointer(pointer), phase: .init()) + } + + var calledOffsets: [Int] = [] + buffer.applyChanged { offset in + calledOffsets.append(offset) + } + + #expect(calledOffsets.count == 2) + #expect(calledOffsets.contains(0)) + #expect(calledOffsets.contains(MemoryLayout.size * 2)) + } + + // MARK: - Destroy Tests + + @Test + func destroyEmptyBuffer() { + let buffer = _DynamicPropertyBuffer() + buffer.destroy() + } + + @Test + func destroyBufferWithBoxes() { + var buffer = _DynamicPropertyBuffer() + + buffer.append(TestBox(value: 1), fieldOffset: 0) + buffer.append(TestBox(value: 2), fieldOffset: 8) + + buffer.destroy() + } + + @Test + func destroyWithDeinitTracking() async throws { + final class DeinitBox: DynamicPropertyBox { + typealias Property = TestProperty + let deinitBlock: () -> Void + + init(deinitBlock: @escaping () -> Void) { + self.deinitBlock = deinitBlock + } + + deinit { + deinitBlock() + } + + func destroy() {} + func reset() {} + func update(property: inout TestProperty, phase: _GraphInputs.Phase) -> Bool { false } + func getState(type: T.Type) -> Binding? { nil } + } + + await confirmation { confirm in + var buffer = _DynamicPropertyBuffer() + let box = DeinitBox { confirm() } + buffer.append(box, fieldOffset: 0) + buffer.destroy() + } + } +}