Unexpected push behavior

Consider the following bit of code:

using Random
# starting point
X₀ = [-1.0,0.0];

X_vals = [X₀]
X = similar(X₀)

n = 5;

@. X = X₀;

for j in 1:n
    @. X = X + randn();
    println(j, ": X[1] = ", X[1], "X[2] = ", X[2])
    push!(X_vals, X)


This outputs:

1: X[1] = -1.6581360512008891X[2] = 0.3837531807164861
2: X[1] = -2.2595570449176217X[2] = 0.17223651755470998
3: X[1] = -0.6808178044351569X[2] = 2.20480043688056
4: X[1] = -0.2847472811211521X[2] = 1.3340975860113324
5: X[1] = -0.9904282830550172X[2] = 2.9718085271757393

6-element Array{Array{Float64,1},1}:
 [-1.0, 0.0]         
 [-0.990428, 2.97181]
 [-0.990428, 2.97181]
 [-0.990428, 2.97181]
 [-0.990428, 2.97181]
 [-0.990428, 2.97181]

As you can see, it seems to only be storing a reference to the X variable, instead of its value. If I instead use push!(X_vals, copy(X)), I get the expected results.

Is this the expected functionality?

Your terminology is a bit off for Julia semantics. There are no “references to variables” or “values of variables”. All variables can basically be thought of as “pointers” to “objects” living independently of variables that happens to currently bind to them. It is not like in C++ where the variable itself has a specific place in memory and can be mutated. In Julia, assignments are always no-ops, they just create a mapping between a variable (the LHS) and the object (the RHS). Variables themselves can never be mutated, the object they “point” (or bind) to can sometimes be mutated.

Of course, when speaking, one often say things like "the value of b", but what is meant then is the value of the object that b currently binds to.

Hopefully this makes it clearer that push!(X_vals, X) just puts the object pointed to by X into X_vals (and this will always be the same object).