Peer macro: how modify copy of source function body?

As a way to learn about writing complex macros, I want to write a memoize macro that's able support recursion. Basically, the following code:

@Memoize
func something(foo: Int) -> Int {
    if foo > 0 {
        return something(foo: foo - 1)
    }
    
    return foo
}

should produce a new function like this:

func memoizeSomething(foo: Int) -> Int {
    var cache: [Int: Int] = [:]
    
    func something(cache: inout [Int: Int], foo: Int) -> Int {
        if let cached = cache[foo] {
            return cached
        }
        
        if foo > 0 {
            let result = something(cache: &cache, foo: foo - 1)
            
            cache[foo] = result
            return result
        }
        
        cache[foo] = foo
        return foo
    }
    return something(cache: &cache, foo: foo)
}

The part I'm struggling with is patching a copy of the source function's body in expansion(of:providingPeersOf:in:). For starters, I want to walk the tree, find all function calls, and if it's a call to itself (recursion), inject a new argument to the list.

I can walk the tree, successfully find the function calls, and modify it… but I don't understand how to replace the old expression in the tree, or modify directly the existing one.

// How to implement this?
parent.replace(oldFuncCall, with: newFuncCall)

How can you walk an arbitrary syntax tree and modify it (replace some nodes with modified versions of said node, or insert new nodes)?

In the general case, this cannot be implemented using macros because the code may be calling an overloaded function that has the same signature but takes different argument types or returns a different type. If you still want to attempt this, you can implement a SyntaxRewriter subclass and override the visit method that takes a FunctionCallExprSyntax. Make sure that you call super.visit on the arguments in all cases, and the entire expression if you don’t replace it.

An alternate approach to consider here: since the cache is declared as a variable, you don’t need to pass it into the nested function in order to mutate it. If you give the inner function the same name as the original function, it might allow the type checker to pick the inner function instead of the outer one when doing overload resolution and avoid the need for you to do any modification to the body of the inner function.