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!