Россия, Челябинск, Южно-Уральский Государственный Университет (НИУ) |
Синтаксические анализаторы. Нисходящие анализаторы
Классы синтаксических анализаторов
Большинство известных методов анализа принадлежат одному из двух классов, один из которых объединяет нисходящие (top-down) алгоритмы, а другой - восходящие (bottom-up) алгоритмы. Происхождение этих терминов связано с тем, каким образом строятся узлы синтаксического дерева: либо от корня (аксиомы грамматики) к листьям (терминальным символам), либо от листьев к корню.
Нисходящие анализаторы строят вывод, начиная от аксиомы грамматики и заканчивая цепочкой терминальных символов. С нисходящими анализаторами связаны так называемые LL-грамматики, которые обладают следующими свойствами:
- Они могут быть проанализированы без возвратов
- Первая буква L означает, что мы просматриваем входную цепочку слева направо (left-to-right scan)
- Вторая буква L означает, что строится левый вывод цепочки (leftmost derivation).
Популярность нисходящих анализаторов связана с тем, что эффективный нисходящий анализатор достаточно легко может быть построен вручную, например, методом рекурсивного спуска. Кроме того, LL-грамматики легко обобщаются: грамматики, не являющиеся LL-грамматиками, обычно могут быть проанализированы методом рекурсивного спуска с возвратами.
С другой стороны, восходящие анализаторы могут анализировать большее количество грамматик, чем нисходящие, и поэтому именно для таких методов существуют программы, которые умеют автоматически строить анализаторы. С восходящими анализаторами связаны LR-грамматики. В этом обозначении буква L по-прежнему означает, что входная цепочка просматривается слева направо (left-to-right scan), а буква R означает, что строится правый вывод цепочки (rightmost derivation). С помощью LR-грамматик можно определить большинство использующихся в настоящее время языков программирования.
Метод рекурсивного спуска
Одним из наиболее простых и потому одним из наиболее популярных методов нисходящего синтаксического анализа является метод рекурсивного спуска (recursive descent method) .
Для объяснения принципов, лежащих в основе метода рекурсивного спуска, рассмотрим задачу вычисления значения арифметической формулы. Будем рассматривать формулы, состоящие из целочисленных значений, бинарных операций сложения ( + ), вычитания ( - ), умножения ( * ) и деления нацело ( / ), а также круглых скобок. Как обычно, приоритеты операций умножения и деления равны и их приоритет больше, чем приоритеты операций сложения и вычитания, причем приоритеты этих операций также равны. Будем называть операции + и - операциями типа сложения, а операции * и / - операциями типа умножения. Круглые скобки используются для изменения стандартного порядка выполнения операций. Наша задача заключается в написании программы, вычисляющей значение формулы.
Изучаемые нами формулы можно представить следующим образом:
T1 +T2 +…+Tn,
где Ti - это формула вида F1i *F2i *…*Fmi . В свою очередь, Fji - это либо число, либо произвольная формула, заключенная в круглые скобки.
Представим себе процесс вычисления значения формулы. Вначале вычисляется F11, далее мы выясняем, какая операция следует за F11 . Если это операция типа умножения, то мы, зная ее левый операнд, вычисляем правый операнд и выполняем операцию. Тем самым, мы получим левый операнд для возможных следующих операций типа умножения. Когда мы закончим вычисление формулы F1*F2*…*Fn, то, возможно, увидим далее операцию типа сложения, и процесс вычисления такой формулы будет аналогичен только что описанному процессу.
Вычисление значения формулы
Итак, мы можем разделить все формулы на следующие классы:
- Простейшие формулы: числа и произвольные формулы, заключенные в круглые скобки, например, 354, (17+3-18)
- Формулы, содержащие операции типа умножения, т.е. умножение и деление, например, 18*2*(35+2)*7
- Формулы, содержащие операции типа сложения, т.е. сложение и вычитание, например, 18*2*(35+2)*7+354+ (17+3-18)*(12-7) .
Теперь мы можем представить себе процесс вычисления значения формулы, как следующий вызов:
Expression (Term (Factor ())),
где Factor - процедура вычисления простейшей формулы, являющейся либо числом, либо произвольной формулой, заключенной в круглые скобки, Term - процедура вычисления значения формулы, содержащей операции типа умножения, Expression - процедура вычисления значения формулы, содержащей операции типа сложения.
Опишем процедуру, вычисляющую значение простейшей формулы:
int Factor () { char ch = getChar(); if (isDigit (ch)) return getValue(ch); if (ch == '(') { int result = Formula (); if (getChar() == ')') return result; error ("Неожиданный символ"); return 0; } return error ("Неожиданный символ"); }