Скучая на очередном уроке биологии, я вспомнил о таком прекрасном творении как 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)


  1. rsashka
    12.05.2025 09:42

    a == 5 и a > 5 и есть осложняющая часть в if и while соответственно

    Не будет ли проще и понятнее назвать это "условием"?


    1. Desvor Автор
      12.05.2025 09:42

      Ну в таких примерах - да, но в microC есть и макросы, где осложняющая часть — это не условие


  1. datacompboy
    12.05.2025 09:42

    Для использования символа '!' как отрицание следует использовать '`'

    а как использовать тильду как тильду? (~ -- это побитовое отрицание)


    1. Desvor Автор
      12.05.2025 09:42

      ... Я только щас вспомнил что есть такая операция...

      В старых версиях microC были макросы, но они были удалены

      А вообще есть спец. синтаксис: все что записано между двумя обратными слэшами — включается в исходный файл без изменений


  1. CBET_TbMbI
    12.05.2025 09:42

    И это на 15 байт меньше чем оригинал!

    А как это отразилось на скорости компиляции и скомпилированного? Никак? Или какие-нибудь отличия могут быть? Даёшь тест!


    1. Desvor Автор
      12.05.2025 09:42

      вообще, так как microC компилируется в Си (причем с неочень большими изменениями), то и итоговая скорость работы не будет отличаться, как и размер бинарника

      Разница в размерах бинарника и скорости, соответственно, будет заметна только на очень больших и хитрых проектах


  1. wataru
    12.05.2025 09:42

    Вообще странный подход. Если вы пишите программу, которая генерирует код, то почему бы не ввести специальную команду "@" - которая означает "интерпретатор брейнфака". Вот вы и ужали всю программу до 1 символа!


    1. Desvor Автор
      12.05.2025 09:42

      вообще да, но такгда почему бы создателям Си не ввести такую же команду с таким же функционалом?
      Си компилируется в асм, а microC в Си


  1. Reposlav
    12.05.2025 09:42

    Это забавный эксперимент, однако думаю, что лучше смотреть на размеры бинарника, а не исходного кода


  1. osmanpasha
    12.05.2025 09:42

    Взяв первый попавшийся, неиспользуемый символ в Си - тильда (~)

    Как это неиспользуемый, это же оператор побитового отрицания


  1. pwn3r
    12.05.2025 09:42

    Идея с microC крутая: макросы, тильды и всё это минималистичное безумие - прям мозговыносяще, но работает. Особенно понравилось, как типы через количество бит реализованы - странно, но логично.

    Формат записи, конечно, не для слабонервных, но 145 байт - это уже серьёзно. По сути, это просто препроцессор для сжатия исходников, и это работает.


  1. Paket236
    12.05.2025 09:42

    Вспомним, что сколько в С выделяется байт под каждый тип в памяти:

    А ниже в таблице, почему-то, указаны биты.

    int

    32 (ну не всегда, но примем все-таки за 32)

    char тоже не всегда 8 бит и short тоже не всегда 16 бит.


    1. Desvor Автор
      12.05.2025 09:42

      ну на размерность int наиболее частая к изменению