Грамматика: разница между сверху вниз и снизу вверх?

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

2 ответа

Решение

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

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

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

expression() { term() [-+] expression }
term() { factor() [*/] term() }
factor() { operand() | '(' expression() ')' }

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

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

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void expression(void);

void show(int ch) { 
    putchar(ch);
    putchar(' ');
}

int token() { 
    int ch;
    while (isspace(ch=getchar()))
        ;
    return ch;
}

void factor() { 
    int ch = token();
    if (ch == '(') {
        expression();
        ch = token();
        if (ch != ')') {
            fprintf(stderr, "Syntax error. Expected close paren, found: %c\n", ch);
            exit(EXIT_FAILURE);
        }
    }
    else
        show(ch);
}

void term() { 
    int ch;
    factor();
    ch = token();
    if (ch == '*' || ch == '/') {
        term();
        show(ch);
    }
    else
        ungetc(ch, stdin);
}

void expression() {
    int ch;
    term();
    ch = token();
    if (ch == '-' || ch=='+') {
        expression();
        show(ch);
    }
    else 
        ungetc(ch, stdin);
}

int main(int argc, char **argv) {
    expression();
    return 0;
}

Обратите внимание, что здесь лексизм довольно глупый (он просто принимает один символ в качестве токена) и допустимые выражения весьма ограничены (только +-*/). ОТО, это достаточно хорошо, чтобы обрабатывать ввод, как:

1 + 2 * (3 + 4 * (5/6))

из которого он производит то, что я считаю правильным выводом:

1 2 3 4 5 6 / * + * +

На самом деле это не имеет никакого значения для самой грамматики, но это делает для парсера.

В Википедии есть довольно длинное объяснение как восходящего, так и нисходящего анализа.

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

Другие вопросы по тегам