diff --git a/Sources/StructTransaction/Source.swift b/Sources/StructTransaction/Source.swift index ea9b3d5..48008c6 100644 --- a/Sources/StructTransaction/Source.swift +++ b/Sources/StructTransaction/Source.swift @@ -1,44 +1,42 @@ -@attached(extension, conformances: DetectingType, names: named(Modifying), named(modify(source:modifier:)), named(read(source:reader:)), named(ModifyingTarget)) +@attached( + extension, + conformances: DetectingType, + names: named(Accessing), + named(modify(source:modifier:)), + named(read(source:reader:)), + named(AccessingTarget) +) public macro Detecting() = #externalMacro(module: "StructTransactionMacros", type: "WriterMacro") +/** + Use ``Detecting()`` macro to adapt struct + */ public protocol DetectingType { - associatedtype Modifying + associatedtype Accessing @discardableResult - static func modify(source: inout Self, modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult + static func modify(source: inout Self, modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult @discardableResult - static func read(source: Self, reader: (Modifying) throws -> Void) rethrows -> ReadResult + static func read(source: Self, reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult } extension DetectingType { @discardableResult - public mutating func modify(modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult { + public mutating func modify(modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult { try Self.modify(source: &self, modifier: modifier) } @discardableResult - public mutating func read(reader: (Modifying) throws -> Void) rethrows -> ReadResult { + public borrowing func read(reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult { try Self.read(source: self, reader: reader) } } -public struct ReadResult { - - public let readIdentifiers: Set - - public init( - readIdentifiers: Set - ) { - self.readIdentifiers = readIdentifiers - } -} - - -public struct ModifyingResult { +public struct AccessingResult { public let readIdentifiers: Set public let modifiedIdentifiers: Set diff --git a/Sources/StructTransactionMacros/WriterMacro.swift b/Sources/StructTransactionMacros/WriterMacro.swift index c3e23fc..ad9558e 100644 --- a/Sources/StructTransactionMacros/WriterMacro.swift +++ b/Sources/StructTransactionMacros/WriterMacro.swift @@ -23,10 +23,48 @@ extension WriterMacro: ExtensionMacro { in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + let accessingDecl = try Self.makeModifying( + of: node, + attachedTo: declaration, + providingExtensionsOf: type, + conformingTo: protocols, + in: context + ) + + let extensionDecl = """ + extension \(type.trimmed): DetectingType { + + // MARK: - Accessing + \(accessingDecl) + + } + """ as DeclSyntax + + return [ + extensionDecl + .formatted( + using: .init( + indentationWidth: .spaces(2), + initialIndentation: [], + viewMode: .fixedUp + ) + ) + .cast(ExtensionDeclSyntax.self) + ] + + } + + private static func makeModifying( + of node: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, + conformingTo protocols: [SwiftSyntax.TypeSyntax], + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> CodeBlockItemListSyntax { // Decode the expansion arguments. guard let structDecl = declaration.as(StructDeclSyntax.self) else { context.addDiagnostics(from: WriterMacroError.foundNotStructType, node: node) - return [] + return "" } let c = PropertyCollector(viewMode: .all) @@ -75,14 +113,14 @@ extension WriterMacro: ExtensionMacro { } let modifyingStructDecl = """ - public struct Modifying /* want to be ~Copyable */ { + public struct Accessing /* want to be ~Copyable */ { public private(set) var $_readIdentifiers: Set = .init() public private(set) var $_modifiedIdentifiers: Set = .init() - private let pointer: UnsafeMutablePointer + private let pointer: UnsafeMutablePointer - init(pointer: UnsafeMutablePointer) { + init(pointer: UnsafeMutablePointer) { self.pointer = pointer } @@ -92,53 +130,41 @@ extension WriterMacro: ExtensionMacro { let modifyingDecl = (""" - extension \(type.trimmed): DetectingType { - - typealias ModifyingTarget = Self + typealias AccessingTarget = Self - @discardableResult - public static func modify(source: inout Self, modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult { + @discardableResult + public static func modify(source: inout Self, modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult { - try withUnsafeMutablePointer(to: &source) { pointer in - var modifying = Modifying(pointer: pointer) - try modifier(&modifying) - return ModifyingResult( - readIdentifiers: modifying.$_readIdentifiers, - modifiedIdentifiers: modifying.$_modifiedIdentifiers - ) - } + try withUnsafeMutablePointer(to: &source) { pointer in + var modifying = Accessing(pointer: pointer) + try modifier(&modifying) + return AccessingResult( + readIdentifiers: modifying.$_readIdentifiers, + modifiedIdentifiers: modifying.$_modifiedIdentifiers + ) } + } - @discardableResult - public static func read(source: Self, reader: (Modifying) throws -> Void) rethrows -> ReadResult { - // FIXME: avoid copying - var reading = source - - return try withUnsafeMutablePointer(to: &reading) { pointer in - let modifying = Modifying(pointer: pointer) - try reader(modifying) - return ReadResult( - readIdentifiers: modifying.$_readIdentifiers - ) - } - } + @discardableResult + public static func read(source: consuming Self, reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult { - \(modifyingStructDecl) - } - """ as DeclSyntax) + // TODO: check copying costs + var tmp = source - return [ - modifyingDecl - .formatted( - using: .init( - indentationWidth: .spaces(2), - initialIndentation: [], - viewMode: .fixedUp + return try withUnsafeMutablePointer(to: &tmp) { pointer in + var modifying = Accessing(pointer: pointer) + try reader(&modifying) + return AccessingResult( + readIdentifiers: modifying.$_readIdentifiers, + modifiedIdentifiers: modifying.$_modifiedIdentifiers ) - ) - .cast(ExtensionDeclSyntax.self) - ] + } + } + + \(modifyingStructDecl) + """ as CodeBlockItemListSyntax) + return modifyingDecl } } @@ -151,24 +177,22 @@ private func makeMutatingGetter(_ block: AccessorBlockSyntax) -> AccessorBlockSy let decl = MutatingGetterConverter().visit(list) return """ -{ - \(decl) -} -""" as AccessorBlockSyntax + { + \(decl) + } + """ as AccessorBlockSyntax case .getter(let getter): return """ -{ - mutating get { \(getter) } -} -""" as AccessorBlockSyntax + { + mutating get { \(getter) } + } + """ as AccessorBlockSyntax } } -/** -convert get-accessor into mutating-get-accessor - */ +/// convert get-accessor into mutating-get-accessor final class MutatingGetterConverter: SyntaxRewriter { override func visit(_ node: AccessorDeclSyntax) -> DeclSyntax { @@ -257,4 +281,3 @@ final class PropertyCollector: SyntaxVisitor { } } - diff --git a/Tests/StructTransactionMacroTests/WriterMacroTests.swift b/Tests/StructTransactionMacroTests/WriterMacroTests.swift index b75c42f..34438ca 100644 --- a/Tests/StructTransactionMacroTests/WriterMacroTests.swift +++ b/Tests/StructTransactionMacroTests/WriterMacroTests.swift @@ -39,15 +39,16 @@ final class WriterMacroTests: XCTestCase { extension MyState: DetectingType { - typealias ModifyingTarget = Self + // MARK: - Accessing + typealias AccessingTarget = Self @discardableResult - public static func modify(source: inout Self, modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult { + public static func modify(source: inout Self, modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult { try withUnsafeMutablePointer(to: &source) { pointer in - var modifying = Modifying(pointer: pointer) + var modifying = Accessing(pointer: pointer) try modifier(&modifying) - return ModifyingResult( + return AccessingResult( readIdentifiers: modifying.$_readIdentifiers, modifiedIdentifiers: modifying.$_modifiedIdentifiers ) @@ -55,27 +56,29 @@ final class WriterMacroTests: XCTestCase { } @discardableResult - public static func read(source: Self, reader: (Modifying) throws -> Void) rethrows -> ReadResult { - // FIXME: avoid copying - var reading = source - - return try withUnsafeMutablePointer(to: &reading) { pointer in - let modifying = Modifying(pointer: pointer) - try reader(modifying) - return ReadResult( - readIdentifiers: modifying.$_readIdentifiers + public static func read(source: consuming Self, reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult { + + // TODO: check copying costs + var tmp = source + + return try withUnsafeMutablePointer(to: &tmp) { pointer in + var modifying = Accessing(pointer: pointer) + try reader(&modifying) + return AccessingResult( + readIdentifiers: modifying.$_readIdentifiers, + modifiedIdentifiers: modifying.$_modifiedIdentifiers ) } } - public struct Modifying /* want to be ~Copyable */ { + public struct Accessing /* want to be ~Copyable */ { public private (set) var $_readIdentifiers: Set = .init() public private (set) var $_modifiedIdentifiers: Set = .init() - private let pointer: UnsafeMutablePointer + private let pointer: UnsafeMutablePointer - init(pointer: UnsafeMutablePointer) { + init(pointer: UnsafeMutablePointer) { self.pointer = pointer } @@ -88,8 +91,9 @@ final class WriterMacroTests: XCTestCase { $_modifiedIdentifiers.insert("stored_property_wrapper") yield &pointer.pointee.stored_property_wrapper } + } } - } + } """ } @@ -117,15 +121,16 @@ final class WriterMacroTests: XCTestCase { extension MyState: DetectingType { - typealias ModifyingTarget = Self + // MARK: - Accessing + typealias AccessingTarget = Self @discardableResult - public static func modify(source: inout Self, modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult { + public static func modify(source: inout Self, modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult { try withUnsafeMutablePointer(to: &source) { pointer in - var modifying = Modifying(pointer: pointer) + var modifying = Accessing(pointer: pointer) try modifier(&modifying) - return ModifyingResult( + return AccessingResult( readIdentifiers: modifying.$_readIdentifiers, modifiedIdentifiers: modifying.$_modifiedIdentifiers ) @@ -133,27 +138,29 @@ final class WriterMacroTests: XCTestCase { } @discardableResult - public static func read(source: Self, reader: (Modifying) throws -> Void) rethrows -> ReadResult { - // FIXME: avoid copying - var reading = source - - return try withUnsafeMutablePointer(to: &reading) { pointer in - let modifying = Modifying(pointer: pointer) - try reader(modifying) - return ReadResult( - readIdentifiers: modifying.$_readIdentifiers + public static func read(source: consuming Self, reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult { + + // TODO: check copying costs + var tmp = source + + return try withUnsafeMutablePointer(to: &tmp) { pointer in + var modifying = Accessing(pointer: pointer) + try reader(&modifying) + return AccessingResult( + readIdentifiers: modifying.$_readIdentifiers, + modifiedIdentifiers: modifying.$_modifiedIdentifiers ) } } - public struct Modifying /* want to be ~Copyable */ { + public struct Accessing /* want to be ~Copyable */ { public private (set) var $_readIdentifiers: Set = .init() public private (set) var $_modifiedIdentifiers: Set = .init() - private let pointer: UnsafeMutablePointer + private let pointer: UnsafeMutablePointer - init(pointer: UnsafeMutablePointer) { + init(pointer: UnsafeMutablePointer) { self.pointer = pointer } @@ -166,8 +173,9 @@ final class WriterMacroTests: XCTestCase { $_modifiedIdentifiers.insert("constant_has_initial_value") yield &pointer.pointee.constant_has_initial_value } + } } - } + } """ } @@ -226,15 +234,16 @@ final class WriterMacroTests: XCTestCase { extension MyState: DetectingType { - typealias ModifyingTarget = Self + // MARK: - Accessing + typealias AccessingTarget = Self @discardableResult - public static func modify(source: inout Self, modifier: (inout Modifying) throws -> Void) rethrows -> ModifyingResult { + public static func modify(source: inout Self, modifier: (inout Accessing) throws -> Void) rethrows -> AccessingResult { try withUnsafeMutablePointer(to: &source) { pointer in - var modifying = Modifying(pointer: pointer) + var modifying = Accessing(pointer: pointer) try modifier(&modifying) - return ModifyingResult( + return AccessingResult( readIdentifiers: modifying.$_readIdentifiers, modifiedIdentifiers: modifying.$_modifiedIdentifiers ) @@ -242,56 +251,59 @@ final class WriterMacroTests: XCTestCase { } @discardableResult - public static func read(source: Self, reader: (Modifying) throws -> Void) rethrows -> ReadResult { - // FIXME: avoid copying - var reading = source - - return try withUnsafeMutablePointer(to: &reading) { pointer in - let modifying = Modifying(pointer: pointer) - try reader(modifying) - return ReadResult( - readIdentifiers: modifying.$_readIdentifiers + public static func read(source: consuming Self, reader: (inout Accessing) throws -> Void) rethrows -> AccessingResult { + + // TODO: check copying costs + var tmp = source + + return try withUnsafeMutablePointer(to: &tmp) { pointer in + var modifying = Accessing(pointer: pointer) + try reader(&modifying) + return AccessingResult( + readIdentifiers: modifying.$_readIdentifiers, + modifiedIdentifiers: modifying.$_modifiedIdentifiers ) } } - public struct Modifying /* want to be ~Copyable */ { + public struct Accessing /* want to be ~Copyable */ { public private (set) var $_readIdentifiers: Set = .init() public private (set) var $_modifiedIdentifiers: Set = .init() - private let pointer: UnsafeMutablePointer + private let pointer: UnsafeMutablePointer - init(pointer: UnsafeMutablePointer) { + init(pointer: UnsafeMutablePointer) { self.pointer = pointer } public var computed_read_only: Int - { - mutating get { - constant_has_initial_value - } - } + { + mutating get { + constant_has_initial_value + } + } - public var computed_read_only2: Int - { - mutating - get { - constant_has_initial_value - } - } + public var computed_read_only2: Int + { + mutating + get { + constant_has_initial_value + } + } - public var computed_readwrite: String - { - mutating - get { - variable_no_initial_value - } - set { - variable_no_initial_value = newValue - } - } + public var computed_readwrite: String + { + mutating + get { + variable_no_initial_value + } + set { + variable_no_initial_value = newValue + } + } } + } """ } diff --git a/Tests/StructTransactionTests/TransactionTests.swift b/Tests/StructTransactionTests/ModifyingTests.swift similarity index 53% rename from Tests/StructTransactionTests/TransactionTests.swift rename to Tests/StructTransactionTests/ModifyingTests.swift index 8209c96..044e7b6 100644 --- a/Tests/StructTransactionTests/TransactionTests.swift +++ b/Tests/StructTransactionTests/ModifyingTests.swift @@ -1,76 +1,7 @@ import XCTest import StructTransaction -@propertyWrapper -struct JustWrapper { - - var wrappedValue: Value - -} - -@propertyWrapper -struct Clamped { - private var value: Value - let min: Value - let max: Value - - init(wrappedValue: Value, min: Value, max: Value) { - self.min = min - self.max = max - self.value = Swift.min(Swift.max(wrappedValue, min), max) - } - - var wrappedValue: Value { - get { return value } - set { value = Swift.min(Swift.max(newValue, min), max) } - } -} - -final class WritingStateTests: XCTestCase { - - @Detecting - struct MyState { - - @Clamped(min: 0, max: 300) var height: Int = 0 - - var age: Int = 18 - var name: String - - @JustWrapper var edge: Int = 0 - - var computedName: String { - get { - "Mr. " + name - } - } - - var computedAge: Int { - let age = age - return age - } - - var computed_setter: String { - get { - name - } - set { - name = newValue - } - } - - var nested: Nested = .init(name: "hello") - var nestedAttached: NestedAttached = .init(name: "") - - struct Nested { - var name = "" - } - - @Detecting - struct NestedAttached { - var name: String = "" - } - - } +final class ModifyingStateTests: XCTestCase { func testPropertyWrapper() { diff --git a/Tests/StructTransactionTests/MyState.swift b/Tests/StructTransactionTests/MyState.swift new file mode 100644 index 0000000..b9f3605 --- /dev/null +++ b/Tests/StructTransactionTests/MyState.swift @@ -0,0 +1,45 @@ +import StructTransaction + +@Detecting +struct MyState { + + @Clamped(min: 0, max: 300) var height: Int = 0 + + var age: Int = 18 + var name: String + + @JustWrapper var edge: Int = 0 + + var computedName: String { + get { + "Mr. " + name + } + } + + var computedAge: Int { + let age = age + return age + } + + var computed_setter: String { + get { + name + } + set { + name = newValue + } + } + + var nested: Nested = .init(name: "hello") + var nestedAttached: NestedAttached = .init(name: "") + + struct Nested { + var name = "" + } + + @Detecting + struct NestedAttached { + var name: String = "" + } + +} diff --git a/Tests/StructTransactionTests/PropertyWrappers.swift b/Tests/StructTransactionTests/PropertyWrappers.swift new file mode 100644 index 0000000..c4e28a1 --- /dev/null +++ b/Tests/StructTransactionTests/PropertyWrappers.swift @@ -0,0 +1,25 @@ + +@propertyWrapper +struct JustWrapper { + + var wrappedValue: Value + +} + +@propertyWrapper +struct Clamped { + private var value: Value + let min: Value + let max: Value + + init(wrappedValue: Value, min: Value, max: Value) { + self.min = min + self.max = max + self.value = Swift.min(Swift.max(wrappedValue, min), max) + } + + var wrappedValue: Value { + get { return value } + set { value = Swift.min(Swift.max(newValue, min), max) } + } +} diff --git a/Tests/StructTransactionTests/ReadingTests.swift b/Tests/StructTransactionTests/ReadingTests.swift new file mode 100644 index 0000000..dba8ca9 --- /dev/null +++ b/Tests/StructTransactionTests/ReadingTests.swift @@ -0,0 +1,30 @@ +import XCTest +import StructTransaction + +final class ReadingStateTests: XCTestCase { + + func test_read_stored_property() { + + let myState = MyState(name: "") + + let r = myState.read { + _ = $0.name + } + + XCTAssertEqual(r.readIdentifiers, ["name"]) + + } + + func test_read_computed_property() { + + let myState = MyState(name: "") + + let r = myState.read { + _ = $0.computedAge + } + + XCTAssertEqual(r.readIdentifiers, ["age"]) + + } + +}