Редкая профессия - [5]

Шрифт
Интервал

, [], пустой список параметров (void), завершитель списка параметров ...) встречаются очень часто. После служебных слов if, switch, while всегда должна стоять левая круглая скобка, после break и continue — точка с запятой, а после слова goto располагаются идентификатор и точка с запятой. Таких регулярностей набирается несколько десятков, так что если рассматривать их как отдельные лексемы, объем синтаксиса заметно сокращается. Введение каждой такой "суперлексемы" экономит по крайней мере одно обращение синтаксического анализатора к таблице разбора. Усложнение распознавателя лексем (сканера), вынужденного составлять суперлексемы из пар или троек обычных лексем, при этом получается весьма незначительное; более того, если сканер во время одного вызова распознает, например, не только служебное слово switch, но и левую круглую скобку, идущую за ним, получится экономия и на числе обращений к сканеру!

Во-вторых, в синтаксисе есть неоднозначности. Это надо оценить: в Стандарте (!) языка программирования прямо написано, что некоторые конструкции можно трактовать двояко — либо как объявление, либо как оператор! В несколько упрощенном виде формулировка из стандарта выглядит так: "выражение, содержащее в качестве своего самого левого подвыражения явное преобразование типа, которое записано в функциональном стиле, может быть неотличимо от объявления, в котором первый декларатор начинается с левой круглой скобки". Классический пример: что такое 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 расценивает грамматику языка как некорректную и на ее основе не может построить синтаксический анализатор. Для традиционных языков синтаксическому анализатору для разбора конструкции достаточно информации о том, что в данной позиции этой конструкции может (или должен) находиться идентификатор. Более простые языки сконструированы так, что семантика идентификатора не влияет на корректность синтаксического разбора. Вид программной сущности, обозначаемой этим идентификатором (подпрограмма, переменная, имя типа, исключение, метка и т.п.), смысл данного конкретного вхождения (объявление или использование) — все это выявляется далее, как правило, являясь предметом следующей фазы компиляции — семантического анализа.