My macro generates no code (?)

I am quite stumped!

I have a macro, complicated yes, but I believe you can ignore most of its code, and still help me: If at the end of the macro definition, I @show the code it is to generate, it looks perfect. But if I invoque the macro, it injects zero code, so no function or variable created). If I launch @macroexpand on it: same, nothing to show. :face_with_spiral_eyes:

Any ideas? (I have an awkward feeling this will turn out to be a very silly question! :smiley: ).
Julia 1.11, Windows. I use Revise.jl, and yes I did restart Julia.

(Context, if you are curious: I am developing code that takes user-defined functions as input. If the user does this is a script, their input-function definition is reparsed at each script execution. Even if unchanged the input-function gets a new type, triggering recompilations. This code intends to work around this issue, while not imposing restrictions on type that would get in the way for forward automatic differentiation…)

using Printf, MacroTools

muscadeerror = error

struct Functor{name,Ta} <: Function
    captured::Ta
    function Functor{name}(;kwargs...) where{name}
        nt = NamedTuple(kwargs)
        return new{name,typeof(nt)}(nt)
    end
end

# to debug, use 'Base.dump' on expressions
macro functor(capture,foo)
    # Build capargname, a vector of names of captured variables, to later replace a -> o.captured.a in the body of foo -> (o::Functor{:foo})
    if capture.head==:call # function call
        if length(capture.args)==1 # no captured arguments
            caparg     = Any[]
            capargname = Symbol[]
            ncaparg    = 0
        else
            if capture.args[2] isa Expr && capture.args[2].head isa Symbol && capture.args[2].head == :parameters # user not supposed to prefix captured args with ;, but I'm in a good mood
                error("Do not use ; in list of captured arguments")
            else
                caparg     = capture.args[2:end]
            end
            ncaparg    = length(caparg)
            capargname = Vector{Symbol}(undef,ncaparg)
            for icaparg = 1:ncaparg
                if caparg[icaparg] isa Symbol
                    capargname[icaparg] = caparg[icaparg]
                elseif caparg[icaparg] isa Expr
                    if caparg[icaparg].head == :kw
                        capargname[icaparg] = caparg[icaparg].args[1]
                    elseif caparg[icaparg].head == :parameters
                        muscadeerror("Invalid @functor definition 3")
                    end
                else
                    muscadeerror("Invalid @functor definition 4")
                end
            end
        end
    else
        muscadeerror("Invalid @functor definition 2")
    end

    # Build the code for the method associated to the functor 
    # TODO all variables must be either capturedargs or fooargs, no closure. Throw error otherwise
    foodict            = splitdef(foo)
    foodict[:body]     = MacroTools.postwalk(foodict[:body]) do ex
        ex isa Symbol && ex∈capargname ? :(o.captured.$ex) : ex # prefix captured args with `o.captured.` in method body
    end    
    fooname            = foodict[:name]                           # :foo
    functortype        = Expr(:curly,:Functor,QuoteNode(fooname))#:(Functor{$fooname})                     # Functor{:foo}

    foodict[:name]     = Expr(:(::),:o,functortype)             # (o::Functor{:foo}), name of the method that implements foo(x)
    foo                = combinedef(foodict)                    # code of said method
    quotefoo           = MacroTools.postwalk(rmlines,foo)
    quotefoo           = MacroTools.postwalk(unblock,quotefoo)
    quotefoo           = QuoteNode(quotefoo)                    # quote of unannotated code for said method, to decide wether foo-code changed or not 

    # obscure variable name, to prevent reparsing of the foo definition
    tag                = Symbol("tag_for_the_functor_macro_",fooname)

    # build the code for the call to the functor constructor
    caparg           = Expr(:parameters,caparg...)     # place a ; in front of the argument list (any prefixed ; was cleaned earlier)
    constrcall       = Expr(:call,functortype,caparg)  # Functor{:foo}(;a,b=2)
    constructfunctor = Expr(:(=),fooname,constrcall)
    
    code = quote 
        $constructfunctor
        if  ~@isdefined($tag) || $tag ≠ $quotefoo
           $tag = $quotefoo
           $foo
        end                                                          
    end
    @show prettify(code) # looks perfect
    return code
end
####################
@macroexpand @functor with(a=2)     cost7(x) = a*x  # no output
@functor with(a=2)     cost7(x) = a*x               # no error
@show cost7(3)                                      # error: what is cost7?

I believe you want Expr(:(=), esc(fooname), constrcall) here. If you want fooname to be available globally and not just inside the macro, you need to consider macro hygiene. That’s why, if you look at the output of @macroexpand it will show something like:

quote
    #= REPL[14]:59 =#
    var"#46#cost7" = Main.Functor{:cost7}(; a = 2)
    #= REPL[14]:60 =#
    if ~($(Expr(:isdefined, Symbol("#47#tag_for_the_functor_macro_cost7")))) || var"#47#tag_for_the_functor_macro_cost7" ≠ $(QuoteNode(:(function (o::Functor{:cost7})(x; )
      o.captured.a * x
  end)))
        #= REPL[14]:61 =#
        var"#47#tag_for_the_functor_macro_cost7" = $(QuoteNode(:(function (o::Functor{:cost7})(x; )
      o.captured.a * x
  end)))
        #= REPL[14]:62 =#
        function (var"#48#o"::Main.Functor{:cost7})(var"#50#x"; )
            #= REPL[15]:2 =#
            (var"#48#o").captured.a * var"#50#x"
        end
    end
end

Note how it doesn’t say cost7 = ... in the first line, but instead cost7 gets mangled in such a way that it doesn’t leak outside the macro.

Hi Simeon,

Great, I will fix that . thanks. But did you get output at all (e.g. from @Macroexpand). Again, I get nothing out. Not bad code, just nothing.

OK, so I had a combination of two problems: fooled by macro-hygiene, combined with something, that causes @macroexpand to return nothing on my computer, probably not on yours.

Well, this solves my problem, thank you, case closed.