Dazbo's Advent of Code solutions, written in Python
We’re back with the Assembunny computer from Day 12! This time, we have a new instruction: tgl x. This instruction toggles the instruction x instructions away from the current one.
The toggle logic is as follows:
inc becomes dec, and all others become inc.jnz becomes cpy, and all others become jnz.Our goal is to run the Assembunny program and find the value in register a.
With register a starting at 7, what value is left in register a?
My approach is to extend the Computer class from Day 12. I’ll create a new class, Assembunny2, that inherits from Computer and adds the _op_tgl method.
class Assembunny2(Computer):
    def _op_tgl(self, instr_params:list):
        tgl_param = instr_params[0]
        offset = tgl_param if isinstance(tgl_param, int) else self.registers[tgl_param]
        
        if self._ip + offset >= len(self._instructions):
            return
        
        old_instr, params = self._instructions[self._ip + offset]
        if len(params) == 1:
            if old_instr == "inc":
                self._instructions[self._ip + offset][0] = "dec"
            else:
                self._instructions[self._ip + offset][0] = "inc"
        elif len(params) == 2:
            if old_instr == "jnz":
                self._instructions[self._ip + offset][0] = "cpy"
            else:
                self._instructions[self._ip + offset][0] = "jnz"
This method implements the toggle logic as described in the puzzle. The rest of the Computer class remains the same.
With register a starting at 12, what value is left in register a?
Running the program with a set to 12 takes a very long time. This is a strong hint that there’s an inefficient loop in the Assembunny code that needs to be optimized.
By inspecting the instructions, I found patterns that correspond to multiplication and addition. For example, a sequence like inc a, dec b, jnz b -2 is equivalent to a += b. Similarly, there’s a pattern for multiplication.
To optimize this, I created another subclass, MultiplyingAssembunny, which overrides the run_program method. Before running the program, it uses regular expressions to find and replace these inefficient patterns with new, more efficient instructions: add and mul.
class MultiplyingAssembunny(Assembunny2):
    def run_program(self, instructions_input: list):
        new_instructions = self.optimise(instructions_input)
        super().run_program(new_instructions)
    
    def optimise(self, instructions_input: list) -> list[str]:
        code = '\n'.join(line for line in instructions_input)
        replacements = [
            (   # Multiplication
                r'inc ([a-d])\ndec ([a-d])\njnz \2 -2\ndec ([a-d])\njnz \3 -5',
                r'mul \2 \3 \1\ncpy 0 \2\ncpy 0 \3\nnop\nnop',
            ), 
            (   # Addition
                r'inc ([a-d])\ndec ([a-d])\njnz \2 -2',
                r'add \1 \2 \1\ncpy 0 \2\nnop',
            ),
            # ... (other replacements)
        ]
        for pattern, replacement in replacements:
            code = re.sub(pattern, replacement, code)
        return code.split('\n')
        
    def _op_nop(self, instr_params:list): ...
    def _op_add(self, instr_params:list): ...
    def _op_mul(self, instr_params:list): ...
The optimise method converts the list of instructions into a single string, performs the regex substitutions, and then converts the string back into a list of instructions. The new add and mul instructions are implemented as methods in the MultiplyingAssembunny class.
This optimization reduces the execution time from over an hour to under a second.
Part 1: Computer{'a': 13685, 'b': 1, 'c': 0, 'd': 0}
Part 2: MultiplyingAssembunny{'a': 479010245, 'b': 1, 'c': 0, 'd': 0}
Execution time: 0.2121 seconds
This puzzle was a great follow-up to the Assembunny introduction. It required not only implementing a new instruction but also analyzing the program’s logic to find and optimize inefficiencies.