LINUX.ORG.RU

Отлаживаю кодогенератор

 , ,


2

1

В свободное время попиливаю компилятор ЯП для разминки мозгов и чтобы не забыть ассемблер окончательно.

По семантике ЯП Си-подобный, по синтаксису больше похож на что-то из линейки Паскаль/Модула/Оберон.

За основу брал Context Хохлова, но перепилил практически все сорцы.

Компилятор многопроходный. Сначала строится синтаксическое дерево. Потом на нём выполняются некоторые простые оптимизации. Потом запускается бэкэнд. (Другой подход применён, например, в tcc, где программа компилируется за один проход — ради скорости компиляции.)

Некоторые вещи вычисляются/оптимизируются сразу при парсинге. Например, логическое отрицание вообще не сохраняется в синтаксическое дерево, если может быть заменено на эквивалентную трансформацию отрицаемого выражения:

word Not(word P)
	switch Node[P].ID of
	case iOR:   Node[P].ID = iAND; Not(Node[P].pLeft); Not(Node[P].pRight);
	case iAND:  Node[P].ID = iOR;  Not(Node[P].pLeft); Not(Node[P].pRight);
	case iXOR:  Node[P].ID = iEQV;
	case iEQV:  Node[P].ID = iXOR;
	case iLT:   Node[P].ID = iGE;
	case iLE:   Node[P].ID = iGT;
	case iEQ:   Node[P].ID = iNE;
	case iNE:   Node[P].ID = iEQ;
	case iGE:   Node[P].ID = iLT;
	case iGT:   Node[P].ID = iLE;
	default:
		word P2 = Peek();
		Node[P2].ID    = iNOT;
		Node[P2].pLeft = P;
		return P2;
	end:switch

	return P;
end

Бэкэнд не строит промежуточного представления, на котором можно было бы гонять умные алгоритмы, соответственно 90% оптимизаций, описанных в «Книге дракона» невозможны. Пока я пытаюсь выжать максимум из компиляции на основе синтаксического дерева.

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

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

Оптимизация умножения на константу при помощи add, shl и lea. Пример умножения на 15 при помощи lea:

        mov     EAX,  dword [EBP-24]
        lea     EAX,  [EAX*2+EAX]
        lea     EAX,  [EAX*4+EAX]
        push    EAX
        call    @10013

Компиляция выражения put_word(v100 * 18 + v1 * 27);:

        mov     EAX,  dword [EBP-24]
        lea     EAX,  [EAX*8+EAX]
        add     EAX,  EAX
        mov     EBX,  dword [EBP-8]
        lea     EBX,  [EBX*2+EBX]
        lea     EBX,  [EBX*8+EBX]
        add     EAX,  EBX
        push    EAX
        call    @10013

Здесь умножения заменены более оптимальными инструкциями и разумно распределены регистры.

Компиляция выражения word v500 = v2 * v100 + v3 * v100;:

        mov     EAX,  dword [EBP-12]
        mul     dword [EBP-24]
        push    EAX
        mov     EAX,  dword [EBP-16]
        mul     dword [EBP-24]
        pop     EBX
        add     EAX,  EBX
        mov     dword [EBP-32], EAX

Более оптимальным кодом был бы вариант:

        mov     EAX,  dword [EBP-12]
        mul     dword [EBP-24]
        mov     EBX, EAX
        mov     EAX,  dword [EBP-16]
        mul     dword [EBP-24]
        add     EAX,  EBX
        mov     dword [EBP-32], EAX

Но компилятору в текущей реализации недоступна информация, потребуется ли EBX при вычислении второго слагаемого. Поэтому он сохраняет EAX на стек и затем восстанавливает оттуда значение в EBX уже после того как второе слагаемое вычислено.

Компиляция выражения put_word(v500 - v100 * v5);:

        mov     EAX,  dword [EBP-24]
        mul     dword [EBP-20]
        mov     EBX,  dword [EBP-32]
        xchg    EAX,  EBX
        sub     EAX,  EBX
        push    EAX
        call    @10013

Компилятор требует от выражения результат во вполне определённом регистре. Поэтому столкнувшись с тем, что операнды оказались в обратном порядке, он вынужден вставлять лишний xchg. Ситуацию можно решить, если разрешить выражению в ряде случаев свободно выбирать регистр для результата. Тогда код был бы таким:

        mov     EAX,  dword [EBP-24]
        mul     dword [EBP-20]
        mov     EBX,  dword [EBP-32]
        sub     EBX,  EAX
        push    EBX
        call    @10013

Компиляция выражения put_word((v500 * 2 + v2) + (v200 & v1) + (v200 - 100) * 10 - 100 - v200 - v100);:

        mov     EAX,  dword [EBP-32]
        add     EAX,  EAX
        add     EAX,  dword [EBP-12]
        mov     EBX,  dword [EBP-28]
        and     EBX,  dword [EBP-8]
        add     EAX,  EBX
        mov     EBX,  dword [EBP-28]
        sub     EBX,  100
        lea     EBX,  [EBX*4+EBX]
        add     EBX,  EBX
        add     EAX,  EBX
        sub     EAX,  100
        sub     EAX,  dword [EBP-28]
        sub     EAX,  dword [EBP-24]
        push    EAX
        call    @10013

Пример компиляции ветвлений для блока switch (табличная реализация switch пока отсутствует) :

        mov     EAX,  dword [@@DATA+EAX+7177840]
        sub     EAX, 0xD
        je      @11097
        sub     EAX, 0x3
        je      @11098
        sub     EAX, 0xFFFFFFFE
        je      @11099
        dec     EAX
        je      @11100
        sub     EAX, 0x5
        je      @11101
        dec     EAX
        je      @11102
        dec     EAX
        je      @11103
        dec     EAX
        je      @11104
        dec     EAX
        je      @11105
        dec     EAX
        je      @11106
        jmp     @11107

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

	word v0 = 0;
	word v1 = 1;
	word v2 = 2;
	word v3 = 3;
	word v5 = 5;
	word v100 = 100;
	word v200 = 200;
        xor     EAX,  EAX
        mov     dword [EBP-4], EAX
        inc     EAX
        mov     dword [EBP-8], EAX
        inc     EAX
        mov     dword [EBP-12], EAX
        inc     EAX
        mov     dword [EBP-16], EAX
        mov     EAX,  5
        mov     dword [EBP-20], EAX
        mov     EAX,  100
        mov     dword [EBP-24], EAX
        add     EAX,  EAX
        mov     dword [EBP-28], EAX

Сам компилятор полностью работоспособен, запускается в 32-разрядном режиме под виндой и линуксом и, в принципе, легко может быть портирован на другие ОС. Написан на самом себе (self-hosted) и частично обвешан тестами. Далее в планах научить его генерировать код для какой-нибудь другой системы команд. ARM, MIPS, RISC-V… что-то из этого.

Задавайте ваши вопросы.

★★

Последнее исправление: wandrien (всего исправлений: 2)

Выглядит как пост из раздела «Галерея», только почему-то без картинки. Вопрос: в чем подвох?

annulen ★★★★★
()

Задавайте ваши вопросы.

грамматику языка давай, и лучше полный language report.

тока не думай, что кто-то будет на нем программировать кроме тебя.

для самодельного языка лучше генерить не в асм, а в си… если конечно не тащить llvm.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

лучше генерить не в асм, а в си

Генерировать в Си, чтобы потом запускать gcc. Скучно же.

Я его потом еще может для MS-DOS бинарники научу генерировать.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Скриншот консоли с логом компиляции

Надо в галерее подкатегорию завести. Галерея из листингов вместо картинок.

alpha ★★★★★
()
Ответ на: комментарий от wandrien

Генерировать в Си, чтобы потом запускать gcc. Скучно же.

  1. сгенерировав в си, ты получишь переносимость и автоматический линк с сишными кодом.

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

  3. ты сосредоточишься на возможностях языка, а не его трансляции в код.

alysnix ★★★
()
Ответ на: комментарий от alysnix

Лёгкое связывание с Си не такое уж лёгкое(будет влиять на язык) + сильно зависит от языка, его области применения и пр.

AKonia ★★
()
Последнее исправление: AKonia (всего исправлений: 1)

Уверен, что главный и правильный вопрос: зачем ? Если это для себя, как упражнение для головы, то это одно и тогда - толку вам от вопросов ? Ежели цель получить язык, прикладной для некоторой узкой/широкой области, то тогда вопросы будут привязаны к тому, какую задачу решает или какой задаче язык упрощает решение ?

AKonia ★★
()
Последнее исправление: AKonia (всего исправлений: 1)
Ответ на: комментарий от wandrien

Чел, ты оч странный.

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

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

а какие у тебя выражения? покажи асм код для выражений вида(паскале-подобная запись)

fun()^.field := array[i+const].field1^[inx];

то есть выражение должно быть со сложным адресным вычислением как минимум.

и потом - что значит результат выражения в «определенном регистре»? в нормальном генераторе этого ограничения быть не должно.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от AKonia

Цель по большей части получить удовольствие.

В планах также сделать фронт для Си и получить возможность компилировать свой сишный софт своим же компилятором.

То есть тут на самом деле три направления:

  • Эксперименты с кодогенерацией.

  • Получение компилятора Си в будущем.

  • Эксперименты с дизайном ЯП, но для этого нужно сначала подготовить фундамент в виде архитектуры самого компилятора, которая у Хохлова была не в лучшей форме.

wandrien ★★
() автор топика

Ничего не понятно, но очень интересно ;) Приятно что есть специалисты, которые в состоянии делать такие вещи

I-Love-Microsoft ★★★★★
()
Ответ на: комментарий от wandrien

Эксперименты с дизайном ЯП, но для этого нужно сначала подготовить фундамент в виде архитектуры самого компилятора, которая у Хохлова была не в лучшей форме.

Вот тут спорно, по моему архитектуру компилятора лучше продумывать с учётом языковых возможностей, дабы не получилось так, что негибкость этой самой архитектуры порождала костыли или напротив, чтобы гибкость архитектуры не была катастрофичной по КПД(используемым возможностей/доступные возможности), либо тогда проектировать компилятор в духе llvm(или более лёгких qbe, mir) - выделяя отдельно компилятор промежуточного языка.

AKonia ★★
()
Ответ на: комментарий от alysnix

fun()^.field := array[i+const].field1^[inx];

getA().field2 = array[i + 2].pointer[5];
        mov     EAX,  dword [EBP-44]     ; i
        add     EAX,  2
        imul    EAX,  12
        mov     EBX,  5
        shl     EBX,  2
        add     EBX,  dword [EBP+EAX-156]
        mov     EAX,  dword [EBX+0]
        push    EAX
        call    @10014                   ; getA
        pop     EBX
        mov     dword [EAX+4], EBX
wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Цель по большей части получить удовольствие.

хочешь получать удовольствие - не занимайся велосипедами кодогенерации.

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

то есть делать сегодня «нечто_типа_си», это все равно, что кататься на деревянной повозке с паровым двигателем. это время уже ушло.

когда придумывают новый язык, прежде всего преполагают некий новый подход, или сверхзадачу, некую фишку которая выделит этот язык среди прочих сотен, а может тысяч.

вот сначала придумай фишку, потом язык, а потом делай его имплементацию.

alysnix ★★★
()
Ответ на: комментарий от wandrien

getA().field2 = array[i + 2].pointer[5];

а почему у тебя адрес приемника вычисляется позже вычисления выражения? ты это оговариваешь как-то?

обычно выражения вычисляются слева направо, то есть первой надо вызывать функцию getA(), брать адрес поля, держать его в регистре, а потом только вычислять выражение. не?

alysnix ★★★
()

Задавайте ваши вопросы.

А какие вопросы могут быть на то что ты подро%ил? Ну подро%ил и молодец.

slovazap ★★★★★
()

Задавайте ваши вопросы.

Зачем?

ox55ff ★★★★★
()
Ответ на: комментарий от alysnix

хочешь получать удовольствие - не занимайся велосипедами кодогенерации.

Чо-т ору.

wandrien ★★
() автор топика
Ответ на: комментарий от alysnix

обычно выражения вычисляются слева направо, то есть первой надо вызывать функцию getA(), брать адрес поля, держать его в регистре, а потом только вычислять выражение. не?

Это в каком сломанном языке так?

Ни в одном нормальном ЯП порядок вычислений в пределах одного одного выражения не гарантирован, за исключением ограниченного набора операторов, сохраняющих порядок вычислений.

хочешь получать удовольствие - не занимайся велосипедами кодогенерации.

Чел, серьёзно. Ты не в курсе, как работает вычисление выражений в том же Си хотя бы, но с умными видом рассказываешь, как получать удовольствие?

Я делаю то, что мне нравится, не спрашивая твоего мнения о своих вкусах. =)

когда придумывают новый язык, прежде всего преполагают некий новый подход, или сверхзадачу, некую фишку которая выделит этот язык среди прочих сотен, а может тысяч.

Ага, поэтому я должен пойти и изобрести очередной Rust. =) Не. Я тебе ничего не должен. Прикинь, как бывает?

wandrien ★★
() автор топика
Ответ на: комментарий от xmikex

Было такое. Оптимизирующие макросы к FASM-у я тоже писал лет 15 назад.

Синтаксис в процессе переделки. О нём тоже когда-нибудь топик создам, когда будет более близко к тому, что я хочу.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Чел, серьёзно. Ты не в курсе, как работает вычисление выражений в том же Си хотя бы, но с умными видом рассказываешь, как получать удовольствие?

не. я не на полном серьезе.

на полном серьезе, я б хотел видеть лангрепорт, поскольку без грамматики разбираться в кодогенерации(а больше ничего тут и не приведено) - как-то перебор.

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

alysnix ★★★
()

и чтобы не забыть ассемблер окончательно

А под ARM кодогенерация будет?

hobbit ★★★★★
()
Ответ на: комментарий от alysnix

Ну бл тебя интересуют каньсепсии, его кодогенерация, ты чего душный-то такой?

filosofia
()
Ответ на: комментарий от AKonia

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

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

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

В целом же моя задача, сделать практическую реализацию ЯП в таком объеме, чтобы человек, имеющий навыки программирования, мог её понять в одно лицо. И при этом это был не игрушечный компилятор, а практически полезный.

То есть там, где мы можем оптимизировать код без страшной математики на графах, мы делаем такой оптимизатор. А когда речь заходит о техниках оптимизации, грозящих вылиться в переизобретение половины gcc, мы говорим «нет, спасибо».

wandrien ★★
() автор топика

А где исходники посмотреть?

anonymous
()

Компиляторы — оч интересный топик. Я свои штуки бэкал либо подобием виртуальной машины с JIT, либо llvm в зависимости от юз-кейзов, поэтому тема генерации мне не особо интересна и сказать/спросить нечего, но я продолжу свои наблюдения :)

filosofia
()

Вспомнил про книжку Криса Касперски, нашел в сети: https://studizba.com/pdf_reader/web/viewer.html?file=/uploads/unziped/real/219234/djvu/2944-52618.pdf

Со страницы 407 начинается глава со сравнением компиляторов.

Какими же ограниченными были возможности компиляторов по анализу кода в относительно недавнем прошлом! Сейчас весьма интересно почитать.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Это в каком сломанном языке так?

я ты не обратил внимание, что если б ты сначала вычислял адрес приемника, а потом значение выражения, ты б избавился от спасения результа выражения перед call? то есть минус 2 команды работы с памятью.

push eax; 
...
pop ebx
alysnix ★★★
()
Ответ на: комментарий от wandrien

если там imul до сих пор делается только на eax, а ты результат вызова возвращаешь в eax, то результат можно перепихнуть в сразу после вызова в другой регистр, освободив eax.

вроде ж imul там можно давно и на других регистрах делать? вот не помню.

alysnix ★★★
()
Ответ на: комментарий от alysnix

Делать-то можно, только этому нужно еще учить бэк.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien
getA().field2 = array[i + 2].pointer[5];

поскольку грамматики нет,.. то левая часть или должна содержать явное разыменование типа getA()^.field2 или у тебя неявное разыменование имеется… или это просто ошибка.

-это реальный код что твой компилятор сьел?

если да, то зачем тебе неявное разыменование?

alysnix ★★★
()
Ответ на: комментарий от alysnix

В целом кодогенерация для обращений к полям структур и индексации массивов в очень плохом состоянии. Фрагмент

        mov     EBX,  5
        shl     EBX,  2

об этом намекает.

О причинах я писал выше - там используется форма дерева шиворот навыворот, которую нужно переделать на нормальную прежде чем заниматься улучшениями алгоритма по существу.

Тут сегодня я вообще поймал баг:

word Mults = MulTricks[n].Multipliers[i];

Пришлось разбить эту строку на две.

wandrien ★★
() автор топика
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от alysnix

Хохлов придумал. Я еще не переделал на нормальные указатели.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

Тут сегодня я вообще поймал баг:

мне вот кажется, что у этого Хохлова(не знаю, кто это) какой-то самостройный кодогенератор, и ты решил его копировать и дорабатывать.

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

alysnix ★★★
()

Задавайте ваши вопросы.

Что такое кампелятор?

Молодец, чё. На первый взгляд мне язык от Дрю понравился. Почему к такому проекту не присоединишься?

urxvt ★★★★★
()

где сорцы

anonymous
()
Ответ на: комментарий от wandrien

Чем тебе для задачи понять в одно лицо не угодил lcc?

luke ★★★★★
()

Молодец, лисонька. Продолжай в том же духе и держи нас в курсе \0/

perl5_guy ★★★★★
()

Далее в планах научить его генерировать код для какой-нибудь другой системы команд. ARM, MIPS, RISC-V… что-то из этого.

Отчего не в язык LLVM? Хочешь именно языки ассемблеров освоить?

apt_install_lrzsz ★★★
()
Ответ на: комментарий от alysnix

хочешь получать удовольствие - не занимайся велосипедами кодогенерации.

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

ergo ★★★
()
Ответ на: комментарий от alysnix

для самодельного языка лучше генерить не в асм, а в си

И получится Vala. Точно так и работает, как транслятор перед gcc.

генерить не в асм

А это уже потом GCC, который генерит asm, который потом транслируется as-ом уже в бинарник.

Так что, ТС решил обойтись без лишней прослойки в лице gcc :) Ну а что, прикольное развлечение. Вон FPC тоже пишут и у них даже неплохо получается, вполне себе рабочее.

SkyMaverick ★★★★★
()
Ответ на: комментарий от SkyMaverick

``as``` – тоже не обязательная прослойка. Но асмовский листинг для документирования и тестирования не помешает.

vM ★★
()
Ответ на: комментарий от luke

Да и 64-разрядному статическому elf заголовков много не нужно.

vM ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.