SciML / RuntimeGeneratedFunctions.jl

Functions generated at runtime without world-age issues or overhead
https://docs.sciml.ai/RuntimeGeneratedFunctions/stable/
MIT License
100 stars 14 forks source link

Can't use `:block` #35

Closed dpsanders closed 2 years ago

dpsanders commented 3 years ago
julia> ex = Expr(:block, :(x=3))
quote
    x = 3
end

julia> eval(ex)   # works fine
3

julia> @RuntimeGeneratedFunction(ex)
ERROR: ArgumentError: Function definition contains invalid function head `:block`
Expr
  head: Symbol block
  args: Array{Any}((1,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol x
        2: Int64 3
valentinsulzer commented 2 years ago

Same issue for let:


julia> ex = :(let a = 2
       function f(x::Int)
           return x + a
       end
       end)
:(let a = 2
      #= REPL[6]:2 =#
      function f(x::Int)
          #= REPL[6]:2 =#
          #= REPL[6]:3 =#
          return x + a
      end
  end)

julia> eval(ex)(2)
4

julia> @RuntimeGeneratedFunction(ex)(2)
ERROR: ArgumentError: Function definition contains invalid function head `:let`
Expr
  head: Symbol let
  args: Array{Any}((2,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol a
        2: Int64 2
    2: Expr
      head: Symbol block
      args: Array{Any}((2,))
        1: LineNumberNode
          line: Int64 2
          file: Symbol REPL[6]
        2: Expr
          head: Symbol function
          args: Array{Any}((2,))
            1: Expr
              head: Symbol call
              args: Array{Any}((2,))
                1: Symbol f
                2: Expr
            2: Expr
              head: Symbol block
              args: Array{Any}((3,))
                1: LineNumberNode
                2: LineNumberNode
                3: Expr
ChrisRackauckas commented 2 years ago

@c42f is this solvable?

c42f commented 2 years ago

The block example isn't a function. So I'm not sure what it's even meant to do?

The let example defines a closure — it could be supported automatically, but there's a much more straightforward way — define the RGF with an extra parameter a, then use a normal Julia closure to call the RGF:

julia> using RuntimeGeneratedFunctions

julia> RuntimeGeneratedFunctions.init(@__MODULE__)

julia> ex = :(function g(a, x)
           return x + a
       end)
       g = @RuntimeGeneratedFunction(ex)

julia> f = let a = 2
           x -> g(a, x)
       end
#12 (generic function with 1 method)

julia> f(2)
4
valentinsulzer commented 2 years ago

Thanks @c42f . The way I'm currently doing things, the entire let closure is generated as a string (by a python package) and then I apply Meta.parse and evaluate. In particular the constants that go into the let block (a bunch of sparse matrices and vectors) are also generated. I can easily generate the string that goes into the let block (e.g. a=2) and the string for the internal function separately, but I'm not sure how to adapt the code to "evaluate" the string that goes into the let block in this case?

c42f commented 2 years ago

I can easily generate the string that goes into the let block (e.g. a=2)

Ok, so presuming you've only got strings coming from python and you can't modify these, you can still extract the variable names as follows:

julia> bindings_str = "a=1, b=2, c=3"
"a=1, b=2, c=3"

julia> binding_names = [a.args[1] for a in Meta.parse("($bindings_str)").args]
3-element Vector{Symbol}:
 :a
 :b
 :c

Then you need to construct the RGF function argument list to include these extra variables as parameters. There is a subtle issue with this scheme when your bindings_str is dynamically generated — you need a fixed wrapping closure to avoid world age issues.

This can be done as follows:

using RuntimeGeneratedFunctions
RuntimeGeneratedFunctions.init(@__MODULE__)

function make_rgf(bindings_str, body_str)
    binding_names = [a.args[1] for a in Meta.parse("($bindings_str)").args]
    main_body = Meta.parse("begin\n$body_str\nend")

    param_tuple = eval(Meta.parse("($bindings_str)"))

    ex = :(function f(params, x)
        ($(binding_names...),) = params
        $main_body
    end)

    @info "Generating RGF" param_tuple ex

    let params=param_tuple
        g = @RuntimeGeneratedFunction(ex)
        (x)->g(params, x)
    end
end

Running this code, we have

julia> bindings_str_from_python = "a=1, b=2, c=3"
"a=1, b=2, c=3"

julia> body_str_from_python = "y = x*x\nreturn y + a + b"
"y = x*x\nreturn y + a + b"

julia> f = make_rgf(bindings_str_from_python, body_str_from_python)
┌ Info: Generating RGF
│   param_tuple = (a = 1, b = 2, c = 3)
│   ex =
│    :(function f(params, x)
│          #= /home/chris/rgf_test.jl:10 =#
│          #= /home/chris/rgf_test.jl:11 =#
│          (a, b, c) = params
│          #= /home/chris/rgf_test.jl:12 =#
│          begin
│              #= none:2 =#
│              y = x * x
│              #= none:3 =#
│              return y + a + b
│          end
└      end)
#21 (generic function with 1 method)

julia> f(2)
7
valentinsulzer commented 2 years ago

Thanks! I will try this out