Advent of Code 2020 Day 18
[2020-12-18]
#development #adventofcode #julia
Previous: Advent of Code 2020 Day 17
Next: Advent of Code 2020 Day 19
Part 1
It seems the first example of language parsing is writing a basic four-function calculator… not really remembering any of that, I’m going to write a recursive state machine, with an accumulator and three states:
- Scanning for next token. If we encounter a digit or open paren, change state. If an operator, store it.
- In a number. Once it ends (encounter whitespace or close paren, or reach the end of the expression), we apply it to the accumulator with the saved operator.
- In a paren block of depth n (each encountered open paren, including the first one, while in this state increases depth by one, close parens decrease by one). Once depth is 0 we recurse on the internal expression, applying the result to accumulator. If we’re in this state at the end of the expression it’s an error.
State machines can be tricky, since it’s easy to get into an unexpected state,
or forget a state change, or forget to clean up based on the state it’s in at
the end. I had to deal with a couple of those, using liberal @debug
statements. Then there was a stack overflow, since I was recursing with the
parenthentical expression including parens. But overall pretty straightforward
following the outlined logic.
Part 2
Well I suppose now I do have to fully parse the expression into an AST and then evaluate it. I’ll store each node in a mutable struct:
mutable struct Node
expression::AbstractString
left::Union{Node, Nothing}
right::Union{Node, Nothing}
op::Union{Function, Nothing}
end
The parser will be a simplified version of the state machine from part 1:
- Scanning for next token. If we encounter an asterisk then set
op
to*
and everything before it goes intoleft
and everything after goes intoright
for further parsing. If we encounter a plus then note its location. If an open paren then change state. - In a paren block of depth n. If a closing paren decrease depth. If depth now
is 0, set
left
to everything enclosed in the top-level parens (this may be overwritten later) and go back to scanning.
We need to do some cleanup at the end of parsing if op
is nothing
:
- If we noted a location for a plus then set
op
to+
and everything before it goes intoleft
and everything after goes intoright
for further parsing. - If we set something for
left
but there’s nothing forright
then it’s a fully enclosed parenthentical, so just promoteleft
.
After parsing we need to eval()
the expression, which is pretty simple:
- If both
left
andright
arenothing
, then we’re at a leaf (a number), so parse it into an integer. - Otherwise call
op
witheval(left)
andeval(right)
as arguments.
I’m sure there’s easier/better ways to do this, but I’m happy I was able to work it out. It did take quite a bit of trial-and-error with test cases though. And worse, all the test cases passed but the answer was wrong! I ended up grabbing someone’s solution code from r/adventofcode and having it print out the answer for every line of my test input (without looking at the logic. No, really!), then comparing to my output. I then turned one of the wrong expressions into another test case. It was pretty clear what was wrong - I wasn’t allowing multiple sub-expressions to be on the left - which was actually from over-complicated logic.
Next: Advent of Code 2020 Day 19