Day 8: I Heard You Like Registers

Notice: If you have coding experience, I highly recommend to try and solve the puzzle in a language you already know. It's a lot of fun and you'll be able to focus on Go more later on.

Day 8, puzzle 8? You like pogramming, don’t you? Do you like cpu’s and how they work? If you haven’t done so yet, go and read the problem description. Go on, I’ll wait here until you’re ready.

Good, you’re back! Let’s get started. On the agenda for today: More about structs and the switch statement!

If this years questions look anything like last years, then todays question will be the base for future questions. To get a good foundation for those next questions, it feels like a good idea to spend a bit more time on this solution, even though there aren’t many new concepts.

Let’s start with the processing unit. There’s a map of register names to integers and an integer to keep track of the highest value at any point in any register. There is a method to execute instructions. Functions in the same package can use the add method to change the value of a register. Add the Max method finds the maximum value that is currently in the registers.

type CPU struct {
	reg map[string]int

	HighValue int
}

func (cpu *CPU) Max() int {
	max := 0
	for _, v := range cpu.reg {
		if v > max {
			max = v
		}
	}
	return max
}

func (cpu *CPU) Execute(program []instruction) {
	for _, i := range program {
		i.execute(cpu)
	}
}

func (cpu *CPU) add(reg string, val int) {
	cpu.reg[reg] += val
	if cpu.reg[reg] > cpu.HighValue {
		cpu.HighValue = cpu.reg[reg]
	}
}

Okay, this CPU struct should give us all the functionality we need to track the values of the registers. Time to take a look at the instruction object. In CPU.Execute, we’re looping of a slice of instruction objects and calling their execute method with a pointer to the cpu object as argument. In the execute method, we evaluate the instructions condition and update the register value if needed.

type instruction struct {
	reg string
	op  string
	val int
	c   condition
}

func (i instruction) execute(cpu *CPU) {
	if !i.c.evaluate(cpu) {
		return
	}

	switch i.op {
	case "inc":
		cpu.add(i.reg, i.val)
	case "dec":
		cpu.add(i.reg, -i.val)
	}
}

type condition struct {
	reg string
	op  string
	val int
}

func (c condition) evaluate(cpu *CPU) bool {
	switch c.op {
	case "==":
		return cpu.reg[c.reg] == c.val
	case "!=":
		return cpu.reg[c.reg] != c.val
	case "<":
		return cpu.reg[c.reg] < c.val
	case ">":
		return cpu.reg[c.reg] > c.val
	case "<=":
		return cpu.reg[c.reg] <= c.val
	case ">=":
		return cpu.reg[c.reg] >= c.val
	}
	panic("Unknown condition " + c.op)
}

The new construct in both the execute and evaluate functions, is the switch statement. It can be used with any primitive type, including strings. If you have experience witht he switch statement from other languages like Java, you may have noticed that there is no break statement at the end of each case. That’s because the switch statement in Go doesn’t fall through to the next case by default. If you want to fall through to the next case, you have to explicitely add the fallthrough statement at the end of the case.

There’s a little more wiring to read the instructions from the input file, but there’s nothing new there:

func load(fname string) []*instruction {
	file, err := os.Open(fname)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	cmds := []instruction{}

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		i := instruction{c: condition{}}

		fmt.Sscanf(scanner.Text(), "%s %s %d if %s %s %d", &(i.reg), &(i.op), &(i.val), &(i.c.reg), &(i.c.op), &(i.c.val))

		cmds = append(cmds, i)
	}
	return cmds
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Expected the input file name as commandline argument.")
		os.Exit(1)
	}

	cmds := load(os.Args[1])

	cpu := &CPU{}
	cpu.execute(cmds)

	fmt.Println("Part A", cpu.Max())
	fmt.Println("Part B", cpu.HighValue)
}

As usual, my full solution is available in my github repository. See you all tomorrow!