Все говорят, что с этим языком можно поломать мозг. И я решил попробовать, настолько ли страшен BrainFuck как его рисуют. Хороший способ разобраться с языком - написать интерпретатор.
Могу сказать, что BrainFuck - очень легкая версия ассемблера, поскольку в ассемблере купа регистров, память с адресами, разные команды, а в BrainFuck - память, один регистр, указывающий на ячейку памяти с которой работаем и пару простых операций:
> - увеличение регистра на 1 (перемещение по ленте-памяти вправо)
< - уменьшение регистра на 1 (перемещение по ленте-памяти вправо)
+ - увеличение значения ячейки памяти на 1
- - уменьшение значения ячейки памяти на 1
. - ввод данных в ячейку памяти
, - вывод данных из ячейки памяти
[ - начало цикла, если текущая ячейка памяти не 0 то заходим в цикл (следующая команда)
] - конец цикла, если текущая ячейка памяти равна 0 - то выходим из цикла, иначе идем к следующей команде после открывающей [
Я добавил в свой интерпретатор еще пару отладочных команд.
@ - остановить программу в этом месте
# - записать состояние памяти и указатель на память (потом можно будет вывести все)
Интерфейс у меня вышел таким.
В коде я специально оставил класс BrainFuckInterpreterNOOOP, который можно, по моему, использовать для проверки того как трейни разобрался в ООП - попросив его зарефакторить этот класс со множеством ответственностей. Взглянуть только на его список полей
Алгоритм перемножения очень неоптимальный, так как фактически суммирует от 0 до результата, при этом бегая по памяти влево вправо, но все же работает.
Могу сказать, что BrainFuck - очень легкая версия ассемблера, поскольку в ассемблере купа регистров, память с адресами, разные команды, а в BrainFuck - память, один регистр, указывающий на ячейку памяти с которой работаем и пару простых операций:
> - увеличение регистра на 1 (перемещение по ленте-памяти вправо)
< - уменьшение регистра на 1 (перемещение по ленте-памяти вправо)
+ - увеличение значения ячейки памяти на 1
- - уменьшение значения ячейки памяти на 1
. - ввод данных в ячейку памяти
, - вывод данных из ячейки памяти
[ - начало цикла, если текущая ячейка памяти не 0 то заходим в цикл (следующая команда)
] - конец цикла, если текущая ячейка памяти равна 0 - то выходим из цикла, иначе идем к следующей команде после открывающей [
Я добавил в свой интерпретатор еще пару отладочных команд.
@ - остановить программу в этом месте
# - записать состояние памяти и указатель на память (потом можно будет вывести все)
Интерфейс у меня вышел таким.
public interface Interpreter { Interpreter compile(String program, String input); String getOutput(); String printMemory(); List<String> getTrace(); Interpreter compile(String program); Interpreter run(); Interpreter useBreakpoint(); Interpreter withMemory(int... memory); }А использую я его как-то так (это тесты, которые мне помогли в разработке)
public class BrainFuckInterpreterTest { private static final String SYMBOL_Q = "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"; protected Interpreter getInterpreter() { return new BrainFuckInterpreter(); } @Test public void shouldPrintHelloWorld() { assertEquals("Hello World!\n", getInterpreter().compile( "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>" + "++.>+.+++++++..+++.>++.<<++++++++++" + "+++++.>.+++.------.--------.>+.>.").run().getOutput()); } @Test public void shouldPrintHelloWorld2() { assertEquals("Hello World!", getInterpreter().compile( ">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.>>>++++++++[<++++>-]" + "<.>>>++++++++++[<+++++++++>-]<---.<<<<.+++.------.--------.>>+.").run().getOutput()); } @Test public void shouldOut() { assertEquals("Q", getInterpreter().compile( SYMBOL_Q + ".").run().getOutput()); } @Test public void shouldIncrease() { assertEquals("P", getInterpreter().compile( SYMBOL_Q + "-.").run().getOutput()); } @Test public void shouldIncreaseTwice() { assertEquals("O", getInterpreter().compile( SYMBOL_Q + "--.").run().getOutput()); } @Test public void shouldNextPreviousVar() { assertEquals("QP", getInterpreter().compile( SYMBOL_Q + "->" + SYMBOL_Q + "><.<.").run().getOutput()); } @Test public void shouldPrintFirst() { assertEquals("\u0000", getInterpreter().compile( ".").run().getOutput()); } @Test public void shouldIncreaseFromCurrentPosition() { assertEquals("QS", getInterpreter().compile( SYMBOL_Q + "<<<<" + SYMBOL_Q + "++>>>>.<<<<.").run().getOutput()); } @Test public void shouldLoop() { assertEquals("QQ", getInterpreter().compile( SYMBOL_Q + ".[>+<-]>.").run().getOutput()); } @Test public void shouldInput() { assertEquals("R", getInterpreter().compile( ",+.", "Q").run().getOutput()); } @Test public void shouldInputFromEmpty() { assertEquals("\u0000Q", getInterpreter().compile( ",>,.<.", "Q").run().getOutput()); } @Test public void shouldPrintMemory() { assertEquals("0 1 2 3 >4 ", getInterpreter().compile( ">+>++>+++>++++").run().printMemory()); } @Test public void shouldBreakpointAt() { assertEquals("0 1 2 >1 ", getInterpreter().compile( ">+>++>+@++>++++").useBreakpoint().run().printMemory()); } @Test public void shouldMoveRight() { assertEquals("\u0003", getInterpreter().compile( "+++" + BrainFuckUtils.MOVE_RIGHT + ">.").run().getOutput()); } @Test public void shouldCopyValue() { assertEquals("\u0003\u0003", getInterpreter().compile( "+++" + BrainFuckUtils.COPY_RIGHT + ".>.").run().getOutput()); } @Test public void shouldCopyInputToOutput() { assertEquals("sdgsjmfgl", getInterpreter().compile( BrainFuckUtils.READ_ALL_INPUT + BrainFuckUtils.GOTO_TO_MEMORY_START + BrainFuckUtils.WRITE_ALL_OUTPUT, "sdgsjmfgl").run().getOutput()); } @Test public void shouldWithMemory() { assertEquals(">14 ", getInterpreter() .withMemory(14).printMemory()); } @Test public void shouldMultiple() { assertEquals( "[>2 0 3 , " + "2 0 >3 , " + "2 0 >3 3 0 , " + ">2 0 3 3 0 , " + "2 0 >3 3 0 , " + "2 3 >0 3 0 , " + "2 3 0 >3 0 , " + "2 3 3 >0 0 , " + "2 3 >3 0 0 , " + "2 3 >3 3 0 , " + ">1 3 3 3 0 , " + "1 3 >3 3 0 , " + "1 6 >0 3 0 , " + "1 6 0 >3 0 , " + "1 6 3 >0 0 , " + "1 6 >3 0 0 , " + "1 6 >3 3 0 , " + ">0 6 3 3 0 , " + "0 >6 3 3 0 ]" , getInterpreter() .withMemory(2, 0, 3) .useBreakpoint() .compile("#>>#" + COPY_RIGHT + "#<<#[>># [<+>-]# ># " + "[<+>-]# <# [>+>+<<-]>>[<<+>>-]<<# <<-#]>#") .run().getTrace().toString()); } @Test public void shouldNotIgnoreNextCommandAfterCycle() { assertEquals("<0 0 1", getInterpreter() .withMemory(0, 0, 1) .compile("<[-]>") .run().printMemory()); } }Что касается реализации, то ее можно найти тут.
В коде я специально оставил класс BrainFuckInterpreterNOOOP, который можно, по моему, использовать для проверки того как трейни разобрался в ООП - попросив его зарефакторить этот класс со множеством ответственностей. Взглянуть только на его список полей
public class BrainFuckInterpreterNOOOP implements Interpreter { public List<Integer> memory; public int index; private String program; private String input; private String output; private boolean useBreakpoint; private List<String> trace;Но не интерпретатор был целью, а кодинг на самом языке. А потому я написал небольшую подборку простеньких алгоритмов, которые можно повторно использовать.
public class BrainFuckUtils { public static final String MOVE_RIGHT = "[>+<-]"; public static final String MOVE_LEFT = "[<+>-]"; public static final String COPY_RIGHT = "[>+>+<<-]>>[<<+>>-]<<"; public static final String COPY_LEFT = "[<+<+>>-]<<[>>+<<-]>>"; public static final String GOTO_TO_MEMORY_START = "<[<]>"; public static final String READ_ALL_INPUT = ">,[>,]"; public static final String WRITE_ALL_OUTPUT = "[.>]"; public static final String CLEAN = "[-]"; public static final String MULTIPLE = COPY_LEFT + ">>" + COPY_RIGHT + "<<[>>" + MOVE_LEFT + ">" + MOVE_LEFT + "<" +COPY_RIGHT + "<<-]>>" + ">" + CLEAN + "<<<<" + MOVE_RIGHT + ">>";
Алгоритм перемножения очень неоптимальный, так как фактически суммирует от 0 до результата, при этом бегая по памяти влево вправо, но все же работает.
Года 4 назад написал такой в коридоре универа, пока ждал сдачу лабораторок. Хотел еще написать компилятор BF в .NET CLR но так руки и не дошли(
ОтветитьУдалитьЕще есть идея написать декомпилятор BF из .NET CLR
Компилятор написал)
УдалитьОчень любопытно с декомпилятором должно получиться, Саша. Кинешь линкой, плиз.
ОтветитьУдалитьИх сначала надо написать:) Декомпилятор .NET -> BF должен быть немного легче
УдалитьСлова compile и интерпретатор не очень сочетаются.
ОтветитьУдалитьТак точно :)
УдалитьgetInterpreter().compile(
А когда писал не задумался....
Спасибо
Как умножением пользоваться? К примеру, как записать 16*4 ?
ОтветитьУдалить