Metaprogramming: macro calling another macro making named variables

Ooof, I need some metaprogramming help… There is the macro @parameters x = 0.5. The macro, in a simplified sense, does the following: makes a named parameter with name :x and default value 0.5 and returns the container as a custom type Num(:x, 0.5) and assigns this to the variable x. I want to make a new macro @named_parameters vars... to be called like e.g., @named_parameters x y which would apply the @paramaters macro above to two existing variables x, y with real values. It would make two Num(:x, x) and Num(:y, y) and assign that to x, y, using @parameters.

Here is what I have, but it is not correct:

using ModelingToolkit: @parameters

macro named_parameters(vars...)
    expr = Expr(:macrocall, Symbol("@parameters"))
    for v in vars
        push!(expr.args, :($(QuoteNode(v)) = $(esc(v))))
    end
    return expr
end

A = 0.5
B = 0.5
@named_parameters A B # should make the parameters

what would be the “correct way” to do this, if any?

For reference @parameters comes from ModelingToolkit.jl and can be called with multiple parameters as

@parameters begin
   A = 0.3
   B = 0.2
end

which is what inspired the for loop in my macro version.

x = :A
y = :B
@parameters $x $y

and use loops, etc. The interpolation works into it, so you don’t need to metaprogram around it.

That’s not what I want. I want

A = 0.5 # notice its a real number
B = 0.5
@parameters A B # makes parameter A with default value 0.5

in my code A, B are the keyword arguments to functions that provide the default values to parameters that participate into equations. So making them symbols would require lots of boilerplate code. That’s exactly why I want this macro, so that the existing expressions in my code like

x ~ A*y - B

automatically become “parameterized” as A, B are no longer Reals but Nums with default values.

You’ll get better advice if you include the result of @macroexpand on what you have now.

I suggest grabbing the code for @parameters and modifying it to do what you want. It’s legal to use a macro to build up another macro, but it can produce all sorts of strange edge cases which are tricky to reason about.

It sounds like what you want is a small modification of @parameters, the easy way to achieve this is to make small modifications to @parameters.

Right, yes. Well, the macroexpand of the macro I want to “re-create” is:

 @macroexpand @parameters begin
       a = 0.2
       b = 0.2
       end
quote
    a = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}(:a), 0.2), Symbolics.VariableSource, (:parameters, :a))))
    b = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}(:b), 0.2), Symbolics.VariableSource, (:parameters, :b))))
    [a, b]
end

Inspired by this I thought I would make a loop that pushes expressions and then “quotes” them, like so

macro named_parameters(vars...)

    return quote
    out = []
    for v in vars
        res = (ModelingToolkit.toparam)((Symbolics.wrap)((Symbolics.SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}($(QuoteNode(v))), $(esc(v))), Symbolics.VariableSource, (:parameters, $(QuoteNode(v))))))
        push!(out, res)
    end
        $(out...)
    end

end

but alas this doesn’t work either irrespecitvely of if I put the loop inside the quote or not. When inside the loop it says “v” not defined when I run the macro…

Is this approximately what you were looking for?

macro named_params(vars...)
           expr = Expr(:block)
           for var in vars
               binding = esc(var)
               varname = QuoteNode(var)
               push!(expr.args, 
                   :($binding = (; name=$varname, val=$binding) )
                   )
           end
           push!(expr.args, Expr(:vect, esc.(vars)...))
           return expr
       end
@named_params (macro with 1 method)

julia> @macroexpand @named_params A B
quote
    A = (; name = :A, val = A)
    B = (; name = :B, val = B)
    [A, B]
end

julia> function testfun(; a=1, b=2, c=3)
           @named_params a b c
       end
testfun (generic function with 1 method)

julia> testfun()
3-element Vector{@NamedTuple{name::Symbol, val::Int64}}:
 (name = :a, val = 1)
 (name = :b, val = 2)
 (name = :c, val = 3)

This is obviously just a demo for using both the name and previously assigned value without actually using the MTK Num.

2 Likes

macrocall takes a hidden argument, so you are better off generating the initial expression with the parser instead of manually (the splatted initial args list is optional):

expr = :(@parameters $(args…))

omg yes hell yeah thank you so much!!! That is exactly what I Was looking for!!! For reference, the final solution is:

macro named_params(vars...)
    expr = Expr(:block)
    for var in vars
        binding = esc(var)
        varname = QuoteNode(var)
        push!(expr.args,
            :($binding = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}($varname), $binding), Symbolics.VariableSource, (:parameters, $varname)))))
            )
    end
    push!(expr.args, Expr(:vect, esc.(vars)...))
    return expr
end