Confusing difference: ^literal vs ^variable

I’m confused by the difference when ^ is used with a literal vs a variable:

julia> p = -13;

julia> 3 ^ p
ERROR: DomainError with -13:
Cannot raise an integer x to a negative power -13.
Make x a float by adding a zero decimal (e.g., 2.0^-13 instead of 2^-13), or write 1/x^13, float(x)^-13, or (x//1)^-13
Stacktrace:
 [1] throw_domerr_powbysq(::Int64, ::Int64) at ./intfuncs.jl:176
 [2] power_by_squaring(::Int64, ::Int64) at ./intfuncs.jl:196
...

julia> 3 ^ -13
6.272254743863065e-7

julia> @code_llvm 3 ^ p

; Function ^
; Location: intfuncs.jl:220
define i64 @"julia_^_38072"(i64, i64) {
top:
  %2 = call i64 @julia_power_by_squaring_24844(i64 %0, i64 %1)
  ret i64 %2
}

julia> @code_llvm 3 ^ -13

; Function ^
; Location: intfuncs.jl:220
define i64 @"julia_^_38072"(i64, i64) {
top:
  %2 = call i64 @julia_power_by_squaring_24844(i64 %0, i64 %1)
  ret i64 %2
}

It’s surprising to me that although the code should do the same, something magically changes behaviour.

I found various issues concerning this topic, some of them even mentioning this exact behaviour (https://github.com/JuliaLang/julia/pull/20637#issuecomment-280593009) but it’s hard to decipher what has been discussed/decided/implemented/documented.

@StefanKarpinski Could you please explain the current thinking on this topic or link me to the right documentation?

3 Likes

That is intriguing…

julia> f() = 3 ^ -13
f (generic function with 1 method)

julia> g() = (p=-13; 3^p)
g (generic function with 1 method)

julia> @code_warntype f()
Body::Float64
1 1 ─ %1 = $(Expr(:foreigncall, "llvm.pow.f64", Float64, svec(Float64, Float64), :(:llvmcall), 2, 0.3333333333333333, 13.0, 13.0, 0.3333333333333333))::Float64
  └──      return %1                                                                                      │

julia> @code_warntype g()
Body::Union{}
1 1 ─ %1 = π (-13, Core.Compiler.Const(-13, false))                                                                       │
  │        invoke Main.:^(3::Int64, %1::Int64)                                                                            │
  │        $(Expr(:unreachable))                                                                                          │
  └──      $(Expr(:unreachable))

https://github.com/JuliaLang/julia/pull/24240

5 Likes

Is this documented somewhere for new users? (except the news-entry)

That’s a good question. I’m not sure that it is, but it definitely should be. If you grep Julia’s doc directory for literal_pow and don’t for anything, that means that it’s not. If so, please do file a doc issue. I would do it but I’m on a phone and won’t have a chance for a while.

This was a controversial change and there are still many people who are not entirely comfortable with it. The starting point is that people do not really often expect or want x^2 and x^-1 to be pow(x, 2) and pow(x, -1), respectively. Rather, people want x^2 to be syntax for x*x and x^-1 to be a syntax for 1/x. These can both generally be implemented much more efficiently than calling a general power function—but in completely different ways. With sufficiently clever optimizations (constant propagation + power reduction), one can potentailly optimize pow(x, 2) into x*x but it would be even better if we didn’t have to do such a clever optimization in the first place. It’s been a fairly common and successful strategy in the design of Julia to arrange things so that you can get great performance in a straightforward way without need for clever optimizations instead of trying to make the optimizers smarter. So what this design does is it causes x^n where n is a literal integer value to call Base.literal_pow(x, Val(n)), which allows us to specialize the syntax on individual literal values like 2 or -1. This allow us to make x^2 actually mean x*x instead of needing to try to optimize it to that. It also allows us to make negative literal exponents work without introducing a type instability in the general ^ function. And indeed, we used to get regular complaints from new users that 2^-1 doesn’t return 0.5 as they would expect, instead giving them a domain error because of the negative exponent. There are good reasons for it but users don’t care about involved language design reasons, they just want it to work. A way to think about this is that ^2 is its own operator, as is ^3 and ^-1, and so on. Hopefully that helps.

9 Likes

Thx, thats very helpful.
Although ^ is documented I think this could be improved and therefore filed https://github.com/JuliaLang/julia/issues/28685