Всем привет, и, сегодня вы узнаете как я писал компилятор Си.

Вот части статьи:

1 - Что за компилятор

2 - Как я его создал

Ну, как понимаете он чуть-чуть плохой но ладно.

Что за компилятор?

Я его назвал pycc. Как вы поняли, он написан на python. Ведь pycc переводится как Python C Compiler.

Он поддерживает такие правила:

  • Два типа данных (int и char)

  • Цикл while и цикл do-while.

  • Условие if-else.

  • Минимальный препроцессор (только директива #include)

  • Функции через void.

Их мало, но и чтобы эти правила работали я пропотел.

Где то щас 700 - 724 строчек кода (вроде бы).

Вот пример кода (C в хабре нету):

#include <stdio.h>

void fac(n) {
  int f = 1;
  while (n > 0) {
    f = f * n;
    n = n - 1;
  }
}

void main() {
  fac(5);
  printf("'factorial of 5: '+str(f)");
}

Из этого примера кода вот что мы видим:

  1. Все переменные - глобальные что и хорошо, что и плохо.

  2. Функция printf принимает питонью строку.

Вывод:

factorial of 5: 120

Как и ожидалось.

Да. Пока что нечего больше.

Вот так мы и прошлись по его правилам.

О том как я писал этот компилятор

Это мне далось очень не легко.

И я это знал за ранее.

Начал я с лексера. Написать Скопировать лексер с одного сайта очень легко.

Да и парсер дался достаточно легко.

Ну. По началу было всё хорошо. Я легко справлялся. И багов пока не было.

Но всё началось с void. Я реально понял что оказывается я делал всё с багами.

Когда я делал такой код:

void main() {
  int n = 5;
  while (n > 0) {
    n = n - 1;
  }
}

То, парсеру это не нравилось. Но в скором я начал писать данный код:

d = Parser()
d.parse(...)
block.append(...)

Это всё ОЧЕНЬ ПЛОХО. Но я делал этот проект не из-за красивого кода, а чтобы всё работало. Чего я добился.

В скором ошибка типизации... int x = "h"; это было нормально... Ну ладно эта типизация (я быстро решил) но потом и ошибка с char.

Которая была из-за пробелов. Да. Но... К концу я всё таки справился и исправил все баги кроме того самого (с пробелами, но я был доволен).

Сейчас я работаю над другими изменениями.

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

CHANGELOG (только у меня, больше не у кого)

Пре-альфа 1.0

Очень мало было.

Циклы, условия и переменные.

При этом было очень примитивно и не работающее.

Альфа 1.0

Добавился сам полный функционал. Больше багов было устранено.

Но, они были. За то вы могли написать такой код:

void main() {
  int n = 5;
  while (n > 0) {
    n = n - 1;
  }
}

И он мог работать. Что самое главное.

Бета 1.0

Ну прям все баги исправлены.

И добавлена возможность выводить другие данные.

Релиз 1.0

Официальный выпуск. Тогда я опубликовал это на гитхаб.

Релиз 1.1

Добавлен препроцессор.

К примеру:

#include <stdio.h>

Теперь можно было импортировать заголовочные обычные файлы.

Релиз 1.1.2 (Последний)

Я исправил баг с больше и меньше.

Раньше было не так:

if stack[-2] > stack[-1]:
  ...
else:
  ...

А так:

if stack[-2] >= stack[-1]:
  ...
else:
  ...

И я это исправил (да, самое легкое исправление)

Итог

Статья получилась плохой и большой, но, вот ссылка на проект:

SystemSoftware2/pycc: Компилятор C на Python

В скором я добавлю больше всего. И возможно компилироваться будет в x86 ASM.

UPD: компилируется в байткод

Комментарии (37)


  1. SystemSoft Автор
    17.05.2025 18:28

    CHANGELOG бонусом.


  1. alex5jb
    17.05.2025 18:28

    Но зачем?


    1. SystemSoft Автор
      17.05.2025 18:28

      Скорее для опыта. Для развлечения. Мне оно нравится.


      1. alex5jb
        17.05.2025 18:28

        Тогда годно!


      1. saipr
        17.05.2025 18:28

        Я бы вам рекомендовал обратить внимание то на книгу Эндрю Таненбаума «Operating Systems Design and Implementation» на ее первое издание и на то, как в ней представлен язык Си. И может это описание Си и положить в основу вашего компилятора. Удачи вам.


        1. SystemSoft Автор
          17.05.2025 18:28

          Спасибо вам. Буду читать.


      1. RealOd
        17.05.2025 18:28

        Простите, вы не поможете выучить данный язык программирования?


  1. pnmv
    17.05.2025 18:28

    Прошу поощения. Возможно, я невнимательно прочёл, но что и во что он "компилирует"? Если это перевод кода, написанного на си, в код на питоне, то почему именно компилятор, а не транслятор?


    1. SystemSoft Автор
      17.05.2025 18:28

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


      1. SystemSoft Автор
        17.05.2025 18:28

        созданной тоже мной (для понимания)


        1. HyperWin
          17.05.2025 18:28

          У нас тоже есть подобный проект, я разрабатываю свою ISA с эмулятором и ассемблером, а один чувак в нашей команде пишет компилятор ANSI C для него.


      1. perfect_genius
        17.05.2025 18:28

        Это надо бы добавить в статью.


        1. SystemSoft Автор
          17.05.2025 18:28

          Добавил


  1. JBFW
    17.05.2025 18:28

    "Но зачем?" - в смысле, зачем именно C?

    Что если вместо всем известного C попробовать придумать другой язык, решающий какую-то в принципе иную задачу?

    Не A * B, а что-нибудь из статистического анализа, например, или еще что-то довольно специальное, то, что сейчас решается через программирование хотя бы на том же С, а могло бы быть записано на своем языке в несколько операторов?

    И вот к нему - компилятор в байткод?


    1. pnmv
      17.05.2025 18:28

      Такую задачу вредно навешивать на единственного разработчика.


      1. JBFW
        17.05.2025 18:28

        Почему нет? )

        Если бы тот же Линус Торвальдс начал с подбора команды разработчиков - никакого линукса вообще бы не получилось никогда, завязли бы в согласованиях, утверждениях, бюджетировании, зависли на митингах и признали бы экономически необоснованным и нецелесообразным.


        1. pnmv
          17.05.2025 18:28

          Тогда, всю эту дичь ещё не придумали, до такой степени.


    1. SystemSoft Автор
      17.05.2025 18:28

      Идея хороша. Осталось придумать и создать.


  1. CatAssa
    17.05.2025 18:28

    Посмотрите на старую книжку Р.Берри и Б.Микинза, там и компилятор С есть:

    Скрытый текст


    1. SystemSoft Автор
      17.05.2025 18:28

      Ок


  1. CrazyHackGUT
    17.05.2025 18:28

    Увидев printf, можно понять, что это не Си. Вовсе. gcc это скомпилировать - скомпилирует вполне, но по факту работать не будет так же.


    1. alex5jb
      17.05.2025 18:28

      printf - это как раз самый не на есть C без всяких плюсов.


      1. CatAssa
        17.05.2025 18:28

        Там от printf только название.


        1. alex5jb
          17.05.2025 18:28

          Разверните вашу мысль пжлста.


          1. blueboar2
            17.05.2025 18:28

            Автор туда засунул питоновые строки, а не сишные


  1. imsyava
    17.05.2025 18:28

    Аж захотелось узнать, как эта реализация факториала

    #include <stdio.h>
    void fac(n) { int f = 0; while (n > 0) { f = f * n; n = n - 1; } }
    void main() {
    fac(5);
    printf("'factorial of 5: '+str(f)");
    }

    Смогла выдать

    factorial of 5: 120

    Там же что ни подставь f будет 0. Либо этот самодельный компилятор умеет распознавать человеческий фактор и корректировать вычисления для соответствия результата ожиданиям, либо при инициализации там всё же f = 1.


    1. SystemSoft Автор
      17.05.2025 18:28

      Спасибо за поправку


  1. kmatveev
    17.05.2025 18:28

    Я почитал исходники. Питона не знаю, но что-то попробовал понять.

    Лексер нужно улучшать. Во-первых, он принципиально неправильно написан: вы тупо пытаетесь регулярки применять к вводу, из-за чего, встретив, например, "iframe", он выделит "if" как ключевое слово. Правильный способ - конечный автомат, то есть у лексера появляется состояние. Ну и напоследок, вы бы хоть компиляцию регулярок не делали каждый раз, очень уж тормознуто.

    С компилятором не понял вот чего: все функции, получается, инлайнятся?

    С виртуальной машиной много непоняток. Почему-то FETCH, который у вас для сохранения глобальной переменной на стеке, в случае, если переменная неинициализированна (или тупо опечатка), пушит 0. А STORE наоборот, ругается, если глобальной переменной нет. Я бы сделал наоборот. Не очень понял смысла операции POP, и мне кажется, что она pc недостаточно увеличивает.


    1. SystemSoft Автор
      17.05.2025 18:28

      Все функции инлайнятся потому что к тому времени я не понимал как это делать.

      С виртуальной машиной я убрал эти непонятности.

      Лексер... Ну... Да, согласен.

      С парсером тоже согласен. Там теги просто тратят смысл.


  1. GambitOZ
    17.05.2025 18:28

    Писать компилятор скриптового движка си написанного на скриптовом движке питона, который в свою очередь изначально написан на си - это оригинально. Браво))


    1. SystemSoft Автор
      17.05.2025 18:28

      угу))


  1. Oeaoo
    17.05.2025 18:28

    Это был смешной стендап.


    1. SystemSoft Автор
      17.05.2025 18:28

      Что вы имели в виду?


  1. perfect_genius
    17.05.2025 18:28

    Использовались ли нейросети при написании кода?


    1. SystemSoft Автор
      17.05.2025 18:28

      Нет. Писал всё сам. Кроме лексера


  1. SkylineXXX
    17.05.2025 18:28

    Кажется первые версии компиляторов gcc, RMS писал "одну каску". И ничего...не помер. При чем не с двумя типами данных, а полные.


    1. SystemSoft Автор
      17.05.2025 18:28

      Раскрою секрет: мне лень было добавлять double, float