A few years ago, my journey into coding led me to the dark alleys of GNU Assembly. I quickly realized that the difficulties of Assembly made it almost impossible for me to use it in my daily coding life.
When it takes you three hours to reimplement
strlen
correctly, and you're used to processing natural strings of characters, you may begin to feel like this is not the language for you.
-- Me, two years ago
On my way up from that dark pit of black magic I encountered a curious little fella I had heard of many years prior : BrainFuck.
BrainFuck is Turing-Complete and while it could theoretically let us implement any and all computable problem... BrainFuck may not be the best fit for most complex purposes.
I mean, just look at this hello world.
++++++++++[
>+++++++
>++++++++++
>+++
>+
<<<<-
]
>++.
>+.+++++++..
+++.>
++.
<<+++++++++++++++.
>.
+++.
------.--------.
>+.>.
Of course, I was not content with simply making an interpreter. I started Moostar with the intent of making an interpreter, but quickly decided to add one features to standard brainfuck that I found gave it more power : the ability to creature functions, along with the inclusion of many fundamental arithmetic operations right into the interpreter.
For example, here's how you can write two multiplications in Moostar.
>++>+++++<<
~mul;.
The hexadecimal dump of the output should reveal a single byte at value 0x08.
Currently, Moostar implements all of the following standard subroutines.
/* 'this' is the object representing the interpreter */
this->function_register["sub_5"] = "-----";
this->function_register["sub_10"] = "----------";
this->function_register["add_5"] = "+++++";
this->function_register["add_10"] = "++++++++++";
this->function_register["imove"] = "[->+<]";
this->function_register["dmove"] = "[-<+>]";
this->function_register["clean"] = "[-]";
std::string scan8;
for (size_t i = 0; i < 8; i++)
scan8.append(".>");
for (size_t i = 0; i < 8; i++)
scan8.append("<");
this->function_register["scan8"] = scan8;
this->function_register["scan16"] = scan8.substr(0,16) + scan8.substr(0,16) + scan8.substr(16,8) + scan8.substr(16,8);
this->function_register["foreach_cpy"] = ">[>[->+<<<+>>]>[-<+>]<<-]>[-]<<";
this->function_register["add"] = ">[-<+>]<"; // :R:X:
this->function_register["sub"] = ">[-<->]<"; // :R:Y:
this->function_register["mul"] = this->function_register["foreach_cpy"];
this->function_register["pow"] = ">>>+<<[>>[->+<]<[->>>+>+<<<<]>>>>[-<<<<+>>>>]<<<~foreach_cpy;<<-]>>[-<<<+>>>]<[-]<<";
this->function_register["eq"] = "[->-<]+>[<[-]>[-]][-]<";
this->function_register["neq"] = "[->-<]>[[-]<+>][-]<";
this->function_register["lt"] = ">[->+<]<[->+<]>+>+>>+<<<[->-[>]<<]>>>[<<[-]<<+>>>]>-<<[-]<[-]<";
this->function_register["gt"] = ">[->+<]>>>+<<<<[->+<]>>[-<-[<]>>]>[-<<[-]<]>[-<<<[-]<+>>]<<";
this->function_register["m@0"] = "^[-]\\";
/* Stuff */
(let's all walk past that hilarious mistake of "interpreter"/"interpretor", younger me's English was more than dubious)
In general, one would declare a procedure thusly.
(proc_name):{instructions}
It is useful to add a comment alongside a function's definition to denote the position of its parameters, results, and all the cells it will need for its processing. For example, see that comment at the end of a definition for ~power
.
:R:X:Y:0_:1_:2_:3_:
This comment indicates that once pow
is called, it will compute X to the power of Y, require the four neighbouring cells to do so, and place the result in the cell we call it from.
Currently, Moostar doesn't check for dependencies of functions, and one could write a program that would send the interpreter into an infinite loop of procedures calling each other.
Yet, there is more to Moostar than simply adding standard functions. As you might have noticed from the weird circumflex character in my last command, m@0
, I extended the charset beyond the simple ability to declare procedures. Two characters were introduced beyond those used in standard BrainFuck, and those for procedural syntax.
The first such character, the circumflex accent, switches the interpreter into 'Meta Tape' mode, while the backslash switches the interpreter back into 'Data Tape' mode.
The 'Meta Tape', or 'Meta Registers Tape' is a set of cells containing and controlling data related to the machine's function itself. Originally, they were designed to directly modify CPU registers during execution. Two seconds after I had that idea, when I realized it could easily be exploited to bug the heck out of Moostar, I slowed down and only implemented a control register for the data pointer's position. Interestingly, a legacy name for that meta tape remains in the code : this->ioregs
(for I/O registers).
Now, the pointer on the meta tape and the pointer on the data tape are not the same. Thus, by switching to the meta tape's zero register, we can chose what cell we will land on when we switch back to data.
This is actually what the m@0
procedure does, by resetting the value in meta register 0 (data_pointer_pos
) to 0, and switching back to the data tape.
You can find the source code for moostar on GitHub.