Организация лексического разбора: отдельно или вместе?

Проектирование и разработка лексических и синтаксических компонент компилятора - хорошо изученная тема. Любая книга по компиляции начинается с обсуждения этой проблематики и зачастую посвящает ей большую часть своего объёма. Однако при создании компилятора промышленного ЯП (например, Си++) возникает ряд проблем, недостаточно отражённых в литературе. С несколькими такими проблемами мы и столкнулись.

Первая проблема касается взаимоотношения лексического анализатора и препроцессора. Как известно, компиляция программ на

Си/Си++ предусматривает логически отдельный этап - препроцес- сорную обработку исходного текста. Эта фаза является откровенной архаикой, сохранившейся еще с конца 60-х годов[1], приносит при программировании больше проблем, нежели решает, и, строго говоря, не имеет прямого отношения к языку Си++ как таковому. Тем не менее слишком многие реальные программы активно используют средства препроцессирования (это, можно даже сказать, общепринятая парадигма программирования на Си/Си++), и потому компилятор, безусловно, обязан их поддерживать.

Большинство систем реализует этот этап в виде отдельного процессора, выход которого в виде текстового файла поступает на вход компилятора. Однако на практике взаимодействие двух компонент - препроцессора и компилятора - посредством дискового файла является слишком неэффективным, так как в этом случае мы имеем две дополнительные и весьма затратные операции - запись результата препроцессирования на дисковую память и повторное считывание ее компилятором (в большинстве реальных ситуаций время записи в файл и последующего чтения как минимум сравнимо со временем собственно препроцессирования и даже превышает его). Стандарт языка Си++ не фиксирует конкретного способа реализации этих фаз; единственное, что требуется, - обеспечить логическое соответствие очередности этапов. Более конкретно, в разделе 2.2 Стандарта («Фазы трансляции») сказано следующее:

Implementations must behave as if these separate phases occur, although in practice different phases might be folded together.

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

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

Кроме того, характер обработки текста на этапах препроцессирования и лексического разбора очень схож; в обоих случаях основным понятием является «лексема»: на этапе препроцессирования это

Организация лексического разбора: отдельно или вместе?

133

«лексема препроцессора» (pp-token), на этапе лексического разбора - просто «лексема» (token), при том что содержательно эти понятия отличаются друг от друга весьма незначительно. На практике это приводит к тому, что препроцессор и лексический анализатор, реализованные как отдельные компоненты, во многом повторяют друг друга и попросту содержат значительные фрагменты одинакового кода.

Что касается таких возможностей «раздельной» организации, как, скажем, препроцессирование без компиляции или, наоборот, компиляция без предварительного препроцессирования, которые вполне естественно реализуются в модели с двумя отдельными компонентами (препроцессором и компилятором), то обеспечить такую возможность достаточно легко и в «объединённой» схеме, введя соответствующие режимы её работы.

По этим причинам после некоторого этапа пробных разработок было принято решение, характерное далеко не для всех известных нам компиляторов, - реализовать «объединённую» схему, при которой лексический анализатор выполняет также и все функции препроцессора. Мы, конечно, не проводили специальных сравнений эффективности, однако можно с уверенностью утверждать, что в результате общий код существенно сократился, а производительность заметно повысилась.

На рисунке схематически показаны две описанные архитектурные альтернативы:

Описанная проблема, а также выбранный способ её решения показывает сложность и неоднозначность проектирования любой сколько-нибудь нетривиальной программной системы. В самом деле, на первый взгляд, наше решение нарушает один из основополагающих принципов проектирования - принцип модульности, согласно которому компоненты системы должны представлять собой относительно небольшие единицы с простыми и строго определенными интерфейсами. Мы же, наоборот, объединили две немаленькие компоненты в одну. В пользу такого «неправильного» решения говорит сильная схожесть этих двух компонент, большее удобство последующего сопровождения, а также явный выигрыш в быстродействии.

  • [1] На конференции SECR-2010 в Москве Б. Страуструп, создатель Си++,в своем выступлении прямо сказал: « Будь моя воля, я бы уничтожил препроцессор» («I would have killed preprocessor»).
 
Посмотреть оригинал
< Пред   СОДЕРЖАНИЕ ОРИГИНАЛ   След >