Introduction
The Day 24 problem was not a typical problem. It includes reverse engineering some pseudo-assembly code to predict the output of program. Let’s see how we can deal with some similar problems and how to use programming languages when solving such problems.
Solution
As in most of the “exploitation” problems, we need to come up with some smart observation to solve the problem What I’ve found is the structure of the assembly code, i.e. the fact that it’s built with 14 blocks (each for each input digit) with the same structure
|
|
and when we see how it works, we can notice that only w
and z
initial values are important because
x
and y
are zeroed before usage of them.
So we found some 14 blocks for checking each of the digit of the final code, that transform the z
value
and read the next digit to w
. What we have to find is such a combination of digits for which, after all
transformations, we would end up with the z
equal to 0.
We can try to solve this kind of problems by reverse engineering these simple gadgets and remembering what are
their outputs for every of the combination of given digit and value of z
. So in reverseDigitMappings
we try to
run every of these gadgets for every digit from 1..9
and a huge number of different z
variables in initial state.
It’s worth mentioning here that the selected range for z
was fixed to get a repeatable answer when decreasing
tested ranges, so it may require increasing for some specific inputs.
Having these mapping we can start actual reversing the answer. So we look at the computed values and read from
them the set of pairs of input digit and z
value, for which after transformation of 14th gadget we get 0 in
z
. For these values we also need to solve similar problem, be recursively checking next digits backwards.
We do this with recursive function go
which is defined as recursive because it can have at most 15 levels
of nesting, and it’s the easiest way of remembering the list of digits that we’ve tried along our
current path of searching.
To solve both parts of the problem with the same code, we define findDigits
with Comparator<Long>
parameter,
so we can select an order of searching through the digits in each step. In this way we’re able to find the
solution in a few seconds, where most of the time is used for generating the reverse mappings, so it can be reduced
because the gadgets blocks are repeating in input and some computations are not needed at all (but we leave them
for simpler code).
Day24.kt
|
|
Extra notes
We’ve used in our solution some use of sealed interface
as well as the enum class
so it’s worth mentioning
what’s the actual difference between them and where we should use every of them.
So the enum class
es in Kotlin are similar to the enums from C
-like languages as they are some kind of
fixed, singleton objects of specified type that hold some type information. Then can of course also have extra
methods and override the others, but this kind of code becomes quite hard to read. From my experience, they
should be used when some kind of label is needed, so we could take some specific actions in different
contexts for the labels.
In case of the sealed interface
s we get somehow similar possibilities, but the classes that implement this
kind of interfaces are intended to hold some values. In Kotlin, the compiler is pretty smart, so we can
use when
statements to check for type of some object and use the fields of some checked class in the
case body, as the checked value is automatically cast to checked type. Let’s take a look e.g. at the ALU::process
method that uses the instr
fields and they are quite different in different cases of when
.