Is there a way to detect and automatically fix type-unstable code?

E.g. lets say you do:

julia> if x % 2 == 0
         x/2
       else
         2x
       end

I believe there is a macro to check, I forget which, but could a macro “conform” the type of both code paths, i.e. here to Float64 (as it’s an (almost) superset)? I think a macro could do it, I’m not sure, as I’m not good with writing macros yet. That would be a step in the right direction, and even better if the compiler could do it automatically for you.

A first step might be to just give you a Union of both types, whatever they are.

That’s just an example, I just used one with a Float64, as more likely do happen in practice (and I know of div, another solution). I’m just thinking this could be a bit problematic for my solution here (there only involving integers):

Note, there I think I resolved all the issues, at least outlined the loop problem in the end. There are no showstoppers there, including this type-instability problem (while annoying and potentially slowing down, not a safety problem).

The macro to check is @code_warntype.

julia> function f(x)
       if x % 2 == 0
                x/2
              else
                2x
              end
       end
f (generic function with 1 method)

julia> @code_warntype f(4)
Variables
  #self#::Core.Const(f)
  x::Int64

Body::Union{Float64, Int64}
1 ─ %1 = (x % 2)::Int64
│   %2 = (%1 == 0)::Bool
└──      goto #3 if not %2
2 ─ %4 = (x / 2)::Float64
└──      return %4
3 ─ %6 = (2 * x)::Int64
└──      return %6
1 Like

no, Numba does this, but it distorts the user intention so I don’t think it’s a good thing

1 Like

You mean Numba does this automatically (without a macro since Python doesn’t have macros), not that a macro in Julia couldn’t be implemented, only that you don’t want it?

But note it would make the intention explicit (I know there are other ways for that, I’m thinking of better ways), and as is, it’s not at all clear what the user wanted, regarding types, can’t be read from the code as it is (or the programmer actually intended for type-stable AND type-unstable). It could be:

julia> @code_warntype f(1.0)
[..]
Body::Float64

what the programmer had in mind, i.e. always called with Float64 and that IS type-stable. But while the code IS general, it’s not type-stable in general. It is type-unstable as I stated, and shown earlier, or in a different way, if the programmer thinks he’s fixing it this way:

julia> function f(x)
         if x % 2 == 0
           x/2
         else
           2.0x
         end
       end

then instead type-unstable for e.g. Float32 input (also Float16, while not for Float64 and all integers):

@code_warntype f(2.0f0)
[..]
Body::Union{Float32, Float64}

and:

julia> @code_warntype f(Rational(2))
MethodInstance for f(::Rational{Int64})
  from f(x) in Main at REPL[63]:1
Arguments
  #self#::Core.Const(f)
  x::Rational{Int64}
Body::Union{Rational{Int64}, Float64}
1 ─ %1 = (x % 2)::Rational{Int64}
│   %2 = (%1 == 0)::Bool
└──      goto #3 if not %2
2 ─ %4 = (x / 2)::Rational{Int64}
└──      return %4
3 ─ %6 = (2.0 * x)::Float64
└──      return %6
In [5]: @numba.njit
   ...: def f(x):
   ...:     if x>1:
   ...:         return 1
   ...:     else:
   ...:         return 1.0
   ...: 

In [6]: f(2)
Out[6]: 1.0

In [7]: f(0.1)
Out[7]: 1.0

I mean you can already do this by:

julia> function f(x)::Float64
       if x % 2 == 0
                x/2
              else
                2x
              end
       end
1 Like