Base.return_types and `Test.@inferred` conditionally failing unlike `@code_warntype`

Experimenting with world age and ran into a weird thing. This is the session that makes sense:

julia> function evalbad(i)
         ex = :(globali() = $i)
         eval(ex) # evaluates new global method at runtime
         globali() # compiles method existing at compile-time
       end
evalbad (generic function with 1 method)

julia> globali() = 1
globali (generic function with 1 method)

julia> map([Bool, Int32, Int64, Float32, Float64, ComplexF64, String]) do i
         Base.return_types(evalbad, Tuple{i})[1] end |> println
DataType[Int64, Int64, Int64, Int64, Int64, Int64, Int64]

But in a session where the world-age-issue method defined the global method, Base.return_types fails for Int64 specifically. Test.@inferred then also fails for that input (didn’t try other inputs because it would execute the call). @code_warntype works; Test.@inferred only works after the @code_warntype; Base.return_types only works after both @code_warntype and Test.@inferred. I knew that reflection can diverge from actually compiled code because of incorrect assumptions of input type specialization, but this seems different because of the diverging side effects, like type inference isn’t one algorithm.

julia> function evalbad(i)
         ex = :(globali() = $i)
         eval(ex) # evaluates new global method at runtime
         globali() # compiles method existing at compile-time
       end
evalbad (generic function with 1 method)

julia> evalbad(1)
ERROR: MethodError: no method matching globali()
The applicable method may be too new: running in world age...

julia> globali() # definitely exists
1

julia> map([Bool, Int32, Int64, Float32, Float64, ComplexF64, String]) do i
         Base.return_types(evalbad, Tuple{i})[1] end |> println
DataType[Int64, Int64, Any, Int64, Int64, Int64, Int64]

julia> using Test; @inferred evalbad(2)
ERROR: return type Int64 does not match inferred return type Any...

julia> @inferred evalbad(3)
ERROR: return type Int64 does not match inferred return type Any...

julia> @code_warntype evalbad(4)
MethodInstance for evalbad(::Int64)
...
Body::Int64
...
│   %5 = Main.globali()::Core.Const(3)
...

julia> map([Bool, Int32, Int64, Float32, Float64, ComplexF64, String]) do i
         Base.return_types(evalbad, Tuple{i})[1] end |> println
DataType[Int64, Int64, Any, Int64, Int64, Int64, Int64]

julia> @inferred evalbad(5) # now it works
3

julia> map([Bool, Int32, Int64, Float32, Float64, ComplexF64, String]) do i
         Base.return_types(evalbad, Tuple{i})[1] end |> println
DataType[Int64, Int64, Int64, Int64, Int64, Int64, Int64]

On v1.10.3.

I think
ex = :(globali() = $i)
creates the globali in another scope or it creates a “symbol” function name in that scope…

ex = :(Main.globali() = $i)
should work.

Also I think this is something where I tried to make situations to be understood for myself: GitHub - Cvikli/JuliaMacroCheatSheet: Macro CheatSheet

Making the Expr instance itself doesn’t specify or do anything to any global scope, and the eval evaluates it in Main. That’s why the globali() call works in the 2nd session. This does not involve macro hygiene choosing whether symbols are taken from the definition’s module or call’s module.

You are correct. I mistaken with macros, eval behaves a little bit different.

Basically what you want to achieve is. Create something in the function’s scope and call it from the function scope.

Because if you would create this the first time in the Main module then it would give us world age problem.

I’m aware of invokelatest, and I ran into this because I was trying out RuntimeGeneratedFunctions.jl. I was atypically using Base.return_types in a loop to contrast type inference among the various options (evalbad, evallatest, rgf), but evidently it’s not reliable. Having elaborated on the context, I want to emphasize that dynamic method evaluation and execution is not the intended topic. This thread is intended for the reflection issue.

1 Like

I know I am not the best at macros and evals. So I definitely think there is a better solution then this.
But this worked for me now:

macro create_globali(t) 
    fn_name=:globali
    :($(esc(fn_name))() = $(esc(t)))
end
function evalbad(i)
    @create_globali i
    globali() # compiles method existing at compile-time
end

That would expand to a local globali() = i at the definition of evalbad, it does not instantiate then eval an expression with a value at each call and thus wouldn’t involve world age issues. I figured and verified that Base.return_types infers it just fine as localmacroexpr(::T)::T.

1 Like