Should a reassigning begin block execute before a variable's value is set?

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)
1 Like

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)
1 Like

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!