Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f90f6fe
Initial implementation of Macro based data cache
mikolasstuchlik Jan 14, 2025
be0b2e7
Update Package manifest
mikolasstuchlik Jan 14, 2025
adbc3a0
Synchronize name of product and target
mikolasstuchlik Jan 14, 2025
e8c6351
Fix typo type
mikolasstuchlik Jan 14, 2025
9ad7ef9
Expose versioned property macro
mikolasstuchlik Jan 14, 2025
44521f5
Move to undirect storage access
mikolasstuchlik Jan 14, 2025
d6dfa4c
Fix wrong operator
mikolasstuchlik Jan 14, 2025
08344f2
Add init accessor
mikolasstuchlik Jan 14, 2025
72d1955
Add names to accessor macro
mikolasstuchlik Jan 14, 2025
28d45f4
Separate logic and clean macro declarations
mikolasstuchlik Jan 15, 2025
54ff892
Fix incorrect use of covariant Self
mikolasstuchlik Jan 15, 2025
a2d32ef
Fix generated conformances
mikolasstuchlik Jan 15, 2025
eadda8c
Remove faulty specialization
mikolasstuchlik Jan 15, 2025
23f63ba
Simplify cache expansion
mikolasstuchlik Jan 20, 2025
b6a20ac
Separate main into a different file
mikolasstuchlik Jan 20, 2025
8ed28c7
Implement extension based conformance
mikolasstuchlik Jan 20, 2025
1954181
Diagnostics for real-life usage
mikolasstuchlik Jan 20, 2025
c164831
Fix inverted conditions for extension expansion
mikolasstuchlik Jan 20, 2025
248b329
Return back diagnostics
mikolasstuchlik Jan 20, 2025
1e051df
Experiment with syntax version 6.0
mikolasstuchlik Jan 21, 2025
7174678
Merge branch 'feature/extension-based-conformance' into experiment/da…
mikolasstuchlik Jan 22, 2025
1f730ac
Add actor annotation to Proxy macro
mikolasstuchlik Jan 22, 2025
52c88a9
Fix global actor Swift 6 warnings (without tests)
mikolasstuchlik Jan 26, 2025
b2ddd1e
Fix protocol conformance (without tests)
mikolasstuchlik Jan 26, 2025
116f36b
Fix typo
mikolasstuchlik Jan 26, 2025
0ff1c76
Make data cache submit on subscription
mikolasstuchlik Jan 26, 2025
2af2890
Fix default argument for macros
mikolasstuchlik Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 23 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ let package = Package(
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
.library(
name: "FuturedMacros",
name: "EnumIdentable",
targets: ["EnumIdentable"]
)
),
.library(
name: "DataCache",
targets: ["DataCache"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.1"),
],
targets: [
// Macro implementation that performs the source transformation of a macro.
Expand All @@ -24,9 +28,17 @@ let package = Package(
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.macro(
name: "DataCacheMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),

// Library that exposes a macro as part of its API, which is used in client programs.
.target(name: "EnumIdentable", dependencies: ["EnumIdentableMacros"]),
.target(name: "DataCache", dependencies: ["DataCacheMacros"]),

// A test target used to develop the macro implementation.
.testTarget(
Expand All @@ -36,5 +48,13 @@ let package = Package(
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),

.testTarget(
name: "DataCacheTests",
dependencies: [
"DataCacheMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
67 changes: 67 additions & 0 deletions Sources/DataCache/DataCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// DataCache.swift
// FuturedMacros
//
// Created by Mikoláš Stuchlík on 13.01.2025.
//

public protocol VersionedDataCache {
associatedtype _Versions: Hashable
}

public final class SubscriptionBox<C: VersionedDataCache> {
public init(
initialVersion: C._Versions,
continuation: AsyncStream<C>.Continuation,
predicate: @escaping (_ oldValue: C._Versions, _ newValue: C._Versions) -> Bool
) {
self.lastVersion = initialVersion
self.yield = continuation.yield
self.predicate = predicate
}

private let yield: (sending C) -> AsyncStream<C>.Continuation.YieldResult
private let predicate: (_ oldValue: C._Versions, _ newValue: C._Versions) -> Bool
private var lastVersion: C._Versions

public func emmit(cache: C, ifDiffers newVersion: C._Versions) {
defer {
lastVersion = newVersion
}
if predicate(lastVersion, newVersion) {
_ = yield(cache)
}
}
}

public protocol ProxyObject<Ref> {
associatedtype Ref: AnyObject
}

public protocol ProxySettable {
associatedtype Proxy: ProxyObject<Self>
}

@attached(member, names: named(_version), named(_subscribtions), named(makeSubscriber), named(applyChanges), named(_Versions))
@attached(memberAttribute)
@attached(extension, conformances: VersionedDataCache)
public macro DataCache<GA: GlobalActor>(isolation: GA.Type? = Optional<MainActor.Type>.none) = #externalMacro(
module: "DataCacheMacros",
type: "DataCacheMacro"
)

@attached(extension, conformances: ProxySettable, names: named(Proxy), named(withTransaction))
public macro ProxySetter<GA: GlobalActor>(isolation: GA.Type? = Optional<MainActor.Type>.none) = #externalMacro(
module: "DataCacheMacros",
type: "ProxySetterMacro"
)

@attached(accessor, names: named(init), named(get), named(set))
@attached(peer, names: prefixed(`_`))
public macro VersionedProperty() = #externalMacro(module: "DataCacheMacros", type: "VersionedPropertyMacro")

@freestanding(expression)
public macro cacheSubscribtion<T: VersionedDataCache, each P>(on: T, properties: repeat KeyPath<T, each P>) -> AsyncStream<T> = #externalMacro(
module: "DataCacheMacros",
type: "CacheSubscriptionMacro"
)
65 changes: 65 additions & 0 deletions Sources/DataCacheMacros/CacheSubscriptionMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// CacheSubscriptionMacro.swift
// FuturedMacros
//
// Created by Mikoláš Stuchlík on 13.01.2025.
//
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftDiagnostics
import Foundation

public struct CacheSubscriptionMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
var cacheNameExr: String?
var properties: [String] = []

for listItem in node.arguments {
if cacheNameExr == nil {
if
listItem.label?.text == "on"
{
cacheNameExr = "\(listItem.expression)"
} else {
return ExprSyntax("")
}
} else if properties.count == 0 {
if
listItem.label?.text == "properties",
let kpExpr = listItem.expression.as(KeyPathExprSyntax.self)
{
properties = ["\(kpExpr.components)"]
} else {
return ExprSyntax("")
}
} else {
if
let kpExpr = listItem.expression.as(KeyPathExprSyntax.self)
{
properties.append("\(kpExpr.components)")
} else {
return ExprSyntax("")
}
}
}

guard let cacheNameExr, !properties.isEmpty else {
return ExprSyntax("")
}

return ExprSyntax(
"""
\(raw: cacheNameExr).makeSubscriber(
predicate: {
\( raw: properties.map { "$0\($0) != $1\($0)" }.joined(separator: " || ") )
}
)
"""
)
}
}
188 changes: 188 additions & 0 deletions Sources/DataCacheMacros/DataCacheMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// DataCacheMacro.swift
// FuturedMacros
//
// Created by Mikoláš Stuchlík on 13.01.2025.
//

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftDiagnostics
import Foundation

public struct DataCacheMacro: MemberMacro {
public static func expansion<Declaration, Context>(
of node: SwiftSyntax.AttributeSyntax,
providingMembersOf declaration: Declaration,
in context: Context
) throws -> [SwiftSyntax.DeclSyntax] where
Declaration : SwiftSyntax.DeclGroupSyntax,
Context : SwiftSyntaxMacros.MacroExpansionContext
{
let attributeActor = node.arguments.flatMap { arguments -> String? in
let isolationArg = arguments.as(LabeledExprListSyntax.self)?.first { labeledExpr in
labeledExpr.label?.text == "isolation"
}

guard let memberAccess = isolationArg?.expression.as(MemberAccessExprSyntax.self) else {
return nil
}

guard let base = memberAccess.base else {
return nil
}

return "\(base)"
}


return makeVersionsStruct(members: declaration.memberBlock.members)
+ emmitSubsriptionSupport(className: className(decl: declaration), actor: attributeActor)
+ emmitTransactionSupport(className: className(decl: declaration))
}

private static func className<Declaration: SwiftSyntax.DeclGroupSyntax>(decl: Declaration) -> String! {
guard let clsDecl = decl.as(ClassDeclSyntax.self) else {
return nil
}

return clsDecl.name.text
}

// if some variables are undesirable, you can add filter to this method
private static func storedVariable(members: MemberBlockItemListSyntax) -> [VariableDeclSyntax] {
members.compactMap { member in
guard
let varDecl = member.decl.as(VariableDeclSyntax.self)
else {
return nil
}

return varDecl
}
}

private static func storedVariableNames(members: MemberBlockItemListSyntax) -> [String] {
storedVariable(members: members).compactMap { varDecl in
guard
let ident = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)
else {
return nil
}

return ident.identifier.text
}
}

private static func makeVersionsStruct(members: MemberBlockItemListSyntax) -> [DeclSyntax] {
[
DeclSyntax(
"""
struct _Versions: Hashable {
\(raw: storedVariableNames(members: members).map { " var \($0): UInt = 0" }.joined(separator: "\n") )
}
"""
),
DeclSyntax("private var _version: _Versions = .init()"),
]
}

private static func emmitSubsriptionSupport(className: String, actor: String?) -> [DeclSyntax] {
var makePattern =
"""
func makeSubscriber(predicate: @escaping (_ oldValue: _Versions, _ newValue: _Versions) -> Bool) -> AsyncStream<\(className)> {
let (stream, continuation) = AsyncStream.makeStream(of: \(className).self)
let box = SubscriptionBox(
initialVersion: self._version,
continuation: continuation,
predicate: predicate
)

continuation.onTermination = { [weak self] _ in

"""
if let actor {
makePattern +=
"""
Task { @\(actor) in
self?._subscribtions.removeAll {
$0 === box
}
}
"""
} else {
makePattern +=
"""
self?._subscribtions.removeAll {
$0 === box
}
"""
}
makePattern +=
"""
}
defer { continuation.yield(self) }
self._subscribtions.append(box)
return stream
}
"""

return [
DeclSyntax("private var _subscribtions: [SubscriptionBox<\(raw: className)>] = []"),
DeclSyntax(stringLiteral: makePattern)
]
}

private static func emmitTransactionSupport(className: String) -> [DeclSyntax] {
return [
DeclSyntax(
"""
func applyChanges(during block: () -> Void) {
block()
for subscribtion in _subscribtions {
subscribtion.emmit(cache: self, ifDiffers: self._version)
}
}
"""
)
]
}
}

extension DataCacheMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
guard member.is(VariableDeclSyntax.self) else { return [] }
return ["@VersionedProperty"]
}
}

extension DataCacheMacro: ExtensionMacro {
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo protocols: [SwiftSyntax.TypeSyntax],
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
return []
}

return [
try? ExtensionDeclSyntax(
"""
extension \(raw: classDecl.name.text): VersionedDataCache { }
"""
)
].compactMap(\.self)
}


}
Loading