Permanently Increase World Age in Local Context

Is there a way to permanently increase world age without going to global context?

Most of the world age seems to focus on redefining functions. The problem here is different as it is about dynamically defining a type.

I have a struct which is defined based on the content of a configuration file and read in at program start. The code should run for hours and work with that struct without going through global scope in between. The code involves passing the struct to and from C code, so I think using a struct (instead of a named tuple) might be the only choice.

With simplified versions of creating the struct via genstruct and using the struct via usestruct we get the following.

Example that does not work:

genstruct() = "struct Point; x; y end" |> Meta.parse |> eval
usestruct() = Point(3, 5.6) |> dump

function main()
    genstruct()
    usestruct()
end

main()

Solution ideas

  • Split the program into two programs to first do the code generation and then call the program itself, but that kind of defeats the point of using a dynamic language.
  • Creating the struct in global context obviously works (e.g. with a macro), but this will lead to some additional effort for the user calling this functionality (not seen in the example), which I would like to avoid if possible.
  • Using invokelatest will work, but then the whole program will run in a different world age than the current one and I am not sure about the performance implication.

Example with global context:

genstruct() = "struct Point; x; y end" |> Meta.parse |> eval
usestruct() = Point(3, 5.6) |> dump

function main()
    usestruct()
end

genstruct()
main()

Example with invokelatest

genstruct() = "struct Point; x; y end" |> Meta.parse |> eval
usestruct() = reallyusestruct |> invokelatest
reallyusestruct() = Point(3, 5.6) |> dump

function main()
    genstruct()
    usestruct()
end

main()

It would be simpler to just increase the world age inside of main after genstruct(). I could live with a slow main() function as long as usestruct() runs fast. Is there a way to achieve that.

1 Like

You ahve already listed all good solutions to consider. The reason the function

function main()
    genstruct()
    usestruct()
end

does not work is because the main function does not see the side effects of eval due to being into older world age. To see them, you need to call invokelatest. This is how REPL works.

Since the struct is loaded from a configuration file I presume that it changes rarelly. To support that kind of workflow I would ask the user to call genstruct() themselves in the global scope. It is also possible to put it in the __init__ block for initialization to make the API friendlier.

Note that in all approaches redefining the struct would be an issue so you would likely need to do some name mungling to make it work. Something like throwaway modules which defines interface functions may work out fine.

2 Likes

The right (and only way) to do so is to use invokelatest, indeed invoke latest is such a local world age change and after you return from invoke latest you are back in your original world.

I am not sure what performance implications you are worried about, but I would strongly recommend just using invoke latest, it has almost no performance implications (effectively the cost of a dynamic dispatch)

2 Likes

Would it work for your purpose to use a gensym for the struct and alias it to a consistent variable?

Something like this:

julia> handle = gensym()
Symbol("##225")

julia> eval(:(struct $handle a; b; end))

julia> typeof(handle)
Symbol

julia> name = eval(:($handle))
var"##225"

julia> name
var"##225"

julia> typeof(name)
DataType

julia> name(1, 2)
var"##225"(1, 2)

I find the whole world-age factor somewhat mystifying, so I don’t actually know if this gets around it, but I’m curious if it works for you.

The world-age is kinda the central piece here…

If you try putting the above REPL code you posted in a function you’ll observe it behaving differently:

julia> (function ()
           handle = gensym()
           eval(:(struct $handle a; b; end))
           name = eval(handle)
           name(1, 2)
       end)()
ERROR: MethodError: no method matching var"##233"(::Int64, ::Int64)
The applicable method may be too new: running in world age 31489, while current world is 31490.

Julia functions run in a constant “World age” with a sealed, statically known method table. That means what within a given world age, if you know that function f gets called with arguments of concrete types T and U, then you can statically know exactly what method of f gets called.

The way to advance the world age to get an updated method table is with invokelatest.

julia> (function ()
           handle = gensym()
           eval(:(struct $handle a; b; end))
           name = eval(handle)
           invokelatest() do 
               name(1, 2)
           end
       end)()
var"##234"(1, 2)
2 Likes

Thanks, I find it somewhat less mystifying now. I had gathered the impression that it was about resolving variable names, rather than about access to the method itself. TIL!