How is `x::T` lowered? (the actual post)

I’d like to understand what the compiler does with code like

function f(x)
    local y :: typeof(x)
    y = x
end

The docstring for typeassert says “The syntax x::type calls this function.”. But this isn’t just a simple substitution, since y isn’t defined until later. You can see this in the lowered code, which is entirely rearranged:

julia> @code_lowered f(3)
CodeInfo(
1 ─ %1 = x
│   %2 = Main.typeof(x)
│   %3 = Base.convert(%2, %1)
│        y = Core.typeassert(%3, %2)
└──      return %1
)

So how does this lowering actually happen?

For context, I’m trying to be careful about this sort of thing in Tilde, where it can be important for type stability. It would also be very useful for GeneralizedGenerated.jl to handle this idiom. Currently it can’t (see https://github.com/JuliaStaging/GeneralizedGenerated.jl/issues/71).

cc @thautwarm

2 Likes

This is not a type assertion, it’s a variable declaration. As explained in the manual:

When appended to a variable on the left-hand side of an assignment, or as part of a local declaration, the :: operator means something a bit different: it declares the variable to always have the specified type, like a type declaration in a statically-typed language such as C. Every value assigned to the variable will be converted to the declared type using convert.

5 Likes

Right, but it’s implemented using typeassert, at least in this case.

Anyway, what I’m more curious about is the reordering. In the original code, the declaration comes first, followed by an assignment. I always thought lowered was very conservative in the rewrites it does, and I’m surprised to see things being reordered like this.

I know lowered rewrites for loops, and there are nice overviews like this describing how this works. I was just hoping for something similar to better understand what happens with declarations.

It’s implemented by calling y = convert(T, ...)::T every time you assign to y in the lowered code. The reason for the ::T assertion is to ensure that the compiler gets the correct type of y, in case the convert call is type-unstable (e.g. a buggy convert implementation). In the usual case where the convert call is type-stable and inferred, the ::T assertion is optimized out.

This is clearer if you do multiple assignments to the typed local variable in your function:

julia> function f(x)
           local y::Float64
           y = x
           y = y + x
       end
f (generic function with 1 method)

julia> @code_lowered f(3)
CodeInfo(
1 ─ %1 = x
│   %2 = Base.convert(Main.Float64, %1)
│        y = Core.typeassert(%2, Main.Float64)
│   %4 = y + x
│   %5 = Base.convert(Main.Float64, %4)
│        y = Core.typeassert(%5, Main.Float64)
└──      return %4
)

Note that all of the type assertions are eventually optimized away:

julia> @code_llvm debuginfo=:none f(3)
define double @julia_f_362(i64 signext %0) #0 {
top:
  %1 = sitofp i64 %0 to double
  %2 = fadd double %1, %1
  ret double %2
}
9 Likes

I see, that makes a lot of sense now. Thanks!