
Скучая на очередном уроке биологии, я вспомнил о таком прекрасном творении как 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) |
Душное уточнение
как подсказали мне в коментах: char и short тоже могут быть разных размерностей
Обожаю Си...
Решено! будем обозначать типы их размером. Только вот под само число в знаковом типе выделяется на один бит меньше, чем в верхнеописаной таблице, поэтому будем обозначать типы по количеству бит, отведённых в них под само число, игнорируя бит знака. И того вот итоговая таблица типов:
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++;
!!
Спасибо за прочтение. Код компилятора и гайд по языку лежат на гитхабе:
Комментарии (13)
datacompboy
12.05.2025 09:42Для использования символа '!' как отрицание следует использовать '`'
а как использовать тильду как тильду? (~ -- это побитовое отрицание)
Desvor Автор
12.05.2025 09:42... Я только щас вспомнил что есть такая операция...
В старых версиях microC были макросы, но они были удалены
А вообще есть спец. синтаксис: все что записано между двумя обратными слэшами — включается в исходный файл без изменений
CBET_TbMbI
12.05.2025 09:42И это на 15 байт меньше чем оригинал!
А как это отразилось на скорости компиляции и скомпилированного? Никак? Или какие-нибудь отличия могут быть? Даёшь тест!
Desvor Автор
12.05.2025 09:42вообще, так как microC компилируется в Си (причем с неочень большими изменениями), то и итоговая скорость работы не будет отличаться, как и размер бинарника
Разница в размерах бинарника и скорости, соответственно, будет заметна только на очень больших и хитрых проектах
wataru
12.05.2025 09:42Вообще странный подход. Если вы пишите программу, которая генерирует код, то почему бы не ввести специальную команду "@" - которая означает "интерпретатор брейнфака". Вот вы и ужали всю программу до 1 символа!
Desvor Автор
12.05.2025 09:42вообще да, но такгда почему бы создателям Си не ввести такую же команду с таким же функционалом?
Си компилируется в асм, а microC в Си
Reposlav
12.05.2025 09:42Это забавный эксперимент, однако думаю, что лучше смотреть на размеры бинарника, а не исходного кода
osmanpasha
12.05.2025 09:42Взяв первый попавшийся, неиспользуемый символ в Си - тильда (~)
Как это неиспользуемый, это же оператор побитового отрицания
pwn3r
12.05.2025 09:42Идея с microC крутая: макросы, тильды и всё это минималистичное безумие - прям мозговыносяще, но работает. Особенно понравилось, как типы через количество бит реализованы - странно, но логично.
Формат записи, конечно, не для слабонервных, но 145 байт - это уже серьёзно. По сути, это просто препроцессор для сжатия исходников, и это работает.
Paket236
12.05.2025 09:42Вспомним, что сколько в С выделяется байт под каждый тип в памяти:
А ниже в таблице, почему-то, указаны биты.
int
32 (ну не всегда, но примем все-таки за 32)
char тоже не всегда 8 бит и short тоже не всегда 16 бит.
rsashka
Не будет ли проще и понятнее назвать это "условием"?
Desvor Автор
Ну в таких примерах - да, но в microC есть и макросы, где осложняющая часть — это не условие