Предиктивный синтаксический анализ — различия между версиями
|  (→Пример) |  (→Нерекурсивный нисходящий парсер) | ||
| Строка 154: | Строка 154: | ||
| == Нерекурсивный нисходящий парсер == | == Нерекурсивный нисходящий парсер == | ||
| − | [[Файл:Parse_table.png| | + | [[Файл:Parse_table.png|400px|right]] | 
| Рекурсивные разборщики можно генерировать автоматически, зная множества FIRST и FOLLOW, так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такое анализатор имитирует левое порождение.    | Рекурсивные разборщики можно генерировать автоматически, зная множества FIRST и FOLLOW, так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такое анализатор имитирует левое порождение.    | ||
| Строка 166: | Строка 166: | ||
| === Псевдокод === | === Псевдокод === | ||
| − | + | <code style = "display: inline-block;"> | |
| − | <code> | ||
|   function nonRecursiveParser(w : String): |   function nonRecursiveParser(w : String): | ||
|       s : Stack |       s : Stack | ||
Версия 14:08, 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 станет равен , а разборщик перейдёт к нетерминалу . После того как выражение после '(', которое выводится из , будет полностью разобрано, функция рекурсивного разбора для добавит ')' последним сыном к этому нетерминалу.
Продолжая в том же духе, мы построим всё дерево разбора данного выражения.
Нерекурсивный нисходящий парсер
Рекурсивные разборщики можно генерировать автоматически, зная множества FIRST и FOLLOW, так как они имеют достаточно прозрачный шаблон построения. Альтернативным способом осуществления нисходящего синтаксического анализа является построение нерекурсивного нисходящего парсера. Его можно построить с помощью явного использования стека (вместо неявного при рекурсивных вызовах). Такое анализатор имитирует левое порождение.
Нерекурсивный предиктивный синтаксический анализатор содержит дополнительно стек, содержащий последовательность терминалов и нетерминалов, и таблицу синтаксического анализа. На стеке располагается последовательность символов грамматики с маркером конца строки $ на дне. В начале процесса анализа строки стек содержит стартовый нетерминал грамматики непосредственно над символом $. Таблица синтаксического анализа представляет собой двухмерный массив М[X, а], где X — нетерминал, а — терминал или символ $.
Нерекурсивный синтаксический анализатор смотрит на текущий токен строки a и на символ на вершине стека X, а затем принимает решение в зависимости от одного из возникающих ниже случаев:
- если Х=curToken=$, синтаксический анализатор прекращает работу, так как разбор строки завершён,
- eсли Х=curToken≠$, синтаксический анализатор снимает со стека X и перемещает указатель входного потока к следующему токену (то есть вызывает nextToken),
- eсли X представляет собой нетерминал, программа рассматривает запись M[Х,а] таблицы разбора М. Эта запись представляет собой либо X-продукцию грамматики, либо запись об ошибке. Если, например, М[Х,а] = {X → UVW}, синтаксический анализатор замещает X на вершине стека на WVU (с U на вершине стека). В кач-ве выхода синтаксический анализатор просто выводит использованную продукцию. Если M[Х,а] = error, синтаксический анализатор вызывает программу восстановления после ошибки.
Псевдокод
function nonRecursiveParser(w : String):
    s : Stack
    s.push(bottom)
    s.push(Start)
    do 
        X = s.top()
        if X == c
            c = nextToken()
            s.pop()
        else if X in Sigma
            error("unexpected symbol")
        else if M[X, c] == undefined // запись об ошибке
            error("unexpected symbol")
        else if M[X, c] == X -> Y_1 Y_2 ... Y_k
            s.pop()
            for i = k downto 1
                s.push(Y_i)
    while s.top() != bottom
Пример
| n | ( | ) | + | * | $ | |
|---|---|---|---|---|---|---|
Примечания
Источники информации
- Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы. Принципы, технологии, инструменты. Издательство Вильямс. Второе издание. 2008. Стр. 288 — 294.




