Mastermind is a programming language designed to compile to the well-known esoteric language "Brainfuck".
Brainfuck is essentially a modern interpretation of the classical Turing machine. It consists of a tape of 8-bit values, with simple increment/decrement, move left/right, input/output, and looping operations. The full language only uses 8 control characters: +-><.,[].
Imagine an alternate reality where C was designed for computer architectures that run Brainfuck natively, that is what Mastermind is intended to be.
- Brainfuck
- Variables
- Conditionals
- Loops
- Functions
- Inline Brainfuck
- Standard Library
- Variants
- Optimisations
Brainfuck is an esoteric programming language, originally designed as a theoretical example of a Turing complete language with an extremely minimal compiler. The name is due to its difficulty, it is significantly more difficult to create complex programs than in any popular modern language.
When a Brainfuck program is run, it operates on a array/tape of cells, performing operations on the tape. Each cell contains an integer, initialised to 0 by default. The program operates on one cell at a time based on the position of a "tape head". Brainfuck supports the following operations:
+: increment the value of the current cell-: decrement the value of the current cell>: move the tape head one cell to the right<: move the tape head one cell to the left.: output the current cell as a byte to stdout,: input a byte from stdin, overwriting the current cell[: jump to the corresponding]if the current cell is 0]: jump to the corresponding[if the current cell is not 0
A Brainfuck program consists of a list of these commands, which are executed sequentially. The program terminates if the final operation in the list is executed.
The Mastermind IDE and compiler library contains an implementation of a Brainfuck interpreter. This implementation is intended to match the behaviour of the most popular Brainfuck implementations:
In this implementation, each cell is an 8-bit integer that wraps if an increment or decrement operation overflows or underflows.
E.g. given the current tape cell value is 255, after an increment (+), the cell value is now 0.
Similarly: 0, after a decrement (-) becomes 255
In this implementation, the tape extends infinitely in both directions.
The base data type in Mastermind is the cell, this corresponds to a a single 8-bit cell on the Brainfuck tape.
cell var = 56;
cell c = 'g';
cell bool = true; // true/false equivalent to 1/0
Cells default to 0.
The input and output keywords in Mastermind correspond to the , and . operators in Brainfuck. input simply inputs the next byte from stdin, and output outputs a byte to stdout.
// stdin: 00abc
cell g;
drain 5 {
// do this 5 times
input g;
g += 1;
output g;
}
// stdout: 11bcd
The simplest way to display text is to output valid ASCII characters. If your Brainfuck implementation supports unicode, that is also possible by outputting multiple bytes.
output 240;
output 159;
output 164;
output 145;
output 10;
// displays 🤑 (emoji with green cash for tongue)
Variables can also be defined as contiguous arrays of cells.
// multi-cell:
cell[4] array_example = [1, 2, 3, 4];
cell[5] string_example = "hello";
cell[2] foo;
foo[0] = 204;
Structure types can be defined with named fields, then instantiated as variables.
struct struct_name {
cell x;
cell y;
cell[5] zzzzz;
}
struct struct_name s;
s.x = 4;
s.y = 123;
s.zzzzz[0] += 3;
s.zzzzz[4] = 180;
// nested struct:
struct Nested {
struct struct_name n;
}
Any type can be repeated into an array/contiguous allocation. This includes cells, structs, arrays of cells, and arrays of structs.
cell[4][6] mult_arr; // a 6-length array of cell[4] arrays
cell[4][6][2] mult_arr; // 2 * (6-length arrays of cell[4] arrays)
struct T {
cell a;
cell[4][2] b;
}
struct T[10] ten_T_structs;
ten_T_structs[4].b[1][3] = 45;
struct S {
struct T[2][4] matrix_of_T_structs;
cell other;
}
struct S[3] three_S_structs;
three_S_structs[1].matrix_of_T_structs[3][0] = '5';
This is a limitation of Brainfuck, getting around this problem requires more runtime code than is reasonable to include by default, due to the goals of Mastermind. You can implement equivalent behaviour using in-line Brainfuck, structs, and functions.
The exact memory cells occupied by a variable can be specified:
// value 1 at tape position 4
cell a @4 = 1;
// contiguous array of 1s, starting at cell -1
cell[3] a @-1 = [1, 1, 1];
The byte-order and positioning of a struct's subfields can be specified:
struct T {
cell a @1;
cell b[2] @3;
}
// struct T's layout:
// (-, a, -, b[0], b[1])
// '-' denotes an untracked padding cell
Mastermind supports basic if/else statements. An if statement accepts an expression that evaluates to a cell type, if the expression is evaluated to be truthy (i.e. not equal to 0), then the if block is executed, otherwise the optional else block is executed. This behaviour can be inverted using the not keyword.
if 13 {
output "13";
}
if not true {
// unreachable
}
cell var = 4;
if var {
output "true";
} else {
output "false";
}
// typical equivalence use-case:
if not var - 10 {
// ==
} else {
// !=
}
Mastermind currently supports three forms of loops: while, drain and copy.
It should be noted that there is no early breaking in any of these forms, so all clauses in a loop body are always executed in each iteration.
The while loop operates similarly to other languages, accepting a condition expression, and a loop body.
The clauses inside the loop body are executed until the condition is falsy (i.e. equal to 0). The condition is checked before each iteration.
Note: currently while conditions must be direct variable references, this is subject to future compiler updates.
cell n = 5;
while n {
// do stuff
n -= 1;
}
// n is now equal to 0
The drain loop mirrors a very common pattern found in Brainfuck programs: decrementing a cell. drain accepts an expression, a list of variables to 'drain into', and/or a loop body.
If the expression is a direct variable reference, then the variable is decremented after each iteration. If not, it is evaluated in a temporary cell, then decremented after each iteration.
drain var {
// do stuff
}
// equivalent to:
while var {
// do stuff
var -= 1;
}
With expressions:
drain 6 {
output 'a';
}
// aaaaaa
The following example leaves x unchanged:
cell x = 7;
drain x - 2 {
output 'b';
}
// bbbbb
If the into keyword is used, followed by a whitespace-separated list of target variables, the targets will be incremented after each iteration.
cell i;
drain 10 into i {
output '0' + i;
}
// 0123456789
// equivalent to:
cell i;
cell ten = 10;
while ten {
output '0' + i;
i += 1;
ten -= 1;
}
Another example:
drain var into other_var other_var_2 *spread_array;
// equivalent to:
drain var {
other_var += 1;
other_var_2 += 1;
spread_array[0] += 1;
spread_array[1] += 1;
spread_array[2] += 1;
// ...
}
The copy loop acts similarly to the drain loop, however the expression must be a direct variable reference, and it is left unchanged afterwards, and its original value is accessible within the loop body.
cell y;
copy x into y {
// loop body
};
An equivalence example:
cell var = 5;
copy var {
output '0' + var;
}
// 55555
// equivalent to:
cell var = 5;
cell temp = var;
while temp {
output '0' + var;
temp -= 1;
}
Mastermind supports a minimal function system: functions can be defined with a name and a fixed number of typed arguments.
fn newline() { output '\n'; }
fn print_zeros(cell num) {
copy num {
output '0';
}
newline();
}
// expressions as arguments are currently not supported,
// i.e. print_zeros(9)
cell g = 9;
print_zeros(g);
Unlike most modern programming languages, functions are not considered first-class values. Functions in Mastermind are in-lined at compile-time, and all arguments are passed by reference. Values can be returned by editing passed in arguments, or editing variables in an outer scope, although the latter makes a function less portable.
fn is_zero(cell in, cell out) {
out = true;
if in {
out = false;
}
}
cell value = 'h';
cell falsy;
is_zero(value, falsy);
Example showing a function reading a variable from an outer scope:
fn print_global_g(cell count) {
copy count {
output chr;
}
}
cell chr = 'g';
cell count = 3;
print_global_g(count);
// ggg
{
// inner scope with a new 'g' allocation
cell chr = 'G';
count = 5;
print_global_g(count);
// GGGGG
}
// same call again, now the inner chr has been freed
print_global_g(count);
// ggg
Functions support overloads with different types or number of arguments. Examples of supported behaviour:
fn func1() {
output '1';
}
fn func1(cell a) {
output '2';
}
fn func1(cell a, cell b) {
output '3';
}
struct X { cell a; }
fn func1(struct X x) {
output '4';
}
struct Y { cell a; }
fn func1(struct Y y) {
output '5';
}
fn func1(cell a, struct X x, struct Y y) {
output '6';
}
cell n;
struct X x;
struct Y y;
func1();
func1(n);
func1(n, n);
func1(x);
func1(y);
func1(n, x, y);
// 123456
In-line Brainfuck allows the programmer to define custom behaviour as if writing raw Brainfuck, inspired by in-line assembly in C.
Basic example:
// find the next cell that equals -1
bf {
+[->+]-
}
More advanced example:
// input a line of lowercase letters and output the uppercase version
// this is an intentionally inefficient example
bf @3 clobbers var *spread_var etc {
,----------[++++++++++>,----------]
<[<]>
[
{
cell g @0;
assert g unknown;
output g + ('A' - 'a');
// embedded Mastermind!
}
>
]
// now clear and return
<[[-]<]>
}
It is the programmer's responsibility to clear used cells and return back to the cell in which they started the in-line Brainfuck context. If the programmer does not do this, any following Mastermind code may break.
The exact location to start an in-line Brainfuck context can be specified:
cell var @3 = 4;
// compiled: >>>++++
bf @4 {
<><><>
}
// compiled: >>>><><><>
Variables can also be used:
cell d;
bf @d {
// brainfuck code here
}
struct G {
cell h;
cell i;
cell j;
}
struct G g;
bf @g {
// starts on the first cell of g's allocation
}
// equivalent to:
bf @g.h {}
With optimisations enabled, Mastermind will try to predict the value of cells at compile-time, so it can prevent unnecessary cell clean-ups and unreachable code. If your in-line Brainfuck affects existing Mastermind variables, you should tell the compiler using the clobbers keyword, the syntax is similar to the drain into target list:
bf clobbers var *spread_var other_var etc {}
The compiler will now assume nothing about the values of those variables afterwards.
If instead you want to tell the compiler specifically that a variable has become a certain value, you can use assert:
assert var equals 3;
// most common use cases:
assert var equals 0;
assert var unknown;
Asserting a variable as unknown is equivalent to clobbering.
You can embed high-level Mastermind code within a Brainfuck context. During compilation the embedded Mastermind is compiled and the generated Brainfuck is inserted in place.
// input 3 n-length lines of input
bf {
>+++<,[
{
cell input_char @0;
assert input_char unknown;
cell length_remaining @1;
assert length_remaining unknown;
cell next_char @2;
cell next_length_remaining @3;
if not input_char - '\n' {
length_remaining -= 1;
}
if length_remaining {
drain length_remaining into next_length_remaining;
input next_char;
}
}
>>]
}
Embedded Mastermind can include in-line Brainfuck, this is recursive. For example:
// top-level Mastermind context
bf {
++>>
{
// inner Mastermind context
bf {
++>>
{
// inner inner Mastermind context
bf {
++>>
{
//...
}
<<--
}
}
<<--
}
}
<<--
}
The compiler cannot guarantee the global head position at compile time within an in-line Brainfuck context. Therefore memory location specifiers are relative to the current embedded Mastermind context, not the entire program.
Also, top-level variables are not cleared by default in Mastermind contexts, this allows you to "leave" variables in cells for your Brainfuck to use. If you want variables in your embedded Mastermind to be automatically cleared, you can open a scope at the top level:
bf {
++----++[][][<><><>] // the program doesn't matter for this example
{
// variables here will not be cleared
cell g @2;
assert g unknown;
{
// variables here will be cleared
let b = 32;
}
}
{{
// self-cleaning Mastermind code here
}}
}
Currently the Mastermind standard library is very limited, and is effectively a set of example programs included in the web IDE and source repository.
You can include/import other files using preprocessor directives. The Mastermind preprocessor is intended to mirror the C preprocessor, however it currently only supports the #include directive.
The following is a basic example:
// file1.mmi
struct H {
cell a;
}
fn print(struct H h) {
output h.a;
}
// main file being compiled
#include "file1.mmi"
struct H h;
h.a = 64;
print(h);
// @
The most mature files in the included examples are the following:
bitops: bitshifting operations for cell typesi8: signed type for 8-bit integers and supporting functionsu8: common supporting functions for cell typesu16: a 16-bit unsigned integer type and supporting functionsifp16: a signed 16-bit fixed-point number type and supporting functionsstack: includes a cell stack and associated functions that can hold up to 32 non-zero elements
NOTE: due to current lack of header-guard support, importing multiple of these will likely cause a compiler error, until this is implemented, the best way to work around this is to only include ifp16 as that includes the others.
Example usage:
#include <u16>
// read a 16 bit number from stdin, add 55, then print
struct u16 n;
read(n);
cell ff = 55;
add(n, ff);
print(n);
output ' ';
debug(n); // print the binary representation
// example input: 16000
// output: 16055 0011111010110111
Example fixed-point usage:
#include <ifp16>
struct ifp16 n;
_99p99609375(n); // constant 99.99609375
struct ifp16 m;
__1p5(m); // constant -1.5
divide(n, m);
print(n);
output ' ';
debug(n);
// -66.66 10111101.01010110
The Mastermind compiler can be extended to support Brainfuck variants.
This is the default behaviour, typical Brainfuck implementation as described in Brainfuck.
Mastermind currently supports two-dimensional Brainfuck, this is a Brainfuck variant with an additional dimension in the memory array.
2D Brainfuck support can be enabled in the compiler settings in the web IDE, adding the following features:
- New opcodes for in-line Brainfuck contexts and in generated Brainfuck code:
^: move up one cell in the gridv: move down one cell in the grid
- The ability to specify 2D coordinates for location specifiers:
cell var @(5, -7) = 'a'; bf @var {[-<<<<<^^^^^^^+>>>>>vvvvvvv]} bf @(0, 0) {.....} // aaaaa - Three new memory allocation strategies for generated 2D code:
- Zig Zag
- Spiral
- Tiles
The Mastermind compiler includes optional optimisations for generated code. The original goal of Mastermind was to generate very minimal Brainfuck for use in Code Golf competitions, so most of these are aimed at reducing generated code length.
Optimises clearing cells after they are de-allocated, it does this by tracking their values at compile-time and acting based on a cell's known value. For instance, if a cell can be proven at compile-time to have the value 2, it is more efficient to clear with --, than the typical Brainfuck clear: [-].
When large values are added in Brainfuck, the naive approach is to use the increment - operator for as many times as needed. The constants optimiser will use multiplication to shorten the code needed to add/subtract large values. Example: the value 46 can be achieved by either ++++++++++++++++++++++++++++++++++++++++++++++ or the shorter: +++++[>+++++++++<-]>+ (5 * 9 + 1).
Optimises generated Brainfuck code by shortening trivial program segments.
Currently this is limited to optimising segments of Brainfuck programs with the following operations: +, -, >, <, [-].
--->>><<<++
// becomes:
-
An end-to-end example:
cell h = 4;
cell j = 3;
h += 10;
drain 10 {
j = 5;
h += 4;
j += 1;
}
// compiles to:
++++>+++<++++++++++>>++++++++++[<+<++++>[-]+++++>-]
// after optimisation:
++++++++++++++>+++>++++++++++[-<[-]+++++<++++>>]
This system finds optimal equivalent segments for classic Brainfuck programs, however for the 2D Brainfuck variant it is not guaranteed, as finding the optimal path between memory cells in a 2D grid is more difficult. The Generated Code Permutations setting enables an exhaustive search for the optimal path when using the 2D Brainfuck variant, otherwise a greedy approach is used.
Detects if a code block is empty or has no effect on the program, and prunes the associated clause.
Brainfuck loops will be omitted if the cell they start on can be proven to be 0 at compile-time.