I have been having fun with Advent of Code recently. I only started playing in 2019 (and didn’t finish back then), so I decided to go back to previous years to solve old puzzles for fun. And while powering through year 2017, I’ve ended up using the with JavaScript statement for the very first time. Worth a few lines!

The problem

Day 8 of 2017 has a very straightforward problem statement. Given a set of instructions like the ones below, figure out the maximum value reached by any variable (called “registers”). Quoting directly from the manual:

Each instruction consists of several parts: the register to modify, whether to increase or decrease that register's value, the amount by which to increase or decrease it, and a condition. If the condition fails, skip the instruction without modifying the register. The registers all start at 0. The instructions look like this:

b inc 5 if a > 1
a inc 1 if b < 5
c dec -10 if a >= 1
c inc -20 if c == 10

As we can see, all lines are constructed the same way:

  1. The name of a register.
  2. inc or dec to indicate whether to increment or decrement the register.
  3. A numeric value by which to update the register.
  4. A condition in the form of:
    1. The if keyword.
    2. The name of a register (could be the same as #1).
    3. An operator amongst <, >, <=, >= and ==.
    4. A numeric value to compare the register’s value to.

I guess the safe and healthy way to approach this problem is to break down each line into its components as listed above, but this is Advent of Code and it’s the one time we don’t have to be safe and healthy… 😈

Good ol’ eval

Inspecting my input (which is 1,000 expressions, not just 4), the thing that striked me is that it looks kinda like JavaScript. What if — and hear me out — we did the least amount of work to be able to just evaluate the lines as pieces of code?

It would look something like this:

lines.forEach(line => {
const [action, condition] = line.split(' if ')

eval(`if (${condition}) ${action}`)
})

This first tries to execute if (a > 1) b inc 5, which is not valid JavaScript. We need to change these inc and dec for actual operators.

lines.forEach(line => {
const [action, condition] = line.split(' if ')
const operation = action.replace('inc', '+=').replace('dec', '-=')

eval(`if (${condition}) ${operation}`)
})

It now tries to execute if (a > 1) b += 5 at this stage, which is good! We unfortunately have a new error:

a is not defined

Hard to argue with that — it is not defined. One way to solve the problem would be to manually define the variable a (and all others) at the top of our function, but that’s a tad too cumbersome, especially when there are 1,000 instructions with many many different registers.

What if instead of using individual variables, we used an object with dynamic keys? So we would have a single registers object, and then we would read and write keys in it.

const registers = {}

That’s getting us one step closer, but that’s still not enough because a (and other variables) remains undefined. We could prefix variable names with registers. in our expression. This way, we would run if (registers.a > 1) registers.b += 5, which is what we want, but it’s still a little annoying having to do that.

The with statement

Enters with. If you’ve never heard of it, don’t worry because it’s a discouraged feature which happens to be forbidden in strict mode. 😅 What it does is “extending the scope chain for a statement.”

When doing b += 5, JavaScript looks for the variable b in the current scope (like the current block, or the condition, or the function) then goes up the scopes until reaching the global object, looking for the variable called b. What with does is inject the given object in the scope chain, so the lookup also happens there. MDN has a good snippet to illustrate how it works:

// From: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
let a, x, y
const r = 10

with (Math) {
a = PI * r * r
x = r * cos(PI)
y = r * sin(PI / 2)
}

In this case, using PI, cos and sin — which would typically fail because there are no variables named as such — end up working. That’s because the Math object was added to the lookup chain and therefore PI, cos and sin were all found there.

You might see where we’re going with that. If we inject our registers object to our evaluation context, variables like a, b and c will be read in the registers object.

const registers = {}

lines.forEach(line => {
const [action, condition] = line.split(' if ')
const operation = action.replace('inc', '+=').replace('dec', '-=')

with (registers) eval(`if (${condition}) ${operation}`)
})

Wait but, it still doesn’t work. Injecting registers into the scope chain doesn’t do magic though, and a, b and c are still not defined. And even if the interpreter didn’t crash on this, it would try to increment or decrement undefined which would result in NaN.

So we also need to initialize these values to 0. Are we back to square one? Not exactly. We could just capture everything that looks like a variable name in each line and instantiate them to 0 if they’re not already on the registers object.

line.match(/\w+/g).forEach(variable => {
registers[variable] = registers[variable] || 0
})

Keen observers among you might have noticed that this will also capture inc or dec as well as if to which I say: it doesn’t matter? But if we were precious about it, we could look in the condition and the operation exclusively instead:

;(condition + ' ' + operation).match(/\w+/g).forEach(variable => {
registers[variable] = registers[variable] || 0
})

Once the logical nullish assignment operator gets more widely adopted, we can do registers[variable] ??= 0 to define only if not yet present.

And we’re basically done. Now all at once for good measure:

const run = lines => {
const registers = {}

lines.forEach(line => {
const [action, condition] = line.split(' if ')
const operation = action.replace('inc', '+=').replace('dec', '-=')

line.match(/\w+/g).forEach(variable => {
registers[variable] = registers[variable] || 0
})

with (registers) eval(`if (${condition}) ${operation}`)
})

return Math.max(...Object.values(registers))
}

That’s it! 9 lines of JavaScript for the whole puzzle. Not bad I say.