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:
- The name of a register.
incordecto indicate whether to increment or decrement the register.- A numeric value by which to update the register.
- A condition in the form of:
- The
ifkeyword. - The name of a register (could be the same as #1).
- An operator amongst
<,>,<=,>=and==. - A numeric value to compare the registerâs value to.
- The
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(</span><span class="token string">if (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>condition<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>action<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
})
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(</span><span class="token string">if (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>condition<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>operation<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
})
It now tries to execute if (a > 1) b += 5 at this stage, which is good! We unfortunately have a new error:
ais 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(</span><span class="token string">if (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>condition<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>operation<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
})
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
})
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(</span><span class="token string">if (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>condition<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>operation<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
})
return Math.max(...Object.values(registers))
}
Thatâs it! 9 lines of JavaScript for the whole puzzle. Not bad I say.