brainfuck: java-компилятор

branfuck - это такой простейший язык программирования который реализует некую "полную машину Тьюринга", подробнее в википедии, там же и подробное описание команд. Кому лень - пример, выводящий традиционное Hello world:

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

Для тех, кто видит в первый раз это кажется диким. Ну разе lisp не страшнее?)

Зачем??!

Это - язык-головоломка. Благодаря такому маленькому набору команд компилятор/интерпретатор становится очень простым, а разработка на нём - поистине напряг для мозга, особенно для программистов которые привыкли к роскоши :)

И всё-таки зачем?

В рамках #SumIT был конкурс javathon от Sun Oracle на написание приложения используя последние java-технологии. Напомню, что за 2 недели назад вышла Java 7. Один из вариантов был написать компилятор какого-либо языка в джава-байткод за 23 часа. Подробнее о конкурсе и результатах

Вместе с приятелем, Максимом Колчиным мы взялись за написания такого компилятора, который попутно получился ещё и оптимизирующий. Соответственно цепочка логики: брейнфак компилятор на джаве генерит байткод программы, которая потом запускается и выдаёт то, что должна делать программа на брей,нфаке в исходном виде. Запутал :) Картинка иллюстрирует

Попутно добавили ещё одну команду, которая является расширением языка. "~", "тильда" - создаёт задержку на 300мсек. Вводя тильду у нас удалось достичь эмуляции "набора текста" из за-за задержки при выводе на экран.

Макс предложил название jFuck - клёвое! :)

Поскольку писать на брейфаке большой текст геморой одно удовольствие пришлось на скорую руку написать конвертор(jfuck-gen) любого текста в брейфак код, рандомно добавляя тильды, т.е. задержки в тексте.

jFuck

Для разработки использовали maven, чтоб его... Тормознутая хрень, да. Точно не для нетбука - это был первый опыт работы с ним.

Разумеется, мы использовали библиотеки для генерации байткода - Apache BCEL (org.apache.bcel). Писать руками достаточно сложно, тем более в такое ограниченное время (фактически у нас вышло 4+10=14 часов, не спать было бы глупо - больше протупили бы)

Так же нам очень пригодилась публично выложенный код самого интерпретатора брейнфака, очень грамотно написанный (например есть "минусовая" память). Автор vampirus (vampirus.ru/code/brainfuck)

Как работает компилятор?

Мы имеем некую заготовку, класс Tmpl, который умеет выполнять инструкции брейнфака, вызывая соотвествующий метод. Например "+" должен вызвать метод incValue(). Дальше в этот класс нужно внедрить сами инструкции, т.е. код который будет вызывать методы у заготовки.

Кто внимательно читает сразу увидит что есть большое количество повторений одинаковых инструкций, и вызывать 200 раз метод incValue() - глупо. Есть где опимизировать :) А ещё если посмотреть на брейнфак можно заметить наличие циклов "[...]", которые достаточно сложно реализовать (нужно уметь корректно отмечать добавлять закладки и проверять условия, в случае чего нужно вернуться к этим закладкам. В контексте генерации байткода выглядит устрашающе :)) Поэтому мы добавили оптимизатор - во время компиляции мы эмулируем работу брейнфака, соответственно раскрывая циклы. В этом же месте и считаем количество повторений инструкций.

Например

Кусок кода, который добавляет в instructionList(читать как набор инструкций метода) вызов метода с одним аргументом

il.append(InstructionConstants.THIS);
il.append(new PUSH(factory.getConstantPool(), count));
il.append(factory.createInvoke("Tmpl", getMethod(last),
           Type.VOID, new Type[]{Type.INT}, 
           Constants.INVOKEVIRTUAL));

Сам Tmpl класс:

public class Tmpl {
private int pointerzero = 0;
private char[] memory;
private int pointer = 0;
private String in = "";
private int pin;
public Tmpl() {
memory = new char[1];
}
private void nextCeil(int amount) {
pointer += amount;
}
private void prevCeil(int amount) {
pointer -= amount;
}
private void incValue(int value) {
char cur = getValue(pointer);
setValue(pointer, (char) ((cur + value) % 256));
}
private void decValue(int value) {
char cur = getValue(pointer);
setValue(pointer, (char) ((cur -= value) < 0 ? 255 - cur : cur % 256));
}
private void print(int amount) {
char cur = getValue(pointer);
while (--amount >= 0) System.out.print(cur);
System.out.flush();
}
private void read(int amount) {
setValue(pointer, in.charAt(pin+amount));
pin+=amount;
}
private void sleep(int amount) {
try {
Thread.sleep(300 * amount);
} catch (InterruptedException ex) {}
}
public char getValue(int pointer) {
int newpointer = pointerzero + pointer;
if (memory.length > newpointer && newpointer >= 0) {
return memory[newpointer];
} else {
char[] tmp = new char[(newpointer > 0) ? newpointer + 1 : memory.length + Math.abs(newpointer)];
for (int i = 0; i < tmp.length; i++) {
tmp[i] = 0;
}
for (int i = 0, k = (newpointer > 0 ? 0 : Math.abs(newpointer)); i < memory.length; i++) {
tmp[k++] = memory[i];
}
memory = tmp;
if (newpointer < 0) {
pointerzero += Math.abs(newpointer);
}
return 0;
}
}
public void setValue(int pointer, char value) {
int newpointer = pointerzero + pointer;
if (memory.length > newpointer && newpointer >= 0) {
memory[newpointer] = value;
} else {
char[] tmp = new char[(newpointer > 0) ? newpointer + 1 : memory.length + Math.abs(newpointer)];
for (int i = 0; i < tmp.length; i++) {
tmp[i] = 0;
}
for (int i = 0, k = (newpointer > 0 ? 0 : Math.abs(newpointer)); i < memory.length; i++) {
tmp[k++] = memory[i];
}
memory = tmp;
if (newpointer > 0) {
memory[newpointer] = value;
} else {
memory[0] = value;
pointerzero += Math.abs(newpointer);
}
}
}
public static void main(String[] args) {
String code =  "";
Tmpl tmpl = new Tmpl();
tmpl.run();
System.out.println();
}
private void run() {
// stub
}
public void error(int somevar) {
System.err.println("unknown command");
}
}

Фактически этот класс - изменённый BFInt смерженный с Memory, взятый у вышеупомянутого автора vampirus. Как видно все вызываемые методы имеют один единственный аргумент - целое число повторений.

В Tmpl есть пустой метод run() - именно он заполняется инструкциями на этапе компиляции.

В самом же компиляторе jFuck основной метод, который загружает шаблонный класс Tmpl, и заменяет пустую заглушку (stub) run() на сформированный код.

<br />    private void compile(char[] code) {
ClassGen cg = null;
try {
cg = new ClassGen(Repository.lookupClass("Tmpl"));
} catch (ClassNotFoundException ex) {
Logger.getLogger(Jfuck.class.getName()).log(Level.SEVERE, null, ex);
}
cg.replaceMethod(lookupMethod(cg, "run"), generateRun(cg, code));
try {
cg.getJavaClass().dump("Tmpl.class");
} catch (IOException ex) {
Logger.getLogger(Jfuck.class.getName()).log(Level.SEVERE, null, ex);
}
}

Весь компилятор

Получился не большой:

package ru.ifmo.jfuck;
public class Jfuck {
/**
* Program start point
* @param args 
*/
public static void main(String[] args) throws IOException{
if (args.length > 0){
BufferedReader input = null;
try {
StringBuilder sb = new StringBuilder();
try {
input = new BufferedReader(new FileReader(new File(args[0])));
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}
String line = null;
while (( line = input.readLine()) != null){
sb.append(line);
sb.append(System.getProperty("line.separator"));
}
Jfuck jfuck = new Jfuck();
jfuck.compile(jfuck.interpret(sb.toString().toCharArray()));
} catch (IOException ex) {
ex.printStackTrace();
} finally{
input.close();
}
}else{
System.out.println("Second argument should be path to program on Jfuck language.");
}
}
private Method lookupMethod(ClassGen tmpl, String methodName) {
for (Method m : tmpl.getMethods()) {
if (m.getName().equals(methodName)) {
return m;
}
}
return null;
}
private Method generateRun(ClassGen jc, char[] cmds) {
ConstantPoolGen cp = jc.getConstantPool();
InstructionList il = new InstructionList();
MethodGen m = new MethodGen(Constants.ACC_PRIVATE,
Type.VOID, new Type[]{}, new String[]{}, "run", "Tmpl", il, cp);
InstructionFactory factory = new InstructionFactory(jc);
translate(il, factory, cmds);
il.append(InstructionConstants.RETURN);
m.setMaxStack(5);
return m.getMethod();
}
/**
* Find for corresponding method
* @param ch brainfuck command
* @return method name in Tmpl instance
*/
private final String getMethod(char ch) {
switch (ch) {
case '>':
return "nextCeil";
case '<':
return "prevCeil";
case '+':
return "incValue";
case '-':
return "decValue";
case '.':
return "print";
case ',':
return "read";
case '~':
return "sleep";
default:
return "error";
}
}
/**
* Interprets brainfuck 
* @param src source program
* @return brainfuck w/o loops
*/
private char[] interpret(char[] src) {
return new BFInt(src).emulate();
}
private void compile(char[] code) {
ClassGen cg = null;
try {
cg = new ClassGen(Repository.lookupClass("Tmpl"));
} catch (ClassNotFoundException ex) {
Logger.getLogger(Jfuck.class.getName()).log(Level.SEVERE, null, ex);
}
cg.replaceMethod(lookupMethod(cg, "run"), generateRun(cg, code));
try {
cg.getJavaClass().dump("Tmpl.class");
} catch (IOException ex) {
Logger.getLogger(Jfuck.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void translate(InstructionList il, InstructionFactory factory,
char[] code) {
char last = code[0];
int count = 1;
for (int i = 1; i < code.length; i++) {
if (last == code[i]) {
count++;
} else {
//Invoke with arg last
il.append(InstructionConstants.THIS);
il.append(new PUSH(factory.getConstantPool(), count));
il.append(factory.createInvoke("Tmpl", getMethod(last),
Type.VOID, new Type[]{Type.INT}, 
Constants.INVOKEVIRTUAL));
count = 1;
}
last = code[i];
}
}
}

Ах да, так как Макс и я студенты СПбГУ ИТМО, поэтому назвали соотвественно package: ru.ifmo.jfuck

Скачать jFuck

Все в архиве jFuck.tar.gz(компилятор + генератор)

Вообще достаточно хорошо провели время, весьма интересный формат мероприятия. Узнали то, с чем вряд ли бы когда-нибудь столкнулись. Не часто приходится писать компиляторы в повседневной жизни :D