Предиктивный синтаксический анализ — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW)
(Нерекурсивный нисходящий парсер: более подробные пояснения случаев в тексте к картинке)
Строка 168: Строка 168:
  
 
Нерекурсивный синтаксический анализатор смотрит на текущий токен <tex> c </tex> входного слова и на символ на вершине стека <tex>A</tex>, а затем принимает решение в зависимости от одного из возникающих ниже случаев:
 
Нерекурсивный синтаксический анализатор смотрит на текущий токен <tex> c </tex> входного слова и на символ на вершине стека <tex>A</tex>, а затем принимает решение в зависимости от одного из возникающих ниже случаев:
* если <tex>A\ =\ \perp\ \land\ c\ =\ \$</tex>, то синтаксический анализатор прекращает работу, так как разбор строки завершён,
+
* если <tex>A\ =\ \perp\ \land\ c\ =\ \$</tex>, то синтаксический анализатор прекращает работу, так как разбор строки завершён (на картинке это соответствует <tex>\mathrm{ok}</tex>),
* eсли <tex>A = c</tex>, то синтаксический анализатор снимает со стека токен <tex>A</tex> и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает <tex>\mathtt{nextToken}</tex>), таким образом, происходит выброс символа <tex> c </tex> со стека,
+
* eсли <tex>A = c</tex>, то синтаксический анализатор снимает со стека токен <tex>A</tex> и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает <tex>\mathtt{nextToken}</tex>), таким образом, происходит выброс символа <tex> c </tex> со стека (на картинке данная ситуация соответствует символу <tex>\nearrow</tex>),
 
* eсли <tex>A</tex> является нетерминалом, то программа рассматривает запись <tex>\mathcal{M}[A,c]</tex> таблицы разбора <tex>\mathcal{M}</tex>. Эта запись представляет собой либо правило грамматики вида <tex>A \to \alpha_i,\ \alpha_i = X_1 X_2 \ldots X_t</tex>, и тогда <tex>\mathcal{M}[A,c]</tex> содержит номер <tex>i</tex> данного правила, либо запись об ошибке, и тогда <tex>\mathcal{M}[A,c] = \varnothing</tex>. Если <tex>\mathcal{M}[A,c] \neq \varnothing</tex>, то синтаксический анализатор замещает на стеке нетерминал <tex> A </tex> правой частью правила с номером <tex> i </tex>, помещая символы правила на стек в обратном порядке,
 
* eсли <tex>A</tex> является нетерминалом, то программа рассматривает запись <tex>\mathcal{M}[A,c]</tex> таблицы разбора <tex>\mathcal{M}</tex>. Эта запись представляет собой либо правило грамматики вида <tex>A \to \alpha_i,\ \alpha_i = X_1 X_2 \ldots X_t</tex>, и тогда <tex>\mathcal{M}[A,c]</tex> содержит номер <tex>i</tex> данного правила, либо запись об ошибке, и тогда <tex>\mathcal{M}[A,c] = \varnothing</tex>. Если <tex>\mathcal{M}[A,c] \neq \varnothing</tex>, то синтаксический анализатор замещает на стеке нетерминал <tex> A </tex> правой частью правила с номером <tex> i </tex>, помещая символы правила на стек в обратном порядке,
 
* во всех остальных случаях парсер выдаёт сообщение об ошибке.
 
* во всех остальных случаях парсер выдаёт сообщение об ошибке.

Версия 13:48, 12 августа 2015

Для LL(1)-грамматик возможна автоматическая генерация парсеров, если известны множества FIRST и FOLLOW. Существуют общедоступные генераторы: ANTLR[1], Bison[2], Yacc[3], Happy[4].

Общая схема построения рекурсивных парсеров с помощью FIRST и FOLLOW

Пусть Γ=Σ,N,S,P — LL(1)-грамматика. Построим для нее парсер.

Для каждого нетерминала A:Aα1α2αk создадим функцию A():Node, возвращающую фрагмент дерева разбора, выведенный из нетерминала A.

Здесь Node — структура следующего вида:

struct Node
    children : list<Node>
    value : string          // имя нетерминала или текст терминала
    function addChild(Node) // функция, подвешивающая поддерево к данному узлу

Каждый момент времени парсер работает с определённым токеном (англ. token) входного слова s. Токен — один или несколько терминалов, объединяемые по смыслу. Примерами токенов могут выступать следующие лексические единицы:

  • произвольный символ c,
  • целое слово, например public,
  • число со знаком, обозначаемое далее для краткости как n.

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

String token.png

Здесь $ обозначает маркер конца строки.

В псевдокоде используются следующие обозначения:

  • curToken — текущий токен строки,
  • nextToken() — функция, записывающая в curToken следующий за ним токен.

Тогда шаблон функции рекурсивного парсера для нетерминала A имеет вид:

A() : Node
    Node res = Node("A")
    switch (curToken) : // принимаем решение в зависимости от текущего токена строки
        case FIRST(α1)ε :
            // α1=X1X2Xt 
            for i = 1 .. t
                if Xi — терминал
                    consume(Xi)
                    res.addChild(Node(Xi))
                else // Xi — нетерминал, нужно вызвать соответствующую ему функцию рекурсивного парсера 
                    Node t = Xi()
                    res.addChild(t)
            break
        case FIRST(α2)ε : 
            
            break
        
        case FIRST(αk)ε : 
            
            break
        case FOLLOW(A) if i:εFIRST(αi)
            res.addChild(Node(ε)) // в этом случае нетерминал раскрывается в ε; можно не добавлять детей к res,  
            break                 // подразумевая, что если у нетерминала 0 детей, то было использовано ε-правило
        default :
            error("unexpected char")
    return res
function consume(c: char) 
    if curToken != c
        error("Expected cbutfoundcurToken")
    nextToken()

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

Пример

Рассмотрим построение парсера на примере LL(1)-грамматики арифметических выражений, которая уже была разобрана ранее:

ETEE+TEεTFTTFTεFn(E)

Напомним, что множества FIRST и FOLLOW для неё выглядят так:

Правило FIRST FOLLOW
E { n, ( } { $, ) }
E { +, ε } { $, ) }
T { n, ( } { +, $ , ) }
T { , ε } { +, $ , ) }
F { n, ( } { , +, $ , ) }

Псевдокоды

Построим функции обработки некоторых нетерминалов, используя описанный выше шаблон:

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)) // curToken подпадает под шаблон n, поэтому запишем его в value вершины
            break
        case '(' :
            consume('(')
            res.addChild(Node("("))
            res.addChild(E())
            consume(')')
            res.addChild(Node(")"))
        default :
            error("unexpected char")
    return res

Функции для T и T строятся аналогично.

Дерево разбора

Рассмотрим дерево разбора для выражения (1+2)3 и несколько первых шагов алгоритма рекурсивного разбора. Сначала вызывается функция стартового нетерминала грамматики, то есть E. Так как первым токеном является (, то будет использовано первое правило разбора TE. Поэтому к вершине с меткой E добавятся два ребёнка: T и E, а рекурсивный разборщик перейдёт к нетерминалу T. curToken по-прежнему равен (, поэтому в F сработает второй case, первым ребёнком добавится (, curToken станет равен 1, а разборщик перейдёт к нетерминалу E. После того как выражение после (, которое выводится из E, будет полностью разобрано, функция рекурсивного разбора для F добавит ) последним сыном к этому нетерминалу.

Продолжая в том же духе, мы построим всё дерево разбора данного выражения.

Дерево разбора выражения (1+2)3

Нерекурсивный нисходящий парсер

Parse table.png

Рекурсивные разборщики можно генерировать автоматически, зная множества FIRST и FOLLOW, так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такой анализатор имитирует левое порождение.

Стек нерекурсивного предиктивного синтаксического анализатора содержит последовательность терминалов и нетерминалов и таблицу синтаксического анализа. На стеке располагается последовательность символов грамматики с маркером конца разбора на дне. В начале процесса анализа строки стек содержит стартовый нетерминал грамматики непосредственно над символом . Таблица синтаксического анализа представляет собой двухмерный массив M[A,c], где A — нетерминал или , c — токен или символ конца строки $.

Нерекурсивный синтаксический анализатор смотрит на текущий токен c входного слова и на символ на вершине стека A, а затем принимает решение в зависимости от одного из возникающих ниже случаев:

  • если A =   c = $, то синтаксический анализатор прекращает работу, так как разбор строки завершён (на картинке это соответствует ok),
  • eсли A=c, то синтаксический анализатор снимает со стека токен A и перемещает указатель текущего токена ленты к следующему токену (то есть вызывает nextToken), таким образом, происходит выброс символа c со стека (на картинке данная ситуация соответствует символу ),
  • eсли A является нетерминалом, то программа рассматривает запись M[A,c] таблицы разбора M. Эта запись представляет собой либо правило грамматики вида Aαi, αi=X1X2Xt, и тогда M[A,c] содержит номер i данного правила, либо запись об ошибке, и тогда M[A,c]=. Если M[A,c], то синтаксический анализатор замещает на стеке нетерминал A правой частью правила с номером i, помещая символы правила на стек в обратном порядке,
  • во всех остальных случаях парсер выдаёт сообщение об ошибке.

Рассмотренные случаи отображены на картинке справа, где блок N отвечает нетерминалам грамматики. Из картинки видно, что вместо рассмотрения всех случаев в коде достаточно просто создать таблицу M таким образом, чтобы она учитывала все случаи, что упростит код.

Псевдокод

Здесь по-прежнему curToken обозначает текущий токен строки, а nextToken передвигает указатель на следующий токен.

function nonRecursiveParser():
    st : Stack
    st.push([math]\perp[/math])
    st.push([math]S[/math]) // здесь [math] S [/math] — стартовый нетерминал грамматики
    while s.top() [math]\neq\ \perp[/math] 
        A = st.top()
        if [math]\mathcal{M}[A,\ \mathtt{curToken}]\ ==\ \mathrm{ok}[/math] // [math] A\ ==\ \perp [/math] и [math]\mathtt{curToken}\ ==\ \$[/math]
            break // разбор строки завершён
        else if [math]\mathcal{M}[A,\ \mathtt{curToken}]\ ==\ \nearrow[/math] // выброс
            nextToken()
            st.pop()
            вывести в выходной поток терминал, отвечающий [math]\mathtt{curToken}[/math]
        else if [math]\mathcal{M}[A,\ \mathtt{curToken}][/math] — номер правила [math]A \to \alpha_i,\ \alpha_i = X_1 X_2 \ldots X_t[/math]
            st.pop()
            for k = t downto 1
                st.push([math]X_k[/math])    
            вывести в выходной поток нетерминал, отвечающий [math]A[/math]
        else
            error("unexpected symbol")

Пример

Рассмотрим пример работы нерекурсивного парсера на всё той же грамматике арифметических выражений. Для начала пронумеруем все правила:

Номер Правило
1 ETE
2 E+TE
3 Eε
4 TFT
5 TFT
6 Tε
7 Fn
8 F(E)

Теперь можно построить часть таблицы M, содержащей отвечающие нетерминалам строки. Её построение легко осуществить, если известны FIRST и FOLLOW. По этим множествам можно понять, какое правило использовать для данного нетерминала при текущем токене.

n ( ) + $
E 1 1
E 3 2 3
T 4 4
T 6 6 5 6
F 7 8
ok

На картинке ниже показаны состояния стека на нескольких первых итерациях цикла и указатель на текущий токен.

Parse stack.png

Примечания

Источники информации

  • Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы. Принципы, технологии, инструменты. Издательство Вильямс. Второе издание. 2008. Стр. 288 — 294.