Promote_type in @generated

Hi,

I want to write toggle, a typestable version of the q ? a : b syntax. What I am writing now has the disadvantage of evaluating both a and b for all values of q. A macro might save the day, but this is not the point of my question. Here is my code:

module A
    export toggle
    @generated function toggle(cond::Bool,a::Ta,b::Tb) where{Ta,Tb}
        Tc = promote_type(Ta,Tb)
        return :(cond ? convert($Tc,a) : convert($Tc,b))
    end
end

module B
    export T
    struct T <: Real
        value :: Float64
    end
    Base.promote_rule(::Type{T},::Type{Int}) = T
end

using Main.A
using Main.B 

a = T(3.)
b = 3
@show promote_type(typeof(a),typeof(b))
@show toggle(false,a,b)

So I define my type, and promote rules in module B. I define toggle in module A, that does not (and shall not) import module B. (I want toggle to work for any data type, without having to enumerate the modules defining them in module A).

My code yields

promote_type(typeof(a), typeof(b)) = T
toggle(false, a, b) = 3

So I have the promotion rule right, but toggle is generated in ignorance of it (Tc evaluates to Real)

  • why is toggle unaware of my promote rule? This function gets generated in a context where type T is defined, what did I miss?
  • how can I fix that?
  • Besides, surely Julia has some typestable version of q ? a : b already?

:grinning:

Since functions (including, of course, generated functions) evaluate their arguments, I don’t think this is possible to avoid.

But even if you use a macro, either you have a and b so you can take typeof and promote, or somehow rely on inference, which has its own complications and may not work in general.

I would suggest just calculating a type T and promoting the result. The compiler may be able to deal with the interim type instability of the result just fine when applicable.

1 Like

Is this a variant of the “world age” problem?
I played around a little bit with the code above (to learn) and found this:

julia> @generated function toggle(cond::Bool,a::Ta,b::Tb) where{Ta,Tb}
               Tc = my_promote_type(Ta,Tb)
               return :(cond ? convert($Tc,a) : convert($Tc,b))
           end
toggle (generic function with 1 method)

julia> struct T <: Real
               value :: Float64
           end

julia> my_promote_type(::Type{T},::Type{Int}) = T
my_promote_type (generic function with 1 method)

julia> a = T(3.)
T(3.0)

julia> b = 2
2

julia> my_promote_type(typeof(a),typeof(b))
T

julia> toggle(false,a,b)
ERROR: MethodError: no method matching my_promote_type(::Type{T}, ::Type{Int64})
The applicable method may be too new: running in world age 27830, while current world is 27833.
Closest candidates are:
  my_promote_type(::Type{T}, ::Type{Int64}) at REPL[3]:1 (method too new to be called from this world context.)
Stacktrace:
 [1] #s1#1 at .\REPL[1]:2 [inlined]
 [2] #s1#1(::Any, ::Any, ::Any, ::Any, ::Any, ::Any) at .\none:0
 [3] (::Core.GeneratedFunctionStub)(::Any, ::Vararg{Any,N} where N) at .\boot.jl:527
 [4] top-level scope at REPL[7]:1

I didn’t check with different modules, because the original problem shows that it doesn’t work either if you just ignore modules A and B and put it just into the REPL. Just generic promote_type is called, the promote_rule definition is ignored.

So my guess is, that promote_rule fails over “The applicable method may be too new” too, but no error is shown, because the generic alternative is available.

There are several threads about this problem:
https://discourse.julialang.org/search?q=The%20applicable%20method%20may%20be%20too%20new%3A%20running%20in%20world%20age

1 Like

The generated function docs emphasize the need for purity in the code generating the generated function – including with respect to the method tables.

In your example, there is no reason for toggle to be @generated.

1 Like

Promoting the result is probably the way to go to solve the original problem which brought me to this opportunity to learn. Thanks Thamas.

1 Like

It very much looks like you nailed it. I had heard about “world age” issues before, but never connected it to anything. I’ll follow the links you provide and get wise. Thank you!

2 Likes

I went for generated in order to not introduce runtime work on inferring types. The way I understand you, this is unnecessary because a) a method gets, what was the name, instantiated for each input type combination it gets called with, and b) the promote_type operator is then a no-op. Makes sense.

I reread the doc on generated, and its warnings about “side effects”. I do not understand your remark “including with respect to the method tables”. Would you care to elaborate? Do you maybe mean that the promote_type call may trigger a compilation, which is a side-effect, i.e. impure?

1 Like

Yes. You can look at code_typed:

julia> function toggle(cond::Bool, a::Ta, b::Tb) where{Ta,Tb}
               Tc = promote_type(Ta,Tb)
               cond ? convert(Tc, a) : convert(Tc, b)
       end
toggle (generic function with 1 method)

julia> @code_typed toggle(true, 1, 1f0) # sitofp == Signed Integer TO Floating Point
CodeInfo(
1 ─      goto #3 if not cond
2 ─ %2 = Base.sitofp(Float32, a)::Float32
└──      return %2
3 ─      return b
) => Float32

julia> @code_typed toggle(true, 1, 1.0) # sitofp == Signed Integer TO Floating Point
CodeInfo(
1 ─      goto #3 if not cond
2 ─ %2 = Base.sitofp(Float64, a)::Float64
└──      return %2
3 ─      return b
) => Float64

julia> @code_typed toggle(true, 1f0, 1.0) # fpext == Floating Point EXTend
CodeInfo(
1 ─      goto #3 if not cond
2 ─ %2 = Base.fpext(Base.Float64, a)::Float64
└──      return %2
3 ─      return b
) => Float64

julia> @code_typed toggle(true, one(BigInt), one(UInt32))
CodeInfo(
1 ─      goto #3 if not cond
2 ─      return a
3 ─ %3 = Base.GMP.MPZ.set_ui::typeof(Base.GMP.MPZ.set_ui)
│   %4 = invoke %3(_4::UInt32)::BigInt
└──      return %4
) => BigInt

The compiler has no problem figuring things like this out.

I reread the doc on generated, and its warnings about “side effects”. I do not understand your remark “including with respect to the method tables”. Would you care to elaborate? Do you maybe mean that the promote_type call may trigger a compilation, which is a side-effect, i.e. impure?

I meant the method table itself is global state, but now I think I may have confused this with something said about @pure statements, so please don’t quote me on that.
The documentation however mentions hasmethod, and also says:

Generated functions are only permitted to call functions that were defined before the definition of the generated function. (Failure to follow this may result in getting MethodErrors referring to functions from a future world-age.)

2 Likes

To all of you guys: thank you so much. It’s not the first time I got great help on Discourse, it’s not the first time I got help from you all. It makes a huge difference, both in how fast I learn, and in the fun of doing so. IOU.

THANK YOU TO THE TEAM THAT CREATED JULIA AND PUSHES IT TO MATURITY
THANK YOU TO EVERYONE THAT DEVELOPS GREAT PACKAGES
THANK YOU TO ALL OF YOU WHO TAKE TIME TO ANSWER QUESTIONS, SILLY OR HARD

1 Like