8. Немного философии
ВВЕДЕНИЕ
Этот урок будет отличаться от
других уроков в нашей серии по синтаксическому анализу и конструированию
компиляторов. На этом уроке не будет никаких экспериментов или кода. На
этот раз я хотел бы просто поговорить с вами некоторое время. К счастью,
это будет короткий урок и затем мы сможем продолжить с того места где остановились,
надо надеяться с обновленной энергией.
Когда я учился в университете,
я обнаружил, что могу всегда следить за профессорской лекцией намного лучше,
если знал куда он идет. Готов поспорить, с вами было то же самое.
Так что я подумал, может быть
пришло время расказать вам куда мы идем с этой серией: что нас ждет в будущих
главах и вообще что к чему. Я также поделюсь своими общими мыслями о полезности
того, что мы делали.
ДОРОГА ДОМОЙ
Пока что мы охватили синтаксический
анализ и трансляцию арифметических выражений, булевых выражений и их комбинаций,
связанных операторами отношений. Мы также сделали то же самое для управляющих
конструкций. Во всем этом мы склонялись в основном к использованию нисходящего
синтаксического анализа методом рекурсивного спуска, определение синтаксиса
в БНФ и непосредственной генерации ассемблерного кода. Мы также изучили
значение такого приема как односимвольные токены. В последней главе мы
работали с лексическим анализом и я показал вам простой но мощный способ
преодоления односимвольного барьера.
В течение всех этих исследований,
я особенно выделял философию KISS... Keep It Simple, Sidney... и я надеюсь,
что к настоящему времени вы поняли, насколько простыми могут в действительности
быть эти вещи. Хотя наверняка имеются области в теории компиляции которые
являются по настоящему пугающими, основной мыслью этой серии является то,
что на практике вы можете просто вежливо обойти многие из этих областей.
Если определение языка способствует этому или, как в этой серии, если вы
можете определить язык по ходу дела, то возможно записать определение языка
в БНФ с достаточным удобством. И, как мы видели, вы можете вывести процедуры
синтаксического анализа из БНФ почти также быстро, как вы можете набирать
на клавиатуре.
По мере того, как наш компилятор
принимал некоторую форму, он приобретал больше частей, но каждая часть
довольно мала и проста и очень похожа на все другие.
К этому моменту у нас есть многое
из того, что составляет настоящий практический компилятор. Фактически,
мы уже имеем все что нам нужно для создания игрушечного языка столь же
мощного, как, скажем, Tiny Basic. В следующих двух главах мы пойдем вперед
и определим этот язык.
Для завершения этой серии, у
нас все еще есть несколько тем для раскрытия. Они включают:
-
Вызовы процедур, с параметрами и без.
-
Локальные и глобальные переменные.
-
Базовые типы, такие как символьные и целочисленные типы.
-
Массивы.
-
Строки.
-
Типы и структуры, определяемые пользователем.
-
Синтаксические анализаторы с деревьями и промежуточные языки.
-
Оптимизация.
Все это будет рассмотрено
в будущих главах. Когда мы закончим, вы будете иметь все инструменты, необходимые
для разработки и создания своего собственного языка и компиляторов для
его трансляции.
Я не могу спроектировать
эти языки для вас, но я могу дать некоторые комментарии и рекомендации.
Я уже высказал некоторые из них в прошлых главах. Вы видели, например,
какие управляющие структуры я предпочитаю.
Эти конструкции будут
частью создаваемых мной языков. К этому моменту я представляю три языка,
два из которых вы увидите в очередных главах:
TINY - минимальный, но пригодный для использования
язык уровня Tiny Basic или Tiny C. Он не будет очень практичным, но будет
достаточно мощным, чтобы позволить вам писать и запускать настоящие программы
которые делают что-нибудь заслуживающее внимание.
KISS - язык, который я создаю для своего собственного
использования. KISS предназначен быть языком системного программирования.
Он не будет иметь строгого контроля типов или причудливых структур данных,
но он будет поддерживать большинство вещей, которые я хочу делать с языком
более высокого уровня (HOL), за исключением возможно написания компиляторов.
Я также играл в течение
нескольких лет с идеей HOL-подобного ассемблера со структурными управляющими
конструкциями и HOL-подобными операциями присваивания. Это фактически было
стимулом для моего первоначального углубления в джунгли теории компиляции.
Этот язык возможно никогда не будет создан просто потому, что я узнал,
что проще реализовать язык типа KISS, который использует только подмножество
инструкций ЦПУ. Как вы знаете, ассемблер может быть предельно причудливым
и нерегулярным, и язык, который отображается в него один к одному, может
быть настоящим вызовом. Однако я всегда чувствовал, что синтаксис, используемый
в стандартных ассемблерах тупой... почему
MOVE.L A,B
лучше или проще для трансляции, чем
B=A?
Я думаю, было бы интересным
упражнением разработка "компилятора" который дал бы программисту полный
доступ и к контролю над полным набором инструкций ЦПУ, и позволил бы вам
генерировать программы настолько же эффективные как язык ассемблер без
болезненного изучения набора мнемоник. Это может быть сделано? Я не знаю.
Настоящим вопросом может быть вопрос "будет ли полученный язык проще, чем
ассемблер?" Если нет, то в нем нет никакого смысла. Я думаю, что это может
быть сделано, но я полностью еще не уверен в том, как должен выглядеть
синтаксис.
Возможно у вас есть некоторые
комментарии или предложения об этом. Буду рад услышать их.
Вы возможно не будете
удивлены узнав, что уже работал в большинстве тех областей, которые мы
рассмотрим. Я имею несколько хороших новостей: дела никогда не будут намного
более сложными, чем они были до этого. Возможно построить завершенный,
работающий компилятор для реального языка используя только те самые методы
которые вы изучили до этого. И это поднимет некоторые интересные вопросы.
ПОЧЕМУ ЭТО ТАК ПРОСТО?
Перед осуществлением этой
серии я всегда думал, что компиляторы были просто естественно сложными
компьютерными программами... предельно вызывающими. Однако то, что мы здесь
делали обычно оказывалось совершенно простым, иногда даже тривиальным.
Некоторое время я думал,
что это было просто потому, что я еще не залез в глубь темы. Я только охватил
простые части. Я легко признаюсь вам что даже когда я начинал эту серию
я не был уверен в том, как далеко мы будем способны продвинуться прежде
чем дела станут слишком сложными для работы имеющимися способами. Но сейчас
я уже нахожусь достаточно близко, чтобы увидеть конец пути. Какой вывод?
ЗДЕСЬ НЕТ НИЧЕГО СЛОЖНОГО!
Затем я думал, что причина
в том, что мы не генерировали очень хороший объектный код. Те из вас, кто
следовали этой серии и пытались компилировать примеры, знают, что хотя
код работает и достаточно отказоустойчив, его эффективность довольно ужасна.
Я подчеркивал, что если бы мы сконцентрировались на получении компактного
кода, то быстро бы получили всю недостающую сложность.
В какой то степени это
так. В частности, мои первые небольшие усилия при попытке повысить эффективность
подняли сложность до опасного уровня. Но с той поры когда я возился с некоторыми
простыми методами оптимизацией и обнаружил некоторые, которые приводят
к очень приличному качеству кода без добавления больших сложностей.
Наконец я подумал, что
возможно причина была в "игрушечной" природе компилятора Я не претендовал
на то, что мы когда-нибудь будем способны построить компилятор, конкурирующий
с Borland и Microsoft. И однако снова, когда я забираюсь глубже в эти дела
различия начинают стираться.
Просто чтобы удостовериться
что до вас дошла эта мысль, позвольте мне ее высказать напрямую:
Используя методы, которые мы здесь применяли,
возможно создать работающий, промышленного качества компилятор не добавляя
много сложности к тому, что мы уже сделали.
С тех пор, как началась
эта серия, я получил от вас некоторые комментарии. Большинство из них повторяют
мои собственные мысли: "Это просто! Почему учебники представляют это настолько
сложным?" Хороший вопрос.
Недавно я возвратился
и взглянул на некоторые из этих текстов снова и даже купил и читаю некоторые
новые. Каждый раз я возвращался с тем же чувством: эти ребята представляют
это слишком сложным.
Что происходит? Почему
все это кажется сложным в этих книгах, но легким для нас? Действительно
ли мы умней чем Ахо, Ульман, Бринч Хансен и все остальные?
Едва ли. Но мы делаем
некоторые вещи по-другому и все более и более я начинаю ценить значение
нашего подхода и способ, которым он упрощает дело. Кроме очевидных сокращений,
которые я выделил в первой части, типа односимвольных токенов и консольного
ввода/вывода, мы сделали некоторые неявные предположения и сделали некоторые
вещи отличными от того, как разрабатывали компиляторы в прошлом. Как оказалось,
наш метод делает жизнь намного проще.
Но почемы все другие ребята
не используют его?
Вы должны вспомнить контекст
некоторых ранних разработок компиляторов. Эти люди работали на очень небольших
компьютерах с ограниченными возможностями. Объемы памяти были очень ограничены,
набор команд центрального процессора был минимален и программы чаще выполнялись
в пакетном режиме, чем в интерактивном. Как оказалось, это повлияло на
некоторые ключевые решения проекта, которые действительно усложнили проект.
До недавнего времени я не понимал, насколько классический дизайн компилятора
был обусловлен доступным оборудованием.
Даже в тех случаях, где
эти ограничения больше не накладывались, люди предпочитали структурировать
их программы тем же самым образом, так как это способ, которому они обучались.
В нашем случае мы начали
с чистого листа бумаги. Имеется опасность, конечно, что вы попадетесь в
ловушки, которые другие люди давно научились избегать. Но это также позволило
нам использовать различные подходы, которые, частично из-за проекта, частично
из-за чистой удачи, позволили нам добиться простоты.
Имеются области, которые,
я думаю, в прошлом приводили к сложности:
-
Ограниченная оперативная память, вынуждающая выполнять множество
проходов.
Я только что прочитал "Brinch Hansen on Pascal Compilers"
(отличная книга, BTW). Он разработал компилятор Pascal для PC, но он начал
в 1981 г. с систем с 64К памяти и поэтому почти каждое решение проекта
который он делал, было нацелено на то, чтобы уместить компилятор в ОЗУ.
Чтобы сделать это, его компилятор выполнял три прохода, один из которых
- лексический анализ. Не было никакого способа, с помощью которого он мог
бы, например, использовать распределенный сканер, который я представил
в последней главе, потому что структура программы не позволяла этого. Ему
также требовались не один а два промежуточных языка для обеспечения связи
между фазами.
Все ранние создатели компиляторов были вынуждены
иметь дело с такой проблемой: разбить компилятор на достаточные части так,
чтобы они поместились в памяти. Когда у вас есть множество проходов, вы
должны добавить структуры данных для поддержки информации которую каждый
проход оставляет для следующего. Это добавляет сложность и завершает управление
проектом. В книге Ли "The Anatomy of a Compiler" упоминается компилятор
Fortran, разработанный для IBM 1401. Он имел не менее 63 отдельных проходов!
Само собой разумеется, в компиляторе, подобном этому, разделение на фазы
доминировало бы над дизайном.
Даже в ситуации, когда ОЗУ достаточно, люди предпочитали
использовать те же самые методы, с которыми они хорошо знакомы. До тех
пор, пока не появился Turbo Pascal, мы не знали насколько может быть простым
компилятор если бы вы начали с других предположений.
В ранние дни пакетная обработка была единственным выбором...
не существовало никаких интерактивных вычислений. Даже сегодня компиляторы
по существу выполняются в пакетном режиме.
В компиляторах для майнфреймов, так же как и во
многих микро компиляторах, значительные усилия расходуются на восстановление
после ошибок... это может занять 30-40% компилятора и полностью управлять
проектом. Идея состоит в том, чтобы избежать остановки на первой ошибке,
а скорее идти любой ценой, так чтобы вы могли сказать программисту о как
можно большем количестве ошибок во всей программе насколько возможно.
Все это возвращает нас к дням ранних майнфреймов,
где время выполнения измерялось в часах и днях и было важно выжать каждую
последнюю унцию информации из каждого выполнения.
В этой серии я был очень осторожен и избежал проблемы
восстановления после ошибок и вместо этого наш компилятор просто останавливается
с сообщением на первой ошибке. Я откровенно признаюсь, что это было в основном
потому, что я захотел использовать легкий путь и сохранить простоту. Но
этот метод, заданный изначально Borland в Turbo Pascal также имеет много
полезного в любом случае. Кроме сохранения простоты компилятора это также
очень хорошо соответствует идее интерактивной системы. Когда компиляция
происходит быстро и, особенно, когда вы имеете редактор типа Borland
который будет правильно указывать вам на точку ошибки, тогда имеет смысл
остановиться там и просто перезапустить компиляцию после того, как ошибка
исправлена.
Ранние компиляторы были разработаны
для поддержки больших программ... по существу бесконечных. В те дни существовал
небольшой выбор; идея с библиотеками подпрограмм и раздельной компиляцией
была еще в будущем. Снова, это предположение вело к многопроходному дизайну
и промежуточным файлам для поддержки результатов раздельной обработки.
Поставленная Бринч Хансеном
цель состояла в том, чтобы компилятор был способен компилировать сам себя.
Снова, из-за ограничений оперативной памяти это приводило его к многопроходному
дизайну. Он нуждался в таком маленьком резидентном коде компилятора, насколько
возможно, так чтобы необходимые таблицы и другие структуры данных поместились
в оперативную память.
Я не заявил об этом пока,
потому что не было необходимости... мы всегда просто читали и записывали
данные как потоки, в любом случае. Но, для заметки, мой план всегда был
в том, чтобы в промышленном компиляторе исходные и обьектные данные должны
сосуществовать в ОЗУ с компилятором, аля ранний Turbo Pascal. Вот почему
я был осторожен и сохранил подпрограммы типа GetChar и Emit как отдельные
попрограммы, несмотря на их небольшой размер. Будет просто изменить их
на чтение и запись из памяти.
Джон Бэкус заявил, что когда
он и его коллеги разработали первоначальный компилятор Fortran они знали,
что они |