Eval a string with runtime defined functions

Hello, how to make this work? And if possible, work performantly.

julia> function foo(str, n)
           f(x) = x+n
           eval(Meta.parse(str))
       end
foo (generic function with 2 methods)

julia> foo("f(1)", 5)
ERROR: UndefVarError: `f` not defined
Stacktrace:
 [1] top-level scope
   @ none:1
 [2] eval
   @ ./boot.jl:385 [inlined]
 [3] eval
   @ ./client.jl:491 [inlined]
 [4] foo(str::String, n::Int64)
   @ Main ./REPL[6]:3
 [5] top-level scope
   @ REPL[7]:1
1 Like
julia> function foo(str, n)
           expr = Meta.parse(str)
           eval(quote f(x)=x+$n; $expr end)
       end
foo (generic function with 2 methods)

julia> foo("f(1)", 5)
6

julia> f(1)
6

I find this works, but the function f is defined to global scope, which is not good.

1 Like

Generally speaking, you don’t. We recommend very strongly against string-based metaprogramming here. There is usually a better, different way to accomplish these sorts of goals, but we’d need to know your actual usecase to give clearer advice than that.

eval always operates in the global scope, and most of julia’s performance optimizations would not be possible if it was able to access the local scope.

6 Likes

Hello, thank you for your advice. I’m writing an application that need to parse some math expressions from user input. Which involve a custom function, say foo(x), whose behavior is context dependent. I want to reuse julia’s parser so that I need not to write one myself.

If you’re using Julia’s parser, then the user inputs must be Julia expressions. Why not distribute these custom functions in a package to the users so they can import it and execute the math expressions in a REPL, or even use it to write their own scripts?

2 Likes

I’d be interested in hearing more about why that is. I would have thought given this:

julia> shmeval = eval
eval (generic function with 1 method)

julia> shmeval
eval (generic function with 1 method)

That the compiler doesn’t have to harbor the suspicion that any old function call might be eval in disguise. It knows eval. Why would allowing use of eval in local scope disturb performance optimizations in local scopes where it isn’t invoked?

The problem with eval is that it changes the definition of what an arbitrary function does at the time it is called. Pretty much every optimization a compiler does involves being able to change the time when an operation happens. If you have a local eval in the language, you need to be able to prove that a function never calls eval anywhere in it’s call graph in order to be able to optimize anything about the function.

2 Likes

On a lower level, the native code completely lacks any concept of Julia variables and manipulates offsets in a stack frame or addresses on the heap. An eval call inside a local scope actually working within that local scope like the rest of the code would require jumps to arbitrary instructions and a very dynamically sized stack frame. You would have to retain the concept of variables at that level, like storing a dictionary of symbols to values. Type inference would be impossible, in other words the local variables can only be inferred as ::Any when compiling the method:.

function foo(string::String)
  a = 1
  localeval(string)
  a
end

foo("a = 1im; a = 2.5; a = false") # local a is not just Int

Besides preventing almost every compiler optimization, it’s also a nightmare to work with. foo("a + b") would throw an undefined variable error, and nobody wants to track down method bodies to figure out the local scope. Even the less optimized dynamic languages don’t do this.

Ok, I’m with you so far. Something like

function slow_fn(a,to_eval)
     x = 5
     y = local_eval(to_eval)
     x *= a
     return return x + y
end

This function can do anything to x or a, nothing can be assumed about y, it’s pessimal.

This is what I don’t get.

function deep_eval_call(a...)
    acc = Any[]
    push!(acc, slow_fn(acc[end], "code goes here")
    for i in 1:10000
         push!(acc, optimizable(i, a...))
    end
    acc
end

I’m not seeing how the eval call in slow_fn, which can only affect the lexical scope it’s defined in, can affect optimizing the code in the rest of the scope. That’s not a disagreement, I just don’t get it yet: it looks like an ordinary type-unstable function call, and those don’t cascade around breaking things.

Okey, I come to a solution that to write a eval function that gives result of an expr myself. I realized I want to reuse the parser not the eval function. Then I can completely customize the function behavior.

1 Like

The problem is that the lexical scope slow_fn is defined in can include the global scope. For a dramatic example, consider

badfun() = local_eval("Base.(+)(a::Int, b::Int) = 1")

function f(x)
    badfun()
    return x + 1
end
f(2)
1 Like

What are you doing here exactly? How would a different eval function help?

But if local_eval somehow disallows undesired effects on the global scope, then it seems right that the only bad effects that slow_fn has on deep_eval_call in mnemnion’s example is the poor return type inference, and the rest of deep_eval_call can be optimized normally. “Undesired global effects” doesn’t seem easy to delineate, and it doesn’t seem worth working with that limitation just to compile the least performant caller methods.

Sorry for not speak clearly. The user input string may contains a custom operator, lets call it ⊗. I need to calculate the value of the expression, but with a runtime generated parameter passed to all function call of ⊗. So once I get the expr, I can walk and calculate it top to bottom, and insert the parameter when I meet operator ⊗.

Edit: I have a even better idea. Let me write this:

function new_param()
    data = 1
    get() = data
    set!(val) = (data = val)
    return (get, set!)
end

get_param, set_param! = new_param()

⊗(a, b; param = get_param()) = ...

function main()
    # Do something
    set_param!(some_value)
    eval(user_str)
end

This new method introduces some global state, I guess it may not be safe when doing async work?

If you just want the expression, you could maybe just use Meta.parse to convert the string to an Expr?

julia> Meta.parse("x + 5 ⊗ 3")
:(x + 5 ⊗ 3)

julia> dump(ans)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Symbol x
    3: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol ⊗
        2: Int64 5
        3: Int64 3

Edit: To expand a bit on this: When you walk that expression tree, for every function call you can just check the symbol in args[1]. If it is your special symbol, you call whatever it should do and else it should be one of Julia’s functions which you can get the function object of via getfield(Main, symbol).

Yes, actually I did exactly as what you say. No problem anymore.