Предиктивный синтаксический анализ — различия между версиями
|  (готов) |  (→Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW) | ||
| Строка 5: | Строка 5: | ||
| Пусть <tex>\Gamma =\langle \Sigma, N, S, P \rangle</tex> {{---}} LL(1)-грамматика. Построим для нее парсер. | Пусть <tex>\Gamma =\langle \Sigma, N, S, P \rangle</tex> {{---}} LL(1)-грамматика. Построим для нее парсер. | ||
| − | Для каждого нетерминала <tex>A : A \rightarrow \alpha_1 \mid \alpha_2 \mid \ldots \mid \alpha_k </tex> создадим функцию <tex> \mathtt{A}() : \mathtt{Node} </tex>, возвращающую фрагмент дерева разбора, выведенный из нетерминала <tex>A</tex>. | + | Для каждого нетерминала <tex>A : A \rightarrow \alpha_1 \mid \alpha_2 \mid \ldots \mid \alpha_k </tex> создадим функцию <tex> \mathtt{A}() : \mathtt{Node} </tex>, возвращающую фрагмент [[Контекстно-свободные грамматики, вывод, лево- и правосторонний вывод, дерево разбора#Дерево разбора | дерева разбора]], выведенный из нетерминала <tex>A</tex>. | 
| Здесь <tex>\mathtt{Node}</tex> {{---}} структура следующего вида: | Здесь <tex>\mathtt{Node}</tex> {{---}} структура следующего вида: | ||
Версия 15:23, 25 мая 2015
Для 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.




