Each language is free to define its own rules. You mentioned C, and in C, i = i++
is undefined behavior. This allows compiler writers to either not support this expression, or to decide how it should be translated. People will run to their compiler, test a program, then post an answer based on their particular compiler. Most compiler writers prefer to compile the expression in some sane way and may even document how their particular implementation defines the Undefined Behaviour (UB), but only applies for that implementation, and maybe even that version. The only way to use features classified as UB is to avoid them; the syntax is practically not legal, even though it compiles.
C#, on the other hand, chose to define the behavior, and handles it the way I feel is natural for a stack based implementation (of course there are other views on what is natural and intuitive, hence the variance across languages) though nothing says that C# has to run on the CLR or a stack based implementation.
Rather than use C#, I'll use my Cola .NET compiler which has some similarities to C#, and I chose to use the same rules for prefix and postfix increment.
If you can read stack based opcodes, here is how I implement it, and it is identical in C# as far as I know.
Assuming:
int i = 3;
Here is how the following line compiles:
i = i++;
i++ evaluates to i prior to the increment because i++ is a post increment operation. So we store the original value of i into a temporary (in .NET CLR we store it on the stack). Since the eventual assignment to left hand side will happen after everything else, and the value we will assign is "frozen" at the beginning before the increment, it doesn't matter what happens to i in the process of the increment, we will stomp on it at the end.
ldloc 'i' // push pre-increment value (3) to be assigned to left side
ldloc 'i' // push pre-increment value (3) for the increment expression
ldc.i4.1 // push constant 1, the amount to increment by
add // Do the increment, add top 2 operands, resulting in 4 on stack
stloc 'i' // pop top of stack (4) into i, now the increment is complete and i is 4
stloc 'i' // pop the top of stack (3), final assignment to i on left hand side
The i of 4 is a ghost value that exists only temporarily.
It is simpler if you change the left hand side to k, but the end result in i will actually be different:
k = i++;
Since we only assign to i once, there is no lost or ghost value.
ldloc 'i'
ldloc 'i'
ldc.i4.1
add
stloc 'i' // pop top of stack (4) into 'i', now the increment is complete and 'i' is 4
stloc 'k' // pop the top of stack (3) off and assigns to 'k' on left hand side
Now k == 3 and i == 4