Inspect inferred types and generated codes for inner functions

Often it is convenient to have inner functions, eg for functional ones like mapreduce. How can I apply code inspection macros like @code_llvm or @code_warntype to the “full” function?

Example:

julia> function test(a)
           inner() = a + 3
           inner()
       end
test (generic function with 1 method)

julia> @code_warntype test(3)
Variables
  #self#::Core.Const(test)
  a::Int64
  inner::var"#inner#1"{Int64}

Body::Int64
1 ─ %1 = Main.:(var"#inner#1")::Core.Const(var"#inner#1")
│   %2 = Core.typeof(a)::Core.Const(Int64)
│   %3 = Core.apply_type(%1, %2)::Core.Const(var"#inner#1"{Int64})
│        (inner = %new(%3, a))
│   %5 = (inner)()::Int64
└──      return %5

In that case, I can see that inner returns Int64. But when inner is more complex, it can still have type problems even when the final return type is inferred ok. Also, when one wants to inspect the generated code, how can one do this?

1 Like

One way would be to use the debugger, put a breakpoint right after the definition of the inner function and use @code_warntype from within the debugger.

In practice, I tend to use a more hacky way: manually “exflitrate” the inner function in the global scope, so that it can be inspected from the REPL:

julia> function test(a)
           inner() = a + 3
           global TEST_INNER=inner
       
           inner()
       end
test (generic function with 1 method)

julia> test(3)
6

julia> @code_warntype TEST_INNER()
MethodInstance for (::var"#inner#1"{Int64})()
  from (::var"#inner#1")() in Main at REPL[1]:2
Arguments
  #self#::var"#inner#1"{Int64}
Body::Int64
1 ─ %1 = Core.getfield(#self#, :a)::Int64
│   %2 = (%1 + 3)::Int64
└──      return %2

But none of this entirely satisfies me (*), so I’m also interested in what others have to say about this: maybe there is some tooling that I don’t know about…



(*) one thing that bothers me in particular is that both those techniques require the outer function to actually be executed

2 Likes

Does that still work for functions with closures and inside a module?

Yes, it should. The example above is already a closure (inner closes over a), and adding modules into the mix does change much:

julia> module Foo
           function foo(a)
               inner() = rand() > a ? a : false  # a type-unstable inner function
               global TEST_INNER = inner
       
               inner()
           end
       end
Main.Foo

julia> Foo.foo(2)
false

julia> @code_warntype Foo.TEST_INNER()
MethodInstance for (::Main.Foo.var"#inner#1"{Int64})()
  from (::Main.Foo.var"#inner#1")() in Main.Foo at REPL[1]:4
Arguments
  #self#::Main.Foo.var"#inner#1"{Int64}
Body::Union{Bool, Int64}
1 ─ %1 = Main.Foo.rand()::Float64
│   %2 = Core.getfield(#self#, :a)::Int64
│   %3 = (%1 > %2)::Bool
└──      goto #3 if not %3
2 ─ %5 = Core.getfield(#self#, :a)::Int64
└──      return %5
3 ─      return false

As far as I understand that is what Cthulhu.jl is for?

OK, so maybe I should elaborate: I tried Cthulhu in the past and it seems to me pretty intimidating, a manual process similar to the use of the debugger @ffevotte is describing. So this would be some kind of last resort for me.

I prefer to using a profiler and JET.jl to spot irregularities.

As to the other questions: there are @code_typed, @code_lowered, @code_llvm and @code_native which allow to inspect the whole compilation pipeline.

1 Like

For me, watching this JuliaCon video helped a bunch.

Once you get your head around the menus, it becomes a lot clearer

1 Like