diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 50c0e55e..efc61dbf 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -15,6 +15,12 @@ jobs: swift_version: ["5.10-SNAPSHOT-2024-03-30-a"] # "5.10-RELEASE" is not release for WASM, tracked via https://github.com/swiftwasm/swift/issues/5570 os: [ubuntu-22.04] runs-on: ${{ matrix.os }} + env: + OPENSWIFTUI_WERROR: 1 + OPENSWIFTUI_SWIFT_TESTING: 0 + OPENGRAPH_ATTRIBUTEGRAPH: 0 + OPENSWIFTUI_COMPATIBILITY_TEST: 0 + OPENSWIFTUI_SWIFT_LOG: 1 steps: - uses: actions/checkout@v4 - uses: swiftwasm/setup-swiftwasm@v1 diff --git a/Package.resolved b/Package.resolved index 3e9f799c..179ef982 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenGraph", "state" : { "branch" : "main", - "revision" : "315468c6339cdbcbaf7d8797b866745d5bf11d8e" + "revision" : "036b8d28a8a1220a768a81bbc20624759094137c" } }, { diff --git a/Sources/OpenSwiftUI/Core/Data/Location/AnyLocation.swift b/Sources/OpenSwiftUI/Core/Data/Location/AnyLocation.swift index fef854f7..6e128a31 100644 --- a/Sources/OpenSwiftUI/Core/Data/Location/AnyLocation.swift +++ b/Sources/OpenSwiftUI/Core/Data/Location/AnyLocation.swift @@ -16,7 +16,7 @@ class AnyLocation: AnyLocationBase { } func get() -> Value { fatalError() } func set(_ value: Value, transaction: Transaction) { fatalError() } - func projecting

(_ p: P) -> AnyLocation where P: Projection, P.Base == Value { + func projecting(_ p: P) -> AnyLocation where Value == P.Base { fatalError() } func update() -> (Value, Bool) { fatalError() } diff --git a/Sources/OpenSwiftUI/Core/Data/Location/LocationBox.swift b/Sources/OpenSwiftUI/Core/Data/Location/LocationBox.swift index 2570de14..5cb10cdd 100644 --- a/Sources/OpenSwiftUI/Core/Data/Location/LocationBox.swift +++ b/Sources/OpenSwiftUI/Core/Data/Location/LocationBox.swift @@ -27,8 +27,8 @@ class LocationBox: AnyLocation { location.set(value, transaction: transaction) } - override func projecting

(_ p: P) -> AnyLocation where L.Value == P.Base, P : Projection { - cache.reference(for: p, on: location) + override func projecting(_ projection: P) -> AnyLocation where L.Value == P.Base { + cache.reference(for: projection, on: location) } override func update() -> (L.Value, Bool) { diff --git a/Sources/OpenSwiftUI/Core/Data/Location/TODO/StoredLocationBase.swift b/Sources/OpenSwiftUI/Core/Data/Location/TODO/StoredLocationBase.swift deleted file mode 100644 index 8b137891..00000000 --- a/Sources/OpenSwiftUI/Core/Data/Location/TODO/StoredLocationBase.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Sources/OpenSwiftUI/Core/Data/Property/PropertyList.swift b/Sources/OpenSwiftUI/Core/Data/Property/PropertyList.swift index 8b5df40f..080cad6e 100644 --- a/Sources/OpenSwiftUI/Core/Data/Property/PropertyList.swift +++ b/Sources/OpenSwiftUI/Core/Data/Property/PropertyList.swift @@ -6,6 +6,7 @@ // Status: Blocked by merge // ID: 2B32D570B0B3D2A55DA9D4BFC1584D20 +internal import COpenSwiftUI internal import OpenGraphShims // MARK: - PropertyList @@ -18,6 +19,11 @@ struct PropertyList: CustomStringConvertible { @inlinable init() { elements = nil } + + @inline(__always) + init(elements: Element?) { + self.elements = elements + } @usableFromInline var description: String { @@ -86,13 +92,23 @@ struct PropertyList: CustomStringConvertible { fatalError("TODO") } - mutating private func override(with plist: PropertyList) { + mutating func override(with plist: PropertyList) { if let element = elements { elements = element.byPrepending(plist.elements) } else { elements = plist.elements } } + + @inline(__always) + static var current: PropertyList { + if let data = _threadTransactionData() { + // FIXME: swift_dynamicCastClassUnconditional + PropertyList(elements: data.assumingMemoryBound(to: Element.self).pointee) + } else { + PropertyList() + } + } } // MARK: - PropertyList Help functions diff --git a/Sources/OpenSwiftUI/Core/Util/ThreadUtils.swift b/Sources/OpenSwiftUI/Core/Util/ThreadUtils.swift new file mode 100644 index 00000000..9c224b6c --- /dev/null +++ b/Sources/OpenSwiftUI/Core/Util/ThreadUtils.swift @@ -0,0 +1,22 @@ +// +// ThreadUtils.swift +// +// +// Created by Kyle on 2024/4/21. +// + +import Foundation + +@inline(__always) +func performOnMainThread(_ block: @escaping () -> Void) { + #if os(WASI) + // See #76: Thread and RunLoopMode.common is not available on WASI currently + block() + #else + if Thread.isMainThread { + block() + } else { + RunLoop.main.perform(inModes: [.common], block: block) + } + #endif +} diff --git a/Sources/OpenSwiftUI/Data/Model/Binding/Binding.swift b/Sources/OpenSwiftUI/Data/Model/Binding/Binding.swift index 91300f86..403244b9 100644 --- a/Sources/OpenSwiftUI/Data/Model/Binding/Binding.swift +++ b/Sources/OpenSwiftUI/Data/Model/Binding/Binding.swift @@ -343,7 +343,12 @@ extension Binding: DynamicProperty { } } - public static func _makeProperty(in buffer: inout _DynamicPropertyBuffer, container: _GraphValue, fieldOffset: Int, inputs: inout _GraphInputs) { + public static func _makeProperty( + in buffer: inout _DynamicPropertyBuffer, + container _: _GraphValue, + fieldOffset: Int, + inputs _: inout _GraphInputs + ) { buffer.append(Box(), fieldOffset: fieldOffset) } } diff --git a/Sources/OpenSwiftUI/Data/Model/DynamicProperty/DynamicPropertyBox.swift b/Sources/OpenSwiftUI/Data/Model/DynamicProperty/DynamicPropertyBox.swift index bf120d8c..a9da6438 100644 --- a/Sources/OpenSwiftUI/Data/Model/DynamicProperty/DynamicPropertyBox.swift +++ b/Sources/OpenSwiftUI/Data/Model/DynamicProperty/DynamicPropertyBox.swift @@ -8,7 +8,7 @@ protocol DynamicPropertyBox: DynamicProperty { associatedtype Property: DynamicProperty func destroy() - func reset() + mutating func reset() mutating func update(property: inout Property, phase: _GraphInputs.Phase) -> Bool func getState(type: Value.Type) -> Binding? } diff --git a/Sources/OpenSwiftUI/Data/Model/State/State.swift b/Sources/OpenSwiftUI/Data/Model/State/State.swift index 8e726147..e2d9d611 100644 --- a/Sources/OpenSwiftUI/Data/Model/State/State.swift +++ b/Sources/OpenSwiftUI/Data/Model/State/State.swift @@ -3,9 +3,11 @@ // OpenSwiftUI // // Audited for RELEASE_2021 -// Status: Blocked by DynamicProperty +// Status: Complete // ID: 08168374F4710A99DCB15B5E8768D632 +internal import OpenGraphShims + /// A property wrapper type that can read and write a value managed by OpenSwiftUI. /// /// Use state as the single source of truth for a given value type that you @@ -206,11 +208,6 @@ public struct State { } } -extension State: DynamicProperty { - // TODO: - public static func _makeProperty(in _: inout _DynamicPropertyBuffer, container _: _GraphValue, fieldOffset _: Swift.Int, inputs _: inout _GraphInputs) {} -} - extension State where Value: ExpressibleByNilLiteral { /// Creates a state property without an initial value. /// @@ -237,3 +234,49 @@ extension State { } } } + +extension State: DynamicProperty { + public static func _makeProperty( + in buffer: inout _DynamicPropertyBuffer, + container _: _GraphValue, + fieldOffset: Int, + inputs _: inout _GraphInputs + ) { + let attribute = Attribute(value: ()) + let box = StatePropertyBox(signal: WeakAttribute(attribute)) + buffer.append(box, fieldOffset: fieldOffset) + } +} + +private struct StatePropertyBox: DynamicPropertyBox { + let signal: WeakAttribute + var location: StoredLocation? + + typealias Property = State + func destroy() {} + mutating func reset() { location = nil } + mutating func update(property: inout State, phase: _GraphInputs.Phase) -> Bool { + let locationChanged = location == nil + if location == nil { + location = property._location as? StoredLocation ?? StoredLocation( + initialValue: property._value, + host: .currentHost, + signal: signal + ) + } + let signalChanged = signal.changedValue()?.changed ?? false + property._value = location!.updateValue + property._location = location! + return (signalChanged ? location!.wasRead : false) || locationChanged + } + func getState(type _: V.Type) -> Binding? { + guard Value.self == V.self, + let location + else { + return nil + } + let value = location.get() + let binding = Binding(value: value, location: location) + return binding as? Binding + } +} diff --git a/Sources/OpenSwiftUI/Data/Model/State/StoredLocation.swift b/Sources/OpenSwiftUI/Data/Model/State/StoredLocation.swift new file mode 100644 index 00000000..cff04332 --- /dev/null +++ b/Sources/OpenSwiftUI/Data/Model/State/StoredLocation.swift @@ -0,0 +1,176 @@ +// +// StoredLocation.swift +// OpenSwiftUI +// +// Audited for RELEASE_2021 +// Status: WIP +// ID: EBDC911C9EE054BAE3D86F947C24B7C3 + +internal import OpenGraphShims +internal import COpenSwiftUI + +class StoredLocationBase: AnyLocation, Location { + private struct LockedData { + var currentValue: Value + var savedValue: [Value] + var cache: LocationProjectionCache + + init(currentValue: Value, savedValue: [Value], cache: LocationProjectionCache = LocationProjectionCache()) { + self.currentValue = currentValue + self.savedValue = savedValue + self.cache = cache + } + } + + fileprivate struct BeginUpdate: GraphMutation { + weak var box: StoredLocationBase? + + func apply() { + box?.beginUpdate() + } + + func combine(with mutation: Mutation) -> Bool { + guard let otherBeginUpdate = mutation as? BeginUpdate, + let box, + let otherBox = otherBeginUpdate.box, + box === otherBox + else { + return false + } + + box.$data.withMutableData { data in + _ = data.savedValue.removeFirst() + } + return true + } + } + + @UnsafeLockedPointer + private var data: LockedData + + var _wasRead: Bool + + init(initialValue value: Value) { + _wasRead = false + _data = UnsafeLockedPointer(wrappedValue: LockedData(currentValue: value, savedValue: [])) + super.init() + } + + fileprivate var isValid: Bool { true } + + // MARK: - abstract method + + fileprivate var isUpdating: Bool { + fatalError("abstract") + } + + fileprivate func commit(transaction: Transaction, mutation: BeginUpdate) { + fatalError("abstract") + } + + fileprivate func notifyObservers() { + fatalError("abstract") + } + + // MARK: - AnyLocation + + override var wasRead: Bool { + get { _wasRead } + set { _wasRead = newValue } + } + + override func get() -> Value { + data.currentValue + } + + override func set(_ value: Value, transaction: Transaction) { + guard !isUpdating else { + Log.runtimeIssues("Modifying state during view update, this will cause undefined behavior.") + return + } + guard isValid else { + $data.withMutableData { data in + data.savedValue.removeAll() + } + return + } + let shouldCommit = $data.withMutableData { data in + guard !compareValues(data.currentValue, value) else { + return false + } + data.savedValue.append(data.currentValue) + data.currentValue = value + return true + } + guard shouldCommit else { + return + } + var newTransaction = transaction + newTransaction.override(.current) + performOnMainThread { [weak self] in + guard let self else { + return + } + let update = BeginUpdate(box: self) + commit(transaction: newTransaction, mutation: update) + } + } + + override func projecting(_ projection: P) -> AnyLocation where Value == P.Base { + data.cache.reference(for: projection, on: self) + } + + override func update() -> (Value, Bool) { + _wasRead = true + return (updateValue, true) + } + + // MARK: - final properties and methods + + deinit { + $data.destroy() + } + + final var updateValue: Value { + $data.withMutableData { data in + data.savedValue.first ?? data.currentValue + } + } + + private final func beginUpdate() { + data.savedValue.removeFirst() + notifyObservers() + } +} + +final class StoredLocation: StoredLocationBase { + weak var host: GraphHost? + @WeakAttribute var signal: Void? + + init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute) { + self.host = host + _signal = signal + super.init(initialValue: value) + } + + override fileprivate var isValid: Bool { + host?.isValid ?? false + } + + override fileprivate var isUpdating: Bool { + host?.isUpdating ?? false + } + + override fileprivate func commit(transaction: Transaction, mutation: StoredLocationBase.BeginUpdate) { + host?.asyncTransaction( + transaction, + mutation: mutation, + style: ._1, + mayDeferUpdate: true + ) + } + + override fileprivate func notifyObservers() { + $signal?.invalidateValue() + } +} diff --git a/Sources/OpenSwiftUI/Core/Data/UnsafeLockedPointer.swift b/Sources/OpenSwiftUI/Data/Model/State/UnsafeLockedPointer.swift similarity index 86% rename from Sources/OpenSwiftUI/Core/Data/UnsafeLockedPointer.swift rename to Sources/OpenSwiftUI/Data/Model/State/UnsafeLockedPointer.swift index aa5e24c3..d04eae16 100644 --- a/Sources/OpenSwiftUI/Core/Data/UnsafeLockedPointer.swift +++ b/Sources/OpenSwiftUI/Data/Model/State/UnsafeLockedPointer.swift @@ -3,11 +3,10 @@ // OpenSwiftUI // // Audited for RELEASE_2021 -// Status: Blocked by StoredLocationBase +// Status: Complete internal import COpenSwiftUI -// TODO: caller StoredLocationBase @propertyWrapper struct UnsafeLockedPointer: Destroyable { private var base: LockedPointer @@ -16,9 +15,7 @@ struct UnsafeLockedPointer: Destroyable { base = LockedPointer(type: Data.self) base.withUnsafeMutablePointer { $0.initialize(to: wrappedValue) } } - - @_transparent - @inline(__always) + var wrappedValue: Data { _read { base.lock() @@ -33,9 +30,7 @@ struct UnsafeLockedPointer: Destroyable { } var projectedValue: UnsafeLockedPointer { self } - - @_transparent - @inline(__always) + func destroy() { base.withUnsafeMutablePointer(Data.self) { $0.deinitialize(count: 1) } base.delete() diff --git a/Sources/OpenSwiftUI/View/Transaction/Transaction.swift b/Sources/OpenSwiftUI/View/Transaction/Transaction.swift index d79d04c7..07c1d0a6 100644 --- a/Sources/OpenSwiftUI/View/Transaction/Transaction.swift +++ b/Sources/OpenSwiftUI/View/Transaction/Transaction.swift @@ -22,12 +22,26 @@ public struct Transaction { plist = PropertyList() } + @inline(__always) + init(plist: PropertyList) { + self.plist = plist + } + @usableFromInline var plist: PropertyList @inline(__always) - @inlinable var isEmpty: Bool { plist.elements == nil } + + @inline(__always) + mutating func override(_ transaction: Transaction) { + plist.override(with: transaction.plist) + } + + @inline(__always) + static var current: Transaction { + Transaction(plist: .current) + } } extension Transaction { @@ -55,9 +69,10 @@ public func withTransaction( _ body: () throws -> Result ) rethrows -> Result { try withExtendedLifetime(transaction) { - let data = _threadTransactionData() - defer { _setThreadTransactionData(data) } - _setThreadTransactionData(transaction.plist.elements.map { Unmanaged.passUnretained($0).toOpaque() }) + let oldData = _threadTransactionData() + defer { _setThreadTransactionData(oldData) } + let data = transaction.plist.elements.map { Unmanaged.passUnretained($0).toOpaque() } + _setThreadTransactionData(data) return try body() } }