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