Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/StateStruct/PropertyNode.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public struct PropertyNode: Equatable, CustomDebugStringConvertible {
public struct PropertyNode: Sendable, Equatable, CustomDebugStringConvertible {

public struct Status: OptionSet, Sendable {
public let rawValue: Int8
Expand Down Expand Up @@ -258,7 +258,7 @@ extension PropertyNode {

modify(&nodes)
}

}

extension PropertyNode {
Expand Down
12 changes: 11 additions & 1 deletion Sources/StateStruct/Source.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@ public macro COWTrackingProperty() =
)
@attached(peer, names: prefixed(`_backing_`))
public macro WeakTrackingProperty() =
#externalMacro(module: "StateStructMacros", type: "WeakTrackingPropertyMacro")
#externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro")

@attached(
accessor,
names: named(init), named(_read), named(set), named(_modify)
)
@attached(peer, names: prefixed(`_backing_`))
public macro PrimitiveTrackingProperty() =
#externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro")


#if DEBUG


@Tracking
struct OptinalPropertyState {

Expand Down
27 changes: 0 additions & 27 deletions Sources/StateStruct/Tracking.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,2 @@
import Foundation
import os.lock

private enum ThreadDictionaryKey {
case tracking
case currentKeyPathStack
}

extension NSMutableDictionary {

var tracking: TrackingResult? {
get {
self[ThreadDictionaryKey.tracking] as? TrackingResult
}
set {
self[ThreadDictionaryKey.tracking] = newValue
}
}
}

public enum _Tracking {

public static func _tracking_modifyStorage(_ modifier: (inout TrackingResult) -> Void) {
guard Thread.current.threadDictionary.tracking != nil else {
return
}
modifier(&Thread.current.threadDictionary.tracking!)
}
}
35 changes: 16 additions & 19 deletions Sources/StateStruct/TrackingObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,30 @@ import Foundation.NSThread
/// A type that represents an object that can be tracked for changes.
/// This protocol is automatically implemented by types marked with `@Tracking` macro.
public protocol TrackingObject {
var _tracking_context: _TrackingContext { get }
var _tracking_context: _TrackingContext { get set }
}

extension TrackingObject {

public var trackingResult: TrackingResult? {
_tracking_context.trackingResultRef?.result
}

public func tracking(
using graph: consuming PropertyNode? = nil,
_ applier: () throws -> Void
) rethrows -> TrackingResult {
let current = Thread.current.threadDictionary.tracking
startTracking()
defer {
Thread.current.threadDictionary.tracking = current
endTracking()
}

Thread.current.threadDictionary.tracking = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))
try applier()
let result = Thread.current.threadDictionary.tracking!
return result
public consuming func tracked(using graph: consuming PropertyNode? = nil) -> Self {
startNewTracking(using: graph)
return self
}

private func startTracking() {
public mutating func startNewTracking(using graph: consuming PropertyNode? = nil) {

let newResult = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))

_tracking_context = .init(trackingResultRef: .init(result: newResult))
_tracking_context.path = .root(of: Self.self)
}

private func endTracking() {
public func endTracking() {
_tracking_context.trackingResultRef = nil
}

}
35 changes: 31 additions & 4 deletions Sources/StateStruct/TrackingResult.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
import os.lock

public final class TrackingResultRef {
public final class TrackingResultRef: Sendable {

public var result: TrackingResult
public let resultBox: OSAllocatedUnfairLock<TrackingResult>

public var result: TrackingResult {
resultBox.withLock {
$0
}
}

public init(result: TrackingResult) {
self.result = result
self.resultBox = .init(initialState: result)
}

public func modify(_ closure: (inout TrackingResult) -> Void) {
resultBox.withLockUnchecked(closure)
}
public func accessorRead(path: PropertyPath?) {
resultBox.withLockUnchecked { result in
result.accessorRead(path: path)
}
}

public func accessorSet(path: PropertyPath?) {
resultBox.withLockUnchecked { result in
result.accessorSet(path: path)
}
}

public func accessorModify(path: PropertyPath?) {
resultBox.withLockUnchecked { result in
result.accessorModify(path: path)
}
}
}

public struct TrackingResult: Equatable {
public struct TrackingResult: Equatable, Sendable {

public var graph: PropertyNode

Expand Down
77 changes: 52 additions & 25 deletions Sources/StateStruct/_TrackingContext.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os.lock
import Foundation.NSThread

public final class _TrackingContext: Sendable, Hashable {
public struct _TrackingContext: Sendable, Hashable {

public static func == (lhs: _TrackingContext, rhs: _TrackingContext) -> Bool {
// ``_TrackingContext`` is used only for embedding into the struct.
Expand All @@ -13,52 +13,79 @@ public final class _TrackingContext: Sendable, Hashable {
public func hash(into hasher: inout Hasher) {
0.hash(into: &hasher)
}

public struct Info {

public var path: PropertyPath?

public var identifier: AnyHashable?

public var currentResultRef: TrackingResultRef?

@inlinable
init(
path: PropertyPath? = nil,
identifier: AnyHashable? = nil,
currentResultRef: TrackingResultRef?
) {
self.path = path
self.identifier = identifier
self.currentResultRef = currentResultRef
}

}

private let infoBox: OSAllocatedUnfairLock<Info>

public var path: PropertyPath? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.path
$0.path
}
}
set {
nonmutating set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].path = newValue
$0.path = newValue
}
}
}

public var identifier: AnyHashable? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.identifier
$0.identifier
}
}
set {
nonmutating set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].identifier = newValue
$0.identifier = newValue
}
}
}

@usableFromInline
struct Info {
@usableFromInline
var path: PropertyPath?

@usableFromInline
var identifier: AnyHashable?

@inlinable
init() {

public var trackingResultRef: TrackingResultRef? {
get {
infoBox.withLock { $0.currentResultRef }
}
nonmutating set {
infoBox.withLock {
$0.currentResultRef = newValue
}
}
}

@usableFromInline
let infoBox: OSAllocatedUnfairLock<[UnsafeMutableRawPointer: Info]> = .init(
uncheckedState: [:]
)


public init(trackingResultRef: TrackingResultRef) {
infoBox = .init(
uncheckedState: .init(
currentResultRef: trackingResultRef
)
)
}

public init() {
infoBox = .init(
uncheckedState: .init(
currentResultRef: nil
)
)
}
}
59 changes: 45 additions & 14 deletions Sources/StateStructMacros/COWTrackingPropertyMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ extension COWTrackingPropertyMacro: AccessorMacro {

let isConstant = variableDecl.bindingSpecifier.tokenKind == .keyword(.let)
let propertyName = identifierPattern.identifier.text
let typeName = variableDecl.typeSyntax!.trimmed
let backingName = "_backing_" + propertyName
let hasWillSet = variableDecl.willSetBlock != nil

Expand All @@ -157,12 +158,23 @@ extension COWTrackingPropertyMacro: AccessorMacro {

let readAccessor = AccessorDeclSyntax(
"""
_read {
(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
_Tracking._tracking_modifyStorage {
$0.accessorRead(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
get {

let component = PropertyPath.Component.init("\(raw: propertyName)")
_tracking_context.trackingResultRef?.accessorRead(path: _tracking_context.path?.pushed(component))

if var value = \(raw: backingName).value as? TrackingObject, let ref = _tracking_context.trackingResultRef {

if value._tracking_context.trackingResultRef !== ref {
value._tracking_context = _TrackingContext(trackingResultRef: ref)
}

value._tracking_context.path = _tracking_context.path?.pushed(component)

return value as! \(typeName)
}
yield \(raw: backingName).value

return \(raw: backingName).value
}
"""
)
Expand All @@ -174,10 +186,10 @@ extension COWTrackingPropertyMacro: AccessorMacro {
// willset
\(variableDecl.makeWillSetDoBlock())

(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
_Tracking._tracking_modifyStorage {
$0.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
if let ref = _tracking_context.trackingResultRef {
ref.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
}

if !isKnownUniquelyReferenced(&\(raw: backingName)) {
\(raw: backingName) = .init(newValue)
} else {
Expand All @@ -193,15 +205,34 @@ extension COWTrackingPropertyMacro: AccessorMacro {
let modifyAccessor = AccessorDeclSyntax(
"""
_modify {
(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
_Tracking._tracking_modifyStorage {
$0.accessorModify(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))

if let ref = _tracking_context.trackingResultRef {
ref.accessorModify(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
}
if !isKnownUniquelyReferenced(&\(raw: backingName)) {

if !isKnownUniquelyReferenced(&\(raw: backingName)) {
\(raw: backingName) = .init(\(raw: backingName).value)
}
yield &\(raw: backingName).value


let oldValue = \(raw: backingName).value

if var value = \(raw: backingName).value as? TrackingObject,
let ref = _tracking_context.trackingResultRef {

let component = PropertyPath.Component.init("\(raw: propertyName)")

if value._tracking_context.trackingResultRef !== ref {
value._tracking_context = _TrackingContext(trackingResultRef: ref)
}
value._tracking_context.path = _tracking_context.path?.pushed(component)

\(raw: backingName).value = value as! \(typeName)

yield &\(raw: backingName).value
} else {
yield &\(raw: backingName).value
}

// didSet
\(variableDecl.makeDidSetDoBlock())
}
Expand Down
Loading