Разрабатываем интерпретатор brainfuck на Arduino

Шла последняя неделя Августа, я неожиданно подумал. Пора бы сделать что, нибудь полезное для этого мира, так появился он — порт языка Brainfuck на ардуино. Каждый кто достаточно долгое время увлекается программированием слышал про такой миниатюрный язык программирования Brainfuck, который не зря получил такое название. Ну а если вы не знаете то вот вам краткий экскурс:

Brainfuck — экзотический тьюринг-полный язык программирования, содержащий в себе всего 8 операторов. У программы на brainfuck есть память , с которой она (программа) может взаимодействовать. Память представляет из себя массив чисел. Программа взаимодействует с ячейкой на которой в данный момент стоит курсор. 

Оператор
Значение
>
Сдвинуть курсор вправо
<
Сдвинуть курсор
+
увеличить значение в ячейке на единицу

уменьшить значение в ячейке на единицу
[
начало цикла, команды внутри цикла повторяются до тех пор пока значение в ячейке не равно нулю
]
конец цикла
.
вывести данные из ячейки на экран в виде ASCII символа
,
получить один символ от пользователя и преобразовать его в число по таблиц ASCII

Думаю что суть языка понятная и можно переходить к разработке.

Разработка

Для начала создадим функцию в которую мы будем передавать код и которая будет его выполнять.

void BrainFuck(const char code[]){}

Как мы видим на вход функция принимает константный массив символов — это и есть код программы на BrainFuck.

Теперь внутри функции создадим несколько переменных необходимых для нашего интерпретатора.

int codeCursor = 0;
byte memory[255] = {0};
byte memCursor;

Первая переменная — это указатель на текущий символ в коде который мы выполняем, второе — это память доступная нашей программе (в нашем случае это массив байт), ну и последнее это указатель (курсор) на область памяти в которой сейчас мы находимся.

Создадим цикл в котором мы будем выполнять программу.

while (code[codeCursor] != 0){
codeCursor++;
}

Как мы видим цикл у нас идет до тех пор пока ячейка в коде (на которую указывает codeCursor) не станет равной нулю. Что за магия здесь происходит? А дело все в том, что любая строка (массив символов) в ASCII заканчивается нулевым байтом т.е нулем. Это необходимо что бы мы понимали когда закачивается строка и не лезли в ту область памяти которая не относится к строке.

В теле цикла мы увеличиваем codeCursor каждый раз на единицу. Таким образом мы пройдемся по всем операторам в нашей строке. Теперь необходимо научить нашу программу как, то реагировать на эти операторы.

while (code[codeCursor] != 0){
switch(code[codeCursor]){
case ‘>’:
memCursor++;
break;
}
codeCursor++;
}

Для обработки операторов мы будем использовать конструкцию switch — case . Данная конструкция позволяет нам избавился от кучи Ifов. В начале мы указываем переменную с которой мы будем сравнивать в нашем случае это текущая ячейка в коде (code[codeCursor]) далее внутри тела swich мы прописываем сами сравнения. Сравнения выглядят следующим образом

case константа_с_которой_мы_сравниваем :
//код который выполняется в случае если переменная и константа равны
break

Разобравшись с тем как это работает, думаю не составит труда понять данный код

while (code[codeCursor] != 0){
switch(code[codeCursor]){
case ‘>’:
memCursor++;
break;

case ‘<‘:
memCursor—;
break;

case ‘+’:
memory[memCursor]++;
break;

case ‘-‘:
memory[memCursor]—;
break;
codeCursor++;
}
}

Думаю с первыми 4мя операторами все понятно. Перейдем к более сложным вещам, а именно ввод и вывод в нашем языке.

Он будет выглядеть следующим образом.

case ‘.’:
Serial.print(char(memory[memCursor]));
break;

case ‘,’:
while(true){
if(Serial.available() >0){
memory[memCursor] = Serial.read();
break;
}
}
break;

Кстати, поскольку мы используем Serial, незабываем в начале функции добавить

Serial.begin(9600);

Думаю что с выводом данных все понятно, мы просто берем число из ячейки, трансформируем его в символ и выводим на монитор. То с вводом все немного сложнее. Здесь мы запускаем бесконечный цикл и внутри с помощью команды Serial.available() проверяем пришли ли нам данные от пользователя (возвращает длину данных в serial буфере, если их нет то длина равна 0). Если в буфере появились данные мы записываем первый байт в текущую ячейку (Serial.read() возвращает первый байт из буфера) и выходим из цикла.

Вот мы уже почти доделали наш интерпретатор языка brainfuck осталось самое сложное, но и самое интересное, именно циклы.

Циклы мы реализуем с помощью стека поэтому добавим в начале нашей функции следующие переменные

int stack[255] = {0};
byte stackCursor = 0;

Думаю логику циклов лучше разобрать сразу на примере кода так, что приступим.

case ‘[‘:
if(memory[memCursor]){
stackCursor++;
stack[stackCursor] = codeCursor;
}
else{
while(code[codeCursor] != ‘]’){
codeCursor++;
}
}
break;

Разберем код обработки начала цикла. Если вы вдруг забыли, циклы исполняют команды внутри себя если значение в текущей ячейке не ноль. Так что вначале мы проверяем значение в текущей ячейке. Если оно не ноль, то сдвигаем указатель стека на одну клетку и записываем в шапку стека индекс текущей позиции в коде (на нее мы будем возвращаться когда дойдем до конца цикла). Кстати, если вы не знаете что такое стек, советую вам почитать. Теперь обработаем обратную ситуацию, если значение в ячейке — ноль, то просто пропускаем все операторы пока не дойдем до конца цикла.

Разберем код обработки конца цикла.

case ‘]’:
if(memory[memCursor])
codeCursor = stack[stackCursor];
else
stackCursor—;
break;

Тут код намного проще по сравнению с предыдущим. А именно, если значение в ячейке не ноль, то присваиваем текущую позицию в коде шапке объявления цикла, иначе просто убираем цикл из стека (передвигаем курсор на ячейку вниз).

Вот и весь код, а вы ожидали чего, то сложного ?)

void BrainFuck(const char code[]){
int codeCursor = 0;

byte memory[255] = {0};
byte memCursor;

int stack[255] = {0};
byte stackCursor = 0;

Serial.begin(9600);

while (code[codeCursor] != 0){
switch(code[codeCursor]){
case ‘>’:
memCursor++;
break;

case ‘<‘:
memCursor—;
break;

case ‘+’:
memory[memCursor]++;
break;

case ‘-‘:
memory[memCursor]—;
break;

case ‘.’:
Serial.print(char(memory[memCursor]));
break;

case ‘,’:
while(true){
if(Serial.available() >0){
memory[memCursor] = Serial.read();
break;
}
}
break;
case ‘[‘:
if(memory[memCursor]){
stackCursor++;
stack[stackCursor] = codeCursor;
}
else{
while(code[codeCursor] != ‘]’){
codeCursor++;
}
}
break;

case ‘]’:
if(memory[memCursor])
codeCursor = stack[stackCursor];
else
stackCursor—;
break;
}

codeCursor++;
}
}

void setup() {
}

void loop() {
}

Ну и напоследок Hello World

Hello World на Brainfuck будет выглядеть следующим образом

++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.——.———.>+.>.

Для того что бы наш код заработал, просто добавим в функцию setup следующую строчку.

BrainFuck(«++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.——.———.>+.>.»);

Таким образом мы исполним код и получим следующе сообщение в Serial-мониторе.

А зачем?

Ну во первых это очень интересный опыт который можно приобрести при разработке подобных проектов, а во вторых у меня есть проект программируемого калькулятора на ардуино и Brainfuck вполне подходит под эти задачи. Жду ваших комментариев по данному проекту

Чуть не забыл, исходный код:

Репозиторий проекта: тык

Онлайн интерпретатор Brainfuck: жмак

Добавить комментарий

Ваш адрес email не будет опубликован.