Why aren't expressions such as `isinteger` simplified in case that the result is inferrable from the arguments?

julia> @code_typed (n -> isinteger(n+0.5))(2)
CodeInfo(
1 ─ %1 = Base.sitofp(Float64, n)::Float64
│   %2 = Base.add_float(%1, 0.5)::Float64
│   %3 = Base.trunc_llvm(%2)::Float64
│   %4 = Base.sub_float(%2, %3)::Float64
│   %5 = Base.eq_float(%4, 0.0)::Bool
│   %6 = Base.and_int(%5, true)::Bool
│   %7 = Base.and_int(%6, true)::Bool
└──      return %7
) => Bool

Given that n is an integer, and the constant 0.5, it should be possible to infer that the n+0.5 isn’t an integer. I wonder this simplification isn’t carried out here, although the compiler does propagate constants if n is a known at compile-time as well?

julia> @code_typed (() -> (n = 1; isinteger(n+0.5)))()
CodeInfo(
1 ─     return false
) => Bool

Ah well,

julia> isinteger(typemax(Int) + 0.5)
true

Although,

julia> isinteger(typemax(Int32) + 0.5)
false

julia> isinteger(typemin(Int32) + 0.5)
false

but this is still not inferred

julia> @code_typed (n -> isinteger(n+0.5))(Int32(1))
CodeInfo(
1 ─ %1 = Base.sitofp(Float64, n)::Float64
│   %2 = Base.add_float(%1, 0.5)::Float64
│   %3 = Base.trunc_llvm(%2)::Float64
│   %4 = Base.sub_float(%2, %3)::Float64
│   %5 = Base.eq_float(%4, 0.0)::Bool
│   %6 = Base.and_int(%5, true)::Bool
│   %7 = Base.and_int(%6, true)::Bool
└──      return %7
) => Bool
3 Likes

There are some corner cases where the value of isinteger can be predicted, but it’s a pretty tricky interaction between the eps(x) graph for the float type, the range of the integer type, and the specific float constant that’s being added. Let’s say we have this function:

f(x::AbstractFloat, n::Integer) = isinteger(x + n)

and we say F = typeof(x) and T = typeof(n). When could we constant fold based on the value of x? It’s a pretty complex set of rules that LLVM doesn’t know.

6 Likes

Can it truly be known that n+0.5 is not an integer?

julia> a+b = round(Int, a)*round(Int, b)
+ (generic function with 1 method)

julia> 1.5+2.3
4

julia> isinteger(1.5+2.3)
true

:grin:

I’m referring to Base.+(n, 0.5) for an integer n

Mathematically yea, but it fails when a value is big enough because floats stop being able to represent non-integral values.

1 Like

:thinking:

julia> Base.:+(a::Int, b::Float64) = a+round(Int, b)

julia> n=2
2

julia> isinteger(n+0.5)
true

This is type-piracy, which I’m assuming isn’t happening. In any case, the point not about methods being overwritten, it’s about the method as defined in Base not being able to infer the result. I’m not talking about a general case where I don’t know the method that’s called, given the arguments.

1 Like

Sorry, you’re right; even with my type piracy, the types are inferred and constants propagated. My input unfortunately degraded the conversation’s SNR.

julia> @code_typed (() -> (n = 1; isinteger(n+0.5)))()
CodeInfo(
1 ─     return false
) => Bool

julia> Base.:+(a::Int, b::Float64) = a+round(Int, b)

julia> @code_typed (() -> (n = 1; isinteger(n+0.5)))()
CodeInfo(
1 ─     return true
) => Bool

Whereas when n is an argument to the function, rather than being locally declared (fresh Julia process):

julia> @code_typed ((n::Int) -> (isinteger(n::Int+0.5)))(1::Int)
CodeInfo(
1 ─ %1 = Base.sitofp(Float64, n)::Float64
│   %2 = Base.add_float(%1, 0.5)::Float64
│   %3 = Base.trunc_llvm(%2)::Float64
│   %4 = Base.sub_float(%2, %3)::Float64
│   %5 = Base.eq_float(%4, 0.0)::Bool
│   %6 = Base.and_int(%5, true)::Bool
│   %7 = Base.and_int(%6, true)::Bool
└──      return %7
) => Bool

julia> Base.:+(a::Int, b::Float64) = a+round(Int, b)

julia> @code_typed ((n) -> (isinteger(n+0.5)))(1)
CodeInfo(
1 ─     return true
) => Bool

even with all the type annotations, it doesn’t work the same because