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
}