What matters is the aliasing semantics, not how something is passed. If you pass a ref or ptr to an object, then changes to the object's fields will be seen by the caller. Ditto for var parameters--a change to the parameter, and to its fields if it is an object, will be seen in the caller's version. For non-var parameters, let the compiler figure out which is best, pass by address or pass by copy.
Assignments are copies, either of a ref or ptr to an object, in which case you're creating an alias, or of an object, in which case you're copying the whole object. Normally such a copy is deep (copied refs/ptrs within the object become refs/ptrs to copies), but you can make it shallow with `shallowCopy` (this will change when the language adopts move semantics). A tricky case is when a proc has a var return type (e.g., the `mitems` iterator, or `tables.mgetOrPut`). The return value is aliased, so modifying it modifies the original (e.g., the elements of `seq` iterated over, or the table entry), but if you assign it you've made a copy and are no longer accessing the original. e.g., # for key not already in tab mgetOrPut(tab, key, newEnt).field = 3 # sets newEnt.field var ent = mgetOrPut(tab, key, newEnt) ent.field = 3 # does not set newEnt.field; only changes the copy in ent Run If you want to do complex operations on a value returned by var, then pass it to a proc with a var parameter, which provides the needed aliasing: proc messWithEnt(ent: var MyEnt) = ent.field = 3 # changes caller's version ... messWithEnt(mgetOrPut(tab, key, newEnt)) # sets newEnt.field Run (Note: a better API design than `mgetOrPut` would be `getOrPut[K,V](tab: Table[K,V], key: K, getValIfKeyNotPresent: proc (): V)` which avoids having to create a new object before knowing whether it is needed.)