Как стать автором
Обновить

Комментарии 31

А в чём смысл статьи? В качестве обучающего материала для написания интерпретаторов статья не годится, потому что в основном языки программирования имеют более сложную структуру, и для интерпретации придётся писать лексический анализатор и синтаксический анализатор, что в статье не рассматривается. В качестве хорошего обучающего материала могу предложить книгу "Build your own lisp". Есть так же её вариации на отличных от си языках

ПОЕСНИТЕЛЬНАЯ БРИГАДА:

Смысл статьи - максимально быстро и незапарно получить полные права.

Теперь нужна граммар наци бригада

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

Ну типа поделился человек опытом

Да, только написать интерпретатор bf - это на уровне решенения линейного уравнения, т.е. настолько просто, что писать об 100500 раз не стоит. Плюс, как вы могли заметить, в программе автора нашли несколько значительных недочётов. Ещё сам автор, как бы, написал, что статью он писал не для того, чтобы чем-то интересным поделиться, а чтобы получить инвайт.

char command[4096];
	int i = 1;
	do {
		printf("BF> ");
		gets(command);

Ух. Писали интерпретатор bf, а написали учебную программу на переполнение стэка.

Верно подметили. Лучше использовать char* fgets(char * restrict str, int size, FILE * restrict stream);

Здесь полностью согласен, но данный пример интерпретатор не предназначен для ввода слишком длинной строки, и писался за 5 минут, поэтому этот недочёт не был замечен.

Почему бы Вам не сделать интерпретатор чуть более корректным?

  • 30000 - откуда число? Больше быть не может? И если его всё же используете, то лучше бы контролировать границы;

  • char * plus = "+"; . Э-э-э может лучше так?: char plus = '+'; ... if (buf[i] == plus) ...

  • Загонять весь файл в стэк? Это очень сурово. Попробуйте открыть файл размером ну хотя бы 1 гигабайт. Подозреваю, у вас всё рухнет;

  • команду интерпретатора не ввести более 4096 байт? А если ввод перенаправить из файла? (1 гигабайт)

  • Отрицательный индекс в ячейки быть не может?

Число 30000 взято из всех стандартных интерпретаторов bf.

char plus = "+" ; работать не будет, потому что тип данных "" это указатель.

Для примера попробуйте скомпилировать и запустить такой код

#include <studio.h>
int main()
{
  char test = "i";
  printf("%c\n", test);
  return 0;
}

Он выведет только перевод строки, т.к в переменную test записан указатель на символ "i".

Ваш напор похвален.

"Число 30000" Вы можете взять любое. Только желательно контролировать, что в результате скачки по ячейкам вы за него не выйдите. Здесь это никто, кроме вас, контролировать не будет.

Про char plus ... - там ставятся одинарные кавычки. И тип данных сразу станет char.

А в целом: Удачи, язык чистый-C - это хорошо.

Хранить заведомо односимвольные команды в char * — тоже так себе идея. Если объявить команды, как const char и сравнивать в конструкции switch — будет намного симпатичнее (и эффективнее тоже):
switch (buf[i]) {
  case cmd_plus:
    stack[indexx]++;
    break;
  case cmd_minus:
    stack[indexx]--;
    break;
  ...
}

Или вообще олдскульно:
#define CMD_PLUS '+'
#define CMD_MINUS '-'

Это все хорошо, но почему именно switch case?

if/else тоже хорошо, чем if/else не устроило?

switch-case выполняется быстрее

По-моему char buf[size] в "методе" main не скомпилируется, так как size не известно на момент компиляции

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

Брейнфак - это язык для очень упрощённой машины Тьюринга. А МТ, как известно, работает на ленте, бесконечной в обе стороны.
Поэтому следующая итерация (после исправления всех ошибок и недочётов, которые вам тут сказали) - это поддержка этой самой бесконечной ленты.

Самый тупой способ на голом Си, - это, конечно, просто реаллокация. Выделили новый, бОльший блок памяти, скопировали в него старый (со смещением вправо, если двигались влево и выбежали в отрицательные индексы) и вуаля.

Более затейливо - сделать страничную организацию... Ну, в общем, простор для творчества, особенно, на Си.

---

Следующий пункт для улучшения - это интерпретатор.

Каждой скобке соответствует своя парная скобка. Как минимум, логично вместо тупого поиска вперёд-назад по исходнику завести табличку быстрого перехода к паре.

Серии однотипных действий (+/-, </>) также можно хранить в виде групповой операции +n, >n, где n - целое число (положительное или отрицательное по итогу серии).

Итого, у нас получается следующий промежуточный код:

  • '+' n -- инкремент ячейки

  • '>' n -- инкремент адреса

  • '[' n -- условный переход на n команд (промежуточных!) - если текущая ячейка нулевая и вперёд, или если ненулевая и назад

  • '.' n -- вывод текущей ячейки n раз

  • ',' n -- ввод в текущую ячейку n раз (что означает проглатывание n-1 символов)

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

---

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

Например, обнуление ячейки: "[-]"

Или вывод нуль-терминированной строки "[.>]"

Или даже просто вывод n символов подряд ".>.>.>"

Можно расширить промежуточный код специальными командами для этих идиом.

---

Ну и дальше, насколько фантазии хватит.

Этот интерпретатор - очень упрощённый пример, поэтому я не стал заморачиваться, хотя можно было транслировать код bf в код C или ассемблера, а потом компилировать, но зто уже был бы компилятор а не интерпретатор.

перевод с одного языка на другой это транспилятор

gcc — набор компиляторов насколько я помню. Компилятор переводит с языка высокого уровня на низкий уровень, транспилятор с высокого на другой высокий.

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

Интерпретатор вполне может транслировать во внутреннее представление, а не заниматься синтаксическим разбором каждой команды каждый раз!

Хорошо у брейнфака каждая элементарная команда - это один символ, и поэтому разбор может быть вырожденным. Но вот уже структурная команда цикла требует чуть более сложного действия. Формально, грамматика-то контекстно-свободная, а не регулярная.

Так почему бы не отделить фазу разбора от фазы исполнения?

В общем, что хочу сказать. Плюсик за смелость, минусик за незрелость :) Стремительно исправляйте и улучшайте, пока народ не заклевал.

Чтобы интерпретатор работал надо подключить определённые библиотеки

С каких это пор стандартные заголовки стали назваться "библиотеками"?

char * plus = "+";

Язык С конечно же не требует const здесь, но это не причина не придерживаться элементарных правил константной корректности.

memset(stack, 0, 30000);

Обнуление агрегата при помощи memset в 2022 году? Да еще и с явным указанием размера через магическую константу??? И это при том, что верный термин "инициализация" вам таки знаком. Может быть стоило действительно воспользоваться инициализацией

char stack[30000] = { 0 };

gets(command);

O_o. Лолшто???

for (int x = 0; x < sizeof(command); x++) {

Интересно заметить, что в этом месте автор догадался использовать sizeof, пусть даже и с лишними скобками. Почему же тогда в memset выше была использована магическая константа?

int bf_shell()

Зачем эта функция возвращает int? И, пока не наступила эра C23, все таки пожалуйста

int bf_shell(void)

long size = ftell(file); fseek(file, 0L, SEEK_SET);

char buf[size];

Чтение целого файла в локальный VLA - весьма рискованная идея.

Чем плох memset?

В исходнике нет функции bf_shell вообще, там все действия происходят в main.

И почему возврат int чем-то может не устраивать?

Перед чтением файла программа узнает размер файла и создаёт массив который соответствует размеру файла и только потом файл считается в массив.

Чем плох memset?

Только тем, что незачем преумножать сущности без необходимости. Зачем притягивать сюда за уши библиотечную функцию memset, когда ядро языка уже предоставляет вам встроенные возможности для выполнения инициализации?

Точно так же можно вызывать какую-нибудь библиотечную функцию для того, чтобы увеличить переменную на 1, вместо классического ++i. Чем это плохо? Да, вроде, ничем... Но зачем?!

И, еще раз, в глаза бросается именно то, что вы сами использовали термин "инициализация", но при этом почему-то не воспользовались предоставляемыми вам языком С возможностями инициализации.

И почему возврат int чем-то может не устраивать?

"Не устраивает" не сам возврат int, а тот факт, что функция очевидно ничего осмысленного не возвращает в этом int. Зачем тогда было делать ее int, если можно было сделать ее void? Зачем взваливать на плечи читателя кода вопрос о том, что же это за возвращаемое значение, только для того, чтобы он в результате выяснил, что это возвращаемое значение никакого смысла не несет?

Перед чтением файла программа узнает размер файла и создаёт массив который соответствует размеру файла и только потом файл считается в массив.

Так о том и речь. Вы создаете автоматический VLA, размер которого равен размеру файла. Размер файла запросто может оказаться слишком большим для автоматического объекта. Не создавайте потенциально большие объекты "на стеке". Это добром не кончится.

Я в студенческие годы написал транслятор BF в C, это несложно, плюс, после оптимизации gcc даже терпимо работало. Потом поверх этого писал C-подобный язык, транслируемый в BF (да-да, транслируемый затем в С и компилируемый с помощью gcc). В общем, развлекался как умел.

А вот я как-то написал компилятор Brain**** в EXE.
Правда это был скорее транспилятор так как он переводил Brain**** в C а потом компилировал с помощью GCC...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории