Во-вторых, в синтаксисе есть неоднозначности. Это надо оценить: в Стандарте (!)языка программирования прямо написано, что некоторые конструкции можнотрактовать двояко — либо как объявление, либо как оператор! В несколькоупрощенном виде формулировка из стандарта выглядит так: "выражение, содержащеев качестве своего самого левого подвыражения явное преобразование типа, котороезаписано в функциональном стиле, может быть неотличимо от объявления, в которомпервый декларатор начинается с левой круглой скобки". Классический пример: чтотакое T(a); если T — некоторый тип? С одной стороны, это как быобъявление переменной с именем a, тип которой задан как T. С другой — конструкцию можно трактовать как преобразование типа уже объявленной где-торанее переменной a к типу T. Все дело в том, что в Си++ статусоператоров и объявлений полностью уравнен; последние даже и называютсяdeclaration-statements — операторы-объявления, то есть традиционныеоператоры и объявления могут записываться вперемежку. Все же радости с круглымискобками перекочевали в Си++ прямо из Си, в котором типы конструируются подобновыражениям, и тривиальное объявление можно задать либо как "int a;", либокак "int(a);". Все это понятно, но от этого не легче. И такой язык любятмиллионы программистов?! Мир сошел с ума. Яду мне, яду!..
Смысл правил разрешения неоднозначностей сводится, по существу, к поразительнойфразе, простодушно выведенной в "Зеленой книге": "если конструкция выглядит какобъявление, то это и есть объявление. В противном случае это оператор". Инымисловами, чтобы разрешить неоднозначность, следует рассмотреть всю конструкциюцеликом; фрагмент "T(a)" для анализа недостаточен — за ним сразу можетследовать либо точка с запятой, тогда выбор делается в пользу объявления, либо"что-то еще". Например, вся конструкция может выглядеть как "T(a)→m = 7;"или "T(a)++;" — это, конечно, операторы (точнее,операторы-выражения, в терминах стандарта). Ну а как понимать следующее:"T(e)[5];" или "T(c)=7;"? А это, будьте уверены, еще не самыеразительные примеры — загляните в разд. 6.8 Стандарта.
Человеку хорошо, он ко всему привыкает, рано или поздно он разберется, но какзаставить анализатор понимать эту чехарду? Пока он не доберется до точки сзапятой, он, в общем случае, ничего не сможет сказать о конструкции. Друзья, непишите объявления, которые невозможно отличить от операторов! Пожалейтекомпилятор, ему же тяжело! Кроме того, можно запросто ошибиться и самому…
Несколько дней прошли в бесплодных попытках выразить неоднозначности на входномязыке YACC. Выход был похоже, только в организации просмотра вперед, причем назаранее не известное количество лексем. Алгоритм разбора,заложенный в YACC, этого делать не умеет. В принципе известны и доступнысистемы, в которых заявлена подобная возможность, однако мы были ограниченытребованием: синтаксический анализатор писать на YACCе, более того, на еговерсии, сделанной в одном европейском университете… Пришлось пойти наухищрения и "сломать" классическую схему разбора: делать предварительный анализеще на уровне разбора лексем и, встретив левую скобку после имени типа (а ещепойди распознай, что идентификатор — имя типа, а не какой-то другой сущности!),"отменять" автоматический анализ и организовывать "ручной" перебор последующихлексем, складывая их про запас в буфер.
Спасибо, в "Зеленой книге" подсказали схему такого анализа. Не знаем, как иблагодарить, сами бы ни за что не придумали…
Что такое идентификатор?
Помимо неоднозначностей в синтаксисе быстро обнаружились другие неприятности.На примерах их показать сложнее, так что придется рассказывать словами.
Синтаксис языка Си++ неудобен еще и в другом отношении. Если говоритькоротко, то прямое использование в формальном описании одного из базовыхсинтаксических понятий — идентификатора — приводит к тому, что YACC расцениваетграмматику языка как некорректную и на ее основе не может построитьсинтаксический анализатор. Для традиционных языков синтаксическому анализаторудля разбора конструкции достаточно информации о том, что в данной позиции этойконструкции может (или должен) находиться идентификатор. Более простые языкисконструированы так, что семантика идентификатора не влияет на корректностьсинтаксического разбора. Вид программной сущности, обозначаемой этимидентификатором (подпрограмма, переменная, имя типа, исключение, метка и т.п.),смысл данного конкретного вхождения (объявление или использование) — все этовыявляется далее, как правило, являясь предметом следующей фазы компиляции — семантического анализа.
Для языка Си++ такая схема не проходит. Чтобы быть в состояниисинтаксически распознать многие конструкции, требовалась семантическаяинтерпретация имени. Иными словами, на вход синтаксическому анализаторуследовало поставлять не абстрактную лексему "идентификатор", а результатанализа того, что именно представляет собой этот идентификатор: "имя типа","новое имя в объявлении", "имя не-типа в выражении" и т.д. Заметим, чтосинтаксическому анализатору для Java — непосредственного потомка Си++ — вполне хватает понятия идентификатора без каких-либо уточнений.
Всего для Си++ получилось около десятка таких "суперлексем", а лексема"идентификатор" вообще исчезла из синтаксиса. Понятно, что лексическийанализатор, который и поставляет лексемы, пришлось наделить дополнительным"интеллектом". Теперь он должен был не просто выделять из текста программыочередную лексему, но и обращаться в таблицы трансляции за информацией о том,что за идентификатор он выловил. Реально эти действия выполняет отдельныймодуль, названный "расширенным лексическим анализатором". Введениедополнительного модуля не привело к усложнению компилятора в целом, так какидентификация имен так или иначе должна производиться; мы просто перенесли еена более ранний этап компиляции. А синтаксис заметно упростился, стал болеенаглядным, информативным и в конечном счете более эффективным.
Компилятор как таковой: таблицы и деревья
Однако синтаксис — это мелочи жизни. Основное в любом компиляторе — этоинтерпретация семантики языковых конструкций, и подавляющая часть кодаприходится именно на семантические алгоритмы.
Есть два основных вида семантической информации, которые компилятор извлекаетиз текста исходной программы. Во-первых, это информация о различных объектах,которые используются в программе (переменные, типы, функции и т.д.), причем нетолько об объектах как таковых, но и об областях действия, в которых этиобъекты существуют (имеют смысл), а также об отношениях этих областей междусобой (контекстах). Чем сложнее устроен язык, тем больше в нем правил,связанных с объектами, и тем более изощренной должна быть та структура вкомпиляторе, которая описанную информацию содержит. Такая структура обычноназывается семантическими таблицами.
Во-вторых, компилятор должен формировать некоторый образ исходной программы — внутреннее представление программы в целом или ее некоторой части, которая вданный момент обрабатывается. Именно на основе такой структуры обычновыполняется семантический анализ программы, производятся различные оптимизациии осуществляется генерация результирующего кода. Как правило, такое внутреннеепредставление строится в виде дерева и потому называется деревом программы.
Эта пара — таблицы и деревья, вместе с различными алгоритмами, работающими надними, без преувеличения составляет две трети текста компилятора. Почти вся нашаработа на протяжении всех этих лет так или иначе была связана с ними.
Структура таблиц была придумана в целом по образцам из книг по теории ипрактике компиляции, которые в изобилии выходили у нас в 70-80-х годах иописывали, как правило, языки с относительно простой и, самое главное,регулярной структурой и несложной семантикой,-- такие как Алгол-60, Паскаль,Модула-2. Многое из того, что есть в Си++, с трудом "втискивалось" вакадемические построения, и приходилось дополнять и развивать их. В результатетаблицы представляют собой причудливую смесь классической стековой модели сдисплеем для отображения текущего контекста и наворотов вроде средствдинамического перестроения контекста для обработки функций-членов классов,нетривиальной поддержки областей действия имен (namespaces), буферов дляотложенной компиляции и т.д. К тому же таблицы должны быть динамическирасширяемыми, чтобы быть в состоянии вобрать в себя очень большое количествоимен, типичное для программ на Си++. Помучиться пришлось изрядно, идалеко не сразу таблицы заработали стабильно и надежно.