Предиктивный синтаксический анализ
Для LL(1)-грамматик возможна автоматическая генерация парсеров, если известны множества FIRST и FOLLOW. Существуют общедоступные генераторы: ANTLR[1], Bison[2], Yacc[3], Happy[4].
Содержание
Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW
Пусть
— LL(1)-грамматика. Построим для нее парсер.Для каждого нетерминала дерева разбора, выведенный из нетерминала .
создадим функцию , возвращающую фрагментЗдесь
— структура следующего вида:struct Node children : list<Node> value : string // имя нетерминала или текст терминала function addChild(Node) // функция, подвешивающая поддерево к данному узлу
Каждый момент времени парсер работает с определённым токеном (англ. token) входного слово
. Токен — один или несколько нетерминалов, для удобства объединяемые по смыслу в одну логическую единицы. Примерами токенов могут выступать следующие лексические единицы:- произвольный символ ,
- целое слово, например ,
- число со знаком, обозначаемое далее для краткости как .
В общем случае, токеном может являться любое слово, удовлетворяющее произвольному регулярному выражению.
Здесь
обозначает маркер конца строки.В псевдокоде используются следующие обозначения:
- — текущий токен строки,
- — функция, записывающая в следующий за ним токен.
Тогда шаблон функции рекурсивного парсера для нетерминала
имеет вид:A() : Node Node res = Node("A") switch (curToken) : // принимаем решение в зависимости от текущего токена строки case: // for i = 1 .. t if — нетерминал consume( ) res.addChild(Node(" ") nextToken() else // — терминал, нужно вызвать соответствующую ему функцию рекурсивного парсера Node t = res.addChild(t) break case : break default : error("unexpected char") return res
function consume(c: char) if curToken != c error("expected " + c) nextToken()
Такой парсер не только разбирает строку, но и находит ошибки в неудовлетворяющих грамматике выражениях.
Пример
Рассмотрим построение парсера на примере LL(1)-грамматики арифметических выражений, которая уже была разобрана ранее:
Напомним, что множества
и для неё выглядят так:Правило | FIRST | FOLLOW |
---|---|---|
Псевдокоды
Построим функции обработки некоторых нетерминалов, используя описанный выше шаблон:
E() : Node Node res = Node("E") switch (curToken) case n, '(' : res.addChild(T()) res.addChild(E'()) break default : error("unexpected char") return res
E'() : Node Node res = Node("E'") switch (curToken) case '+' : consume('+') res.addChild(Node("+")) res.addChild(T()) res.addChild(E'()) break case '$', ')' : break default : error("unexpected char") return res
F() : Node Node res = Node("F") switch (curToken) case n : consume(n) res.addChild(Node(curToken)) //подпадает под шаблон , поэтому запишем его в вершины break case '(' : consume('(') res.addChild(Node("(")) res.addChild(E()) consume(')') res.addChild(Node(")")) default : error("unexpected char") return res
Функции для
и строятся аналогично.Дерево разбора
Рассмотрим дерево разбора для выражения
и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть . Так как первым токеном является '(', то будет использовано первое правило разбора . Поэтому к вершине с меткой добавятся два ребёнка: и . А рекурсивный разборщик перейдёт к нетерминалу . По-прежнему curToken равен '(', поэтому в сработает второй case, первым ребёнком добавится '(', curToken станет равен , а разборщик перейдёт к нетерминалу . После того как выражение после '(', которое выводится из , будет полностью разобрано, функция рекурсивного разбора для добавит ')' последним сыном к этому нетерминалу.Продолжая в том же духе, мы построим всё дерево разбора данного выражения.
Нерекурсивный нисходящий парсер
Рекурсивные разборщики можно генерировать автоматически, зная множества стека (вместо неявного при рекурсивных вызовах). Такой анализатор имитирует левое порождение.
и , так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использованияСтек нерекурсивного предиктивного синтаксического анализатора содержит последовательность терминалов и нетерминалов и таблицу синтаксического анализа. На стеке располагается последовательность символов грамматики с маркером конца разбора
на дне. В начале процесса анализа строки стек содержит стартовый нетерминал грамматики непосредственно над символом . Таблица синтаксического анализа представляет собой двухмерный массив , где — нетерминал или , — токен или символ конца строки .Нерекурсивный синтаксический анализатор смотрит на текущий токен
входного слова и на символ на вершине стека , а затем принимает решение в зависимости от одного из возникающих ниже случаев:- если , то синтаксический анализатор прекращает работу, так как разбор строки завершён,
- eсли , то синтаксический анализатор снимает со стека токен и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает ), таким образом, происходит выброс символа со стека,
- eсли представляет собой нетерминал, то программа рассматривает запись таблицы разбора . Эта запись представляет собой либо продукцию грамматики вида , и тогда содержит номер данной продукции, либо запись об ошибке, и тогда . Если , то синтаксический анализатор замещает на стеке нетерминал правой частью правила с номером , помещая символы правила на стек в обратном порядке,
- во всех остальных случаях парсер бросает сообщение об ошибке.
Рассмотренные случаи отображены коротко на картинке справа, где блок
отвечает нетерминалам грамматики. Из картинки видно, что вместо рассмотрения всех случаев в коде, достаточно просто создать таблицу таким образом, чтобы она учитывала все случаи, что упростит код.Псевдокод
Здесь по-прежнему
обозначает текущий токен строки, а передвигает указатель на следующий токен.
function nonRecursiveParser(): st : Stack st.push() st.push( ) // здесь — стартовый нетерминал грамматики while s.top() A = st.top() if // и break // разбор строки завершён else if // выброс nextToken() st.pop() вывести в выходной поток нетерминал, отвечающий else if — номер правила st.pop() for k = t downto 1 st.push( ) вывести в выходной поток терминал, отвечающий else error("unexpected symbol")
Пример
Рассмотрим пример работы нерекурсивного парсера на всё той же грамматике арифметических выражений. Для начала пронумеруем все правила:
Номер | Правило |
---|---|
Теперь можно построить часть таблицы
, содержащей строки, отвечающие нетерминалам. Её построение легко осуществить, если известны и . По сути по этим множествам можно понять, какое правило использовать для данного нетерминала при текущем токене.На картинке ниже показаны состояние стека на нескольких первых итерациях цикла и указатель на текущий токен.
Примечания
Источники информации
- Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы. Принципы, технологии, инструменты. Издательство Вильямс. Второе издание. 2008. Стр. 288 — 294.