Avoiding eval() - struggling with correct macro syntax

I’m again struggling with correct macro syntax. I have a macro that is doing it’s job as required but relies on eval(), which I understand is not ideal.
The macro should insert fields of an existing struct into a new struct that is constructed with Base.@kwdef. For testing purposes, I rely on the @kwredef macro from
RedefStructs.

using RedefStructs

macro mixin(expr)
    x = eval(expr)
    T = typeof(x)
    values = [getfield(x, f) for f in fieldnames(T)]

    output = Expr(:block)
    for (f, type, v) in zip(fieldnames(T), fieldtypes(T), values)
        push!(output.args, :($(esc(f))::$type = $v) )
    end
    
    :($output)
end

@kwredef mutable struct TabMixin
    tab::String = "mail"
    animate::Bool = false
end

@kwredef mutable struct Example
    name::String = "Me"
    button::Bool = true

    @mixin TabMixin()
end

ex = Example()
propertynames(ex)

I tried to move all into the quote part, but I failed…

macro mixin(expr)
    quote
        local x = $expr
        local T = typeof(x)
        local values = [Stipple.Observables.to_value(getfield(x, f)) for f in fieldnames(T)]

        local output = Expr(:block)
        for (f, type, v) in zip(fieldnames(T), fieldtypes(T), values)
            push!(output.args, :( ($(esc(f)))::$type = $v) )
        end
        
        output
    end
end

Can anyone help me here?

I don’t think this can work without the eval because the only info the macro gets is :TabMixin which is just a Symbol and does not contain any information on the type-def which used that same symbol. Going beyond that you’d need something like a generated type (akin to a generated function) which does not exist.

One possibility would be if you define a custom macro upon definition of TableMixin (via decorating it with some other macro), this would mean though that you can only mixin structs which were created thus. Would look something like

@mixinable struct TabMixin
  a
  b
end
# -> this would, besided the typedef create a macro, say @mixin_TabMixin
struct Example
  name
  button
  @mixin_TabMixin
end

This is a similar to the @unpack_MyType macro that Parameters.jl’s @with_kw macro creates. Maybe you can find inspiration there.

1 Like

you can use getfield(__module__, expr) if expr is the name of an unqualified nonparametric type.

If expr can be a parametric type or a qualified type then its a bit more work but still feasible.

Note that this assumes @mixin TabMixin instead of @mixin TabMixin()

Thank you all for your comments. I took me a while to digest and to further work on this.
Finally I succeeded to build at least a version that can handle evaluation at the calling scope, which I previously didn’t know how to achieve.
Today I found out that @mauro3 already pointed me in the right direction with __module__ bit I didn’t get it at that time.
This is what I ended up with:

macro mixin(expr, prefix = "", postfix = "")
  if hasproperty(expr, :head) && expr.head == :(::)
      prefix = string(expr.args[1])
      expr = expr.args[2]
  end

  x = Core.eval(__module__, expr)
  pre = Core.eval(__module__, prefix)
  post = Core.eval(__module__, postfix)

  T = x isa DataType ? x : typeof(x)
  mix = x isa DataType ? x() : x
  values = getfield.(Ref(mix), fieldnames(T))
  output = quote end
  for (f, type, v) in zip(Symbol.(pre, fieldnames(T), post), fieldtypes(T), values)
      push!(output.args, :($f::$type = Stipple._deepcopy($v)) )
  end

  esc(:($output))
end