Грамматика: разница между сверху вниз и снизу вверх?
В чем разница между грамматикой сверху вниз и снизу вверх? Пример был бы потрясающим.
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) более интуитивный способ - сверху вниз. Вы начинаете с начального символа и применяете подходящие правила преобразования, в то время как с восходящим движением вам нужно применять правила преобразования в обратном направлении (что обычно создавало для меня головную боль).