Давайте создадим компилятор!
Шрифт:
У меня нет намерения заниматься здесь дебатами на тему войн языков, так что я не буду затрагивать эту тему в дальнейшем. Даже я больше не использую Pascal в своей работе... я использую C на работе и С++ для своих статей в Embedded Systems Programming и других журналах. Поверьте мне, когда я намеревался возродить эту серию, я думал долго и интенсивно о переключении и языка и целевой системы на те, которые мы все используем в эти дни, C/C++ и архитектуру PC и возможно также и объектно-ориентированные методы. В конце концов я понял, что это вызовет больше беспорядка, чем сам перерыв. И в конце концов, Pascal все еще остается одним из лучших возможных языков для обучения, не говоря о промышленном программировании. Наконец, TP все еще компилирует на скорости света, гораздо быстрее чем конкурирующие C/C++ компиляторы. А интеллектуальный компоновщик Borland,
Главное в действительности в следующем: используя механизм модулей TP мы можем иметь все преимущества и удобства написания маленьких, на вид автономных тестовых программ, без необходимости постоянно переписывать необходимые функции поддержки. Однажды написанные, модули TP сидят там, тихонько ожидая возможности выполнить свой долг и дать нам необходимую поддержку, когда будет необходимо.
Используя этот принцип, в Главе 15 я намеревался минимизировать нашу тенденцию заново изобретать колесо, организуя наш код в отдельные модули Turbo Pascal, каждый из которых содержит различные части компилятора. Мы завершили со следующими модулями:
Input
Output
Errors
Scanner
Parser
CodeGen
Каждый из этих модулей обслуживает разные функции и изолирует специфические области функциональных возможностей. Модули Input и Output, как подразумевают их имена, обеспечивают ввод/вывод символьного потока и важнейший предсказывающий символ, на котором основан наш предсказывающий синтаксический анализатор. Модуль Errors конечно обеспечивает стандартную обработку ошибок. Модуль Scanner содержит все наши булевы функции типа IsAlpha и подпрограммы GetName и GetNumber, которые обрабатывают многосимвольные токены.
Два модуля, с которыми мы будем в основном работать и те, которые больше всего представляют индивидуальность нашего компилятора – это Parser и CodeGen. Теоретически модуль Parser должен изолировать все аспекты компилятора, которые зависят от синтаксиса компилируемого языка (хотя, как мы видели последний раз, небольшое количество этого синтаксиса перетекает в Scanner). Аналогично, модуль генератора кода, CodeGen, содержит весь код, зависящий от целевой машины. В этой главе мы продолжим разработку функций в этих двух важнейших модулях.
Совсем как классический?
Прежде чем мы продолжим, однако, я думаю что должен разъяснить связи между модулями и функциональные возможности этих модулей. Те из вас, кто знаком с теорией компиляции как обучавшиеся в университетах, конечно распознают имена Scanner, Parser и CodeGen, все из которых являются компонентами классической реализации компилятора. Вы можете думать, что я отказался от своих обязательств по отношению к философии KISS и отдрейфовал к более стандартной архитектуре чем мы имели. Более пристальный взгляд, однако, должен убедить вас, что хотя имена схожи, функциональность совершенно различна.
Вместе, сканер и парсер классической реализации составляют так называемый «front end», а генератор кода «back end». Подпрограммы «front end» обрабатывают языкозависимые, связанные с синтаксисом аспекты исходного языка, в то время как генератор кода, или «back end», работает с зависимыми от целевой машины частями проблемы. В классических компиляторах два конца (ends) сообщаются через файл инструкций, написанный на промежуточном языке (IL).
Как правило, классический сканер это одиночная процедура, оперирующая как сопроцедура с синтаксическим анализатором. Она «токенизирует» исходный файл, считывая его символ за символом, распознавая элементы языка, транслируя их в токены и передавая их синтаксическому анализатору. Вы можете думать о синтаксическом анализаторе как об абстрактной машине, выполняющей «op кода», которыми являются
токены. Точно также, синтаксический анализатор генерирует «op кода» второй абстрактной машины, которая механизирует IL. Как правило, IL файл записывается на диск синтаксическим анализатором и считывается снова генератором кода.Наша организация совершенно другая. Мы не имеем лексического анализатора в классическом смысле; наш модуль Scanner, хотя и имеет схожее имя, не является одиночной процедурой или сопроцедурой, а просто набором раздельных подпрограмм, которые вызываются синтаксическим анализатором когда необходимо.
Аналогично, классический генератор кода, «back end», в своем роде тоже транслятор, считывающий «исходный» IL файл и выдающий объектный файл. Наш генератор кода не работает таким способом. В нашем компиляторе нет никакого промежуточного языка; каждая конструкция в синтаксисе исходного языка преобразуется в ассемблер как только она распознана синтаксическим анализатором. Подобно Scanner, модуль CodeGen состоит из индивидуальных процедур, которые вызываются синтаксическим анализатором когда необходимо.
Философия «кодируй как только найдешь» не может производить самый эффективный код в мире – например, мы не обеспечили (пока!) удобное место для оптимизатора – но она несомненно упрощает компилятор, не правда ли?
И этот наблюдение заставляет меня повторить снова то, как нам удавалось сводить функции компилятора к таким сравнительно простым условиям. Я набрался красноречивости на эту тему в прошлых главах, поэтому здесь я не буду слишком ее трогать. Однако, из-за времени, прошедшего с этих последних монологов, я надеюсь что вы предоставите мне совсем немного времени напомнить себе, так же как и вам, как мы попали сюда. Мы дошли до этого применяя несколько принципов, которые создатели коммерческих компилятором редко имеют роскошь использовать. Вот они:
• Философия KISS – никогда не делай сложные вещи без причины.
• Ленивое кодирование – Никогда не откладывай на завтра то, что можешь отложить навсегда. (П. Дж. Плоджер).
• Скептицизм – упрамо отказывайтесь делать что-либо только потому, что это всегда делалось таким способом.
• Принятие неэффективного кода.
• Отклонение произвольных ограничений.
Когда я сделал обзор истории конструирования компиляторов, я узнал, что практически каждый промышленный компилятор в истории страдал из-за предналоженных условий, которые сильно влияли на его дизайн. Первоначальный компилятор Fortran Джона Бэкуса должен был конкурировать с ассемблером и следовательно был вынужден производить чрезвычайно эффективный код. Компиляторы IBM для мини ЭВМ 70-х должны были выполняться в очень небольших объемах ОЗУ тогда доступных – таких небольших как 4k. Ранние компиляторы Ada должны были компилировать себя. Бринч Хансен решил, что его компилятор Паскаля, разработанный для IBM PC должен выполняться на 64k машинах. Компиляторы, разработанные на курсах Computer Science должны были компилировать широкий диапазон языков и следовательно требовали LALR синтаксических анализаторов.
В каждом из этих случаев эти предвзятые ограничения буквально доминировали над проектом компилятора.
Хороший пример – компилятор Бринч Хансена, описанный в его превосходной книге «Brinch Hansen on Pascal Compilers» (строго рекомендую). Хотя его компилятор один из самых ясных и незатемненных реализаций компилятора, что я видел, одно решение, компилировать большие файлы в небольшом ОЗУ, полностью управляло дизайном и он закончил не на одном а многих промежуточных файлах, как и управляющими ими программах для их записи и считывания.
Временами, архитектуры, возникающие из таких решений, находили свое место в учениях компьютерной науки и принимались на веру. По мнению одного человека, пришло время чтобы они были критически пересмотрены. Условия, требования, среды, которые вели к классическим архитектурам не такие же, какие мы имеем сейчас. Нет никакой причины полагать, что решения тоже должны быть те же самыми.
В этой обучающей серии мы следовали по шагам таких пионеров в мире маленьких компиляторов для PC как Леор Золман, Рон Каин и Джеймс Хендрих, тех кто не знал достаточно теорию компиляции чтобы знать, что они «не могли делать это таким способом». Мы решительно отказались принимать произвольные ограничения, а скорее делали так, как было проще В результате мы развили архитектуру, которая, хотя и совершенно отлична от классической, делает работу простым и прямым способом.