Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СПО юнита 2.doc
Скачиваний:
39
Добавлен:
17.11.2019
Размер:
5.82 Mб
Скачать

2.7 Синтаксический анализ

Задача синтаксического анализа – нахождение порождения конкретного выражения с использованием данной грамматики.

Методы разбора, применяемые в компиляторах, классифицируются как нисходящие или восходящие.

Нисходящие синтаксические анализаторы строят дерево разбора сверху (от корня) вниз (к листьям), тогда как восходящие начинают с листьев и идут к корню. В обоих случаях входной поток синтаксического анализатора сканируется посимвольно слева направо.

Наиболее эффективные нисходящие и восходящие методы работают только с подклассами грамматик. Однако некоторые из этих подклассов, такие как LR-грамматики, достаточно выразительны для описания большинства синтаксических конструкций языков программирования. Реализованные вручную синтаксические анализаторы чаще работают с LL-грамматикам. Синтаксические анализаторы для несколько большего класса LR-грамматик создаются с помощью автоматизированных инструментов.

Обработка синтаксических ошибок

Если компилятор будет иметь дело исключительно с корректными программами, его разработка и реализация существенно упрощаются. Однако часто программисты пишут программы с ошибками, и компилятор должен помочь программисту обнаружить их и локализовать. Примечательно, что хотя ошибки – явление чрезвычайно распространенное, лишь в некоторых языках вопрос обработки ошибок рассматривается на фазе создания языка. Большинство спецификаций языков программирования не определяет реакции компилятора на ошибки – этот вопрос отдается на откуп разработчикам компилятора. Однако планирование системы обработки ошибок с самого начала работы над компилятором может как упростить его структуру, так и улучшить его реакцию на ошибки.

Любая программа потенциально содержит ошибки разного уровня:

– лексические – неверно записанные идентификаторы, ключевые слова или операторы;

– синтаксические – арифметические выражения с несбалансированными скобками;

– семантические – операторы, применяемые к несовместимым с ними операндам;

– логические – бесконечная рекурсия.

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

Цели обработчика ошибок синтаксического анализатора:

– ясно и точно сообщать о наличии ошибок;

– обеспечивать быстрое восстановление после ошибки, чтобы продолжить поиск последу-ющих ошибок;

– существенно не замедлять обработку корректной программы.

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

Некоторые методы разбора, такие как LL и LR, выявляют ошибки, как только это становится возможным. Они обладают свойством проверки корректности префиксов, т.е. выявляют ошибку, как только выясняется, что префикс входной информации не является префиксом ни одной корректной строки языка.

LL(1)-грамматики

Рассмотрим свойства грамматик, поддерживающих методы нисходящего синтаксического анализа с одним символом предпросмотра. Будем считать, что грамматики являются однозначными, так что каждому предложению языка соответствует единственное левое порождение – порождение, при котором на каждом этапе заменяется крайний левый нетерминал. Нетерминал – символ, используемый грамматикой для генерации предложений языка. В данном случае для каждого нетерминала, который находится в левой части нескольких продукций, необходимо найти такие непересекающиеся множества символов предпросмотра, чтобы каждое множество содержало символы, соответствующие точно одной возможной правой части. Выбор конкретной продукции для замены данного нетерминала будет определяться символом предпросмотра и множеством, к которому принадлежит данный символ. Объединение различных непересекающихся множеств для заданного нетерминала не обязательно должно составлять алфавит, на котором определен язык. Если символ предпросмотра не принадлежит ни одному из непересекающихся множеств, можно сделать вывод о наличии синтаксической ошибки.

Множество символов предпросмотра, соотнесенных с применением определенной продукции, называется ее множеством первых порождаемых символов. Определим:

– стартовый символ данного нетерминала определяется как любой символ, который может появиться в начале строки, генерируемой нетерминалом;

– символ-последователь данного нетерминала определяется как любой символ, который может следовать за нетерминалом в любой сентенциальной форме.

Вычисление множества стартовых символов выполняется в процессе генерации программы синтаксического анализа, а не при каждом запуске этой программы. Например, для нетерминала Т грамматика содержит только две продукции. В этом случае имеем следующие множества стартовых символов:

Продукция Множество стартовых символов

T → aG {a}

T → bG {b}

В общем случае, если продукция начинается с терминала, ее множество стартовых символов состоит из этого терминала. Если продукция не начинается с терминала, для нее нужно вычислить множество стартовых символов. Пусть в рассматриваемой грамматике имеются следующие продукции для нетерминала R:

R → BG

R → CH

а для нетерминала B имеются продукции:

B  сD

B  TV

Тогда множеством стартовых символов для продукции R  BG будет набор {а, b, с}, состоящий из всех стартовых символов для В.

Введение символа с во множество стартовых является очевидным, а введение набора {а, b} объясняется тем, что эти символы являются стартовыми для Т.

Еще одним источником сложностей являются нетерминалы, которые могут генерировать пустые строки. Допустим, имеются следующие продукции:

A  BC

B  

В этом случае множество стартовых символов для продукции A  ВС будет включать стартовые символы С, а также стартовые символы В. Если оба нетерминала В и С могут генерировать пустые строки, то на использование данной продукции будут указывать символы предпросмотра, являющиеся символами-последователями А и стартовыми символами ВС.

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

LL(1)-грамматику можно определить как грамматику, в которой для каждого нетерминала, появляющегося в левой части нескольких продукций, множества первых порождаемых символов продукций, в которых появляется этот нетерминал, являются непересекающимися. Термин LL(1) имеет следующее происхождение: первое L означает чтение слева (Left) направо, второе L означает использование левых (.Leftmost) порождений, а 1 – один символ предпросмотра. Если вычислены все множества первых порождаемых символов для всех возможных правых частей продукций, то языки, которые описываются LL(1)-грамматикой, всегда анализируются детерминировано, т.е. без необходимости отменять продукцию после ее применения.

Язык LL(1) – это язык, который можно сгенерировать посредством LL(1)-грамматики. Для любого LL(1)-языка возможен нисходящий синтаксический анализ с одним символом предпросмотра.

Существует алгоритм определения, относится ли данная грамматика к классу LL(1), поэтому грамматику можно проверить на "LL(1)-ность" прежде, чем создавать на ее основе программу синтаксического анализа. В то же время не существует алгоритма определения, относится ли данный язык к классу LL(1), т.е. имеет он LL(1)-грамматику или нет. Это означает, что не-LL(1)-грамматика может иметь или не иметь эквивалентную LL(1), генерирующую тот же язык, и не существует алгоритма, который для данной произвольной грамматики определит, является ли генерируемый ею язык LL(1) или нет. Существуют алгоритмы, которые могут использоваться для частных случаев. Например, если грамматика является LL(1) – язык также является LL(1). Можно выделить определенные классы грамматик, которые никогда не будут генерировать LL(1)-языки. В общем случае задача является неразрешимой в том же смысле, как неразрешимы задача определения однозначности языка и проблема остановки для машин Тьюринга.

Имеются грамматики, не являющиеся LL(1), которые тем не менее генерируют LL(1)-языки, т.е. грамматики имеют эквивалентные LL(1)-грамматики. Это означает, что грамматики нужно преобразовывать, прежде чем использовать с методами нисходящего синтаксического анализа. Грамматики, которые используются в определениях языков, редко являются LL(1) и, следовательно, не могут непосредственно использоваться для эффективного нисходящего анализа. Не существует алгоритма определения, имеет ли грамматика эквивалентную LL(1). Это означает, что в любом случае не существует алгоритма поиска эквивалентной LL(1)-грамматики, если даже такая грамматика и существует.

Существует одно свойство грамматики, которое (если оно присутствует) препятствует тому, чтобы грамматика была LL(1) – левая рекурсия.

Использование данной продукции никогда не даст ни одной строки терминалов, поскольку не существует способа избавиться от нетерминала D, если он появится в сентенциальной форме. Грамматика с продукциями, которые не могут использоваться или по каким-то причинам не являются необходимыми, часто называется нечистой. Предполагается, что все рассматриваемые грамматики являются "чистыми".

Левая рекурсия может быть непрямой, включающей две или более продукции. Как и для прямой левой рекурсии, любая грамматика, имеющая непрямую левую рекурсию, будет характеризоваться пересекающимися множествами первых порождаемых символов для некоторых нетерминалов, и, следовательно, такая грамматика не может быть LL(1). Итак, ни одна леворекурсивная грамматика не является LL(1).

Достоинства и недостатки LL(1) анализа

Причина привлекательности синтаксического анализа LL(1) заключается в его естественности. Данный метод является наглядным и удобным для создания основы для последующей компиляции языка программирования. Кроме того, его легко реализовать и убедиться в корректности его работы. Помимо выполнения собственно синтаксического анализа написанный код может содержать функции по выполнению проверки соответствия типов и других проверок, а также действия этапа синтеза – распределение памяти и генерация кода.

В то же время можно определить и некоторые недостатки синтаксического анализа методом рекурсивного спуска: неэффективность вызовов функций и необходимость преобразования грамматики, даже не зная, существует ли подходящее преобразование. Проблема заключается не только в нахождении преобразования, но и в проверке корректности его применения. Таким образом, имеются веские причины использовать при преобразовании надежные инструментальные средства, а не зависеть от ручного подхода.

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

– создаются очень большие программы синтаксического анализа;

– существует тенденция к появлению в теле одной функции операций, относящихся к разным фазам процесса компиляции.

Для эффективного использования рекурсивного спуска требуется:

– преобразователь грамматики, который в большинстве случаев сможет трансформировать грамматику в форму LL(1);

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

Хорошие преобразователи существуют и временами объединяются с инструментальными средствами и дают "таблицы" LL(1). Кроме того, те же инструменты могут определять операции, которые следует выполнять на определенных этапах синтаксического анализа. Существенным преимуществом является задание операций именно относительно исходной, а не преобразованной грамматики, поскольку пользователю удобнее мыслить понятиями исходной, более естественной грамматики, чем понятиями менее естественной LL(1)-грамматики, порожденной преобразователем. Если в преобразованной грамматике будет отсутствовать левая рекурсия, она может быть в ис-ходной грамматике, обеспечивая, таким образом, более естественную основу для определения опе-раций времени компиляции, таких как генерация кода для вычисления выражений слева направо.

Восходящий синтаксический анализ

Задача синтаксического анализа заключается в нахождении порождения (если таковое существует) конкретного предложения, используя данную грамматику. Порождение – последовательность шагов, когда предложение языка порождается из грамматики, генерирующей данный язык. При восходящем синтаксическом анализе искомым является правое порождение.

Язык

{xmyn  m, n > 0}

генерируется следующими порождениями:

S  XY

X  xX

X  x

Y  yY

Y  y

Рассмотрим, как можно найти правое порождение следующего предложения:

хххуу

Искомое порождение выглядит так:

S => XY => XyY => Хуу => хХуу => ххХуу => хххуу

Отметим, что порождение проходит столько же этапов, что и при левом порождении, и каждая продукция используется такое же число раз, хотя порядок использования продукций несколько отличается. При восходящем синтаксическом анализе этапы порождения определяются в противоположном порядке, а правый синтаксический анализ, который соответствует приведенному выше порождению, можно записать в следующем виде:

хххуу => ххХуу => хХуу => Хуу => XyY => XY => S

На каждом этапе применяется продукция грамматики. Правая часть продукции заменяется ее левой частью, которая состоит из одного символа. Пусть предложение

хххуу

читается слева направо, тогда совсем не очевидно, почему первый х не заменяется X на первом этапе синтаксического анализа, используя соответствующую продукцию грамматики, или почему первый у подобным образом на четвертом этапе не заменяется Y. Для выполнения синтаксического анализа требуется дополнительная информация, отличная от предоставляемой продукциями грамматики.

При восходящем синтаксическом анализе правые части продукций не распознаются, пока не будут полностью считаны. Следовательно, существует необходимость хранения частично распознанных правых частей продукций, пока они не будут заменены соответствующими левыми частями. Для запоминания частично распознанных строк подходящей структурой является стек. Таким образом, подробное описание процесса восходящего синтаксического анализа включает отображение содержимого этого стека, а также информации, указанной в связи с нисходящим синтаксическим анализом. Этапы процесса синтаксического анализа могут рассматриваться как состоящие из двух типов действий:

– перемещение последнего считанного символа в стек – действие переноса;

– замена строки на верху стека посредством применения продукции грамматики – действие свертки.

Действие переноса включает удаление символа из исходной строки и занесение его в стек, а действие свертки – изменения на вершине стека и в сентенциальной форме. В начале синтаксического анализа сентенциальная форма – это в точности читаемое предложение, а стек пуст. В конце синтаксического анализа сентенциальная форма – это символ предложения, строка прочитана, а стек содержит только символ предложения.

Необходимым условием действия свертки является наличие правой части некоторой продукции на вершине стека, в противном случае производится перенос и на вершине стека появится следующий символ. В то же время, появление правой части продукции на вершине стека не является достаточным условием для применения свертки. Проиллюстрируем данный момент: если на первых этапах синтаксического анализа на вершине стека появляется х, он не сворачивается в X по неясным, на первый взгляд, причинам.

Кроме того, в строке символов на вершине стека возможно будут определены правые части более одной продукции, так что на определенном этапе синтаксического анализа могут существовать две или более возможных свертки. Итак, если в определенный момент кажутся возможными действия свертки и переноса, говорят, что имеет место конфликт перенос/свертка. Если возможными кажутся несколько операции свертки, говорят, что имеет место конфликт свертка/свертка. С целью выработки детерминированного метода синтаксического анализа могут применяться различные стратегии разрешения названных конфликтов. На практике данные конфликты разрешаются с использованием следующей информации:

– предшествующая история синтаксического анализа;

– информация, полученная путем предпросмотра.

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

Грамматика, все конфликты которой, возникающие при восходящем синтаксическом анализе слева направо, могут быть разрешены с использованием фиксированною объема информации, касающейся уже проведенного анализа и конечного числа символов предпросмотра, называется LR(k)-грамматикой. Здесь L означает чтение слева (left) направо, R – правые порождения (Rightmost), a k обозначает количество символов предпросмотра. Язык LR(k) – язык, который можно сгенерировать посредством LR(k)-грамматики. Если требуется только один символ предпросмотра, грамматика и язык относятся к классу LR(1).

Пример синтаксического анализа. Имеется грамматика со следующими продукциями:

Е  E + Т

Е  Т

Т  T * F

T  F

F  (E)

F  x

Здесь E – символ предложения. Грамматика может использоваться в качестве основы для восходящего синтаксического анализа

x + x + x * x

Первый х, помещаемый в стек, сворачивается в F, затем в T, затем в E, тогда как второй и третий х сворачиваются в F, потом в T, а четвертый х – только в F. Первый и второй символы х имеют одинаковые символы предпросмотра, и различная их трактовка основана на предшествующей истории синтаксического анализа. Для третьего и четвертого символов х символы предпросмотра отличаются (* и 1, соответственно), и снова имеем различную трактовку, основанную на истории синтаксического анализа. Критерий принятия решения относительно предпринимаемого действия (переноса или свертки) может содержаться в таблице, называемой таблицей синтаксического анализа. Таблица синтаксического анализа формируется при создании компилятора и с этого момента используется для управления каждым синтаксическим анализом.

Особенности LR-анализа

Рассматриваемые особенности LR-анализа:

– существует алгоритм определения, относится ли грамматика к классу LR(k) при данном k;

– не существует алгоритма определения, существует ли k, при котором данная грамматика относится к классу LR(k). В общем случае данная задача не решается;

– любой язык, относящийся к классу LR(k) при данном k, относится к классу LR(1).

Принимая во внимание существование LR(1)-грамматики, первые два результата не являются существенными с практической точки зрения. Существуют алгоритмы, позволяющие определить, относится ли грамматика к классу LR(1), LR(2) и т.д.

Согласно представленной выше теории LR(2)-гpaмматикy можно преобразовать в LR(1)-грамматику.

Особенность исходной грамматики, "благодаря" которой она не относится к классу LR(1), – это использование правой рекурсии плюс двойное использование запятой. В то же время, хотя грамматики с левой рекурсией не могут быть LL(1), утверждение, что LR(1)-грамматики не могут содержать правой рекурсии, в общем случае неверно. Только в относительно редких случаях правая рекурсия порождает проблемы в LR (1)-грамматиках, а если и порождает, то в основном из-за наличия в грамматике помимо правой рекурсии каких-либо иных особенностей. В то же время в большинстве программ синтаксического анализа левая рекурсия предпочтительнее правой, так как позволяет использовать свертку входа по мере чтения, не занося его в стек.

Неоднозначные грамматики, разумеется, не могут быть LR(1). Часто неоднозначность в грамматике – наличие нескольких порождений для пустой строки ().

Особенности LR-анализа:

– можно применить к широкому классу грамматик и языков;

– необходимые преобразования грамматик обычно минимальны;

– время, требуемое для анализа, прямо пропорционально длине входа;

– синтаксические ошибки выявляются на первом недопустимом символе;

– LR-анализ имеет хорошую инструментальную поддержку.

Инструментальная поддержка означает, что разработчик компилятора не обязан точно знать, как формируется таблица синтаксического анализа.

Введение в YACC

Покажем, как генератор синтаксических анализаторов YACC используется для создания анализаторов из контекстно-свободных грамматик. Эти синтаксические анализаторы могут использоваться в качестве основы разнообразных средств анализа, в том числе компиляторов и инструментов вычисления метрик, для чего в правила грамматики будут внедряться действия исходного кода. YACC (Yet Another Compiler-Compiler – "еще один компилятор компиляторов") используется для создания LR-анализатора из любой LALR(l)-гpaмматики. Это средство полностью совместимо с Lex. Сейчас Lex и YACC используются совместно. Вход YACC имеет следующий вид:

объявления

%%

правила

%%

функции, определенные пользователем.

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

%%

правила

Выход YACC – это программа на языке С, компилировать которую можно обычными средствами. Изначально средство YACC разрабатывалось для поддержки версии языка С. В настоящее время доступны версии YACC, поддерживающие различные языки (Турбо Паскаль). YACC подобен Lex по многим пунктам, хотя поддерживает более общие языковые конструкции. Анализируемый язык выражается как контекстно-свободная грамматика. При этом используется форма записи, подобная применяющейся в контекстно-свободных грамматиках, хотя и с некоторыми расширениями. Эти расширения не увеличивают выразительной силы формы записи в том смысле, что она может использоваться для описания языков, которые нельзя описать посредством контекстно-свободной грамматики. Они упрощают описание некоторых языков, а также иногда сокращают описание языка.

Терминалы берутся в одинарные кавычки, а для разделения правой и левой частей продукций используется знак : вместо . Для разделения альтернативных правых частей продукции используется знак |. Расширение формы записи, использованной для контекстно-свободных грамматик, связано с отображением уровней приоритетов операторов.

Данный метод вычисления соответствует использованию в грамматике левой рекурсии. Более высокий приоритет операторов умножения перед операторами сложения можно выразить альтернативно, определив, что выражение должно быть суммой термов, каждый из них является произведением множителей. В то же время во входе YACC этого не требуется, так что грамматика YACC короче и читабельнее. Сами по себе грамматические правила являются неоднозначными.

В то же время правила ассоциативности и приоритетов полностью разрешают эту неоднознач-ность. Грамматики YACC могут быть неоднозначными, если имеются правила, которые позволяют разрешить неоднозначность. Такие правила называются правилами разрешения неоднозначности.

Как и для Lex, действия во вход YACC пишутся на языке С и обычно располагаются в конце синтаксических правил, таким образом соответствуя приведению выражений. Например, к синтаксическому правилу

expr : expr '+' expr;

можно добавить действие, в результате чего получится

expr : expr '+' expr {$$ = $1 + $3;};

Здесь код языка С замкнут в фигурные скобки. Переменные со знаком доллара – это отличительная особенность YACC. $n – численное значение (атрибут), соотнесенное с n-м символом правой части продукции, а $$численное значение, которое будет соотнесено с символом в левой части. Таким образом, в приведенном примере значение переменной со знаком доллара, соотнесенное с выражением в левой части правила, будет равно сумме значений переменных со знаком доллара, соотнесенных с первым и третьим символами правой части правила. Подобным образом значения могут передаваться от правых частей продукций левым частям или от основания синтаксического дерева к вершине. Нахождение значений терминальных узлов синтаксического дерева – задача лексического анализа. В этом случае значение на вершине синтаксического дерева – значение всего выражения, которое можно напечатать.