├── .github └── workflows │ └── checks.yml ├── .gitignore ├── .spi.yml ├── .swiftpm └── swift-macro-state-struct.xctestplan ├── .vscode ├── launch.json └── settings.json ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── StateStruct │ ├── Array+modify.swift │ ├── CopyOnWrite.swift │ ├── Documentation.docc │ │ ├── TestingSpec.md │ │ └── Trackable.md │ ├── PropertyNode.swift │ ├── PropertyPath.swift │ ├── Referencing.swift │ ├── Source.swift │ ├── TrackingObject.swift │ ├── TrackingResult.swift │ ├── TrackingRuntime.swift │ └── _TrackingContext.swift └── StateStructMacros │ ├── COWTrackingPropertyMacro.swift │ ├── KnownTypes.swift │ ├── Plugin.swift │ ├── PrimitiveTrackingPropertyMacro.swift │ ├── TrackingIgnoredMacro.swift │ └── TrackingMacro.swift └── Tests ├── StateStructMacroTests ├── COWTrackingProperyMacroTests.swift └── TrackingMacroTests.swift └── StateStructTests ├── AccessorTests.swift ├── CopyOnWriteTests.swift ├── DidSetTests.swift ├── EquatableTests.swift ├── GraphTests.swift ├── HeavyStructCopyTests.swift ├── MyState.swift ├── PropertyWrappers.swift ├── TrackingExistential.swift ├── TrackingTests.swift └── UpdatingTests.swift /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: macos-15 8 | 9 | steps: 10 | - uses: maxim-lobanov/setup-xcode@v1.1 11 | with: 12 | xcode-version: "16.2" 13 | - uses: actions/checkout@v2 14 | - name: Run Test 15 | run: swift test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # Pods/ 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # It is recommended to not store the screenshots in the git repo. 73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # After new code Injection tools there's a generated folder /iOSInjectionProject 84 | # https://github.com/johnno1962/injectionforxcode 85 | 86 | iOSInjectionProject/ 87 | 88 | .DS_Store 89 | .swiftpm/xcode 90 | # End of https://www.toptal.com/developers/gitignore/api/swift 91 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [StateStruct] 5 | -------------------------------------------------------------------------------- /.swiftpm/swift-macro-state-struct.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "1194A67B-74F2-4259-BCF4-4C67B5C7FAE7", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:", 18 | "identifier" : "StateStructMacroTests", 19 | "name" : "StateStructMacroTests" 20 | } 21 | }, 22 | { 23 | "target" : { 24 | "containerPath" : "container:", 25 | "identifier" : "StateStructTests", 26 | "name" : "StateStructTests" 27 | } 28 | } 29 | ], 30 | "version" : 1 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "lldb", 5 | "request": "launch", 6 | "args": [], 7 | "cwd": "${workspaceFolder:swift-macro-struct-transaction}", 8 | "name": "Debug StructTransactionClient", 9 | "program": "${workspaceFolder:swift-macro-struct-transaction}/.build/debug/StructTransactionClient", 10 | "preLaunchTask": "swift: Build Debug StructTransactionClient" 11 | }, 12 | { 13 | "type": "lldb", 14 | "request": "launch", 15 | "args": [], 16 | "cwd": "${workspaceFolder:swift-macro-struct-transaction}", 17 | "name": "Release StructTransactionClient", 18 | "program": "${workspaceFolder:swift-macro-struct-transaction}/.build/release/StructTransactionClient", 19 | "preLaunchTask": "swift: Build Release StructTransactionClient" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c2bf5e6e6852259c790142e6d5dcce37e42667fe3e3fdec8e648820d6de70af3", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-custom-dump", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 8 | "state" : { 9 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 10 | "version" : "1.3.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-macro-testing", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 17 | "state" : { 18 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", 19 | "version" : "0.5.2" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-snapshot-testing", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 26 | "state" : { 27 | "revision" : "f5bfff796ee8e3bc9a685b7ffba1bf20663eb370", 28 | "version" : "1.18.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-syntax", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/swiftlang/swift-syntax.git", 35 | "state" : { 36 | "revision" : "0687f71944021d616d34d922343dcef086855920", 37 | "version" : "600.0.1" 38 | } 39 | }, 40 | { 41 | "identity" : "xctest-dynamic-overlay", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 44 | "state" : { 45 | "revision" : "b444594f79844b0d6d76d70fbfb3f7f71728f938", 46 | "version" : "1.5.1" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import CompilerPluginSupport 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "swift-macro-state-struct", 9 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 10 | products: [ 11 | .library( 12 | name: "StateStruct", 13 | targets: ["StateStruct"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), 18 | .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.5.2") 19 | ], 20 | targets: [ 21 | .macro( 22 | name: "StateStructMacros", 23 | dependencies: [ 24 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 25 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 26 | ] 27 | ), 28 | .target( 29 | name: "StateStruct", 30 | dependencies: ["StateStructMacros"] 31 | ), 32 | .testTarget( 33 | name: "StateStructMacroTests", 34 | dependencies: [ 35 | "StateStructMacros", 36 | .product(name: "MacroTesting", package: "swift-macro-testing"), 37 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 38 | ] 39 | ), 40 | .testTarget( 41 | name: "StateStructTests", 42 | dependencies: [ 43 | "StateStruct" 44 | ] 45 | ), 46 | ], 47 | swiftLanguageModes: [.v6] 48 | ) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StateStruct - `@Tracking` macro 2 | 3 | ## Introduction 4 | StateStruct is a Swift library designed to simplify state management within structs by implementing dependency tracking and change detection. It leverages Swift macros and compiler plugins to automatically inject tracking code into property reads and writes, constructing dependency graphs that reveal how properties interact and change during execution. 5 | 6 | ## Main Features 7 | 8 | ### Dependency Tracking 9 | - **@Tracking Macro:** Automatically integrates tracking code to monitor property accesses. 10 | - **Read Operations:** When properties are read within a tracking block, their access paths (via PropertyPath objects) are recorded into the read dependency graph. 11 | - **Write Operations:** Similarly, modifications to properties are logged in the write dependency graph. 12 | 13 | ### Change Detection 14 | - By using the function `PropertyNode.hasChanges(writeGraph:readGraph:)`, StateStruct compares read and write dependency graphs to determine if any previously accessed property has changed. This ensures that only pertinent changes trigger reactive updates, avoiding unnecessary processing. 15 | 16 | ### Swift Macros 17 | - Swift macro technology simplifies the insertion of tracking code into structs. This reduces boilerplate and ensures consistent monitoring of property accesses. 18 | 19 | ### Copy-on-Write (COW) Support 20 | - The library implements a copy-on-write (COW) mechanism to optimize state updates. Since the state is structured as a Swift struct and modifications occur frequently, this mechanism ensures that copies are created only when necessary. This optimizes performance by preventing unnecessary data duplication and avoiding inadvertent side effects from shared mutable state. Components like the _BackingStorage class effectively manage this process. 21 | 22 | ### Nested State Support 23 | - StateStruct reliably tracks dependencies and detects changes for both flat properties and deeply nested structures (e.g., state.nested.name). 24 | 25 | ## Usage Example 26 | 27 | ```swift 28 | import StateStruct 29 | 30 | @Tracking 31 | struct MyState { 32 | var height: Int = 0 33 | var name: String = "" 34 | var nested: Nested = .init(name: "") 35 | 36 | @Tracking 37 | struct Nested { 38 | var name: String = "" 39 | var age: Int = 18 40 | } 41 | } 42 | 43 | var state = MyState() 44 | 45 | // Start tracking and record read operations 46 | state.startNewTracking() 47 | _ = state.height 48 | _ = state.nested.name 49 | let readTracking = state.trackingResult! 50 | 51 | // Start tracking and record write operations 52 | state.startNewTracking() 53 | state.height = 200 54 | let writeTracking = state.trackingResult! 55 | 56 | // Change detection: compare dependency graphs to determine if a change occurred 57 | let hasChanged = PropertyNode.hasChanges( 58 | writeGraph: writeTracking.graph, 59 | readGraph: readTracking.graph 60 | ) 61 | 62 | // Output: true 63 | 64 | ``` 65 | 66 | Test Cases in UpdatingTests.swift: 67 | 68 | 1. Nested Property Access and Modification 69 | ```swift 70 | // Reading nested.name 71 | state.startNewTracking() 72 | _ = state.nested.name 73 | let reading = state.trackingResult! 74 | 75 | // Writing to nested.name 76 | state.startNewTracking() 77 | state.nested = .init(name: "Foo") 78 | let writing = state.trackingResult! 79 | // Result: Change detected ✅ 80 | ``` 81 | 82 | 2. Unrelated Property Changes 83 | ```swift 84 | // Reading nested.name 85 | state.startNewTracking() 86 | _ = state.nested.name 87 | let reading = state.trackingResult! 88 | 89 | // Writing to unrelated nested.age 90 | state.startNewTracking() 91 | state.nested.age = 100 92 | let writing = state.trackingResult! 93 | // Result: No change detected ❌ 94 | ``` 95 | 96 | 3. Broad vs Narrow Tracking 97 | ```swift 98 | // Reading entire nested object 99 | state.startNewTracking() 100 | _ = state.nested // Tracks all nested properties 101 | let reading = state.trackingResult! 102 | 103 | // Writing to one nested field 104 | state.startNewTracking() 105 | state.nested.age = 100 106 | let writing = state.trackingResult! 107 | // Result: Change detected ✅ (since we're watching all of nested) 108 | ``` 109 | 110 | Each test case demonstrates how StateStruct intelligently tracks dependencies and detects changes in nested structures. The examples show real code snippets from the test suite with clear expected outcomes. 111 | 112 | -------------------------------------------------------------------------------- /Sources/StateStruct/Array+modify.swift: -------------------------------------------------------------------------------- 1 | 2 | extension Array { 3 | mutating func modify(_ modifier: (inout Element) -> Void) { 4 | for index in indices { 5 | modifier(&self[index]) 6 | } 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Sources/StateStruct/CopyOnWrite.swift: -------------------------------------------------------------------------------- 1 | 2 | import os.lock 3 | 4 | @dynamicMemberLookup 5 | public final class _BackingStorage: @unchecked Sendable { 6 | 7 | public var value: Value 8 | 9 | public subscript (dynamicMember keyPath: KeyPath) -> U { 10 | _read { yield value[keyPath: keyPath] } 11 | } 12 | 13 | public subscript (dynamicMember keyPath: KeyPath) -> U? { 14 | _read { yield value[keyPath: keyPath] } 15 | } 16 | 17 | public subscript (dynamicMember keyPath: WritableKeyPath) -> U { 18 | _read { yield value[keyPath: keyPath] } 19 | _modify { yield &value[keyPath: keyPath] } 20 | } 21 | 22 | public subscript (dynamicMember keyPath: WritableKeyPath) -> U? { 23 | _read { yield value[keyPath: keyPath] } 24 | _modify { yield &value[keyPath: keyPath] } 25 | } 26 | 27 | public init(_ value: consuming Value) { 28 | self.value = value 29 | } 30 | 31 | public func copy(with newValue: consuming Value) -> _BackingStorage { 32 | .init(newValue) 33 | } 34 | 35 | public func copy() -> _BackingStorage { 36 | return .init(value) 37 | } 38 | } 39 | 40 | extension _BackingStorage: Equatable where Value: Equatable { 41 | public static func == (lhs: _BackingStorage, rhs: _BackingStorage) -> Bool { 42 | lhs === rhs || lhs.value == rhs.value 43 | } 44 | } 45 | 46 | extension _BackingStorage: Hashable where Value: Hashable { 47 | public func hash(into hasher: inout Hasher) { 48 | value.hash(into: &hasher) 49 | } 50 | } 51 | 52 | #if DEBUG 53 | private struct Before { 54 | 55 | var value: Int 56 | 57 | } 58 | 59 | private struct After { 60 | 61 | var value: Int { 62 | get { 63 | _cow_value.value 64 | } 65 | set { 66 | if !isKnownUniquelyReferenced(&_cow_value) { 67 | _cow_value = .init(_cow_value.value) 68 | } else { 69 | _cow_value.value = newValue 70 | } 71 | } 72 | _modify { 73 | if !isKnownUniquelyReferenced(&_cow_value) { 74 | _cow_value = .init(_cow_value.value) 75 | } 76 | yield &_cow_value.value 77 | } 78 | } 79 | 80 | private var _cow_value: _BackingStorage 81 | 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /Sources/StateStruct/Documentation.docc/TestingSpec.md: -------------------------------------------------------------------------------- 1 | # Dependency Tracking & Change Detection Test Documentation 2 | 3 | This document outlines the detailed behavior observed from our test cases regarding dependency tracking and change detection in the state management system. 4 | 5 | ## 1. Dependency Tracking 6 | 7 | The system provides a transactional tracking feature that records property access paths in both reading and writing modes. Every time a tracking block is executed using `state.tracking { ... }`, all property accesses performed within that block are automatically recorded into a dependency (or access) graph. This graph is structured hierarchically to represent exactly which properties—and even nested properties—were involved during the execution. 8 | 9 | **Examples:** 10 | 11 | - **Test 1:** 12 | In the reading block, `state.height` is accessed, so the generated read graph includes a node for `height`. In the subsequent writing block, `state.height` is updated (set to 200), which produces a write graph that includes the same node. This precise tracking allows for a direct comparison between the two graphs. 13 | 14 | - **Test 2:** 15 | In the reading block, only `state.nested.name` is accessed, so the read graph specifically records the dependency path `nested → name`. Later, in the writing block, the entire `state.nested` object is replaced with a new instance. Even though the reading block explicitly accessed only `name`, the replacement of the entire `nested` object impacts the whole dependency chain and is duly captured in the write graph. 16 | 17 | - **Additional Considerations:** 18 | The tracking mechanism handles nested and optional property accesses effectively, ensuring that the entire access path is recorded accurately. This means that any modification affecting a part of a nested access chain can influence the overall dependency. 19 | 20 | ## 2. Change Detection Logic 21 | 22 | After generating the read and write dependency graphs, the system uses the function `PropertyNode.hasChanges(writeGraph:readGraph:)` to determine whether any property that was read has been affected by a write operation. 23 | 24 | This function compares the nodes between the two graphs: 25 | - If a property that was read is directly modified or replaced in the write graph, the function reports that a change has occurred (returns `true`). 26 | - If the modifications are outside of the read dependency, then no change is flagged (returns `false`). 27 | 28 | **Examples:** 29 | 30 | - **Test 1:** 31 | Since the read graph includes `state.height` and the write operation directly updates `state.height`, the change detection function finds a matching node and returns `true`. 32 | 33 | - **Test 2:** 34 | Although the read graph only tracks `state.nested.name`, replacing the entire `state.nested` object in the writing block is sufficient to consider the dependency altered, and the function consequently returns `true`. 35 | 36 | - **Test 3:** 37 | Here, the read graph records a dependency on `state.nested.name`, but if the write block only modifies `state.nested.age`, then the property `name` remains unaffected. As a result, the function returns `false`. 38 | 39 | This change detection logic ensures that only relevant modifications—those directly impacting the properties that were read—trigger further updates in the system. It prevents unnecessary processing by ignoring changes to properties that were not part of the initial dependency graph. -------------------------------------------------------------------------------- /Sources/StateStruct/Documentation.docc/Trackable.md: -------------------------------------------------------------------------------- 1 | # ``Trackable`` 2 | 3 | -------------------------------------------------------------------------------- /Sources/StateStruct/PropertyNode.swift: -------------------------------------------------------------------------------- 1 | public struct PropertyNode: Sendable, Equatable, CustomDebugStringConvertible { 2 | 3 | public struct Status: OptionSet, Sendable { 4 | public let rawValue: Int8 5 | 6 | public init(rawValue: Int8) { 7 | self.rawValue = rawValue 8 | } 9 | 10 | public static let read = Status(rawValue: 1 << 0) 11 | public static let write = Status(rawValue: 1 << 1) 12 | } 13 | 14 | /** 15 | A name of property 16 | */ 17 | public let name: String 18 | 19 | public var status: Status { 20 | var result: Status = [] 21 | if readCount > 0 { 22 | result.insert(.read) 23 | } 24 | if writeCount > 0 { 25 | result.insert(.write) 26 | } 27 | return result 28 | } 29 | 30 | /** 31 | The number of times the final destination was accessed. 32 | */ 33 | public private(set) var readCount: UInt16 = 0 34 | 35 | /** 36 | The number of times the final destination was accessed. 37 | */ 38 | public private(set) var writeCount: UInt16 = 0 39 | 40 | public init(name: String) { 41 | self.name = name 42 | } 43 | 44 | public var nodes: [PropertyNode] = [] 45 | 46 | private mutating func mark(status: Status) { 47 | switch status { 48 | case .read: 49 | readCount &+= 1 50 | case .write: 51 | writeCount &+= 1 52 | default: 53 | break 54 | } 55 | } 56 | 57 | public mutating func applyAsRead(path: PropertyPath) { 58 | apply(components: path.components, status: .read) 59 | } 60 | 61 | public mutating func applyAsWrite(path: PropertyPath) { 62 | apply(components: path.components, status: .write) 63 | } 64 | 65 | private mutating func apply( 66 | components: some RandomAccessCollection, status: Status 67 | ) { 68 | 69 | guard let component = components.first else { 70 | return 71 | } 72 | 73 | guard name == component.value else { 74 | return 75 | } 76 | 77 | let next = components.dropFirst() 78 | 79 | guard !next.isEmpty else { 80 | self.mark(status: status) 81 | return 82 | } 83 | 84 | let targetName = next.first!.value 85 | 86 | let foundIndex: Int? = nodes.withUnsafeMutableBufferPointer { bufferPointer in 87 | for index in bufferPointer.indices { 88 | if bufferPointer[index].name == targetName { 89 | bufferPointer[index].apply(components: next, status: status) 90 | return index 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | if foundIndex == nil { 97 | var newNode = PropertyNode(name: targetName) 98 | 99 | newNode.apply(components: next, status: status) 100 | 101 | nodes.append(newNode) 102 | } 103 | 104 | } 105 | 106 | public var debugDescription: String { 107 | prettyPrint() 108 | } 109 | } 110 | 111 | // MARK: PrettyPrint 112 | 113 | extension PropertyNode { 114 | 115 | @discardableResult 116 | public func prettyPrint() -> String { 117 | let output = prettyPrint(indent: 0) 118 | return output 119 | } 120 | 121 | private func prettyPrint(indent: Int = 0) -> String { 122 | 123 | let indentation = String(repeating: " ", count: indent) 124 | 125 | var statusDescription: String { 126 | var result = "" 127 | if readCount > 0 { 128 | result += "-(\(readCount))" 129 | } 130 | if writeCount > 0 { 131 | result += "+(\(writeCount))" 132 | } 133 | return result 134 | } 135 | 136 | var output = "\(indentation)\(name)\(statusDescription)" 137 | 138 | if !nodes.isEmpty { 139 | output += " {\n" 140 | output += nodes.map { $0.prettyPrint(indent: indent + 1) }.joined(separator: "\n") 141 | output += "\n\(indentation)}" 142 | } 143 | 144 | return output 145 | } 146 | 147 | } 148 | 149 | // MARK: Checks if there are changes 150 | extension PropertyNode { 151 | 152 | public static func hasChanges( 153 | writeGraph: consuming PropertyNode, 154 | readGraph: consuming PropertyNode 155 | ) -> Bool { 156 | 157 | writeGraph.shakeAsWrite() 158 | readGraph.shakeAsRead() 159 | 160 | func _hasChanges(writeGraph: borrowing PropertyNode, readGraph: borrowing PropertyNode) -> Bool { 161 | 162 | if writeGraph.name == readGraph.name { 163 | 164 | if readGraph.nodes.isEmpty { 165 | // This is the leaf node. It matches enough to indicate there is change. 166 | return true 167 | } 168 | 169 | if writeGraph.nodes.isEmpty { 170 | // A case where the modification happened beyond the scope of the read. 171 | return true 172 | } 173 | 174 | for readNode in readGraph.nodes { 175 | 176 | if let writeNode = writeGraph.nodes.first(where: { $0.name == readNode.name }) { 177 | 178 | let result = _hasChanges( 179 | writeGraph: writeNode, 180 | readGraph: readNode 181 | ) 182 | 183 | if result { 184 | return true 185 | } 186 | 187 | } 188 | } 189 | 190 | return false 191 | 192 | } 193 | 194 | return false 195 | } 196 | 197 | let result = _hasChanges(writeGraph: writeGraph, readGraph: readGraph) 198 | 199 | return result 200 | } 201 | 202 | public consuming func sorted() -> PropertyNode { 203 | 204 | self.sort() 205 | 206 | return self 207 | } 208 | 209 | private mutating func sort() { 210 | 211 | self.nodes.modify { 212 | $0.sort() 213 | } 214 | 215 | self.nodes.sort { $0.name < $1.name } 216 | 217 | } 218 | 219 | public func hasChanges(for reader: PropertyNode) -> Bool { 220 | if status.contains(.write) { 221 | return true 222 | } 223 | 224 | for node in nodes { 225 | if let readerNode = reader.nodes.first(where: { $0.name == node.name }) { 226 | if node.hasChanges(for: readerNode) { 227 | return true 228 | } 229 | } 230 | } 231 | 232 | return false 233 | } 234 | } 235 | 236 | extension PropertyNode { 237 | 238 | public var isEmpty: Bool { 239 | nodes.isEmpty 240 | } 241 | 242 | /** 243 | Shake all nodes with `.read` status. 244 | */ 245 | public mutating func shakeAsWrite() { 246 | 247 | func modify(_ nodes: inout [PropertyNode]) { 248 | nodes.removeAll { 249 | $0.status == .read 250 | } 251 | nodes.modify { node in 252 | if node.status == .read { 253 | node.nodes.removeAll() 254 | } 255 | modify(&node.nodes) 256 | } 257 | } 258 | 259 | modify(&nodes) 260 | } 261 | 262 | } 263 | 264 | extension PropertyNode { 265 | 266 | public consuming func shakedAsRead() -> PropertyNode { 267 | self.shakeAsRead() 268 | return self 269 | } 270 | 271 | public mutating func shakeAsRead() { 272 | 273 | func modify(_ nodes: inout [PropertyNode]) { 274 | nodes.modify { node in 275 | if node.nodes.isEmpty { 276 | return 277 | } 278 | 279 | if node.readCount != node.totalReadCount() { 280 | node.nodes.removeAll() 281 | } 282 | 283 | modify(&node.nodes) 284 | } 285 | } 286 | 287 | modify(&nodes) 288 | 289 | } 290 | 291 | private func totalReadCount() -> UInt16 { 292 | nodes.reduce(into: UInt16(0), { 293 | $0 &+= $1.readCount 294 | }) 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /Sources/StateStruct/PropertyPath.swift: -------------------------------------------------------------------------------- 1 | 2 | @DebugDescription 3 | public struct PropertyPath: Equatable, CustomDebugStringConvertible { 4 | 5 | public struct Component: Equatable { 6 | 7 | public let value: String 8 | 9 | public init(_ value: String) { 10 | self.value = value 11 | } 12 | 13 | } 14 | 15 | public var components: [Component] = [] { 16 | didSet { 17 | #if DEBUG 18 | _joined = components.map { $0.value }.joined(separator: ".") 19 | #endif 20 | } 21 | } 22 | 23 | public init() { 24 | 25 | } 26 | 27 | public static func root(of type: T.Type) -> PropertyPath { 28 | let path = PropertyPath().pushed(.init(_typeName(type))) 29 | return path 30 | } 31 | 32 | public consuming func pushed(_ component: Component) -> PropertyPath { 33 | self.components.append(component) 34 | return self 35 | } 36 | 37 | #if DEBUG 38 | private var _joined: String = "" 39 | #endif 40 | 41 | #if DEBUG 42 | public var debugDescription: String { 43 | "\(_joined) \(components.count)" 44 | } 45 | #else 46 | public var debugDescription: String { 47 | "Components : \(components.count)" 48 | } 49 | #endif 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/StateStruct/Referencing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Copy-on-Write property wrapper. 3 | Stores the value in reference type storage. 4 | */ 5 | @propertyWrapper 6 | @dynamicMemberLookup 7 | @Tracking 8 | public struct Referencing { 9 | 10 | public subscript (dynamicMember keyPath: KeyPath) -> U { 11 | _read { yield wrappedValue[keyPath: keyPath] } 12 | } 13 | 14 | public subscript (dynamicMember keyPath: KeyPath) -> U? { 15 | _read { yield wrappedValue[keyPath: keyPath] } 16 | } 17 | 18 | public subscript (dynamicMember keyPath: WritableKeyPath) -> U { 19 | _read { yield wrappedValue[keyPath: keyPath] } 20 | _modify { yield &wrappedValue[keyPath: keyPath] } 21 | } 22 | 23 | public subscript (dynamicMember keyPath: WritableKeyPath) -> U? { 24 | _read { yield wrappedValue[keyPath: keyPath] } 25 | _modify { yield &wrappedValue[keyPath: keyPath] } 26 | } 27 | 28 | public init(wrappedValue: consuming Value) { 29 | self._backing_wrappedValue = .init(wrappedValue) 30 | } 31 | 32 | public init(storage: _BackingStorage) { 33 | self._backing_wrappedValue = storage 34 | } 35 | 36 | public var wrappedValue: Value 37 | 38 | public var projectedValue: Self { 39 | get { 40 | self 41 | } 42 | mutating set { 43 | self = newValue 44 | } 45 | } 46 | 47 | } 48 | 49 | extension Referencing where Value : Equatable { 50 | public static func == (lhs: Self, rhs: Self) -> Bool { 51 | lhs._backing_wrappedValue === rhs._backing_wrappedValue 52 | } 53 | } 54 | 55 | extension Referencing: Sendable where Value: Sendable { 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/StateStruct/Source.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Available only for structs 3 | */ 4 | @attached( 5 | memberAttribute 6 | ) 7 | @attached( 8 | extension, conformances: TrackingObject, names: named(_tracking_propagate) 9 | ) 10 | @attached( 11 | member, names: named(_tracking_context) 12 | ) 13 | public macro Tracking() = #externalMacro(module: "StateStructMacros", type: "TrackingMacro") 14 | 15 | @attached( 16 | accessor, 17 | names: named(willSet) 18 | ) 19 | public macro TrackingIgnored() = 20 | #externalMacro(module: "StateStructMacros", type: "TrackingIgnoredMacro") 21 | 22 | @attached( 23 | accessor, 24 | names: named(init), named(_read), named(set), named(_modify) 25 | ) 26 | @attached(peer, names: prefixed(`_backing_`), prefixed(`$`)) 27 | public macro COWTrackingProperty() = 28 | #externalMacro(module: "StateStructMacros", type: "COWTrackingPropertyMacro") 29 | 30 | @attached( 31 | accessor, 32 | names: named(init), named(_read), named(set), named(_modify) 33 | ) 34 | @attached(peer, names: prefixed(`_backing_`)) 35 | public macro WeakTrackingProperty() = 36 | #externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro") 37 | 38 | @attached( 39 | accessor, 40 | names: named(init), named(_read), named(set), named(_modify) 41 | ) 42 | @attached(peer, names: prefixed(`_backing_`)) 43 | public macro PrimitiveTrackingProperty() = 44 | #externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro") 45 | 46 | #if DEBUG 47 | 48 | @Tracking 49 | struct OptinalPropertyState { 50 | 51 | var name: String 52 | var stored_1: Int? 53 | 54 | init() { 55 | self.name = "" 56 | } 57 | 58 | } 59 | 60 | @Tracking 61 | struct LetState { 62 | 63 | let stored_1: Int 64 | 65 | init(stored_1: Int) { 66 | self.stored_1 = stored_1 67 | } 68 | 69 | } 70 | 71 | @Tracking 72 | struct EquatableState: Equatable { 73 | var count: Int = 0 74 | } 75 | 76 | @Tracking 77 | struct HashableState: Hashable { 78 | var count: Int = 0 79 | } 80 | 81 | @Tracking 82 | struct MyState { 83 | 84 | init() { 85 | stored_2 = 0 86 | } 87 | 88 | var stored_1: Int = 18 89 | 90 | var stored_2: Int 91 | 92 | var stored_3: Int = 10 { 93 | didSet { 94 | print("stored_3 did set") 95 | } 96 | } 97 | 98 | var computed_1: Int { 99 | stored_1 100 | } 101 | 102 | var subState: MyTrackingSubState = .init() 103 | 104 | var noTrackingSubState: MyNoTrackingSubState = .init() 105 | 106 | } 107 | 108 | @Tracking 109 | struct MyTrackingSubState { 110 | 111 | var stored_1: Int = 18 112 | 113 | var computed_1: Int { 114 | stored_1 115 | } 116 | 117 | weak var weak_stored: Ref? 118 | 119 | init() { 120 | 121 | } 122 | 123 | } 124 | 125 | struct MyNoTrackingSubState { 126 | 127 | var stored_1: Int = 18 128 | 129 | var computed_1: Int { 130 | stored_1 131 | } 132 | 133 | weak var weak_stored: Ref? 134 | 135 | init() { 136 | 137 | } 138 | 139 | } 140 | 141 | class Ref { 142 | 143 | } 144 | 145 | #if canImport(Observation) 146 | import Observation 147 | 148 | @available(macOS 14.0, iOS 17.0, tvOS 15.0, watchOS 8.0, *) 149 | @Observable 150 | class Hoge { 151 | 152 | let stored: Int 153 | 154 | var stored_2: Int 155 | 156 | var stored_3: Int? 157 | 158 | weak var weak_stored: Hoge? 159 | 160 | var stored_4: Int = 10 { 161 | didSet { 162 | print("stored_4 did set") 163 | } 164 | } 165 | 166 | var name = "" 167 | 168 | init(stored: Int) { 169 | self.stored = stored 170 | self.stored_2 = stored 171 | } 172 | } 173 | #endif 174 | 175 | #endif 176 | -------------------------------------------------------------------------------- /Sources/StateStruct/TrackingObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation.NSThread 2 | 3 | /// A type that represents an object that can be tracked for changes. 4 | /// This protocol is automatically implemented by types marked with `@Tracking` macro. 5 | public protocol TrackingObject { 6 | var _tracking_context: _TrackingContext { get set } 7 | } 8 | 9 | extension TrackingObject { 10 | 11 | public var trackingResult: TrackingResult? { 12 | _tracking_context.trackingResultRef?.result 13 | } 14 | 15 | public consuming func tracked(using graph: consuming PropertyNode? = nil) -> Self { 16 | startNewTracking(using: graph) 17 | return self 18 | } 19 | 20 | public mutating func startNewTracking(using graph: consuming PropertyNode? = nil) { 21 | 22 | let newResult = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self)))) 23 | 24 | _tracking_context = .init(trackingResultRef: .init(result: newResult)) 25 | _tracking_context.path = .root(of: Self.self) 26 | } 27 | 28 | public func endTracking() { 29 | _tracking_context.trackingResultRef = nil 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/StateStruct/TrackingResult.swift: -------------------------------------------------------------------------------- 1 | import os.lock 2 | 3 | public final class TrackingResultRef: Sendable { 4 | 5 | public let resultBox: OSAllocatedUnfairLock 6 | 7 | public var result: TrackingResult { 8 | resultBox.withLock { 9 | $0 10 | } 11 | } 12 | 13 | public init(result: TrackingResult) { 14 | self.resultBox = .init(initialState: result) 15 | } 16 | 17 | public func modify(_ closure: (inout TrackingResult) -> Void) { 18 | resultBox.withLockUnchecked(closure) 19 | } 20 | public func accessorRead(path: PropertyPath?) { 21 | resultBox.withLockUnchecked { result in 22 | result.accessorRead(path: path) 23 | } 24 | } 25 | 26 | public func accessorSet(path: PropertyPath?) { 27 | resultBox.withLockUnchecked { result in 28 | result.accessorSet(path: path) 29 | } 30 | } 31 | 32 | public func accessorModify(path: PropertyPath?) { 33 | resultBox.withLockUnchecked { result in 34 | result.accessorModify(path: path) 35 | } 36 | } 37 | } 38 | 39 | public struct TrackingResult: Equatable, Sendable { 40 | 41 | public var graph: PropertyNode 42 | 43 | public init(graph: consuming PropertyNode) { 44 | self.graph = graph 45 | } 46 | 47 | public mutating func accessorRead(path: PropertyPath?) { 48 | guard let path = path else { 49 | return 50 | } 51 | graph.applyAsRead(path: path) 52 | } 53 | 54 | public mutating func accessorSet(path: PropertyPath?) { 55 | guard let path = path else { 56 | return 57 | } 58 | graph.applyAsWrite(path: path) 59 | } 60 | 61 | public mutating func accessorModify(path: PropertyPath?) { 62 | guard let path = path else { 63 | return 64 | } 65 | graph.applyAsWrite(path: path) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/StateStruct/TrackingRuntime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.lock 3 | 4 | public enum TrackingRuntime { 5 | 6 | /// no tracking 7 | @usableFromInline 8 | static func dynamic_processGet( 9 | component: consuming PropertyPath.Component, 10 | value: Value, 11 | trackingContext: borrowing _TrackingContext 12 | ) -> Value { 13 | 14 | if 15 | let ref = trackingContext.trackingResultRef, 16 | let path = trackingContext.path 17 | { 18 | ref.accessorRead(path: path.pushed(component)) 19 | } 20 | 21 | if var value = value as? TrackingObject, let ref = trackingContext.trackingResultRef { 22 | 23 | if value._tracking_context.trackingResultRef !== ref { 24 | value._tracking_context = _TrackingContext(trackingResultRef: ref) 25 | } 26 | 27 | value._tracking_context.path = trackingContext.path?.pushed(component) 28 | 29 | return value as! Value 30 | } else { 31 | return value 32 | } 33 | 34 | } 35 | 36 | /// no tracking 37 | @inlinable 38 | public static func processGet( 39 | component: consuming PropertyPath.Component, 40 | value: Value, 41 | trackingContext: borrowing _TrackingContext 42 | ) -> Value { 43 | 44 | return dynamic_processGet(component: component, value: value, trackingContext: trackingContext) 45 | 46 | } 47 | 48 | /// no tracking 49 | @inlinable 50 | public static func processGet( 51 | component: consuming PropertyPath.Component, 52 | value: Optional, 53 | trackingContext: borrowing _TrackingContext 54 | ) -> WrappedValue? { 55 | 56 | return dynamic_processGet( 57 | component: component, 58 | value: value, 59 | trackingContext: trackingContext 60 | ) 61 | 62 | } 63 | 64 | /// tracking 65 | @usableFromInline 66 | static func static_processGet( 67 | component: consuming PropertyPath.Component, 68 | value: Value, 69 | trackingContext: borrowing _TrackingContext 70 | ) -> Value { 71 | 72 | trackingContext.trackingResultRef?.accessorRead(path: trackingContext.path?.pushed(component)) 73 | 74 | var value = value 75 | 76 | if let ref = trackingContext.trackingResultRef { 77 | 78 | if value._tracking_context.trackingResultRef !== ref { 79 | value._tracking_context = _TrackingContext(trackingResultRef: ref) 80 | } 81 | 82 | value._tracking_context.path = trackingContext.path?.pushed(component) 83 | 84 | return value 85 | } else { 86 | return value 87 | } 88 | 89 | } 90 | 91 | /// tracking 92 | @inlinable 93 | public static func processGet( 94 | component: consuming PropertyPath.Component, 95 | value: Value, 96 | trackingContext: borrowing _TrackingContext 97 | ) -> Value { 98 | static_processGet(component: component, value: value, trackingContext: trackingContext) 99 | } 100 | 101 | /// tracking 102 | @inlinable 103 | public static func processGet( 104 | component: consuming PropertyPath.Component, 105 | value: Optional, 106 | trackingContext: borrowing _TrackingContext 107 | ) -> WrappedValue? { 108 | guard let value = value else { 109 | return nil 110 | } 111 | return static_processGet(component: component, value: value, trackingContext: trackingContext) 112 | } 113 | 114 | 115 | /// no tracking 116 | @usableFromInline 117 | static func dynamic_processModify( 118 | component: consuming PropertyPath.Component, 119 | trackingContext: borrowing _TrackingContext, 120 | storage: borrowing _BackingStorage 121 | ) { 122 | 123 | trackingContext.trackingResultRef?.accessorModify(path: trackingContext.path?.pushed(component)) 124 | 125 | if var value = storage.value as? TrackingObject, let ref = trackingContext.trackingResultRef { 126 | 127 | if value._tracking_context.trackingResultRef !== ref { 128 | value._tracking_context = _TrackingContext(trackingResultRef: ref) 129 | } 130 | value._tracking_context.path = trackingContext.path?.pushed(component) 131 | 132 | storage.value = value as! Value 133 | } 134 | } 135 | 136 | /// no tracking 137 | public static func processModify( 138 | component: consuming PropertyPath.Component, 139 | trackingContext: borrowing _TrackingContext, 140 | storage: borrowing _BackingStorage 141 | ) { 142 | 143 | dynamic_processModify(component: component, trackingContext: trackingContext, storage: storage) 144 | } 145 | 146 | /// no tracking 147 | public static func processModify( 148 | component: consuming PropertyPath.Component, 149 | trackingContext: borrowing _TrackingContext, 150 | storage: borrowing _BackingStorage> 151 | ) { 152 | 153 | dynamic_processModify(component: component, trackingContext: trackingContext, storage: storage) 154 | } 155 | 156 | /// no tracking 157 | public static func processModify( 158 | component: consuming PropertyPath.Component, 159 | trackingContext: borrowing _TrackingContext, 160 | storage: borrowing _BackingStorage 161 | ) { 162 | 163 | trackingContext.trackingResultRef?.accessorModify(path: trackingContext.path?.pushed(component)) 164 | 165 | var value = storage.value 166 | 167 | if let ref = trackingContext.trackingResultRef { 168 | 169 | if value._tracking_context.trackingResultRef !== ref { 170 | value._tracking_context = _TrackingContext(trackingResultRef: ref) 171 | } 172 | value._tracking_context.path = trackingContext.path?.pushed(component) 173 | 174 | storage.value = value 175 | } 176 | 177 | } 178 | 179 | /// no tracking 180 | public static func processModify( 181 | component: consuming PropertyPath.Component, 182 | trackingContext: borrowing _TrackingContext, 183 | storage: borrowing _BackingStorage> 184 | ) { 185 | 186 | trackingContext.trackingResultRef?.accessorModify(path: trackingContext.path?.pushed(component)) 187 | 188 | guard var value = storage.value else { 189 | return 190 | } 191 | 192 | if let ref = trackingContext.trackingResultRef { 193 | 194 | if value._tracking_context.trackingResultRef !== ref { 195 | value._tracking_context = _TrackingContext(trackingResultRef: ref) 196 | } 197 | value._tracking_context.path = trackingContext.path?.pushed(component) 198 | 199 | storage.value = value 200 | } 201 | 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /Sources/StateStruct/_TrackingContext.swift: -------------------------------------------------------------------------------- 1 | import os.lock 2 | import Foundation.NSThread 3 | 4 | public struct _TrackingContext: Sendable, Hashable { 5 | 6 | public static func == (lhs: _TrackingContext, rhs: _TrackingContext) -> Bool { 7 | // ``_TrackingContext`` is used only for embedding into the struct. 8 | // It always returns true when checked for equality to prevent 9 | // interfering with the actual equality check of the struct. 10 | return true 11 | } 12 | 13 | public func hash(into hasher: inout Hasher) { 14 | 0.hash(into: &hasher) 15 | } 16 | 17 | public struct Info { 18 | 19 | public var path: PropertyPath? 20 | 21 | public var identifier: AnyHashable? 22 | 23 | public var currentResultRef: TrackingResultRef? 24 | 25 | init( 26 | path: PropertyPath? = nil, 27 | identifier: AnyHashable? = nil, 28 | currentResultRef: TrackingResultRef? 29 | ) { 30 | self.path = path 31 | self.identifier = identifier 32 | self.currentResultRef = currentResultRef 33 | } 34 | 35 | } 36 | 37 | private let infoBox: OSAllocatedUnfairLock 38 | 39 | public var path: PropertyPath? { 40 | get { 41 | infoBox.withLockUnchecked { 42 | $0.path 43 | } 44 | } 45 | nonmutating set { 46 | infoBox.withLockUnchecked { 47 | $0.path = newValue 48 | } 49 | } 50 | } 51 | 52 | public var identifier: AnyHashable? { 53 | get { 54 | infoBox.withLockUnchecked { 55 | $0.identifier 56 | } 57 | } 58 | nonmutating set { 59 | infoBox.withLockUnchecked { 60 | $0.identifier = newValue 61 | } 62 | } 63 | } 64 | 65 | public var trackingResultRef: TrackingResultRef? { 66 | get { 67 | infoBox.withLock { $0.currentResultRef } 68 | } 69 | nonmutating set { 70 | infoBox.withLock { 71 | $0.currentResultRef = newValue 72 | } 73 | } 74 | } 75 | 76 | public init(trackingResultRef: TrackingResultRef) { 77 | infoBox = .init( 78 | uncheckedState: .init( 79 | currentResultRef: trackingResultRef 80 | ) 81 | ) 82 | } 83 | 84 | public init() { 85 | infoBox = .init( 86 | uncheckedState: .init( 87 | currentResultRef: nil 88 | ) 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/COWTrackingPropertyMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacroExpansion 5 | import SwiftSyntaxMacros 6 | 7 | public struct COWTrackingPropertyMacro { 8 | 9 | public enum Error: Swift.Error { 10 | case needsTypeAnnotation 11 | } 12 | 13 | } 14 | 15 | extension COWTrackingPropertyMacro: PeerMacro { 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | providingPeersOf declaration: some DeclSyntaxProtocol, 19 | in context: some MacroExpansionContext 20 | ) throws -> [DeclSyntax] { 21 | 22 | guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { 23 | return [] 24 | } 25 | 26 | guard variableDecl.typeSyntax != nil else { 27 | context.addDiagnostics(from: Error.needsTypeAnnotation, node: declaration) 28 | return [] 29 | } 30 | 31 | let isPublic = variableDecl.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) 32 | let isPrivate = variableDecl.modifiers.contains(where: { $0.name.tokenKind == .keyword(.private) }) 33 | 34 | var newMembers: [DeclSyntax] = [] 35 | 36 | let ignoreMacroAttached = variableDecl.attributes.contains { 37 | switch $0 { 38 | case .attribute(let attribute): 39 | return attribute.attributeName.description == "TrackingIgnored" 40 | case .ifConfigDecl: 41 | return false 42 | } 43 | } 44 | 45 | guard !ignoreMacroAttached else { 46 | return [] 47 | } 48 | 49 | for binding in variableDecl.bindings { 50 | if binding.accessorBlock != nil { 51 | // skip computed properties 52 | continue 53 | } 54 | } 55 | 56 | var _variableDecl = variableDecl.trimmed 57 | _variableDecl.attributes = [.init(.init(stringLiteral: "@TrackingIgnored"))] 58 | 59 | if variableDecl.isOptional { 60 | _variableDecl = 61 | _variableDecl 62 | .renamingIdentifier(with: "_backing_") 63 | .modifyingTypeAnnotation({ type in 64 | return "_BackingStorage<\(type.trimmed)>" 65 | }) 66 | 67 | // add init 68 | _variableDecl = _variableDecl.with( 69 | \.bindings, 70 | .init( 71 | _variableDecl.bindings.map { binding in 72 | binding.with(\.initializer, .init(value: "_BackingStorage.init(nil)" as ExprSyntax)) 73 | }) 74 | ) 75 | 76 | } else { 77 | _variableDecl = 78 | _variableDecl 79 | .renamingIdentifier(with: "_backing_") 80 | .modifyingTypeAnnotation({ type in 81 | return "_BackingStorage<\(type.trimmed)>" 82 | }) 83 | .modifyingInit({ initializer in 84 | return .init(value: "_BackingStorage.init(\(initializer.trimmed.value))" as ExprSyntax) 85 | }) 86 | } 87 | 88 | do { 89 | 90 | // remove accessors 91 | _variableDecl = _variableDecl.with( 92 | \.bindings, 93 | .init( 94 | _variableDecl.bindings.map { binding in 95 | binding.with(\.accessorBlock, nil) 96 | } 97 | ) 98 | ) 99 | 100 | } 101 | 102 | newMembers.append(DeclSyntax(_variableDecl)) 103 | 104 | var accessor: String 105 | if isPrivate { 106 | accessor = "private" 107 | } else if isPublic { 108 | accessor = "public" 109 | } else { 110 | accessor = "internal" 111 | } 112 | 113 | do { 114 | let referencingAccessor = """ 115 | \(raw: accessor) var $\(raw: variableDecl.name): Referencing<\(variableDecl.typeSyntax!.trimmed)> { 116 | Referencing(storage: _backing_\(raw: variableDecl.name)) 117 | } 118 | """ as DeclSyntax 119 | 120 | newMembers.append(referencingAccessor) 121 | } 122 | 123 | return newMembers 124 | } 125 | } 126 | 127 | extension COWTrackingPropertyMacro: AccessorMacro { 128 | public static func expansion( 129 | of node: SwiftSyntax.AttributeSyntax, 130 | providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, 131 | in context: some SwiftSyntaxMacros.MacroExpansionContext 132 | ) throws -> [SwiftSyntax.AccessorDeclSyntax] { 133 | 134 | guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { 135 | return [] 136 | } 137 | 138 | guard let binding = variableDecl.bindings.first, 139 | let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) 140 | else { 141 | return [] 142 | } 143 | 144 | let isConstant = variableDecl.bindingSpecifier.tokenKind == .keyword(.let) 145 | let propertyName = identifierPattern.identifier.text 146 | let backingName = "_backing_" + propertyName 147 | let hasDidSet = variableDecl.didSetBlock != nil 148 | let hasWillSet = variableDecl.willSetBlock != nil 149 | 150 | let initAccessor = AccessorDeclSyntax( 151 | """ 152 | @storageRestrictions(initializes: \(raw: backingName)) 153 | init(initialValue) { 154 | \(raw: backingName) = .init(initialValue) 155 | } 156 | """ 157 | ) 158 | 159 | let readAccessor = AccessorDeclSyntax( 160 | """ 161 | get { 162 | return TrackingRuntime.processGet( 163 | component: .init("\(raw: propertyName)"), 164 | value: \(raw: backingName).value, 165 | trackingContext: _tracking_context 166 | ) 167 | } 168 | """ 169 | ) 170 | 171 | let setAccessor = AccessorDeclSyntax( 172 | """ 173 | set { 174 | 175 | // willset 176 | \(variableDecl.makeWillSetDoBlock()) 177 | 178 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)"))) 179 | 180 | \(raw: hasDidSet ? "let oldValue = \(backingName).value" : "") 181 | 182 | if !isKnownUniquelyReferenced(&\(raw: backingName)) { 183 | \(raw: backingName) = .init(newValue) 184 | } else { 185 | \(raw: backingName).value = newValue 186 | } 187 | 188 | // didSet 189 | \(variableDecl.makeDidSetDoBlock()) 190 | } 191 | """ 192 | ) 193 | 194 | let modifyAccessor = AccessorDeclSyntax( 195 | """ 196 | _modify { 197 | 198 | if !isKnownUniquelyReferenced(&\(raw: backingName)) { 199 | \(raw: backingName) = .init(\(raw: backingName).value) 200 | } 201 | 202 | \(raw: hasDidSet ? "let oldValue = \(backingName).value" : "") 203 | 204 | TrackingRuntime.processModify( 205 | component: .init("\(raw: propertyName)"), 206 | trackingContext: _tracking_context, 207 | storage: \(raw: backingName) 208 | ) 209 | 210 | yield &\(raw: backingName).value 211 | 212 | // didSet 213 | \(variableDecl.makeDidSetDoBlock()) 214 | } 215 | """ 216 | ) 217 | 218 | var accessors: [AccessorDeclSyntax] = [] 219 | 220 | if binding.initializer == nil { 221 | accessors.append(initAccessor) 222 | } 223 | 224 | accessors.append(readAccessor) 225 | 226 | if !isConstant { 227 | accessors.append(setAccessor) 228 | 229 | if hasWillSet == false { 230 | accessors.append(modifyAccessor) 231 | } 232 | } 233 | 234 | return accessors 235 | 236 | } 237 | 238 | } 239 | 240 | extension VariableDeclSyntax { 241 | 242 | var name: String { 243 | return self.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text ?? "" 244 | } 245 | 246 | func renamingIdentifier(with newName: String) -> VariableDeclSyntax { 247 | let newBindings = self.bindings.map { binding -> PatternBindingSyntax in 248 | 249 | if let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) { 250 | 251 | let propertyName = identifierPattern.identifier.text 252 | 253 | let newIdentifierPattern = identifierPattern.with( 254 | \.identifier, "\(raw: newName)\(raw: propertyName)") 255 | return binding.with(\.pattern, .init(newIdentifierPattern)) 256 | } 257 | return binding 258 | } 259 | 260 | return self.with(\.bindings, .init(newBindings)) 261 | } 262 | } 263 | 264 | extension VariableDeclSyntax { 265 | func withPrivateModifier() -> VariableDeclSyntax { 266 | 267 | let privateModifier = DeclModifierSyntax.init( 268 | name: .keyword(.private), trailingTrivia: .spaces(1)) 269 | 270 | var modifiers = self.modifiers 271 | if modifiers.contains(where: { $0.name.tokenKind == .keyword(.private) }) { 272 | return self 273 | } 274 | modifiers.append(privateModifier) 275 | return self.with(\.modifiers, modifiers) 276 | } 277 | } 278 | 279 | extension VariableDeclSyntax { 280 | 281 | var isOptional: Bool { 282 | 283 | return self.bindings.contains(where: { 284 | $0.typeAnnotation?.type.is(OptionalTypeSyntax.self) ?? false 285 | }) 286 | 287 | } 288 | 289 | var typeSyntax: TypeSyntax? { 290 | return self.bindings.first?.typeAnnotation?.type 291 | } 292 | 293 | func modifyingTypeAnnotation(_ modifier: (TypeSyntax) -> TypeSyntax) -> VariableDeclSyntax { 294 | let newBindings = self.bindings.map { binding -> PatternBindingSyntax in 295 | if let typeAnnotation = binding.typeAnnotation { 296 | let newType = modifier(typeAnnotation.type) 297 | let newTypeAnnotation = typeAnnotation.with(\.type, newType) 298 | return binding.with(\.typeAnnotation, newTypeAnnotation) 299 | } 300 | return binding 301 | } 302 | 303 | return self.with(\.bindings, .init(newBindings)) 304 | } 305 | 306 | func modifyingInit(_ modifier: (InitializerClauseSyntax) -> InitializerClauseSyntax) 307 | -> VariableDeclSyntax 308 | { 309 | 310 | let newBindings = self.bindings.map { binding -> PatternBindingSyntax in 311 | if let initializer = binding.initializer { 312 | let newInitializer = modifier(initializer) 313 | return binding.with(\.initializer, newInitializer) 314 | } 315 | return binding 316 | } 317 | 318 | return self.with(\.bindings, .init(newBindings)) 319 | } 320 | 321 | } 322 | 323 | extension VariableDeclSyntax { 324 | 325 | func makeDidSetDoBlock() -> DoStmtSyntax { 326 | guard let didSetBlock = self.didSetBlock else { 327 | return .init(body: "{}") 328 | } 329 | 330 | return .init(body: didSetBlock) 331 | } 332 | 333 | func makeWillSetDoBlock() -> DoStmtSyntax { 334 | guard let willSetBlock = self.willSetBlock else { 335 | return .init(body: "{}") 336 | } 337 | 338 | return .init(body: willSetBlock) 339 | } 340 | 341 | var didSetBlock: CodeBlockSyntax? { 342 | for binding in self.bindings { 343 | if let accessorBlock = binding.accessorBlock { 344 | switch accessorBlock.accessors { 345 | case .accessors(let accessors): 346 | for accessor in accessors { 347 | if accessor.accessorSpecifier.tokenKind == .keyword(.didSet) { 348 | return accessor.body 349 | } 350 | } 351 | case .getter: 352 | return nil 353 | } 354 | } 355 | } 356 | return nil 357 | } 358 | 359 | var willSetBlock: CodeBlockSyntax? { 360 | for binding in self.bindings { 361 | if let accessorBlock = binding.accessorBlock { 362 | switch accessorBlock.accessors { 363 | case .accessors(let accessors): 364 | for accessor in accessors { 365 | if accessor.accessorSpecifier.tokenKind == .keyword(.willSet) { 366 | return accessor.body 367 | } 368 | } 369 | case .getter: 370 | return nil 371 | } 372 | } 373 | } 374 | return nil 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/KnownTypes.swift: -------------------------------------------------------------------------------- 1 | 2 | enum KnownTypes { 3 | 4 | static func isCOWType(_ typeName: String) -> Bool { 5 | // Check if the type is a collection type with Copy-on-Write semantics 6 | func isArrayType(_ type: String) -> Bool { 7 | // Array形式または[Type]形式かチェック 8 | return (type.hasPrefix("Array<") && type.hasSuffix(">")) || 9 | (type.hasPrefix("[") && type.hasSuffix("]") && !type.contains(":")) 10 | } 11 | 12 | func isDictionaryType(_ type: String) -> Bool { 13 | // Dictionary形式または[KeyType: ValueType]形式かチェック 14 | return (type.hasPrefix("Dictionary<") && type.hasSuffix(">")) || 15 | (type.hasPrefix("[") && type.hasSuffix("]") && type.contains(":")) 16 | } 17 | 18 | func isStringType(_ type: String) -> Bool { 19 | return type == "String" 20 | } 21 | 22 | func isSetType(_ type: String) -> Bool { 23 | return type.hasPrefix("Set<") && type.hasSuffix(">") 24 | } 25 | 26 | if isArrayType(typeName) { 27 | return true 28 | } 29 | 30 | if isDictionaryType(typeName) { 31 | return true 32 | } 33 | 34 | if isStringType(typeName) { 35 | return true 36 | } 37 | 38 | if isSetType(typeName) { 39 | return true 40 | } 41 | 42 | return false 43 | } 44 | // Integer types 45 | static let primitiveTypes: Set = [ 46 | // Integer types 47 | "Int", 48 | "Int8", 49 | "Int16", 50 | "Int32", 51 | "Int64", 52 | "UInt", 53 | "UInt8", 54 | "UInt16", 55 | "UInt32", 56 | "UInt64", 57 | 58 | // Floating-point types 59 | "Float", 60 | "Double", 61 | "CGFloat", 62 | 63 | // Boolean type 64 | "Bool", 65 | 66 | // Character types 67 | "Character", 68 | 69 | // Optional type 70 | "Optional", 71 | 72 | // Range types 73 | "Range", 74 | "ClosedRange", 75 | "PartialRangeFrom", 76 | "PartialRangeThrough", 77 | "PartialRangeUpTo", 78 | 79 | // Date types 80 | "Date", 81 | "TimeInterval", 82 | 83 | // UUID 84 | "UUID", 85 | 86 | // URL 87 | "URL", 88 | 89 | // Data 90 | "Data", 91 | 92 | // Decimal 93 | "Decimal", 94 | 95 | // CG types 96 | "CGPoint", 97 | "CGSize", 98 | "CGRect", 99 | "CGVector", 100 | 101 | // Color types 102 | "Color", 103 | "NSColor", 104 | "UIColor", 105 | ] 106 | 107 | static func isPrimitiveType(_ typeName: String) -> Bool { 108 | 109 | if typeName.hasSuffix("?") { 110 | let unwrappedTypeName = typeName.replacingOccurrences(of: "?", with: "") 111 | return isPrimitiveType(unwrappedTypeName) 112 | } 113 | 114 | if typeName.hasPrefix("Optional<") && typeName.hasSuffix(">") { 115 | let unwrappedTypeName = typeName 116 | .replacingOccurrences(of: "Optional<", with: "") 117 | .replacingOccurrences(of: ">", with: "") 118 | return isPrimitiveType(unwrappedTypeName) 119 | } 120 | 121 | return primitiveTypes.contains(typeName) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | @main 7 | struct Plugin: CompilerPlugin { 8 | let providingMacros: [Macro.Type] = [ 9 | TrackingMacro.self, 10 | COWTrackingPropertyMacro.self, 11 | PrimitiveTrackingPropertyMacro.self, 12 | TrackingIgnoredMacro.self, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/PrimitiveTrackingPropertyMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacroExpansion 5 | import SwiftSyntaxMacros 6 | 7 | public struct PrimitiveTrackingPropertyMacro { 8 | 9 | public enum Error: Swift.Error { 10 | case needsTypeAnnotation 11 | } 12 | 13 | } 14 | 15 | extension PrimitiveTrackingPropertyMacro: PeerMacro { 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | providingPeersOf declaration: some DeclSyntaxProtocol, 19 | in context: some MacroExpansionContext 20 | ) throws -> [DeclSyntax] { 21 | 22 | guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { 23 | return [] 24 | } 25 | 26 | guard variableDecl.typeSyntax != nil else { 27 | context.addDiagnostics(from: Error.needsTypeAnnotation, node: declaration) 28 | return [] 29 | } 30 | 31 | var newMembers: [DeclSyntax] = [] 32 | 33 | let ignoreMacroAttached = variableDecl.attributes.contains { 34 | switch $0 { 35 | case .attribute(let attribute): 36 | return attribute.attributeName.description == "TrackingIgnored" 37 | case .ifConfigDecl: 38 | return false 39 | } 40 | } 41 | 42 | guard !ignoreMacroAttached else { 43 | return [] 44 | } 45 | 46 | for binding in variableDecl.bindings { 47 | if binding.accessorBlock != nil { 48 | // skip computed properties 49 | continue 50 | } 51 | } 52 | 53 | var _variableDecl = variableDecl.trimmed 54 | _variableDecl.attributes = [.init(.init(stringLiteral: "@TrackingIgnored"))] 55 | 56 | _variableDecl = _variableDecl.renamingIdentifier(with: "_backing_") 57 | 58 | newMembers.append(DeclSyntax(_variableDecl)) 59 | 60 | return newMembers 61 | } 62 | } 63 | 64 | extension PrimitiveTrackingPropertyMacro: AccessorMacro { 65 | public static func expansion( 66 | of node: SwiftSyntax.AttributeSyntax, 67 | providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, 68 | in context: some SwiftSyntaxMacros.MacroExpansionContext 69 | ) throws -> [SwiftSyntax.AccessorDeclSyntax] { 70 | 71 | guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { 72 | return [] 73 | } 74 | 75 | guard let binding = variableDecl.bindings.first, 76 | let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) 77 | else { 78 | return [] 79 | } 80 | 81 | let isConstant = variableDecl.bindingSpecifier.tokenKind == .keyword(.let) 82 | let propertyName = identifierPattern.identifier.text 83 | let backingName = "_backing_" + propertyName 84 | let hasWillSet = variableDecl.willSetBlock != nil 85 | 86 | let initAccessor = AccessorDeclSyntax( 87 | """ 88 | @storageRestrictions(initializes: \(raw: backingName)) 89 | init(initialValue) { 90 | \(raw: backingName) = initialValue 91 | } 92 | """ 93 | ) 94 | 95 | let readAccessor = AccessorDeclSyntax( 96 | """ 97 | _read { 98 | let component = PropertyPath.Component.init("\(raw: propertyName)") 99 | _tracking_context.trackingResultRef?.accessorRead(path: _tracking_context.path?.pushed(component)) 100 | 101 | yield \(raw: backingName) 102 | } 103 | """ 104 | ) 105 | 106 | let setAccessor = AccessorDeclSyntax( 107 | """ 108 | set { 109 | let component = PropertyPath.Component.init("\(raw: propertyName)") 110 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(component)) 111 | 112 | \(raw: backingName) = newValue 113 | } 114 | """ 115 | ) 116 | 117 | let modifyAccessor = AccessorDeclSyntax( 118 | """ 119 | _modify { 120 | let component = PropertyPath.Component.init("\(raw: propertyName)") 121 | _tracking_context.trackingResultRef?.accessorModify(path: _tracking_context.path?.pushed(component)) 122 | 123 | yield &\(raw: backingName) 124 | } 125 | """ 126 | ) 127 | 128 | var accessors: [AccessorDeclSyntax] = [] 129 | 130 | if binding.initializer == nil { 131 | accessors.append(initAccessor) 132 | } 133 | 134 | accessors.append(readAccessor) 135 | 136 | if !isConstant { 137 | accessors.append(setAccessor) 138 | 139 | if hasWillSet == false { 140 | accessors.append(modifyAccessor) 141 | } 142 | } 143 | 144 | return accessors 145 | 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/TrackingIgnoredMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | public struct TrackingIgnoredMacro: Macro { 7 | 8 | } 9 | 10 | extension TrackingIgnoredMacro: AccessorMacro { 11 | public static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] { 12 | return [] 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Sources/StateStructMacros/TrackingMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | public struct TrackingMacro: Macro { 7 | 8 | public enum Error: Swift.Error { 9 | case needsTypeAnnotation 10 | case notFoundPropertyName 11 | } 12 | 13 | public static var formatMode: FormatMode { 14 | .auto 15 | } 16 | 17 | } 18 | 19 | extension TrackingMacro: MemberMacro { 20 | public static func expansion( 21 | of node: AttributeSyntax, 22 | providingMembersOf declaration: some DeclGroupSyntax, 23 | in context: some MacroExpansionContext 24 | ) throws -> [DeclSyntax] { 25 | 26 | let isPublic = declaration.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) 27 | 28 | return [ 29 | """ 30 | \(raw: isPublic ? "public" : "internal") var _tracking_context: _TrackingContext = .init() 31 | """ as DeclSyntax 32 | ] 33 | } 34 | } 35 | 36 | extension TrackingMacro: ExtensionMacro { 37 | public static func expansion( 38 | of node: AttributeSyntax, 39 | attachedTo declaration: some DeclGroupSyntax, 40 | providingExtensionsOf type: some TypeSyntaxProtocol, 41 | conformingTo protocols: [TypeSyntax], 42 | in context: some MacroExpansionContext 43 | ) throws -> [ExtensionDeclSyntax] { 44 | 45 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 46 | fatalError() 47 | } 48 | 49 | return [ 50 | (""" 51 | extension \(structDecl.name.trimmed): TrackingObject { 52 | } 53 | """ as DeclSyntax).cast(ExtensionDeclSyntax.self) 54 | ] 55 | } 56 | } 57 | 58 | extension TrackingMacro: MemberAttributeMacro { 59 | 60 | public static func expansion( 61 | of node: AttributeSyntax, 62 | attachedTo declaration: some DeclGroupSyntax, 63 | providingAttributesFor member: some DeclSyntaxProtocol, 64 | in context: some MacroExpansionContext 65 | ) throws -> [AttributeSyntax] { 66 | 67 | guard let variableDecl = member.as(VariableDeclSyntax.self) else { 68 | return [] 69 | } 70 | 71 | let existingAttributes = variableDecl.attributes.map { $0.trimmed.description } 72 | 73 | let ignoreMacros = Set( 74 | [ 75 | "@TrackingIgnored", 76 | "@PrimitiveTrackingProperty", 77 | "@WeakTrackingProperty", 78 | "@COWTrackingProperty", 79 | ] 80 | ) 81 | 82 | if existingAttributes.filter({ ignoreMacros.contains($0) }).count > 0 { 83 | return [] 84 | } 85 | 86 | // to ignore computed properties 87 | for binding in variableDecl.bindings { 88 | if let accessorBlock = binding.accessorBlock { 89 | // Check if this is a computed property (has a 'get' accessor) 90 | // If it has only property observers like didSet/willSet, it's still a stored property 91 | 92 | switch accessorBlock.accessors { 93 | case .accessors(let accessors): 94 | let hasGetter = accessors.contains { syntax in 95 | syntax.accessorSpecifier.tokenKind == .keyword(.get) 96 | } 97 | if hasGetter { 98 | return [] 99 | } 100 | continue 101 | case .getter: 102 | return [] 103 | } 104 | } 105 | } 106 | 107 | guard variableDecl.bindingSpecifier.tokenKind == .keyword(.var) else { 108 | return [] 109 | } 110 | 111 | let isWeak = variableDecl.modifiers.contains { modifier in 112 | modifier.name.tokenKind == .keyword(.weak) 113 | } 114 | 115 | let isCOWType = KnownTypes.isCOWType(variableDecl.typeSyntax!.trimmed.description) 116 | 117 | let isPrimitiveType = KnownTypes.isPrimitiveType(variableDecl.typeSyntax!.trimmed.description) 118 | 119 | if isPrimitiveType || isCOWType { 120 | return [ 121 | "@PrimitiveTrackingProperty" 122 | ] 123 | } 124 | 125 | if isWeak { 126 | return [AttributeSyntax(stringLiteral: "@WeakTrackingProperty")] 127 | } else { 128 | return [AttributeSyntax(stringLiteral: "@COWTrackingProperty")] 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /Tests/StateStructMacroTests/COWTrackingProperyMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import StateStructMacros 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | 7 | final class COWTrackingProperyMacroTests: XCTestCase { 8 | 9 | override func invokeTest() { 10 | withMacroTesting( 11 | record: false, 12 | macros: [ 13 | "COWTrackingProperty": COWTrackingPropertyMacro.self, 14 | "TrackingIgnored": TrackingIgnoredMacro.self, 15 | ] 16 | ) { 17 | super.invokeTest() 18 | } 19 | } 20 | 21 | func test_did_set() { 22 | 23 | assertMacro { 24 | """ 25 | struct MyState { 26 | 27 | @COWTrackingProperty 28 | var stored_0: Int = 18 { 29 | didSet { 30 | print("stored_0 did set") 31 | } 32 | } 33 | 34 | @COWTrackingProperty 35 | var stored_1: Int = 18 { 36 | willSet { 37 | print("stored_1 will set") 38 | } 39 | didSet { 40 | print("stored_1 did set") 41 | } 42 | } 43 | 44 | @COWTrackingProperty 45 | var stored_2: Int = 18 { 46 | willSet { 47 | print("stored_2 will set") 48 | } 49 | } 50 | 51 | internal let _tracking_context: _TrackingContext = .init() 52 | 53 | } 54 | """ 55 | } expansion: { 56 | """ 57 | struct MyState { 58 | 59 | 60 | var stored_0: Int { 61 | didSet { 62 | print("stored_0 did set") 63 | } 64 | get { 65 | return TrackingRuntime.processGet( 66 | component: .init("stored_0"), 67 | value: _backing_stored_0.value, 68 | trackingContext: _tracking_context 69 | ) 70 | } 71 | 72 | set { 73 | 74 | // willset 75 | do { 76 | } 77 | 78 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_0"))) 79 | 80 | let oldValue = _backing_stored_0.value 81 | 82 | if !isKnownUniquelyReferenced(&_backing_stored_0) { 83 | _backing_stored_0 = .init(newValue) 84 | } else { 85 | _backing_stored_0.value = newValue 86 | } 87 | 88 | // didSet 89 | do { 90 | print("stored_0 did set") 91 | } 92 | } 93 | 94 | _modify { 95 | 96 | if !isKnownUniquelyReferenced(&_backing_stored_0) { 97 | _backing_stored_0 = .init(_backing_stored_0.value) 98 | } 99 | 100 | let oldValue = _backing_stored_0.value 101 | 102 | TrackingRuntime.processModify( 103 | component: .init("stored_0"), 104 | trackingContext: _tracking_context, 105 | storage: _backing_stored_0 106 | ) 107 | 108 | yield &_backing_stored_0.value 109 | 110 | // didSet 111 | do { 112 | print("stored_0 did set") 113 | } 114 | } 115 | } 116 | var _backing_stored_0: _BackingStorage = _BackingStorage.init(18) 117 | 118 | internal var $stored_0: Referencing { 119 | Referencing(storage: _backing_stored_0) 120 | } 121 | 122 | 123 | var stored_1: Int { 124 | willSet { 125 | print("stored_1 will set") 126 | } 127 | didSet { 128 | print("stored_1 did set") 129 | } 130 | get { 131 | return TrackingRuntime.processGet( 132 | component: .init("stored_1"), 133 | value: _backing_stored_1.value, 134 | trackingContext: _tracking_context 135 | ) 136 | } 137 | 138 | set { 139 | 140 | // willset 141 | do { 142 | print("stored_1 will set") 143 | } 144 | 145 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_1"))) 146 | 147 | let oldValue = _backing_stored_1.value 148 | 149 | if !isKnownUniquelyReferenced(&_backing_stored_1) { 150 | _backing_stored_1 = .init(newValue) 151 | } else { 152 | _backing_stored_1.value = newValue 153 | } 154 | 155 | // didSet 156 | do { 157 | print("stored_1 did set") 158 | } 159 | } 160 | } 161 | var _backing_stored_1: _BackingStorage = _BackingStorage.init(18) 162 | 163 | internal var $stored_1: Referencing { 164 | Referencing(storage: _backing_stored_1) 165 | } 166 | 167 | 168 | var stored_2: Int { 169 | willSet { 170 | print("stored_2 will set") 171 | } 172 | get { 173 | return TrackingRuntime.processGet( 174 | component: .init("stored_2"), 175 | value: _backing_stored_2.value, 176 | trackingContext: _tracking_context 177 | ) 178 | } 179 | 180 | set { 181 | 182 | // willset 183 | do { 184 | print("stored_2 will set") 185 | } 186 | 187 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_2"))) 188 | 189 | 190 | 191 | if !isKnownUniquelyReferenced(&_backing_stored_2) { 192 | _backing_stored_2 = .init(newValue) 193 | } else { 194 | _backing_stored_2.value = newValue 195 | } 196 | 197 | // didSet 198 | do { 199 | } 200 | } 201 | } 202 | var _backing_stored_2: _BackingStorage = _BackingStorage.init(18) 203 | 204 | internal var $stored_2: Referencing { 205 | Referencing(storage: _backing_stored_2) 206 | } 207 | 208 | internal let _tracking_context: _TrackingContext = .init() 209 | 210 | } 211 | """ 212 | } 213 | 214 | } 215 | 216 | func test_optional_init() { 217 | assertMacro { 218 | """ 219 | struct OptinalPropertyState { 220 | 221 | @COWTrackingProperty 222 | var stored_1: Int? 223 | 224 | init() { 225 | } 226 | 227 | } 228 | """ 229 | } expansion: { 230 | """ 231 | struct OptinalPropertyState { 232 | 233 | var stored_1: Int? { 234 | 235 | @storageRestrictions(initializes: _backing_stored_1) 236 | 237 | init(initialValue) { 238 | 239 | _backing_stored_1 = .init(initialValue) 240 | 241 | } 242 | 243 | get { 244 | 245 | return TrackingRuntime.processGet( 246 | 247 | component: .init("stored_1"), 248 | 249 | value: _backing_stored_1.value, 250 | 251 | trackingContext: _tracking_context 252 | 253 | ) 254 | 255 | } 256 | 257 | set { 258 | 259 | 260 | // willset 261 | 262 | do { 263 | 264 | } 265 | 266 | 267 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_1"))) 268 | 269 | 270 | 271 | 272 | if !isKnownUniquelyReferenced(&_backing_stored_1) { 273 | 274 | _backing_stored_1 = .init(newValue) 275 | 276 | } else { 277 | 278 | _backing_stored_1.value = newValue 279 | 280 | } 281 | 282 | 283 | // didSet 284 | 285 | do { 286 | 287 | } 288 | 289 | } 290 | 291 | _modify { 292 | 293 | 294 | if !isKnownUniquelyReferenced(&_backing_stored_1) { 295 | 296 | _backing_stored_1 = .init(_backing_stored_1.value) 297 | 298 | } 299 | 300 | 301 | 302 | 303 | TrackingRuntime.processModify( 304 | 305 | component: .init("stored_1"), 306 | 307 | trackingContext: _tracking_context, 308 | 309 | storage: _backing_stored_1 310 | 311 | ) 312 | 313 | 314 | yield &_backing_stored_1.value 315 | 316 | 317 | // didSet 318 | 319 | do { 320 | 321 | } 322 | 323 | } 324 | } 325 | var _backing_stored_1: _BackingStorage = _BackingStorage.init(nil) 326 | 327 | internal var $stored_1: Referencing { 328 | Referencing(storage: _backing_stored_1) 329 | } 330 | 331 | init() { 332 | } 333 | 334 | } 335 | """ 336 | } 337 | } 338 | 339 | func test_macro() { 340 | 341 | assertMacro { 342 | """ 343 | struct MyState { 344 | 345 | @COWTrackingProperty 346 | private var stored_0: Int = 18 347 | 348 | @COWTrackingProperty 349 | public var stored_1: Int = 18 350 | 351 | func compute() { 352 | } 353 | } 354 | """ 355 | } expansion: { 356 | """ 357 | struct MyState { 358 | 359 | 360 | private var stored_0: Int { 361 | get { 362 | return TrackingRuntime.processGet( 363 | component: .init("stored_0"), 364 | value: _backing_stored_0.value, 365 | trackingContext: _tracking_context 366 | ) 367 | } 368 | set { 369 | 370 | // willset 371 | do { 372 | } 373 | 374 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_0"))) 375 | 376 | 377 | 378 | if !isKnownUniquelyReferenced(&_backing_stored_0) { 379 | _backing_stored_0 = .init(newValue) 380 | } else { 381 | _backing_stored_0.value = newValue 382 | } 383 | 384 | // didSet 385 | do { 386 | } 387 | } 388 | _modify { 389 | 390 | if !isKnownUniquelyReferenced(&_backing_stored_0) { 391 | _backing_stored_0 = .init(_backing_stored_0.value) 392 | } 393 | 394 | 395 | 396 | TrackingRuntime.processModify( 397 | component: .init("stored_0"), 398 | trackingContext: _tracking_context, 399 | storage: _backing_stored_0 400 | ) 401 | 402 | yield &_backing_stored_0.value 403 | 404 | // didSet 405 | do { 406 | } 407 | } 408 | } 409 | private var _backing_stored_0: _BackingStorage = _BackingStorage.init(18) 410 | 411 | private var $stored_0: Referencing { 412 | Referencing(storage: _backing_stored_0) 413 | } 414 | 415 | 416 | public var stored_1: Int { 417 | get { 418 | return TrackingRuntime.processGet( 419 | component: .init("stored_1"), 420 | value: _backing_stored_1.value, 421 | trackingContext: _tracking_context 422 | ) 423 | } 424 | set { 425 | 426 | // willset 427 | do { 428 | } 429 | 430 | _tracking_context.trackingResultRef?.accessorSet(path: _tracking_context.path?.pushed(.init("stored_1"))) 431 | 432 | 433 | 434 | if !isKnownUniquelyReferenced(&_backing_stored_1) { 435 | _backing_stored_1 = .init(newValue) 436 | } else { 437 | _backing_stored_1.value = newValue 438 | } 439 | 440 | // didSet 441 | do { 442 | } 443 | } 444 | _modify { 445 | 446 | if !isKnownUniquelyReferenced(&_backing_stored_1) { 447 | _backing_stored_1 = .init(_backing_stored_1.value) 448 | } 449 | 450 | 451 | 452 | TrackingRuntime.processModify( 453 | component: .init("stored_1"), 454 | trackingContext: _tracking_context, 455 | storage: _backing_stored_1 456 | ) 457 | 458 | yield &_backing_stored_1.value 459 | 460 | // didSet 461 | do { 462 | } 463 | } 464 | } 465 | public var _backing_stored_1: _BackingStorage = _BackingStorage.init(18) 466 | 467 | public var $stored_1: Referencing { 468 | Referencing(storage: _backing_stored_1) 469 | } 470 | 471 | func compute() { 472 | } 473 | } 474 | """ 475 | } 476 | 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /Tests/StateStructMacroTests/TrackingMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import StateStructMacros 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | 7 | final class TrackingMacroTests: XCTestCase { 8 | 9 | override func invokeTest() { 10 | withMacroTesting( 11 | record: false, 12 | macros: ["Tracking": TrackingMacro.self] 13 | ) { 14 | super.invokeTest() 15 | } 16 | } 17 | 18 | func test_primitive() { 19 | 20 | assertMacro { 21 | """ 22 | @Tracking 23 | struct MyState { 24 | 25 | var int: Int 26 | 27 | var int_literalOptional: Int? 28 | 29 | var int_literalOptional_literalOptional: Int?? 30 | 31 | var int_typedOptinal: Optional 32 | 33 | var int_typedOptional_typedOptional: Optional> 34 | 35 | var int_typedOptional_literalOptional: Optional 36 | } 37 | """ 38 | } expansion: { 39 | """ 40 | struct MyState { 41 | @PrimitiveTrackingProperty 42 | 43 | var int: Int 44 | @PrimitiveTrackingProperty 45 | 46 | var int_literalOptional: Int? 47 | @PrimitiveTrackingProperty 48 | 49 | var int_literalOptional_literalOptional: Int?? 50 | @PrimitiveTrackingProperty 51 | 52 | var int_typedOptinal: Optional 53 | @PrimitiveTrackingProperty 54 | 55 | var int_typedOptional_typedOptional: Optional> 56 | @PrimitiveTrackingProperty 57 | 58 | var int_typedOptional_literalOptional: Optional 59 | 60 | internal var _tracking_context: _TrackingContext = .init() 61 | } 62 | 63 | extension MyState: TrackingObject { 64 | } 65 | """ 66 | } 67 | 68 | } 69 | 70 | func test_array_dictionary() { 71 | assertMacro { 72 | """ 73 | @Tracking 74 | struct MyState { 75 | 76 | var array: [Int] = [] 77 | 78 | var dictionary: [String: Int] = [:] 79 | 80 | var array1: Array = [] 81 | 82 | var dictionary1: Dictionary = [:] 83 | } 84 | """ 85 | } expansion: { 86 | """ 87 | struct MyState { 88 | @PrimitiveTrackingProperty 89 | 90 | var array: [Int] = [] 91 | @PrimitiveTrackingProperty 92 | 93 | var dictionary: [String: Int] = [:] 94 | @PrimitiveTrackingProperty 95 | 96 | var array1: Array = [] 97 | @PrimitiveTrackingProperty 98 | 99 | var dictionary1: Dictionary = [:] 100 | 101 | internal var _tracking_context: _TrackingContext = .init() 102 | } 103 | 104 | extension MyState: TrackingObject { 105 | } 106 | """ 107 | } 108 | } 109 | 110 | func test_ignore_computed() { 111 | 112 | assertMacro { 113 | """ 114 | @Tracking 115 | struct MyState { 116 | 117 | var c_0: Int { 118 | 0 119 | } 120 | 121 | var c_1: Int { 122 | get { 0 } 123 | } 124 | 125 | var c_2: Int { 126 | get { 0 } 127 | set { } 128 | } 129 | 130 | } 131 | """ 132 | } expansion: { 133 | """ 134 | struct MyState { 135 | 136 | var c_0: Int { 137 | 0 138 | } 139 | 140 | var c_1: Int { 141 | get { 0 } 142 | } 143 | 144 | var c_2: Int { 145 | get { 0 } 146 | set { } 147 | } 148 | 149 | internal var _tracking_context: _TrackingContext = .init() 150 | 151 | } 152 | 153 | extension MyState: TrackingObject { 154 | } 155 | """ 156 | } 157 | 158 | } 159 | 160 | func test_stored_observer() { 161 | 162 | assertMacro { 163 | """ 164 | @Tracking 165 | struct MyState { 166 | 167 | var stored_0: Int = 18 { 168 | didSet { 169 | print("stored_0 did set") 170 | } 171 | } 172 | 173 | } 174 | """ 175 | } expansion: { 176 | """ 177 | struct MyState { 178 | @PrimitiveTrackingProperty 179 | 180 | var stored_0: Int = 18 { 181 | didSet { 182 | print("stored_0 did set") 183 | } 184 | } 185 | 186 | internal var _tracking_context: _TrackingContext = .init() 187 | 188 | } 189 | 190 | extension MyState: TrackingObject { 191 | } 192 | """ 193 | } 194 | 195 | } 196 | 197 | func test_public() { 198 | assertMacro { 199 | """ 200 | @Tracking 201 | public struct MyState { 202 | 203 | private var stored_0: Int = 18 204 | 205 | var stored_1: String 206 | 207 | let stored_2: Int = 0 208 | 209 | var age: Int { 0 } 210 | 211 | var age2: Int { 212 | get { 0 } 213 | set { } 214 | } 215 | 216 | var height: Int 217 | 218 | func compute() { 219 | } 220 | } 221 | """ 222 | } expansion: { 223 | """ 224 | public struct MyState { 225 | @PrimitiveTrackingProperty 226 | 227 | private var stored_0: Int = 18 228 | @PrimitiveTrackingProperty 229 | 230 | var stored_1: String 231 | 232 | let stored_2: Int = 0 233 | 234 | var age: Int { 0 } 235 | 236 | var age2: Int { 237 | get { 0 } 238 | set { } 239 | } 240 | @PrimitiveTrackingProperty 241 | 242 | var height: Int 243 | 244 | func compute() { 245 | } 246 | 247 | public var _tracking_context: _TrackingContext = .init() 248 | } 249 | 250 | extension MyState: TrackingObject { 251 | } 252 | """ 253 | } 254 | } 255 | 256 | func test_macro() { 257 | 258 | assertMacro { 259 | """ 260 | @Tracking 261 | struct MyState { 262 | 263 | private var stored_0: Int = 18 264 | 265 | var stored_1: String 266 | 267 | let stored_2: Int = 0 268 | 269 | var age: Int { 0 } 270 | 271 | var age2: Int { 272 | get { 0 } 273 | set { } 274 | } 275 | 276 | var height: Int 277 | 278 | func compute() { 279 | } 280 | } 281 | """ 282 | } expansion: { 283 | """ 284 | struct MyState { 285 | @PrimitiveTrackingProperty 286 | 287 | private var stored_0: Int = 18 288 | @PrimitiveTrackingProperty 289 | 290 | var stored_1: String 291 | 292 | let stored_2: Int = 0 293 | 294 | var age: Int { 0 } 295 | 296 | var age2: Int { 297 | get { 0 } 298 | set { } 299 | } 300 | @PrimitiveTrackingProperty 301 | 302 | var height: Int 303 | 304 | func compute() { 305 | } 306 | 307 | internal var _tracking_context: _TrackingContext = .init() 308 | } 309 | 310 | extension MyState: TrackingObject { 311 | } 312 | """ 313 | } 314 | 315 | } 316 | 317 | func test_cow_property() { 318 | assertMacro { 319 | """ 320 | @Tracking 321 | struct MyState { 322 | var string: String = "" 323 | 324 | var set: Set = [] 325 | 326 | var customType: CustomType 327 | } 328 | """ 329 | } expansion: { 330 | """ 331 | struct MyState { 332 | @PrimitiveTrackingProperty 333 | var string: String = "" 334 | @PrimitiveTrackingProperty 335 | 336 | var set: Set = [] 337 | @COWTrackingProperty 338 | 339 | var customType: CustomType 340 | 341 | internal var _tracking_context: _TrackingContext = .init() 342 | } 343 | 344 | extension MyState: TrackingObject { 345 | } 346 | """ 347 | } 348 | } 349 | 350 | 351 | func test_weak_property() { 352 | 353 | assertMacro { 354 | """ 355 | @Tracking 356 | struct MyState { 357 | 358 | weak var weak_stored: Ref? 359 | 360 | } 361 | """ 362 | } expansion: { 363 | """ 364 | struct MyState { 365 | @WeakTrackingProperty 366 | 367 | weak var weak_stored: Ref? 368 | 369 | internal var _tracking_context: _TrackingContext = .init() 370 | 371 | } 372 | 373 | extension MyState: TrackingObject { 374 | } 375 | """ 376 | } 377 | 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Tests/StateStructTests/AccessorTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @Suite 4 | struct AccessorTests { 5 | 6 | struct Container { 7 | 8 | var value: Int { 9 | _read { 10 | print("read") 11 | yield stored 12 | } 13 | set { 14 | print("set newValue: \(newValue)") 15 | stored = newValue 16 | } 17 | _modify { 18 | print("modify") 19 | yield &stored 20 | } 21 | } 22 | 23 | var stored: Int 24 | 25 | } 26 | 27 | @Test 28 | func modify() { 29 | 30 | func update(_ value: inout Int) { 31 | defer { 32 | print("defer") 33 | } 34 | print("update") 35 | } 36 | 37 | var container = Container.init(stored: 1) 38 | print("before") 39 | update(&container.value) 40 | print("after") 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/StateStructTests/CopyOnWriteTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @Suite("Copy on Write") 4 | struct CopyOnWriteTests { 5 | 6 | @Test 7 | func cow() { 8 | 9 | let original = MyState.init() 10 | 11 | var copy = original 12 | 13 | copy.height = 100 14 | 15 | #expect(copy.height == 100) 16 | #expect(original.height == 0) 17 | } 18 | 19 | @Test 20 | func cow_nested() { 21 | let original = MyState.init() 22 | var copy = original 23 | 24 | copy.nested.name = "AAA" 25 | 26 | #expect(copy.nested.name == "AAA") 27 | #expect(original.nested.name == "") 28 | } 29 | 30 | @Test 31 | func cow_nested_age() { 32 | let original = MyState.init() 33 | var copy = original 34 | 35 | copy.nested.age = 25 36 | 37 | #expect(copy.nested.age == 25) 38 | #expect(original.nested.age == 10) 39 | } 40 | 41 | @Test 42 | func cow_nested_untracked_age() { 43 | let original = MyState.init() 44 | var copy = original 45 | 46 | copy.nestedUntracked.age = 25 47 | 48 | #expect(copy.nestedUntracked.age == 25) 49 | #expect(original.nestedUntracked.age == 10) 50 | } 51 | 52 | @Test 53 | func cow_array() { 54 | let original = MyState.init() 55 | var copy = original 56 | 57 | copy.array.append(1) 58 | 59 | #expect(copy.array == [1]) 60 | #expect(original.array == []) 61 | } 62 | 63 | @Test 64 | func cow_optional() { 65 | let original = Nesting.init() 66 | var copy = original 67 | 68 | copy._1?._1?.value = "AAA" 69 | 70 | #expect(copy._1?._1?.value == "AAA") 71 | #expect(original._1?._1?.value == "") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/StateStructTests/DidSetTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import StateStruct 3 | 4 | @Suite("DidSet") 5 | struct DidSetTests { 6 | 7 | struct NormalState { 8 | 9 | var count: Int = 0 { 10 | willSet { 11 | willSet_count = newValue 12 | } 13 | didSet { 14 | didSet_count = count 15 | } 16 | } 17 | 18 | var didSet_count: Int = 0 19 | var willSet_count: Int = 0 20 | 21 | } 22 | 23 | @Tracking 24 | struct TrackingState { 25 | 26 | var count: Int = 0 { 27 | willSet { 28 | willSet_count = newValue 29 | } 30 | didSet { 31 | didSet_count = count 32 | } 33 | } 34 | 35 | var didSet_count: Int = 0 36 | var willSet_count: Int = 0 37 | 38 | } 39 | 40 | @Test 41 | func normal() { 42 | 43 | var value = NormalState() 44 | 45 | value.count = 1 46 | 47 | #expect(value.didSet_count == 1) 48 | #expect(value.willSet_count == 1) 49 | } 50 | 51 | @Test 52 | func tracking() { 53 | 54 | var value = TrackingState() 55 | 56 | value.count = 1 57 | 58 | #expect(value.didSet_count == 1) 59 | #expect(value.willSet_count == 1) 60 | 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Tests/StateStructTests/EquatableTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import StateStruct 3 | 4 | @Suite 5 | struct EquatableTests { 6 | 7 | @Tracking 8 | struct DemoStruct: Equatable { 9 | 10 | var value: Int 11 | 12 | } 13 | 14 | @Test("Equatable") 15 | func test() { 16 | 17 | let a = DemoStruct.init(value: 1) 18 | 19 | let b = DemoStruct.init(value: 1) 20 | 21 | let c = DemoStruct.init(value: 2) 22 | 23 | #expect(a == b) 24 | #expect(a != c) 25 | 26 | } 27 | 28 | @Test 29 | func modifying() { 30 | 31 | var base = DemoStruct.init(value: 1) 32 | 33 | let original = base 34 | 35 | base.value = 2 36 | 37 | #expect(base != original) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/StateStructTests/GraphTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import StateStruct 3 | 4 | @Suite("GraphTests") 5 | struct GraphTests { 6 | 7 | @Test 8 | func example() { 9 | 10 | var node = PropertyNode(name: "1") 11 | 12 | node.applyAsWrite(path: PropertyPath().pushed(.init("1")).pushed(.init("1")).pushed(.init("1"))) 13 | node.applyAsWrite(path: PropertyPath().pushed(.init("1")).pushed(.init("2")).pushed(.init("1"))) 14 | node.applyAsWrite(path: PropertyPath().pushed(.init("1")).pushed(.init("1")).pushed(.init("2"))) 15 | 16 | print(node.prettyPrint()) 17 | 18 | #expect( 19 | node.prettyPrint() == 20 | """ 21 | 1 { 22 | 1 { 23 | 1+(1) 24 | 2+(1) 25 | } 26 | 2 { 27 | 1+(1) 28 | } 29 | } 30 | """ 31 | ) 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Tests/StateStructTests/HeavyStructCopyTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import StateStruct 3 | import XCTest 4 | 5 | // Heavy data structure with value types only 6 | struct HeavyData { 7 | // Large data composed of multiple value-type fields 8 | var value1: Double = 1.0 9 | var value2: Double = 2.0 10 | var value3: Double = 3.0 11 | 12 | // Using tuples instead of arrays to avoid CoW optimizations 13 | var tuple1: (Double, Double, Double, Double, Double) = (0.0, 1.0, 2.0, 3.0, 4.0) 14 | var tuple2: (Double, Double, Double, Double, Double) = (5.0, 6.0, 7.0, 8.0, 9.0) 15 | var tuple3: (Double, Double, Double, Double, Double) = (10.0, 11.0, 12.0, 13.0, 14.0) 16 | var tuple4: (Double, Double, Double, Double, Double) = (15.0, 16.0, 17.0, 18.0, 19.0) 17 | var tuple5: (Double, Double, Double, Double, Double) = (20.0, 21.0, 22.0, 23.0, 24.0) 18 | var tuple6: (Double, Double, Double, Double, Double) = (25.0, 26.0, 27.0, 28.0, 29.0) 19 | var tuple7: (Double, Double, Double, Double, Double) = (30.0, 31.0, 32.0, 33.0, 34.0) 20 | var tuple8: (Double, Double, Double, Double, Double) = (35.0, 36.0, 37.0, 38.0, 39.0) 21 | var tuple9: (Double, Double, Double, Double, Double) = (40.0, 41.0, 42.0, 43.0, 44.0) 22 | var tuple10: (Double, Double, Double, Double, Double) = (45.0, 46.0, 47.0, 48.0, 49.0) 23 | 24 | // Large number of primitive value fields (flat structure) 25 | var v1: Double = 50.0 26 | var v2: Double = 51.0 27 | var v3: Double = 52.0 28 | var v4: Double = 53.0 29 | var v5: Double = 54.0 30 | var v6: Double = 55.0 31 | var v7: Double = 56.0 32 | var v8: Double = 57.0 33 | var v9: Double = 58.0 34 | var v10: Double = 59.0 35 | var v11: Double = 60.0 36 | var v12: Double = 61.0 37 | var v13: Double = 62.0 38 | var v14: Double = 63.0 39 | var v15: Double = 64.0 40 | var v16: Double = 65.0 41 | var v17: Double = 66.0 42 | var v18: Double = 67.0 43 | var v19: Double = 68.0 44 | var v20: Double = 69.0 45 | var v21: Double = 70.0 46 | var v22: Double = 71.0 47 | var v23: Double = 72.0 48 | var v24: Double = 73.0 49 | var v25: Double = 74.0 50 | var v26: Double = 75.0 51 | var v27: Double = 76.0 52 | var v28: Double = 77.0 53 | var v29: Double = 78.0 54 | var v30: Double = 79.0 55 | var v31: Double = 80.0 56 | var v32: Double = 81.0 57 | var v33: Double = 82.0 58 | var v34: Double = 83.0 59 | var v35: Double = 84.0 60 | var v36: Double = 85.0 61 | var v37: Double = 86.0 62 | var v38: Double = 87.0 63 | var v39: Double = 88.0 64 | var v40: Double = 89.0 65 | var v41: Double = 90.0 66 | var v42: Double = 91.0 67 | var v43: Double = 92.0 68 | var v44: Double = 93.0 69 | var v45: Double = 94.0 70 | var v46: Double = 95.0 71 | var v47: Double = 96.0 72 | var v48: Double = 97.0 73 | var v49: Double = 98.0 74 | var v50: Double = 99.0 75 | 76 | // Other properties 77 | var id: UUID = UUID() 78 | var name: String = "HeavyData" 79 | 80 | // This method accesses fields at runtime to prevent compiler optimizations 81 | func computeSum() -> Double { 82 | var sum = value1 + value2 + value3 83 | 84 | // Add tuple values 85 | sum += tuple1.0 + tuple1.1 + tuple1.2 + tuple1.3 + tuple1.4 86 | sum += tuple2.0 + tuple2.1 + tuple2.2 + tuple2.3 + tuple2.4 87 | sum += tuple3.0 + tuple3.1 + tuple3.2 + tuple3.3 + tuple3.4 88 | sum += tuple4.0 + tuple4.1 + tuple4.2 + tuple4.3 + tuple4.4 89 | sum += tuple5.0 + tuple5.1 + tuple5.2 + tuple5.3 + tuple5.4 90 | sum += tuple6.0 + tuple6.1 + tuple6.2 + tuple6.3 + tuple6.4 91 | sum += tuple7.0 + tuple7.1 + tuple7.2 + tuple7.3 + tuple7.4 92 | sum += tuple8.0 + tuple8.1 + tuple8.2 + tuple8.3 + tuple8.4 93 | sum += tuple9.0 + tuple9.1 + tuple9.2 + tuple9.3 + tuple9.4 94 | sum += tuple10.0 + tuple10.1 + tuple10.2 + tuple10.3 + tuple10.4 95 | 96 | // Add individual field values 97 | sum += v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 98 | sum += v11 + v12 + v13 + v14 + v15 + v16 + v17 + v18 + v19 + v20 99 | sum += v21 + v22 + v23 + v24 + v25 + v26 + v27 + v28 + v29 + v30 100 | sum += v31 + v32 + v33 + v34 + v35 + v36 + v37 + v38 + v39 + v40 101 | sum += v41 + v42 + v43 + v44 + v45 + v46 + v47 + v48 + v49 + v50 102 | 103 | return sum 104 | } 105 | } 106 | 107 | // Structure with multiple heavy value-type fields 108 | struct ValueTypeStruct { 109 | var x: Int = 1 110 | var y: Int = 2 111 | var z: Int = 3 112 | 113 | // Multiple value-type fields to increase copy cost 114 | var vals: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 115 | var vals2: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = ( 116 | 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 117 | ) 118 | var vals3: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = ( 119 | 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 120 | ) 121 | 122 | // Additional value-type fields 123 | var i1: Int = 30 124 | var i2: Int = 31 125 | var i3: Int = 32 126 | var i4: Int = 33 127 | var i5: Int = 34 128 | var i6: Int = 35 129 | var i7: Int = 36 130 | var i8: Int = 37 131 | var i9: Int = 38 132 | var i10: Int = 39 133 | var i11: Int = 40 134 | var i12: Int = 41 135 | var i13: Int = 42 136 | var i14: Int = 43 137 | var i15: Int = 44 138 | var i16: Int = 45 139 | var i17: Int = 46 140 | var i18: Int = 47 141 | var i19: Int = 48 142 | var i20: Int = 49 143 | } 144 | 145 | // Structure with high copy cost 146 | struct HeavyStruct { 147 | // Property with large value-type data 148 | var mainData: HeavyData = HeavyData() 149 | 150 | // Nested value-type objects 151 | var valueStruct1: ValueTypeStruct = ValueTypeStruct() 152 | var valueStruct2: ValueTypeStruct = ValueTypeStruct() 153 | var valueStruct3: ValueTypeStruct = ValueTypeStruct() 154 | var valueStruct4: ValueTypeStruct = ValueTypeStruct() 155 | var valueStruct5: ValueTypeStruct = ValueTypeStruct() 156 | 157 | // Nested structures 158 | var nestedData1: NestedHeavyData = NestedHeavyData() 159 | var nestedData2: NestedHeavyData = NestedHeavyData() 160 | var nestedData3: NestedHeavyData = NestedHeavyData() 161 | 162 | // Nested structure with large data 163 | struct NestedHeavyData { 164 | var nestedValueStruct1: ValueTypeStruct = ValueTypeStruct() 165 | var nestedValueStruct2: ValueTypeStruct = ValueTypeStruct() 166 | var nestedValueStruct3: ValueTypeStruct = ValueTypeStruct() 167 | 168 | // Value-type tuples 169 | var tuple1: (Int, Int, Int, Int) = (1, 2, 3, 4) 170 | var tuple2: (Int, Int, Int, Int) = (5, 6, 7, 8) 171 | var tuple3: (Int, Int, Int, Int) = (9, 10, 11, 12) 172 | var tuple4: (Int, Int, Int, Int) = (13, 14, 15, 16) 173 | var tuple5: (Int, Int, Int, Int) = (17, 18, 19, 20) 174 | 175 | // Individual value-type fields 176 | var a1: Int = 21 177 | var a2: Int = 22 178 | var a3: Int = 23 179 | var a4: Int = 24 180 | var a5: Int = 25 181 | var a6: Int = 26 182 | var a7: Int = 27 183 | var a8: Int = 28 184 | var a9: Int = 29 185 | var a10: Int = 30 186 | var a11: Int = 31 187 | var a12: Int = 32 188 | var a13: Int = 33 189 | var a14: Int = 34 190 | var a15: Int = 35 191 | var a16: Int = 36 192 | var a17: Int = 37 193 | var a18: Int = 38 194 | var a19: Int = 39 195 | var a20: Int = 40 196 | 197 | // This method accesses values to prevent compiler optimizations 198 | func accessValues() -> Int { 199 | var sum = 0 200 | 201 | // Add tuple values 202 | sum += tuple1.0 + tuple1.1 + tuple1.2 + tuple1.3 203 | sum += tuple2.0 + tuple2.1 + tuple2.2 + tuple2.3 204 | sum += tuple3.0 + tuple3.1 + tuple3.2 + tuple3.3 205 | sum += tuple4.0 + tuple4.1 + tuple4.2 + tuple4.3 206 | sum += tuple5.0 + tuple5.1 + tuple5.2 + tuple5.3 207 | 208 | // Add individual field values 209 | sum += a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 210 | sum += a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20 211 | 212 | sum += nestedValueStruct1.x + nestedValueStruct2.y + nestedValueStruct3.z 213 | return sum 214 | } 215 | } 216 | } 217 | 218 | // Version with @Tracking macro 219 | @Tracking 220 | struct TrackedHeavyStruct { 221 | // Property with large value-type data 222 | var mainData: HeavyData = HeavyData() 223 | 224 | // Nested value-type objects 225 | var valueStruct1: ValueTypeStruct = ValueTypeStruct() 226 | var valueStruct2: ValueTypeStruct = ValueTypeStruct() 227 | var valueStruct3: ValueTypeStruct = ValueTypeStruct() 228 | var valueStruct4: ValueTypeStruct = ValueTypeStruct() 229 | var valueStruct5: ValueTypeStruct = ValueTypeStruct() 230 | 231 | // Nested structures 232 | var nestedData1: NestedHeavyData = NestedHeavyData() 233 | var nestedData2: NestedHeavyData = NestedHeavyData() 234 | var nestedData3: NestedHeavyData = NestedHeavyData() 235 | 236 | // Nested structure with large data 237 | @Tracking 238 | struct NestedHeavyData { 239 | var nestedValueStruct1: ValueTypeStruct = ValueTypeStruct() 240 | var nestedValueStruct2: ValueTypeStruct = ValueTypeStruct() 241 | var nestedValueStruct3: ValueTypeStruct = ValueTypeStruct() 242 | 243 | // Value-type tuples 244 | var tuple1: (Int, Int, Int, Int) = (1, 2, 3, 4) 245 | var tuple2: (Int, Int, Int, Int) = (5, 6, 7, 8) 246 | var tuple3: (Int, Int, Int, Int) = (9, 10, 11, 12) 247 | var tuple4: (Int, Int, Int, Int) = (13, 14, 15, 16) 248 | var tuple5: (Int, Int, Int, Int) = (17, 18, 19, 20) 249 | 250 | // Individual value-type fields 251 | var a1: Int = 21 252 | var a2: Int = 22 253 | var a3: Int = 23 254 | var a4: Int = 24 255 | var a5: Int = 25 256 | var a6: Int = 26 257 | var a7: Int = 27 258 | var a8: Int = 28 259 | var a9: Int = 29 260 | var a10: Int = 30 261 | var a11: Int = 31 262 | var a12: Int = 32 263 | var a13: Int = 33 264 | var a14: Int = 34 265 | var a15: Int = 35 266 | var a16: Int = 36 267 | var a17: Int = 37 268 | var a18: Int = 38 269 | var a19: Int = 39 270 | var a20: Int = 40 271 | 272 | // This method accesses values to prevent compiler optimizations 273 | func accessValues() -> Int { 274 | var sum = 0 275 | 276 | // Add tuple values 277 | sum += tuple1.0 + tuple1.1 + tuple1.2 + tuple1.3 278 | sum += tuple2.0 + tuple2.1 + tuple2.2 + tuple2.3 279 | sum += tuple3.0 + tuple3.1 + tuple3.2 + tuple3.3 280 | sum += tuple4.0 + tuple4.1 + tuple4.2 + tuple4.3 281 | sum += tuple5.0 + tuple5.1 + tuple5.2 + tuple5.3 282 | 283 | // Add individual field values 284 | sum += a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 285 | sum += a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20 286 | 287 | sum += nestedValueStruct1.x + nestedValueStruct2.y + nestedValueStruct3.z 288 | return sum 289 | } 290 | } 291 | } 292 | 293 | class HeavyStructCopyTests: XCTestCase { 294 | 295 | // Setup before test execution 296 | override func setUp() { 297 | super.setUp() 298 | // Add initialization code if needed 299 | } 300 | 301 | // Cleanup after test execution 302 | override func tearDown() { 303 | // Add cleanup code if needed 304 | super.tearDown() 305 | } 306 | 307 | // Measure copy performance of regular struct 308 | func testHeavyStructCopy() { 309 | let original = HeavyStruct() 310 | 311 | measure { 312 | // Copy the struct 313 | var copy = original 314 | // Make changes to prevent optimizations 315 | copy.mainData.name = "Modified" 316 | 317 | // Verify changes are correctly reflected 318 | XCTAssertEqual(copy.mainData.name, "Modified") 319 | XCTAssertEqual(original.mainData.name, "HeavyData") 320 | } 321 | } 322 | 323 | // Measure copy performance of @Tracking struct 324 | func testTrackedHeavyStructCopy() { 325 | let original = TrackedHeavyStruct() 326 | 327 | measure { 328 | // Copy the struct 329 | var copy = original 330 | // Make changes to prevent optimizations 331 | copy.mainData.name = "Modified" 332 | 333 | // Verify changes are correctly reflected 334 | XCTAssertEqual(copy.mainData.name, "Modified") 335 | XCTAssertEqual(original.mainData.name, "HeavyData") 336 | } 337 | } 338 | 339 | // Measure performance of multiple copies with regular struct 340 | func testMultipleHeavyStructCopies() { 341 | let original = HeavyStruct() 342 | let iterations = 10 343 | 344 | measure { 345 | for _ in 0.. original.valueStruct1.x) 350 | } 351 | } 352 | } 353 | 354 | // Measure performance of multiple copies with @Tracking struct 355 | func testMultipleTrackedHeavyStructCopies() { 356 | let original = TrackedHeavyStruct() 357 | let iterations = 10 358 | 359 | measure { 360 | for _ in 0.. original.valueStruct1.x) 365 | } 366 | } 367 | } 368 | 369 | // Measure performance of deep nested property modification in regular struct 370 | func testDeepNestedModificationRegular() { 371 | let original = HeavyStruct() 372 | 373 | measure { 374 | var copy = original 375 | // Modify deeply nested property 376 | copy.nestedData1.nestedValueStruct3.vals3.9 = 999 377 | 378 | XCTAssertEqual(copy.nestedData1.nestedValueStruct3.vals3.9, 999) 379 | XCTAssertEqual(original.nestedData1.nestedValueStruct3.vals3.9, 29) 380 | } 381 | } 382 | 383 | // Measure performance of deep nested property modification in @Tracking struct 384 | func testDeepNestedModificationTracked() { 385 | let original = TrackedHeavyStruct() 386 | 387 | measure { 388 | var copy = original 389 | // Modify deeply nested property 390 | copy.nestedData1.nestedValueStruct3.vals3.9 = 999 391 | 392 | XCTAssertEqual(copy.nestedData1.nestedValueStruct3.vals3.9, 999) 393 | XCTAssertEqual(original.nestedData1.nestedValueStruct3.vals3.9, 29) 394 | } 395 | } 396 | 397 | // Measure performance of multiple property modifications in regular struct 398 | func testMultipleModificationsRegular() { 399 | let original = HeavyStruct() 400 | 401 | measure { 402 | var copy = original 403 | 404 | // Modify multiple properties 405 | copy.valueStruct1.x = 100 406 | copy.valueStruct2.y = 200 407 | copy.valueStruct3.z = 300 408 | copy.nestedData1.nestedValueStruct1.x = 400 409 | copy.nestedData2.nestedValueStruct2.y = 500 410 | 411 | // Verify changes are correctly reflected 412 | XCTAssertEqual(copy.valueStruct1.x, 100) 413 | XCTAssertEqual(copy.nestedData1.nestedValueStruct1.x, 400) 414 | 415 | XCTAssertEqual(original.valueStruct1.x, 1) 416 | XCTAssertEqual(original.nestedData1.nestedValueStruct1.x, 1) 417 | } 418 | } 419 | 420 | // Measure performance of multiple property modifications in @Tracking struct 421 | func testMultipleModificationsTracked() { 422 | let original = TrackedHeavyStruct() 423 | 424 | measure { 425 | var copy = original 426 | 427 | // Modify multiple properties 428 | copy.valueStruct1.x = 100 429 | copy.valueStruct2.y = 200 430 | copy.valueStruct3.z = 300 431 | copy.nestedData1.nestedValueStruct1.x = 400 432 | copy.nestedData2.nestedValueStruct2.y = 500 433 | 434 | // Verify changes are correctly reflected 435 | XCTAssertEqual(copy.valueStruct1.x, 100) 436 | XCTAssertEqual(copy.nestedData1.nestedValueStruct1.x, 400) 437 | 438 | XCTAssertEqual(original.valueStruct1.x, 1) 439 | XCTAssertEqual(original.nestedData1.nestedValueStruct1.x, 1) 440 | } 441 | } 442 | 443 | // Measure property access performance after copy for regular struct 444 | func testPropertyAccessAfterCopyRegular() { 445 | let original = HeavyStruct() 446 | var copy = original 447 | 448 | // Initial modification 449 | copy.valueStruct1.x = 100 450 | 451 | measure { 452 | // Access multiple properties 453 | let val1 = copy.valueStruct1.x 454 | let val2 = copy.valueStruct2.y 455 | let val3 = copy.nestedData1.nestedValueStruct3.z 456 | let val4 = copy.nestedData2.a10 457 | 458 | // Assertions to prevent optimization 459 | XCTAssertEqual(val1, 100) 460 | XCTAssertEqual(val2, 2) 461 | XCTAssertEqual(val3, 3) 462 | XCTAssertEqual(val4, 30) 463 | } 464 | } 465 | 466 | // Measure property access performance after copy for @Tracking struct 467 | func testPropertyAccessAfterCopyTracked() { 468 | let original = TrackedHeavyStruct() 469 | var copy = original 470 | 471 | // Initial modification 472 | copy.valueStruct1.x = 100 473 | 474 | measure { 475 | // Access multiple properties 476 | let val1 = copy.valueStruct1.x 477 | let val2 = copy.valueStruct2.y 478 | let val3 = copy.nestedData1.nestedValueStruct3.z 479 | let val4 = copy.nestedData2.a10 480 | 481 | // Assertions to prevent optimization 482 | XCTAssertEqual(val1, 100) 483 | XCTAssertEqual(val2, 2) 484 | XCTAssertEqual(val3, 3) 485 | XCTAssertEqual(val4, 30) 486 | } 487 | } 488 | 489 | // 複数回のコピー生成のパフォーマンスを測定(通常の構造体) 490 | func testMultipleCopiesRegular() { 491 | let original = HeavyStruct() 492 | 493 | measure { 494 | for _ in 0..<100000 { 495 | var copy1 = original 496 | var copy2 = copy1 497 | var copy3 = copy2 498 | var copy4 = copy3 499 | var copy5 = copy4 500 | } 501 | 502 | } 503 | } 504 | 505 | // 複数回のコピー生成のパフォーマンスを測定(@Tracking構造体) 506 | func testMultipleCopiesTracked() { 507 | let original = TrackedHeavyStruct() 508 | 509 | measure { 510 | for _ in 0..<100000 { 511 | var copy1 = original 512 | var copy2 = copy1 513 | var copy3 = copy2 514 | var copy4 = copy3 515 | var copy5 = copy4 516 | } 517 | } 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /Tests/StateStructTests/MyState.swift: -------------------------------------------------------------------------------- 1 | import StateStruct 2 | 3 | @Tracking 4 | struct Nesting { 5 | 6 | var value: String = "" 7 | 8 | var _1: Nesting? = nil 9 | 10 | var _2: Nesting? = nil 11 | 12 | var _3: Nesting? = nil 13 | 14 | init(_1: Nesting?, _2: Nesting?, _3: Nesting?) { 15 | self._1 = _1 16 | self._2 = _2 17 | self._3 = _3 18 | } 19 | 20 | init() { 21 | self.value = "root" 22 | self._1 = Nesting.init( 23 | _1: .init( 24 | _1: .init( 25 | _1: .init( 26 | _1: .init(_1: nil, _2: nil, _3: nil), 27 | _2: .init(_1: nil, _2: nil, _3: nil), 28 | _3: .init(_1: nil, _2: nil, _3: nil) 29 | ), 30 | _2: .init( 31 | _1: .init(_1: nil, _2: nil, _3: nil), 32 | _2: .init(_1: nil, _2: nil, _3: nil), 33 | _3: .init(_1: nil, _2: nil, _3: nil) 34 | ), 35 | _3: .init( 36 | _1: .init(_1: nil, _2: nil, _3: nil), 37 | _2: .init(_1: nil, _2: nil, _3: nil), 38 | _3: .init(_1: nil, _2: nil, _3: nil) 39 | ) 40 | ), 41 | _2: .init( 42 | _1: .init( 43 | _1: .init(_1: nil, _2: nil, _3: nil), 44 | _2: .init(_1: nil, _2: nil, _3: nil), 45 | _3: .init(_1: nil, _2: nil, _3: nil) 46 | ), 47 | _2: .init( 48 | _1: .init(_1: nil, _2: nil, _3: nil), 49 | _2: .init(_1: nil, _2: nil, _3: nil), 50 | _3: .init(_1: nil, _2: nil, _3: nil) 51 | ), 52 | _3: .init( 53 | _1: .init(_1: nil, _2: nil, _3: nil), 54 | _2: .init(_1: nil, _2: nil, _3: nil), 55 | _3: .init(_1: nil, _2: nil, _3: nil) 56 | ) 57 | ), 58 | _3: .init( 59 | _1: .init( 60 | _1: .init(_1: nil, _2: nil, _3: nil), 61 | _2: .init(_1: nil, _2: nil, _3: nil), 62 | _3: .init(_1: nil, _2: nil, _3: nil) 63 | ), 64 | _2: .init( 65 | _1: .init(_1: nil, _2: nil, _3: nil), 66 | _2: .init(_1: nil, _2: nil, _3: nil), 67 | _3: .init(_1: nil, _2: nil, _3: nil) 68 | ), 69 | _3: .init( 70 | _1: .init(_1: nil, _2: nil, _3: nil), 71 | _2: .init(_1: nil, _2: nil, _3: nil), 72 | _3: .init(_1: nil, _2: nil, _3: nil) 73 | ) 74 | ) 75 | ), 76 | _2: .init( 77 | _1: .init( 78 | _1: .init( 79 | _1: .init(_1: nil, _2: nil, _3: nil), 80 | _2: .init(_1: nil, _2: nil, _3: nil), 81 | _3: .init(_1: nil, _2: nil, _3: nil) 82 | ), 83 | _2: .init( 84 | _1: .init(_1: nil, _2: nil, _3: nil), 85 | _2: .init(_1: nil, _2: nil, _3: nil), 86 | _3: .init(_1: nil, _2: nil, _3: nil) 87 | ), 88 | _3: .init( 89 | _1: .init(_1: nil, _2: nil, _3: nil), 90 | _2: .init(_1: nil, _2: nil, _3: nil), 91 | _3: .init(_1: nil, _2: nil, _3: nil) 92 | ) 93 | ), 94 | _2: .init( 95 | _1: .init( 96 | _1: .init(_1: nil, _2: nil, _3: nil), 97 | _2: .init(_1: nil, _2: nil, _3: nil), 98 | _3: .init(_1: nil, _2: nil, _3: nil) 99 | ), 100 | _2: .init( 101 | _1: .init(_1: nil, _2: nil, _3: nil), 102 | _2: .init(_1: nil, _2: nil, _3: nil), 103 | _3: .init(_1: nil, _2: nil, _3: nil) 104 | ), 105 | _3: .init( 106 | _1: .init(_1: nil, _2: nil, _3: nil), 107 | _2: .init(_1: nil, _2: nil, _3: nil), 108 | _3: .init(_1: nil, _2: nil, _3: nil) 109 | ) 110 | ), 111 | _3: .init( 112 | _1: .init( 113 | _1: .init(_1: nil, _2: nil, _3: nil), 114 | _2: .init(_1: nil, _2: nil, _3: nil), 115 | _3: .init(_1: nil, _2: nil, _3: nil) 116 | ), 117 | _2: .init( 118 | _1: .init(_1: nil, _2: nil, _3: nil), 119 | _2: .init(_1: nil, _2: nil, _3: nil), 120 | _3: .init(_1: nil, _2: nil, _3: nil) 121 | ), 122 | _3: .init( 123 | _1: .init(_1: nil, _2: nil, _3: nil), 124 | _2: .init(_1: nil, _2: nil, _3: nil), 125 | _3: .init(_1: nil, _2: nil, _3: nil) 126 | ) 127 | ) 128 | ), 129 | _3: .init( 130 | _1: .init( 131 | _1: .init( 132 | _1: .init(_1: nil, _2: nil, _3: nil), 133 | _2: .init(_1: nil, _2: nil, _3: nil), 134 | _3: .init(_1: nil, _2: nil, _3: nil) 135 | ), 136 | _2: .init( 137 | _1: .init(_1: nil, _2: nil, _3: nil), 138 | _2: .init(_1: nil, _2: nil, _3: nil), 139 | _3: .init(_1: nil, _2: nil, _3: nil) 140 | ), 141 | _3: .init( 142 | _1: .init(_1: nil, _2: nil, _3: nil), 143 | _2: .init(_1: nil, _2: nil, _3: nil), 144 | _3: .init(_1: nil, _2: nil, _3: nil) 145 | ) 146 | ), 147 | _2: .init( 148 | _1: .init( 149 | _1: .init(_1: nil, _2: nil, _3: nil), 150 | _2: .init(_1: nil, _2: nil, _3: nil), 151 | _3: .init(_1: nil, _2: nil, _3: nil) 152 | ), 153 | _2: .init( 154 | _1: .init(_1: nil, _2: nil, _3: nil), 155 | _2: .init(_1: nil, _2: nil, _3: nil), 156 | _3: .init(_1: nil, _2: nil, _3: nil) 157 | ), 158 | _3: .init( 159 | _1: .init(_1: nil, _2: nil, _3: nil), 160 | _2: .init(_1: nil, _2: nil, _3: nil), 161 | _3: .init(_1: nil, _2: nil, _3: nil) 162 | ) 163 | ), 164 | _3: .init( 165 | _1: .init( 166 | _1: .init(_1: nil, _2: nil, _3: nil), 167 | _2: .init(_1: nil, _2: nil, _3: nil), 168 | _3: .init(_1: nil, _2: nil, _3: nil) 169 | ), 170 | _2: .init( 171 | _1: .init(_1: nil, _2: nil, _3: nil), 172 | _2: .init(_1: nil, _2: nil, _3: nil), 173 | _3: .init(_1: nil, _2: nil, _3: nil) 174 | ), 175 | _3: .init( 176 | _1: .init(_1: nil, _2: nil, _3: nil), 177 | _2: .init(_1: nil, _2: nil, _3: nil), 178 | _3: .init(_1: nil, _2: nil, _3: nil) 179 | ) 180 | ) 181 | ) 182 | ) 183 | 184 | self._2 = Nesting.init( 185 | _1: .init( 186 | _1: .init( 187 | _1: .init( 188 | _1: .init(_1: nil, _2: nil, _3: nil), 189 | _2: .init(_1: nil, _2: nil, _3: nil), 190 | _3: .init(_1: nil, _2: nil, _3: nil) 191 | ), 192 | _2: .init( 193 | _1: .init(_1: nil, _2: nil, _3: nil), 194 | _2: .init(_1: nil, _2: nil, _3: nil), 195 | _3: .init(_1: nil, _2: nil, _3: nil) 196 | ), 197 | _3: .init( 198 | _1: .init(_1: nil, _2: nil, _3: nil), 199 | _2: .init(_1: nil, _2: nil, _3: nil), 200 | _3: .init(_1: nil, _2: nil, _3: nil) 201 | ) 202 | ), 203 | _2: .init( 204 | _1: .init( 205 | _1: .init(_1: nil, _2: nil, _3: nil), 206 | _2: .init(_1: nil, _2: nil, _3: nil), 207 | _3: .init(_1: nil, _2: nil, _3: nil) 208 | ), 209 | _2: .init( 210 | _1: .init(_1: nil, _2: nil, _3: nil), 211 | _2: .init(_1: nil, _2: nil, _3: nil), 212 | _3: .init(_1: nil, _2: nil, _3: nil) 213 | ), 214 | _3: .init( 215 | _1: .init(_1: nil, _2: nil, _3: nil), 216 | _2: .init(_1: nil, _2: nil, _3: nil), 217 | _3: .init(_1: nil, _2: nil, _3: nil) 218 | ) 219 | ), 220 | _3: .init( 221 | _1: .init( 222 | _1: .init(_1: nil, _2: nil, _3: nil), 223 | _2: .init(_1: nil, _2: nil, _3: nil), 224 | _3: .init(_1: nil, _2: nil, _3: nil) 225 | ), 226 | _2: .init( 227 | _1: .init(_1: nil, _2: nil, _3: nil), 228 | _2: .init(_1: nil, _2: nil, _3: nil), 229 | _3: .init(_1: nil, _2: nil, _3: nil) 230 | ), 231 | _3: .init( 232 | _1: .init(_1: nil, _2: nil, _3: nil), 233 | _2: .init(_1: nil, _2: nil, _3: nil), 234 | _3: .init(_1: nil, _2: nil, _3: nil) 235 | ) 236 | ) 237 | ), 238 | _2: .init( 239 | _1: .init( 240 | _1: .init( 241 | _1: .init(_1: nil, _2: nil, _3: nil), 242 | _2: .init(_1: nil, _2: nil, _3: nil), 243 | _3: .init(_1: nil, _2: nil, _3: nil) 244 | ), 245 | _2: .init( 246 | _1: .init(_1: nil, _2: nil, _3: nil), 247 | _2: .init(_1: nil, _2: nil, _3: nil), 248 | _3: .init(_1: nil, _2: nil, _3: nil) 249 | ), 250 | _3: .init( 251 | _1: .init(_1: nil, _2: nil, _3: nil), 252 | _2: .init(_1: nil, _2: nil, _3: nil), 253 | _3: .init(_1: nil, _2: nil, _3: nil) 254 | ) 255 | ), 256 | _2: .init( 257 | _1: .init( 258 | _1: .init(_1: nil, _2: nil, _3: nil), 259 | _2: .init(_1: nil, _2: nil, _3: nil), 260 | _3: .init(_1: nil, _2: nil, _3: nil) 261 | ), 262 | _2: .init( 263 | _1: .init(_1: nil, _2: nil, _3: nil), 264 | _2: .init(_1: nil, _2: nil, _3: nil), 265 | _3: .init(_1: nil, _2: nil, _3: nil) 266 | ), 267 | _3: .init( 268 | _1: .init(_1: nil, _2: nil, _3: nil), 269 | _2: .init(_1: nil, _2: nil, _3: nil), 270 | _3: .init(_1: nil, _2: nil, _3: nil) 271 | ) 272 | ), 273 | _3: .init( 274 | _1: .init( 275 | _1: .init(_1: nil, _2: nil, _3: nil), 276 | _2: .init(_1: nil, _2: nil, _3: nil), 277 | _3: .init(_1: nil, _2: nil, _3: nil) 278 | ), 279 | _2: .init( 280 | _1: .init(_1: nil, _2: nil, _3: nil), 281 | _2: .init(_1: nil, _2: nil, _3: nil), 282 | _3: .init(_1: nil, _2: nil, _3: nil) 283 | ), 284 | _3: .init( 285 | _1: .init(_1: nil, _2: nil, _3: nil), 286 | _2: .init(_1: nil, _2: nil, _3: nil), 287 | _3: .init(_1: nil, _2: nil, _3: nil) 288 | ) 289 | ) 290 | ), 291 | _3: .init( 292 | _1: .init( 293 | _1: .init( 294 | _1: .init(_1: nil, _2: nil, _3: nil), 295 | _2: .init(_1: nil, _2: nil, _3: nil), 296 | _3: .init(_1: nil, _2: nil, _3: nil) 297 | ), 298 | _2: .init( 299 | _1: .init(_1: nil, _2: nil, _3: nil), 300 | _2: .init(_1: nil, _2: nil, _3: nil), 301 | _3: .init(_1: nil, _2: nil, _3: nil) 302 | ), 303 | _3: .init( 304 | _1: .init(_1: nil, _2: nil, _3: nil), 305 | _2: .init(_1: nil, _2: nil, _3: nil), 306 | _3: .init(_1: nil, _2: nil, _3: nil) 307 | ) 308 | ), 309 | _2: .init( 310 | _1: .init( 311 | _1: .init(_1: nil, _2: nil, _3: nil), 312 | _2: .init(_1: nil, _2: nil, _3: nil), 313 | _3: .init(_1: nil, _2: nil, _3: nil) 314 | ), 315 | _2: .init( 316 | _1: .init(_1: nil, _2: nil, _3: nil), 317 | _2: .init(_1: nil, _2: nil, _3: nil), 318 | _3: .init(_1: nil, _2: nil, _3: nil) 319 | ), 320 | _3: .init( 321 | _1: .init(_1: nil, _2: nil, _3: nil), 322 | _2: .init(_1: nil, _2: nil, _3: nil), 323 | _3: .init(_1: nil, _2: nil, _3: nil) 324 | ) 325 | ), 326 | _3: .init( 327 | _1: .init( 328 | _1: .init(_1: nil, _2: nil, _3: nil), 329 | _2: .init(_1: nil, _2: nil, _3: nil), 330 | _3: .init(_1: nil, _2: nil, _3: nil) 331 | ), 332 | _2: .init( 333 | _1: .init(_1: nil, _2: nil, _3: nil), 334 | _2: .init(_1: nil, _2: nil, _3: nil), 335 | _3: .init(_1: nil, _2: nil, _3: nil) 336 | ), 337 | _3: .init( 338 | _1: .init(_1: nil, _2: nil, _3: nil), 339 | _2: .init(_1: nil, _2: nil, _3: nil), 340 | _3: .init(_1: nil, _2: nil, _3: nil) 341 | ) 342 | ) 343 | ) 344 | ) 345 | 346 | self._3 = Nesting.init( 347 | _1: .init( 348 | _1: .init( 349 | _1: .init( 350 | _1: .init(_1: nil, _2: nil, _3: nil), 351 | _2: .init(_1: nil, _2: nil, _3: nil), 352 | _3: .init(_1: nil, _2: nil, _3: nil) 353 | ), 354 | _2: .init( 355 | _1: .init(_1: nil, _2: nil, _3: nil), 356 | _2: .init(_1: nil, _2: nil, _3: nil), 357 | _3: .init(_1: nil, _2: nil, _3: nil) 358 | ), 359 | _3: .init( 360 | _1: .init(_1: nil, _2: nil, _3: nil), 361 | _2: .init(_1: nil, _2: nil, _3: nil), 362 | _3: .init(_1: nil, _2: nil, _3: nil) 363 | ) 364 | ), 365 | _2: .init( 366 | _1: .init( 367 | _1: .init(_1: nil, _2: nil, _3: nil), 368 | _2: .init(_1: nil, _2: nil, _3: nil), 369 | _3: .init(_1: nil, _2: nil, _3: nil) 370 | ), 371 | _2: .init( 372 | _1: .init(_1: nil, _2: nil, _3: nil), 373 | _2: .init(_1: nil, _2: nil, _3: nil), 374 | _3: .init(_1: nil, _2: nil, _3: nil) 375 | ), 376 | _3: .init( 377 | _1: .init(_1: nil, _2: nil, _3: nil), 378 | _2: .init(_1: nil, _2: nil, _3: nil), 379 | _3: .init(_1: nil, _2: nil, _3: nil) 380 | ) 381 | ), 382 | _3: .init( 383 | _1: .init( 384 | _1: .init(_1: nil, _2: nil, _3: nil), 385 | _2: .init(_1: nil, _2: nil, _3: nil), 386 | _3: .init(_1: nil, _2: nil, _3: nil) 387 | ), 388 | _2: .init( 389 | _1: .init(_1: nil, _2: nil, _3: nil), 390 | _2: .init(_1: nil, _2: nil, _3: nil), 391 | _3: .init(_1: nil, _2: nil, _3: nil) 392 | ), 393 | _3: .init( 394 | _1: .init(_1: nil, _2: nil, _3: nil), 395 | _2: .init(_1: nil, _2: nil, _3: nil), 396 | _3: .init(_1: nil, _2: nil, _3: nil) 397 | ) 398 | ) 399 | ), 400 | _2: .init( 401 | _1: .init( 402 | _1: .init( 403 | _1: .init(_1: nil, _2: nil, _3: nil), 404 | _2: .init(_1: nil, _2: nil, _3: nil), 405 | _3: .init(_1: nil, _2: nil, _3: nil) 406 | ), 407 | _2: .init( 408 | _1: .init(_1: nil, _2: nil, _3: nil), 409 | _2: .init(_1: nil, _2: nil, _3: nil), 410 | _3: .init(_1: nil, _2: nil, _3: nil) 411 | ), 412 | _3: .init( 413 | _1: .init(_1: nil, _2: nil, _3: nil), 414 | _2: .init(_1: nil, _2: nil, _3: nil), 415 | _3: .init(_1: nil, _2: nil, _3: nil) 416 | ) 417 | ), 418 | _2: .init( 419 | _1: .init( 420 | _1: .init(_1: nil, _2: nil, _3: nil), 421 | _2: .init(_1: nil, _2: nil, _3: nil), 422 | _3: .init(_1: nil, _2: nil, _3: nil) 423 | ), 424 | _2: .init( 425 | _1: .init(_1: nil, _2: nil, _3: nil), 426 | _2: .init(_1: nil, _2: nil, _3: nil), 427 | _3: .init(_1: nil, _2: nil, _3: nil) 428 | ), 429 | _3: .init( 430 | _1: .init(_1: nil, _2: nil, _3: nil), 431 | _2: .init(_1: nil, _2: nil, _3: nil), 432 | _3: .init(_1: nil, _2: nil, _3: nil) 433 | ) 434 | ), 435 | _3: .init( 436 | _1: .init( 437 | _1: .init(_1: nil, _2: nil, _3: nil), 438 | _2: .init(_1: nil, _2: nil, _3: nil), 439 | _3: .init(_1: nil, _2: nil, _3: nil) 440 | ), 441 | _2: .init( 442 | _1: .init(_1: nil, _2: nil, _3: nil), 443 | _2: .init(_1: nil, _2: nil, _3: nil), 444 | _3: .init(_1: nil, _2: nil, _3: nil) 445 | ), 446 | _3: .init( 447 | _1: .init(_1: nil, _2: nil, _3: nil), 448 | _2: .init(_1: nil, _2: nil, _3: nil), 449 | _3: .init(_1: nil, _2: nil, _3: nil) 450 | ) 451 | ) 452 | ), 453 | _3: .init( 454 | _1: .init( 455 | _1: .init( 456 | _1: .init(_1: nil, _2: nil, _3: nil), 457 | _2: .init(_1: nil, _2: nil, _3: nil), 458 | _3: .init(_1: nil, _2: nil, _3: nil) 459 | ), 460 | _2: .init( 461 | _1: .init(_1: nil, _2: nil, _3: nil), 462 | _2: .init(_1: nil, _2: nil, _3: nil), 463 | _3: .init(_1: nil, _2: nil, _3: nil) 464 | ), 465 | _3: .init( 466 | _1: .init(_1: nil, _2: nil, _3: nil), 467 | _2: .init(_1: nil, _2: nil, _3: nil), 468 | _3: .init(_1: nil, _2: nil, _3: nil) 469 | ) 470 | ), 471 | _2: .init( 472 | _1: .init( 473 | _1: .init(_1: nil, _2: nil, _3: nil), 474 | _2: .init(_1: nil, _2: nil, _3: nil), 475 | _3: .init(_1: nil, _2: nil, _3: nil) 476 | ), 477 | _2: .init( 478 | _1: .init(_1: nil, _2: nil, _3: nil), 479 | _2: .init(_1: nil, _2: nil, _3: nil), 480 | _3: .init(_1: nil, _2: nil, _3: nil) 481 | ), 482 | _3: .init( 483 | _1: .init(_1: nil, _2: nil, _3: nil), 484 | _2: .init(_1: nil, _2: nil, _3: nil), 485 | _3: .init(_1: nil, _2: nil, _3: nil) 486 | ) 487 | ), 488 | _3: .init( 489 | _1: .init( 490 | _1: .init(_1: nil, _2: nil, _3: nil), 491 | _2: .init(_1: nil, _2: nil, _3: nil), 492 | _3: .init(_1: nil, _2: nil, _3: nil) 493 | ), 494 | _2: .init( 495 | _1: .init(_1: nil, _2: nil, _3: nil), 496 | _2: .init(_1: nil, _2: nil, _3: nil), 497 | _3: .init(_1: nil, _2: nil, _3: nil) 498 | ), 499 | _3: .init( 500 | _1: .init(_1: nil, _2: nil, _3: nil), 501 | _2: .init(_1: nil, _2: nil, _3: nil), 502 | _3: .init(_1: nil, _2: nil, _3: nil) 503 | ) 504 | ) 505 | ) 506 | ) 507 | } 508 | } 509 | 510 | class Ref { 511 | 512 | } 513 | 514 | @Tracking 515 | struct SendableState: Sendable { 516 | 517 | @Tracking 518 | struct Level1 { 519 | 520 | init(name: String) { 521 | self.name = name 522 | } 523 | 524 | var level2: Level2 = .init(name: "") 525 | var name: String = "" 526 | var age: Int = 10 527 | } 528 | 529 | @Tracking 530 | struct Level2 { 531 | 532 | init(name: String) { 533 | self.name = name 534 | } 535 | 536 | var name: String = "" 537 | var age: Int = 10 538 | } 539 | 540 | var level1: Level1 = .init(name: "A") 541 | 542 | init() { 543 | 544 | } 545 | 546 | } 547 | 548 | @Tracking 549 | struct MyState { 550 | 551 | init() { 552 | self.name = "" 553 | } 554 | 555 | func customDescription() -> String { 556 | "\(height),\(age)" 557 | } 558 | 559 | var height: Int = 0 560 | 561 | var age: Int = 18 562 | var name: String 563 | 564 | var edge: Int = 0 565 | 566 | var array: [Int] = [] 567 | 568 | var dictionary: [String: Int] = [:] 569 | 570 | var computedName: String { 571 | "Mr. " + name 572 | } 573 | 574 | weak var weak_ref: Ref? 575 | 576 | var computedAge: Int { 577 | let age = age 578 | return age 579 | } 580 | 581 | var computed_setter: String { 582 | get { 583 | name 584 | } 585 | set { 586 | name = newValue 587 | } 588 | } 589 | 590 | var nested: Nested = .init(name: "") 591 | 592 | var nestedUntracked: NestedUntracked = .init(name: "") 593 | var nestedAttached: NestedAttached = .init(name: "") 594 | 595 | var optional_custom_type: CustomType? 596 | var optional_int: Int? 597 | 598 | @Tracking 599 | struct Nested { 600 | 601 | init(name: String) { 602 | self.name = name 603 | } 604 | 605 | var name: String = "" 606 | var age: Int = 10 607 | } 608 | 609 | struct NestedUntracked { 610 | 611 | init(name: String) { 612 | self.name = name 613 | } 614 | 615 | var name: String = "" 616 | var age: Int = 10 617 | } 618 | 619 | 620 | struct NestedAttached { 621 | var name: String = "" 622 | } 623 | 624 | mutating func updateName() { 625 | self.name = "Hiroshi" 626 | } 627 | 628 | } 629 | 630 | enum CustomType { 631 | case a 632 | } 633 | -------------------------------------------------------------------------------- /Tests/StateStructTests/PropertyWrappers.swift: -------------------------------------------------------------------------------- 1 | 2 | @propertyWrapper 3 | struct JustWrapper { 4 | 5 | var wrappedValue: Value 6 | 7 | } 8 | 9 | @propertyWrapper 10 | struct Clamped { 11 | private var value: Value 12 | let min: Value 13 | let max: Value 14 | 15 | init(wrappedValue: Value, min: Value, max: Value) { 16 | self.min = min 17 | self.max = max 18 | self.value = Swift.min(Swift.max(wrappedValue, min), max) 19 | } 20 | 21 | var wrappedValue: Value { 22 | get { return value } 23 | set { value = Swift.min(Swift.max(newValue, min), max) } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/StateStructTests/TrackingExistential.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import StateStruct 3 | 4 | @Suite 5 | struct TrackingExistential { 6 | 7 | @Tracking 8 | struct UI { 9 | var count: Int 10 | 11 | init(count: Int) { 12 | self.count = count 13 | } 14 | } 15 | 16 | @Tracking 17 | struct Root { 18 | 19 | var _any: Any? 20 | 21 | var ui: UI? { 22 | get { 23 | _any as? UI 24 | } 25 | set { 26 | _any = newValue 27 | } 28 | } 29 | 30 | init(ui: UI) { 31 | self.ui = ui 32 | } 33 | } 34 | 35 | 36 | @Test func tracking() { 37 | 38 | var original = Root(ui: UI(count: 1)) 39 | 40 | original.startNewTracking() 41 | _ = original.ui?.count 42 | let reading = original.trackingResult! 43 | 44 | #expect( 45 | reading.graph.prettyPrint() == """ 46 | StateStructTests.TrackingExistential.Root { 47 | _any-(1) { 48 | count-(1) 49 | } 50 | } 51 | """ 52 | ) 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/StateStructTests/TrackingTests.swift: -------------------------------------------------------------------------------- 1 | import StateStruct 2 | import Testing 3 | 4 | @Suite("TrackingTests") 5 | struct TrackingTests { 6 | 7 | @Test 8 | func read_optional_custom_tyupe() { 9 | 10 | var original = MyState.init() 11 | 12 | original.startNewTracking() 13 | _ = original.optional_custom_type 14 | let result = original.trackingResult! 15 | 16 | original.endTracking() 17 | 18 | #expect( 19 | result.graph.prettyPrint() == """ 20 | StateStructTests.MyState { 21 | optional_custom_type-(1) 22 | } 23 | """ 24 | ) 25 | 26 | } 27 | 28 | @Test 29 | func read_optional_int() { 30 | 31 | var original = MyState.init() 32 | 33 | original.startNewTracking() 34 | _ = original.optional_int 35 | let result = original.trackingResult! 36 | 37 | original.endTracking() 38 | 39 | #expect( 40 | result.graph.prettyPrint() == """ 41 | StateStructTests.MyState { 42 | optional_int-(1) 43 | } 44 | """ 45 | ) 46 | 47 | } 48 | 49 | @Test 50 | func tracking_stored_property() { 51 | 52 | var original = MyState.init() 53 | 54 | original.startNewTracking() 55 | original.height = 100 56 | let result = original.trackingResult! 57 | 58 | original.endTracking() 59 | 60 | _ = original.name 61 | 62 | #expect(original.trackingResult == nil) 63 | 64 | #expect( 65 | result.graph.prettyPrint() == """ 66 | StateStructTests.MyState { 67 | height+(1) 68 | } 69 | """ 70 | ) 71 | 72 | } 73 | 74 | @Test 75 | func tracking_write_nested_stored_property() { 76 | 77 | var original = MyState.init() 78 | 79 | original.startNewTracking() 80 | original.nested.name = "AAA" 81 | let result = original.trackingResult! 82 | 83 | #expect( 84 | result.graph.prettyPrint() == """ 85 | StateStructTests.MyState { 86 | nested+(1) { 87 | name+(1) 88 | } 89 | } 90 | """ 91 | ) 92 | 93 | } 94 | 95 | @Test 96 | func tracking_write_nested_stored_property_escaping() async { 97 | 98 | let base = SendableState.init() 99 | 100 | do { 101 | var a = base.tracked() 102 | a.level1.name = "AAA" 103 | 104 | await Task { 105 | a.level1.level2.age = 100 106 | } 107 | .value 108 | 109 | let level2 = a.level1.level2 110 | 111 | await Task { 112 | _ = level2.name 113 | } 114 | .value 115 | 116 | let result = a.trackingResult! 117 | 118 | #expect( 119 | result.graph.prettyPrint() == 120 | """ 121 | StateStructTests.SendableState { 122 | level1-(1)+(2) { 123 | name+(1) 124 | level2-(1)+(1) { 125 | age+(1) 126 | name-(1) 127 | } 128 | } 129 | } 130 | """ 131 | ) 132 | } 133 | 134 | do { 135 | let a = base.tracked() 136 | _ = a.level1.name 137 | 138 | await Task { 139 | _ = a.level1.level2.age 140 | } 141 | .value 142 | 143 | let result = a.trackingResult! 144 | 145 | #expect( 146 | result.graph.prettyPrint() == 147 | """ 148 | StateStructTests.SendableState { 149 | level1-(2) { 150 | name-(1) 151 | level2-(1) { 152 | age-(1) 153 | } 154 | } 155 | } 156 | """ 157 | ) 158 | } 159 | 160 | } 161 | 162 | @Test 163 | func tracking_read_nested_stored_property() { 164 | 165 | var original = MyState.init() 166 | 167 | original.startNewTracking() 168 | _ = original.nested.name 169 | let result = original.trackingResult! 170 | 171 | #expect( 172 | result.graph.prettyPrint() == """ 173 | StateStructTests.MyState { 174 | nested-(1) { 175 | name-(1) 176 | } 177 | } 178 | """ 179 | ) 180 | 181 | } 182 | 183 | @Test 184 | func read_computed_property() { 185 | 186 | var original = MyState.init() 187 | 188 | original.startNewTracking() 189 | _ = original.computedName 190 | let result = original.trackingResult! 191 | 192 | #expect( 193 | result.graph.prettyPrint() == """ 194 | StateStructTests.MyState { 195 | name-(1) 196 | } 197 | """ 198 | ) 199 | 200 | } 201 | 202 | @Test 203 | func read_over_function() { 204 | var original = MyState.init() 205 | 206 | original.startNewTracking() 207 | _ = original.customDescription() 208 | let result = original.trackingResult! 209 | 210 | #expect( 211 | result.graph.prettyPrint() == """ 212 | StateStructTests.MyState { 213 | height-(1) 214 | age-(1) 215 | } 216 | """ 217 | ) 218 | } 219 | 220 | @Test 221 | func tracking_nest() { 222 | 223 | var original = Nesting.init() 224 | 225 | original.startNewTracking() 226 | _ = original._1?._2?.value 227 | let result = original.trackingResult! 228 | 229 | #expect( 230 | result.graph.prettyPrint() == """ 231 | StateStructTests.Nesting { 232 | _1-(1) { 233 | _2-(1) { 234 | value-(1) 235 | } 236 | } 237 | } 238 | """ 239 | ) 240 | } 241 | 242 | @Test 243 | func tracking_nest_set() { 244 | 245 | var original = Nesting.init() 246 | 247 | original.startNewTracking() 248 | original._1?._1?.value = "AAA" 249 | let result = original.trackingResult! 250 | 251 | #expect( 252 | result.graph.prettyPrint() == """ 253 | StateStructTests.Nesting { 254 | _1+(1) { 255 | _1+(1) { 256 | value+(1) 257 | } 258 | } 259 | } 260 | """ 261 | ) 262 | } 263 | 264 | @Test 265 | func tracking_nest_detaching() { 266 | 267 | var original = Nesting.init() 268 | 269 | original.startNewTracking() 270 | let sub = original._1 271 | _ = sub?._1?.value 272 | let result = original.trackingResult! 273 | 274 | #expect( 275 | result.graph.prettyPrint() == """ 276 | StateStructTests.Nesting { 277 | _1-(1) { 278 | _1-(1) { 279 | value-(1) 280 | } 281 | } 282 | } 283 | """ 284 | ) 285 | } 286 | 287 | @Test 288 | func tracking_nest_write_modify() { 289 | 290 | var original = Nesting.init() 291 | 292 | original.startNewTracking() 293 | original._1 = .init(_1: nil, _2: nil, _3: nil) 294 | let result = original.trackingResult! 295 | 296 | #expect( 297 | result.graph.prettyPrint() == """ 298 | StateStructTests.Nesting { 299 | _1+(1) 300 | } 301 | """ 302 | ) 303 | } 304 | 305 | @Test 306 | func tracking_nest2_write_modify() { 307 | 308 | var original = Nesting.init() 309 | 310 | original.startNewTracking() 311 | original._1?._1 = .init(_1: nil, _2: nil, _3: nil) 312 | let result = original.trackingResult! 313 | 314 | #expect( 315 | result.graph.prettyPrint() == """ 316 | StateStructTests.Nesting { 317 | _1+(1) { 318 | _1+(1) 319 | } 320 | } 321 | """ 322 | ) 323 | } 324 | 325 | @Test 326 | func tracking_1() { 327 | 328 | var original = Nesting.init() 329 | 330 | original.startNewTracking() 331 | _ = original._1?._1 332 | _ = original._1 333 | let result = original.trackingResult! 334 | 335 | #expect( 336 | result.graph.shakedAsRead().prettyPrint() == """ 337 | StateStructTests.Nesting { 338 | _1-(2) 339 | } 340 | """ 341 | ) 342 | } 343 | 344 | @Test 345 | func tracking_nest_detaching_write() { 346 | 347 | var original = Nesting.init() 348 | 349 | original.startNewTracking() 350 | var sub = original._1 351 | sub?._1?.value = "AAA" 352 | var result = original.trackingResult! 353 | 354 | result.graph.shakeAsWrite() 355 | 356 | #expect( 357 | result.graph.prettyPrint() == """ 358 | StateStructTests.Nesting 359 | """ 360 | ) 361 | } 362 | 363 | @Test 364 | func modify_endpoint() { 365 | 366 | var original = MyState.init() 367 | 368 | func update(_ value: inout String) { 369 | value = "AAA" 370 | } 371 | 372 | original.startNewTracking() 373 | update(&original.nested.name) 374 | let result = original.trackingResult! 375 | 376 | #expect( 377 | result.graph.prettyPrint() == """ 378 | StateStructTests.MyState { 379 | nested+(1) { 380 | name+(1) 381 | } 382 | } 383 | """ 384 | ) 385 | } 386 | 387 | @Test 388 | func tracking_concurrent_access() async throws { 389 | 390 | try await withThrowingTaskGroup(of: Void.self) { group in 391 | group.addTask { 392 | var original = MyState.init() 393 | original.startNewTracking() 394 | original.height = 100 395 | let result1 = original.trackingResult! 396 | 397 | #expect( 398 | result1.graph.prettyPrint() == """ 399 | StateStructTests.MyState { 400 | height+(1) 401 | } 402 | """ 403 | ) 404 | } 405 | 406 | group.addTask { 407 | var original = MyState.init() 408 | original.startNewTracking() 409 | original.nested.name = "AAA" 410 | let result2 = original.trackingResult! 411 | 412 | #expect( 413 | result2.graph.prettyPrint() == """ 414 | StateStructTests.MyState { 415 | nested+(1) { 416 | name+(1) 417 | } 418 | } 419 | """ 420 | ) 421 | } 422 | 423 | try await group.waitForAll() 424 | } 425 | } 426 | 427 | @Test 428 | func write_is_empty() { 429 | 430 | var original = MyState.init() 431 | 432 | original.startNewTracking() 433 | _ = original.nested.name 434 | var nested = original.nested 435 | nested.name = "AAA" 436 | var result = original.trackingResult! 437 | 438 | result.graph.shakeAsWrite() 439 | 440 | result.graph.prettyPrint() 441 | 442 | #expect( 443 | result.graph.isEmpty == true 444 | ) 445 | } 446 | 447 | @Test 448 | func modify_count() { 449 | 450 | var original = MyState.init() 451 | 452 | func update(_ value: inout MyState.Nested) { 453 | value.name = "AAA" 454 | value.name = "AAA" 455 | value.age = 100 456 | } 457 | 458 | original.startNewTracking() 459 | update(&original.nested) 460 | let result = original.trackingResult! 461 | 462 | #expect( 463 | result.graph.prettyPrint() == """ 464 | StateStructTests.MyState { 465 | nested+(1) { 466 | name+(2) 467 | age+(1) 468 | } 469 | } 470 | """ 471 | ) 472 | } 473 | 474 | @Test 475 | func modify_count_1() { 476 | 477 | var original = MyState.init() 478 | 479 | func update(_ value: inout MyState.Nested) { 480 | value = .init(name: "AAA") 481 | } 482 | 483 | original.startNewTracking() 484 | update(&original.nested) 485 | let result = original.trackingResult! 486 | 487 | #expect( 488 | result.graph.prettyPrint() == """ 489 | StateStructTests.MyState { 490 | nested+(1) 491 | } 492 | """ 493 | ) 494 | } 495 | 496 | @Test 497 | func modify_count_2() { 498 | 499 | var original = MyState.init() 500 | 501 | func update(_ value: inout MyState) { 502 | 503 | } 504 | 505 | original.startNewTracking() 506 | update(&original) 507 | let result = original.trackingResult! 508 | 509 | #expect( 510 | result.graph.prettyPrint() == """ 511 | StateStructTests.MyState 512 | """ 513 | ) 514 | } 515 | 516 | @Test 517 | func referencing() { 518 | 519 | var tree = Tree(another: .init(wrappedValue: .init())) 520 | 521 | tree.startNewTracking() 522 | _ = tree.another?.value 523 | let result = tree.trackingResult! 524 | 525 | #expect( 526 | result.graph.prettyPrint() == """ 527 | StateStructTests.Tree { 528 | another-(1) { 529 | wrappedValue-(1) { 530 | value-(1) 531 | } 532 | } 533 | } 534 | """ 535 | ) 536 | } 537 | 538 | @Test 539 | func weakRef() { 540 | 541 | var state = MyState.init() 542 | 543 | state.startNewTracking() 544 | _ = state.weak_ref 545 | let result = state.trackingResult! 546 | 547 | #expect( 548 | result.graph.prettyPrint() == """ 549 | StateStructTests.MyState { 550 | weak_ref-(1) 551 | } 552 | """ 553 | ) 554 | } 555 | } 556 | 557 | @Tracking 558 | struct AnotherTree { 559 | 560 | var value: Int = 0 561 | 562 | } 563 | 564 | @Tracking 565 | struct Tree { 566 | 567 | var another: Referencing? 568 | 569 | } 570 | -------------------------------------------------------------------------------- /Tests/StateStructTests/UpdatingTests.swift: -------------------------------------------------------------------------------- 1 | import StateStruct 2 | import Testing 3 | 4 | @Suite("UpdatingTests") 5 | struct UpdatingTests { 6 | 7 | @Test 8 | func test_1() { 9 | 10 | var state = MyState() 11 | 12 | state.startNewTracking() 13 | _ = state.height 14 | let reading = state.trackingResult! 15 | 16 | state.startNewTracking() 17 | state.height = 200 18 | let writing = state.trackingResult! 19 | 20 | let hasChangesInReading = PropertyNode.hasChanges( 21 | writeGraph: writing.graph, 22 | readGraph: reading.graph 23 | ) 24 | 25 | #expect(hasChangesInReading == true) 26 | } 27 | 28 | @Test 29 | func test_2() { 30 | 31 | var state = MyState() 32 | 33 | state.startNewTracking() 34 | _ = state.nested.name 35 | let reading = state.trackingResult! 36 | 37 | state.startNewTracking() 38 | state.nested = .init(name: "Foo") 39 | let writing = state.trackingResult! 40 | 41 | let hasChangesInReading = PropertyNode.hasChanges( 42 | writeGraph: writing.graph, 43 | readGraph: reading.graph 44 | ) 45 | 46 | #expect(hasChangesInReading == true) 47 | } 48 | 49 | @Test 50 | func test_3() { 51 | 52 | var state = MyState() 53 | 54 | state.startNewTracking() 55 | _ = state.nested.name 56 | let reading = state.trackingResult! 57 | 58 | state.startNewTracking() 59 | state.nested.age = 100 60 | let writing = state.trackingResult! 61 | 62 | let hasChangesInReading = PropertyNode.hasChanges( 63 | writeGraph: writing.graph, 64 | readGraph: reading.graph 65 | ) 66 | 67 | #expect(hasChangesInReading == false) 68 | } 69 | 70 | @Test("observing larger area than writing") 71 | func test_4() { 72 | 73 | var state = MyState() 74 | 75 | state.startNewTracking() 76 | _ = state.nested 77 | let reading = state.trackingResult! 78 | 79 | state.startNewTracking() 80 | state.nested.age = 100 81 | let writing = state.trackingResult! 82 | 83 | let hasChangesInReading = PropertyNode.hasChanges( 84 | writeGraph: writing.graph, 85 | readGraph: reading.graph 86 | ) 87 | 88 | #expect(hasChangesInReading == true) 89 | } 90 | 91 | @Test 92 | func test_5() { 93 | 94 | var state = Nesting() 95 | 96 | state.startNewTracking() 97 | _ = state._1?.value 98 | _ = state._2?._1?.value 99 | _ = state._3?._2?.value 100 | let reading = state.trackingResult! 101 | 102 | print("reading", reading.graph.prettyPrint()) 103 | 104 | state.startNewTracking() 105 | state._3?._2?.value = "3.2" 106 | let writing = state.trackingResult! 107 | 108 | let hasChangesInReading = PropertyNode.hasChanges( 109 | writeGraph: writing.graph, 110 | readGraph: reading.graph 111 | ) 112 | 113 | #expect(hasChangesInReading == true) 114 | } 115 | 116 | @Test 117 | func test_6() { 118 | 119 | var state = Nesting() 120 | 121 | state.startNewTracking() 122 | _ = state._2 123 | _ = state._2?._1?.value 124 | var reading = state.trackingResult! 125 | 126 | reading.graph.shakeAsRead() 127 | 128 | print("reading", reading.graph.prettyPrint()) 129 | 130 | state.startNewTracking() 131 | state._2?.value = "2.1" 132 | state._3?._2?.value = "3.2" 133 | let writing = state.trackingResult! 134 | 135 | let hasChangesInReading = PropertyNode.hasChanges( 136 | writeGraph: writing.graph, 137 | readGraph: reading.graph 138 | ) 139 | 140 | #expect(hasChangesInReading == true) 141 | } 142 | 143 | @Test 144 | func test_7() { 145 | 146 | var state = Nesting() 147 | 148 | state.startNewTracking() 149 | _ = state._2 150 | _ = state._2?._1?.value 151 | let reading = state.trackingResult! 152 | 153 | print("reading", reading.graph.prettyPrint()) 154 | 155 | state.startNewTracking() 156 | state._2?.value = "2.1" 157 | state._2 = .init() 158 | let writing = state.trackingResult! 159 | 160 | let hasChangesInReading = PropertyNode.hasChanges( 161 | writeGraph: writing.graph, 162 | readGraph: reading.graph 163 | ) 164 | 165 | #expect(hasChangesInReading == true) 166 | } 167 | 168 | @Test 169 | func complex() { 170 | 171 | var original = Nesting.init() 172 | original.startNewTracking() 173 | let sub1 = original._1 174 | let sub2 = original._2 175 | let sub3 = original._3 176 | 177 | _ = sub1?._3?.value 178 | _ = sub1?._2?.value 179 | _ = sub1?._1?.value 180 | 181 | _ = sub2?._2?.value 182 | _ = sub2?._1?.value 183 | 184 | _ = sub3?._1?.value 185 | _ = sub3?._2?.value 186 | _ = sub3?._3?.value 187 | let result = original.trackingResult! 188 | 189 | print("👨🏻") 190 | 191 | #expect( 192 | result.graph.sorted().prettyPrint() == """ 193 | StateStructTests.Nesting { 194 | _1-(1) { 195 | _1-(1) { 196 | value-(1) 197 | } 198 | _2-(1) { 199 | value-(1) 200 | } 201 | _3-(1) { 202 | value-(1) 203 | } 204 | } 205 | _2-(1) { 206 | _1-(1) { 207 | value-(1) 208 | } 209 | _2-(1) { 210 | value-(1) 211 | } 212 | } 213 | _3-(1) { 214 | _1-(1) { 215 | value-(1) 216 | } 217 | _2-(1) { 218 | value-(1) 219 | } 220 | _3-(1) { 221 | value-(1) 222 | } 223 | } 224 | } 225 | """ 226 | ) 227 | } 228 | 229 | } 230 | --------------------------------------------------------------------------------