Search code examples
swiftswift-macro

Is that possible to override the varible using macro?


I want to create a macro to protect the initialization process of lazy varibles. For example:

@ThreadSafeLazy(lock: lock)
lazy var varible: Int = {
    return 1 + 1
}()

// will expanded to 

lazy var varible: Int = {
    lock.around {
        if let _unique_name_varible { return _unique_name_varible }
        _unique_name_varible = { 
            return 1 + 1 
        }()
        return _unique_name_varible 
    }
}()

var _unique_name_varible: Int? = nil

I know how to add a _unique_name_varible varible using PeerMacro here, but I have no idea how to override the varible's initiate closure.


  • #1 Edit:

Due to the lazy var generation compiler crash issue, I changed the solution for workaround. now the macro looks like:

@Lazify(name: "internalClassName", lock: "SomeClass.classLock")
func createLazyVariable() -> String {
  return "__" + NSStringFromClass(type(of: self))
}

// expanded to
private(set) var _lazy_internalClassName: String? = nil

var internalClassName: String {
    if let exsist = self._lazy_internalClassName {
        return exsist
    }
    return SomeClass.classLock.around { [weak self] in
        guard let self else {
            fatalError()
        }
        if let exsist = self._lazy_internalClassName {
            return exsist
        }
        let temp = self.createLazyVariable()
        self._lazy_internalClassName = temp
        return temp
    }
}

But there's another problem I encountered, I raised another topic here


Solution

  • You cannot modify existing declarations using a macro, except for adding accessors to a property.

    As a workaround, you can make the lazy var declarations a parameter of a freestanding declaration macro. That is,

    #ThreadSafeLazy(lock: lock) {
        lazy var variable: Int = {
            return 1 + 1
        }()
    }
    

    You would add an additional () -> Void closure argument to ThreadSafeLazy, so that users of the macro can declare lazy vars in the closure. The macro can then expand to the two declarations you want.

    Here is a simple implementation:

    enum ThreadSafeLazy: DeclarationMacro {
        
        static func generatePair(_ varDecl: VariableDeclSyntax, uniqueName: TokenSyntax, lock: ExprSyntax) -> [DeclSyntax] {
            guard let binding = varDecl.bindings.first,
                  let initialiser = binding.initializer?.value,
                  let type = binding.typeAnnotation?.type,
                  let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
                return []
            }
            let initialiserWithLock: DeclSyntax = """
            lazy var \(name): \(type) = {
                \(lock).around { () -> \(type) in
                    if let \(uniqueName) { return \(uniqueName) }
                    let temp: \(type) = \(initialiser)
                    \(uniqueName) = temp
                    return temp
                }
            }()
            """
            let backingProperty: DeclSyntax = """
            var \(uniqueName): Optional<\(type)> = nil
            """
            return [initialiserWithLock, backingProperty]
        }
        
        static func expansion(
            of node: some FreestandingMacroExpansionSyntax,
            in context: some MacroExpansionContext) throws -> [DeclSyntax]
        {
            let varDecls = node.trailingClosure!.statements.compactMap { $0.item.as(VariableDeclSyntax.self) }
            let lockExpr = node.argumentList.first!.expression
            return varDecls.flatMap {
                generatePair($0, uniqueName: context.makeUniqueName("backingProperty"), lock: lockExpr)
            }
        }
    }
    

    With this approach, you don't even need to write lazy explicitly in the variable declaration, because that can also be added by ThreadSafeLazy. Though IMO that could create confusion.

    It is possible to declare multiple lazy vars in the closure, so you can also consider a design like what I did in this answer:

    #ThreadSafeLazy {
        @Locked(lock1)
        lazy var variable`: Int = {
            return 1 + 1
        }()
    
        @Locked(lock2)
        lazy var variable2: Int = {
            return 2 + 2
        }()
    }
    

    where ThreadSafeLazy expands to all the declaration inside the closure, but if any of the declarations has a @Locked macro attached, ThreadSafeLazy generates the thread safe version of the property, along with the additional unique name property. Locked doesn't need to expand to anything - it only acts as a marker for ThreadSafeLazy.