Somewhere at a secret location on the web, is a message you'll need to find. You are given a starting URL, from which you retrieve a piece of plain text, using an HTTP GET request.
This text is source code, written in a mini-language, Krul, that I invented. Your task will be to write an interpreter for it, in order to be able to run the code you just downloaded.
If you run the first piece of code, you'll get a string as result. This string is a file name for the next piece of code to be downloaded. It is to be appended to the base URL for the next request to be successful.
You continue in this fashion, until you encounter a special instruction in the code, which signifies the hidden message is found. The string result of the last piece of code will be that message.
You'll build a command line tool (console application) for this in C++, which should print the message (and intermediate file names) to the standard output.
You build a command line tool (console application) in C++.
You repeatedly download plain text from different URLs, which all
have
https://www.bobpolis.com/krul/
as a base.
The first URL to download code from is formed by appending
“start.txt” to the base URL. So this becomes:
https://www.bobpolis.com/krul/start.txt
You build an interpreter for the Krul language, as described further below.
You run the downloaded program, using your interpreter, which yields a string as a result.
The string you found is the file name of the next program
to be downloaded. Would you have yielded the string “hooray.txt”,
then the next URL to download from will be:
https://www.bobpolis.com/krul/hooray.txt
Print the string you found to the standard output.
Continue downloading and running code, until you encounter the
“end” instruction. In that case the resulting string
is the solution of the puzzle!
The only external library allowed is
libcurl.1
libcurlYou'll use a very popular library, libcurl, for doing
HTTP GET requests
2. This library is available for almost every platform.
It is capable of doing much more than just GET requests: it supports
a plethora of networking protocols, and will make it easy to download
web pages or API data, for example.
libcurl is a C library, so at least you'll need to write
a C++ class to properly manage the connection to your C++ code.
This is your opportunity to show you know how to handle resource management
correctly.
The interpreter you are going to write is not particularly complicated, but there are some nice challenges to get it implemented properly.
Krul uses reverse polish notation (RPN)3.
For example, the following code:
2
3
add
4
mul
yields “20” as a result.
In your implementation you'll use a stack to store values upon,
or retrieve from. It is convenient to use a std::vector
for that.
The example above will be easy to process, then: you push the value
2 onto the stack, then the value 3. Then,
you'll call the function to execute the “add” instruction.
It will pop the two values from the stack, adds them, and puts the result
“5” onto the stack.
Next, 4 will be pushed onto the stack, and the
“mul” instruction is executed. That will cause two values
to be popped from the stack (5 and 4 in this
case), multiplied, and, finally, the result “20” to be
pushed onto the stack.
Please note! If I write: “pop two values from the stack”, it means that they are being read and removed from the stack. If subsequently the result of a calculation is pushed onto the stack, said result effectively replaces the two values we started with.
Please note! If you pop a value from the stack, it will be the last one pushed onto it. A stack works according to the “LIFO”-principle: last in, first out.
You could see the interpreter as a kind of CPU. That is, as a kind of machine which processes instructions, and has certain resources at its disposal to do so.
You'll discover it's convenient to track line numbers with an
integer value (for example, of type size_t), and
to use it as an indicator from which line the next instruction should
be read, and executed. A real CPU has a special register for that
purpose, called the program counter. It always points
to the next memory location from which the next instruction should
be read. This is a simple mechanism, which also enables you to easily
“jump” to another piece of code. All you need to do is to load the
program counter with the location of the next instruction, and the rest
follows suit!
Your interpreter also needs a stack for the values it works with, as descibed in the example above.
Other things you might need will become clear from the description below.
All values on the stack are strings.
This means that numerical calculations always need their values converted to integer first, before actual calculations can take place. Also, numerical results need to be converted to string first, before being pushed onto the stack.
After the last line of code, when there are no instructions left, the program should stop automatically. If the stack is not empty at that point, the last value on the stack is the end result of this Krul program.
42Digits: integer literal. Push the given integer value,
converted to string, onto the stack. The only allowed values are
positive integers, or the value 0. (For negative
values, a positive value is used, followed by a “neg”
instruction.) The number 42 is just an example.
\textText to end of line. Consider all text after the backslash until end of line as a literal string, and push that onto the stack.
:labelLabel definition. Consider all text after the colon until end of line as a label name, and remember it, together with the next line number, to enable you to jump to it later.
>labelLabel reference. Push the line number, represented by the label, onto the stack. Please note: such a label reference could be encountered before you have seen the corresponding label definition. This allows for “jumping ahead”.
=varVariable assignment. Consider all text after the equal sign until end of line as the name of a variable, to which you assign the current value on the stack. Then, remove the value from the stack.
$varVariable reference. Push the variable's value onto the stack.
Any other literal text is to be interpreted as an instruction for the machine. All possible instructions are described below.
addAdd. Pop 2 values from the stack, convert to integer, add them, convert to string, and push the result onto the stack.
subSubtract. Pop 2 values from the stack, convert to integer, subtract the first from the second, convert the result to string, push onto the stack.
For example, the following code:
5
3
sub
yields 2 as a result, and that will be pushed onto the
stack.
mulMultiply. Pop 2 values from the stack, convert to integer, multiply them, convert to string, and push onto the stack.
divDivision with truncation. Pop 2 values from the stack, convert to integer, divide the second by the first, convert the result to string, and push that onto the stack
For example, the following code:
8
2
div
yields 4 as a result, and this value will be pushed onto the
stack as a string.
modModulo. Pop 2 values from the stack, convert to integer, calculate
second % first, convert the result to string, and push that onto
the stack.
For example, the following code:
7
3
mod
yields 1 as result, which will be pushed onto the stack (as
string).
negNegate. Pop one value from the stack, convert to integer, invert its sign (from plus to minus, or vice versa), convert the result to string, and push it onto the stack.
absAbsolute value. Pop one value from the stack, convert to integer, calculate the absolute value, convert the result to string, and push it onto the stack.
incIncrement. Pop one value from the stack, convert to integer, add the value 1 to it, convert to string, and push it onto the stack.
decDecrement. Pop one value from the stack, convert to integer, subtract the value 1 from it, convert to string, and push onto the stack.
dupDuplicate. Read one value from the stack (do not pop it from the stack), and push it onto the stack. The net result, therefore, is that the same value is now two times in a row on the stack.
revReverse. Pop one value from the stack, do a string reverse, and push that onto the stack.
slcSubstring (slice). Pop 3 values from the stack, to,
from, and the value to take a slice from. The from
and to values are to be converted to integer, so you can derive the
correct arguments for the substr() method of
std::string from them. The from value is the string
index for the first character to be used. The to value is the string
index for the first character not to be included.
Please note: this is not exactly what substr()
expects.
For example, the following code:
\abcdefghijklm
3
6
slc
yields “def” as a result.
idxIndex. Pop 2 values from the stack. The first is used as an index, which you convert to integer first. The second is the string value from which you want to take a single character, as selected by the index (zero-based). Push the indexed character onto the stack.
So:
\text
2
idx
yields “x” as a result.
catConcatenation. Pop 2 values from the stack, and combine them to make a single string value, by putting the first one after the second. Push the result onto the stack.
So:
\hello
\ world
cat
yields “hello world” as a result.
lenLength. Pop one value from the stack, determine its size (number of bytes), convert that numerical result to string, and push it onto the stack.
rotRotate (rot-13). Pop one value from the stack, apply a rot-13 4 transformation to it, and push the result onto the stack.
enlAdd a newline. Pop one value from the stack, append a newline
('\n') to it, and push the result onto the stack.
gtoGoto. Pop one value from the stack, interpret as a program line number, and make sure that the next instruction is taken from that line.
So:
>main
gto
continues interpreting instructions from the line which is referred to by the
label :main (elsewhere in the code).
Please note: it could well be the case that :main
appears later in the code. You will have to have all label
definitions already stored somewhere beforehand, in order to be able to lookup
the corresponding line numbers when the program is being interpreted.
This means it will be beneficial to build a two-pass interpreter. First time through the code to assemble all labels, second time to run the program.
geqGoto if equal. Pop 3 values from the stack: first label value, then val2, and lastly val1. If val1 and val2 are equal, continue interpreting instructions from the line number represented by the label value. If they are not equal, simply continue with the next instruction.
So:
\hello
\there
>greeting
geq
The strings “hello” and “there” are not equal, so the next instruction will
not be taken from the line number right after :greeting, but
simply just from the line after the geq instruction.
Please note: this instruction performs a string comparison!
gneGoto if not equal. Pop 3 values from the stack: first label value, then val2, and lastly val1. If val1 and val2 are not equal, continue taking instructions from the line number represented by the label value. Else, just continue with the next instruction.
So:
\hello
\there
>greeting
gne
will cause the program to continue from the line after the
:greeting label, because the strings are not equal.
Please note: this instruction performs a string comparison!
gltGoto if less. Pop 3 values from the stack: first label value, then val2, and lastly val1. Convert val1 and val2 to integer. If val1 is less than val2, continue by taking instructions from the line referred to by the label. Else, simply continue with the next instruction.
So:
2
3
>smaller
glt
2 is less than 3, so yes, the program continues
from the line just after :smaller.
Please note: this instruction compares integers!
gleGoto if less or equal. Pop 3 values from the stack: first label value, then val2, and lastly val1. Convert val1 and val2 to integer. If val1 is less than, or equal to val2, continue by taking instructions from the line referred to by the label. Else, simply continue with the next instruction.
So:
42
42
>here
gle
The two integers are equal, so the jump will be executed. We'll continue from
the line after the :here label.
Please note: this instruction compares integers!
ggtGoto if greater. Pop 3 values from the stack: first label value, then val2, and lastly val1. Convert val1 and val2 to integer. If val1 is greater than val2, continue by taking instructions from the line referred to by the label. Else, simply continue with the next instruction.
So:
10
37
>greater
ggt
10 is not greater than 37, so in this case we won't
continue after the :greater label, but from the line right after
the ggt instruction.
Please note: this instruction compares integers!
ggeGoto if greater or equal. Pop 3 values from the stack: first label value, then val2, and lastly val1. Convert val1 and val2 to integer. If val1 is greater than, or equal to val2, continue by taking instructions from the line referred to by the label. Else, simply continue with the next instruction.
So:
92
56
>this-place
gge
In this example, 92 is greater than 56, so the jump
is carried out: we will continue from the line just after the
:this-place label.
Please note: this instruction compares integers!
funFunction. Store the line number of the next instruction on a
separate stack (the call stack), and execute a
gto.
retReturn. Pop the last location from the call stack, and make sure that the next instruction is taken from that line.
endEnd of search. The value on the stack is the secret message you are looking for! This instruction will only be present in the last program file you'll download.
3 push 3 onto the stack
=cnt pop 3 from the stack and assign to cnt
\Hello, world push “Hello, world” onto the stack
enl append a newline to it
=hello assign the result to hello
\ push an empty string onto the stack
=result assign it to result
:loop define the next step as location loop
$result push result onto the stack
$hello push hello onto the stack
cat combine into a single string
=result assign it to result
$cnt push cnt onto the stack
dec subtract 1 from it
=cnt assign the result to cnt
$cnt push cnt onto the stack
0 push 0 onto the stack
>loop push location loop onto the stack
ggt if cnt > 0, go to loop, else continue
$result push result onto the stack — this
is the end result
The end result is a 3 line string:
Hello, world
Hello, world
Hello, world
Don't be discouraged by the list of instructions you need to implement. It's less work than you think. Every instruction will be a short function with just a few lines of straightforward code.
Many of you will be tempted to apply a Command design pattern. That's perfectly fine, of course, but will give you more work than needed. Keep it simple! Just build an interpreter class which has the functions for every instruction.
A stack can be implemented easily with a std::vector
5.
Use its methods push_back() and pop_back() to push
and pop objects. Use back() to read the last value without
popping it.
Consider adding a “out” instruction,
which writes the last value on the stack to the standard output. This will
come in handy when testing and debugging!
Keep the connection open until you're done with all requests. It will make your program significantly faster than when you would make a connection for every request, and close it afterwards, every time.
If you would like to be able to test your interpreter with locally stored Krul programs, add the possibility to read and run local files.
Allow comments in your Krul code, by interpreting all text from a
literal “#” to end of line as a comment. POSIX users will then
even be able to put a she-bang line at the top of their scripts:
#!/usr/bin/env krul
If you also make your script executable, you'll be able to run any such Krul script locally without any additional commands.
Finally, if you also add “err” and
“in” instructions, for writing to standard
error and reading from standard input, respectively, you have everything you
need to make Krul command line scripts. This would also be good for writing
your own (unit) tests!
The C++ Standard Library is of course allowed: we do not consider it an external library.↩︎