
Скучая на очередном уроке биологии я вспомнил о таком прекрасном творении как brainfuck и о его интерпретаторах. А вернее я вспомнил что самый маленький интерпритатор был написан ещё в бородатых годах и занимал что-то около 150 байт.
Вернувшись домой я нашел самый маленикий среди них:
s[99],*r=s,*d,c;main(a,b){char*v=1[d=b];for(;c=*v++%93;)for(b=c&2,b=c%7?a&&(c&17?c&1?(*r+=b-1):(r+=b-1):syscall(4-!b,b,r,1),0):v;b&&c|a**r;v=d)main(!c,&a);d=v;}
Он написан на Си, занимает 160 байт иии... Это много.
Посмотрите как много место занимает обьявление функции main. Одним словом: ужас.
Решение очевидно - нужно уменьшить синтаксис насколько это возможно.
Есть два стула...
Первый стул - создать свой язык с минимальным синтаксисом и кучей сахара на основе существующего компилятора Си.
Второй стул - создать свой язык с минимальным синтаксисоми кучей сахара, который будет просто транслироваться в Си.
Выбор очевиден. Писать свой компилятор даже на основе готового - смерть, долгая и мучительная. Наша программа должна просто заменить в исходном файле некоторые части, записать итог в файлик и отдать этот файлик gcc. Так что, сделав умный вид, я принялся обдумывать синтаксис языка microC.
Синтаксис языка и что вообще нужно заменять
Синтаксис я решил построить на макросах - специальных конструкций, развёртывающихся в Си код согласно параметрам.
Взяв первый попавшийся, неиспользуемый символ в Си - тильда (~), я принялся плясать вокруг этого. И решил сделать для макросов подобный синтаксис:
~<назвение макроса (один символ)><тело макроса>!
Или для более сложных конструкций:
~<назвение макроса><осложняющая часть (дальше просто ОЧ)>:<тело>!
Что такое осложняющая часть
На примере if и while хочу показать что такое осложняющая часть
if (a == 5) {
printf_s("a == 5");
}
while (a > 5) {
printf_s("a--");
a--;
}
a == 5 и a > 5 и есть осложняющая часть в if и while соответственно
Некоторые макросы
~M<тело>! |
main функция |
main (a,b) {тело} |
~w<ОЧ>:<тело> |
while цикл |
while (ОЧ) {тело} |
~i<ОЧ>:<тело> |
if |
if (ОЧ) {тело} |
Для использования символа '!' как отрицание следует использовать '`'
Так же переменные и функции претерпели некоторые изменения
// Си код
char Cvar = 5;
/microC код/
7MCvar5;
Для начала я решил опустить смвол равенства, тем самым сделав невозможным использование цифр в названии переменной. Можно догадаться, что тип char превратился в 7, но почему именно 7 и какими цифрами обозначаются другие типы.
Типы данных
Самый компактный способ записи - числа - был найден сразу, но какие числа должы соответствовать каким типам?
Вспомним, что сколько в С выделяется байт под каждый тип в памяти:
char |
8 |
short |
16 |
int |
32 (ну не всегда, но примем все-таки за 32) |
long |
64 (ситуация как с int) |
Решено! будем обозначать типы их размером. Только вот под само число в знаковом типе выделяется на один бит меньше, чем в верхнеописаной таблице, поэтому будем обозначать типы по количеству бит, отведённых в них под само число, игнорируя бит знака. И того вот итоговая таблица типов:
0 |
пустой тип для указания своих, которые microC не потдерживает |
1 |
void |
7 |
char |
8 |
unsigned char |
15 |
short |
16 |
unsigned short |
31 |
int |
32 |
unsigned int |
63 |
long |
64 |
unsigned long |
Небольшая проблемка и как её исправить
Для присвоения одной переменной, значения другой в C используется подобный синтаксис:
a = b;
Но в microC появиться знак | на месте пробела, иначе ab будет расценена как имя переменнойкоторую мы создаем
a|b;
И, к сожалению, итоговое количество знаков не изменилось...
Большая проблемка и как её я не исправил
Если упрощать, то компилятор microC работает так:
1. находит тип из таблицы
2. читает все последующие буквы и интерпритирует их как имя переменной
3. все что идет дальше (или после знака '|') просто записывает в итоговый Си файл
Такой же метод работает и для осложняющей части.
и поэтому какой-нибудь хитрый код, по типу
7var|a==5 ? b|k : G|k;
// в Си это выглядело бы так:
char var = a == 5 ? b=k : G=k;
// но из-за вышеназвонной проблемы в microC это будет выглядеть все же так:
7var|a==5 ? b=k : G=k;
В целом, есть ещё пару изменений в синтаксисе, но вы сможете самостоятельно почитать про них в github репозитории.
Итог. Переписывание интерпритатора на microC
После долгого и мучительного переписывания интерпритатора получилось следующее
7s[99],*d,c;*r|s;~M7*v1[d=v];~wc=*v++%93:b|c&2,b=c%7?a&&(c&17?c&1?(*r+=b-1):(r+=b-1):syscall(4-!b,b,r,1),0):v;~wb&&c|a**r:main(!c,&a);v|d;!!d=v;!
Форматированая версия
7s[99],*d,c;
*r|s;
~M
7*v1[d=v];
~wc=*v++%93:
b|c&2,b=c%7?
a&&
(c&17 ?
c&1 ?
(*r+=b-1):
(r+=b-1):
syscall(4-!b,b,r,1),0):v;
~wb&&c|a**r:
main(!c,&a);
v|d;
!
!
d=v;
!
Итого: 145 байт! И это на 15 байт меньше чем оригинал!
Также я написал ещё и свою версию на 250 байт
~Istdio.h!
7a[30000];7*p|a;
7*l[99];7i0;
~m7*s1[v];7*k|s;
~w*k!='!':
~S*k:
~c60:p--;~b
~c62:p++;~b
~c43:*p+=1;~b
~c45:*p-=1;~b
~c44:*p|getchar();~b
~c46:putchar(*p);~b
~c91:~i*p!=0:l[i++]|k;~b!~w*k!=93:k++;!~b
~c93:~i*p!=0:k|l[i-1];~b!i--;~b!
k++;
!!
Спасибо за прочтение. Код компилятора и гайд по языку лежат на гитхабе:
Комментарии (5)
datacompboy
12.05.2025 09:42Для использования символа '!' как отрицание следует использовать '`'
а как использовать тильду как тильду? (~ -- это побитовое отрицание)
CBET_TbMbI
12.05.2025 09:42И это на 15 байт меньше чем оригинал!
А как это отразилось на скорости компиляции и скомпилированного? Никак? Или какие-нибудь отличия могут быть? Даёшь тест!
wataru
12.05.2025 09:42Вообще странный подход. Если вы пишите программу, которая генерирует код, то почему бы не ввести специальную команду "@" - которая означает "интерпретатор брейнфака". Вот вы и ужали всю программу до 1 символа!
Reposlav
12.05.2025 09:42Это забавный эксперимент, однако думаю, что лучше смотреть на размеры бинарника, а не исходного кода
rsashka
Не будет ли проще и понятнее назвать это "условием"?