Search code examples
c#lambdaclosureslocal-functions

.net core switch expression incrementing with wrong value


I was playing around with my intcode computer implementation (from advent of code 2019) and found that when I implemented the switch (to select which operation to execute) it was taking the wrong value.

The following code demonstrates this. InstructionPointer had a value of 2, opcode had the value of 6 which means that OpcodeJumpIfFalse will be used. Function Jif() is called just fine, and it returns a value, in my case it was returning 0. Jif() also modified the value of InstructionPointer, changing its value to 9. The InstructionPointer would the be increased by 0 (return value of Jif()) and I would expect its value to be 9, but its value would go back to being 2.

         InstructionPointer += opcode switch
         {
            OpcodeAdd => Add(),
            OpcodeMultiply => Mul(),
            OpcodeInput => Inp(),
            OpcodeOutput => Out(),
            OpcodeJumpIfTrue => Jit(),
            OpcodeJumpIfFalse => Jif(),
            OpcodeLessThan => Let(),
            OpcodeEquals => Equ(),
            OpcodeAdjustRelativeBase => Arb(),
            OpcodeHalt => End(),
            _ => throw new ArgumentOutOfRangeException()
         };

Minimal example which shows same behavior:

         int j = 2;
         int i = 1;

         int A()
         {
            j = 10;
            return 0;
         }

         j += i switch
         {
            1 => A(),
            _ => throw new Exception()
         };

         Console.WriteLine(j);

I did notice that Resharper (in the minimal example) is telling me the assignment is unused in A().

My question is, why does this happen? Is the value of j "captured" before the switch? Is this expected behavior?

For now, I have changed my code to use a temporary variable, which resolves the issue, but I would still like to know what is going on here.


Solution

  • Remember that the compound assignment operator a += b is the same as a = a + b (except that a is evaluated only once), see §7.17.2 of the spec.

    Here's a slightly simpler example which doesn't use a switch, and has the same effect:

    int j = 2;
    
    int A()
    {
        j = 10;
        return 0;
    }
    
    j = j + A();
    

    If you don't want to think in terms of local functions, you can also write this as a class:

    class C
    {
        private int j;
    
        public void Test()
        {
            j = 2;
            j = j + A();
        }
    
        private int A()
        {
            j = 10;
            return 0;
        }
    }
    

    The compiler will:

    1. Start by evaluating j + A():
      1. Push the current value of j, which is 2, onto the stack
      2. Call A(), which sets j to 10 and returns 0
      3. Push the return value of A(), which is 0, onto the stack
      4. Add together the two values on the stack: 2 + 0
    2. Assign this value to j

    If you write the assignment the other way around, as j = A() + j, then its final value is 10. If you follow the same sequence of steps as above, you'll see why.


    This general approach -- changing a variable as a side-effect of an expression which also changes that variable -- is a bad idea. It leads to very hard-to-understand code.