Yes, I can replicate the behavior you mentioned. I would guess that when there are multiple function calls in the arguments, they are evaluated right-to-left, the simple a + 0
is not enough to force the compiler to adopt some ordering. I am currently leaning to what you defended and actually have the Julia manual say that each subexpression is evaluated left-to-right (or right-to-left, the exact order does not matter) and bindings alone also count as expressions evaluating in this order.
I often avoid doing such, but as this thread has pieces of code that are behaving clearly arbitrarily (and changing between versions, or when the binding is typed, what should not matter) and the manual does not seem to offer much guidance, I will ping @jeff.bezanson and @StefanKarpinski
This seems indeed a bit odd and reminds me of the quote from On Lisp:
In Common Lisp, the arguments to a function are evaluated left-to-right. In
Scheme, the order of evaluation is deliberately unspecified. (And implementors
delight in surprising those who forget this.)
At least for global variables, Julia seems to be in the latter camp:
julia> fun(a, b, c, d) = @show (a, b, c, d)
fun (generic function with 2 methods)
julia> a = 1; fun(a, a += 1, a, a *= 2)
(a, b, c, d) = (4, 2, 4, 4)
(4, 2, 4, 4)
Interestingly, any compound expression seems to force evaluation before side-effects to the right?
julia> a = 1; fun(a, a += 1, identity(a), a *= 2)
(a, b, c, d) = (4, 2, 2, 4)
(4, 2, 2, 4)
julia> a = 1; fun(identity(a), a += 1, a, a *= 2)
(a, b, c, d) = (1, 2, 4, 4)
(1, 2, 4, 4)
I wasnβt able to find out if this is documented or if this is unspecified behavior, but either way itβs 1 reason to not emulate C/C++.
It seems like any expression except a
, really. Assignments or calling functions of the form function blah(aa, ...) ... return aa end
return a value separate from any global variable. However you do have to watch out for every nested function call: a = 1; (a+0, a+0 + begin global a+=1 end, a+0)
. The middle expression calls +(a, 0, begin global a+=1 end)
, and the begin block evaluates before the a
so itβs a 2+0+2.
Itβs even funnier:
julia> a = 1; (a+0, a + 0 + begin global a+=1 end, a+0)
(1, 4, 2)
# Explicitly associate to the left
julia> a = 1; (a+0, (a + 0) + begin global a+=1 end, a+0)
(1, 3, 2)
# Associates to the left already, i.e., 1 - 0 - 2
julia> a = 1; (a+0, a - 0 - begin global a+=1 end, a+0)
(1, -1, 2)
Chains of +
are not binary but -
is. Just seems like another reason for including global variables in left-to-right evaluation.
julia> dump(:(a+b+c))
Expr
head: Symbol call
args: Array{Any}((4,))
1: Symbol +
2: Symbol a
3: Symbol b
4: Symbol c
julia> dump(:(a-b-c))
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol -
2: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol -
2: Symbol a
3: Symbol b
3: Symbol c
Good point, +
and -
already parse differently. In turn, this effects the order in the lowered and typed code as well:
julia> f1(a) = a + 0 + (a += 1)
f1 (generic function with 1 method)
julia> @code_lowered f1(1)
CodeInfo(
1 β a@_3 = a@_2
β %2 = a@_3
β %3 = a@_3 + 1
β a@_3 = %3
β %5 = %2 + 0 + %3
βββ return %5
)
julia> f2(a) = a - 0 - (a += 1)
f2 (generic function with 1 method)
julia> @code_lowered f2(1)
CodeInfo(
1 β a@_3 = a@_2
β %2 = a@_3 - 0
β %3 = a@_3 + 1
β a@_3 = %3
β %5 = %2 - %3
βββ return %5
)
julia> @code_typed f1(1)
CodeInfo(
1 β %1 = Base.add_int(a@_2, 1)::Int64
β %2 = Base.add_int(a@_2, 0)::Int64
β %3 = Base.add_int(%2, %1)::Int64
βββ return %3
) => Int64
julia> @code_typed f2(1)
CodeInfo(
1 β %1 = Base.sub_int(a@_2, 0)::Int64
β %2 = Base.add_int(a@_2, 1)::Int64
β %3 = Base.sub_int(%1, %2)::Int64
βββ return %3
) => Int64
Further, global variable lookup is compiled differently:
julia> f1() = begin global a; a + 0 + (a += 1) end
f1 (generic function with 2 methods)
julia> a = 1; @code_lowered f1()
CodeInfo(
1 β nothing
β %2 = Main.a + 1
β %3 = Core.get_binding_type(Main, :a)
β %4 = Base.convert(%3, %2)
β %5 = Core.typeassert(%4, %3)
β Main.a = %5
β %7 = Main.a + 0 + %2
βββ return %7
)
julia> f2() = begin global a; a - 0 - (a += 1) end
f2 (generic function with 2 methods)
julia> a = 1; @code_lowered f2()
CodeInfo(
1 β nothing
β %2 = Main.a - 0
β %3 = Main.a + 1
β %4 = Core.get_binding_type(Main, :a)
β %5 = Base.convert(%4, %3)
β %6 = Core.typeassert(%5, %4)
β Main.a = %6
β %8 = %2 - %3
βββ return %8
)
julia> a = 1; @code_typed f1()
CodeInfo(
1 β %1 = Main.a::Any
β %2 = (%1 + 1)::Any
β (Main.a = %2)::Any
β %4 = Main.a::Any
β %5 = (%4 + 0)::Any
β %6 = (%5 + %2)::Any
βββ return %6
) => Any
julia> a = 1; @code_typed f2()
CodeInfo(
1 β %1 = Main.a::Any
β %2 = (%1 - 0)::Any
β %3 = Main.a::Any
β %4 = (%3 + 1)::Any
β (Main.a = %4)::Any
β %6 = (%2 - %4)::Any
βββ return %6
) => Any
Note to myself: Never ever use side-effects nested in function calls!