Реализация парсера рекурсивного спуска - PullRequest
15 голосов
/ 22 марта 2012

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

S -> if E then S | if E then S else S | begin S L | print E

L -> end | ; S L

E -> i

Я должен написать методы S(), L() и E() и вернуть некоторые сообщения об ошибках, но учебники, которые я нашел в Интернете, неочень помог.Может ли кто-нибудь указать мне правильное направление и привести несколько примеров?

Я хотел бы написать это в синтаксисе C # или Java, так как мне легче общаться.


Обновление

public void S() {
    if (currentToken == "if") {
        getNextToken();
        E();

        if (currentToken == "then") {
            getNextToken();
            S();

            if (currentToken == "else") {
                getNextToken();
                S();
                Return;
            }
        } else {
            throw new IllegalTokenException("Procedure S() expected a 'then' token " + "but received: " + currentToken);
        } else if (currentToken == "begin") {
            getNextToken();
            S();
            L();
            return;
        } else if (currentToken == "print") {
            getNextToken();
            E();
            return;
        } else {
            throw new IllegalTokenException("Procedure S() expected an 'if' or 'then' or else or begin or print  token " + "but received: " + currentToken);
        }
    }
}


public void L() {
    if (currentToken == "end") {
        getNextToken();
        return;
    } else if (currentToken == ";") {
        getNextToken();
        S();
        L();
        return;
    } else {
        throw new IllegalTokenException("Procedure L() expected an 'end' or ';' token " + "but received: " + currentToken);
    }
}


public void E() {
    if (currentToken == "i") {
        getNextToken();
        return;
    } else {
        throw new IllegalTokenException("Procedure E() expected an 'i' token " + "but received: " + currentToken);
    }
}

Ответы [ 3 ]

17 голосов
/ 22 марта 2012

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

Так что в вашем случае, как вы упомянули вышеу вас будут процедуры: S(), L() и E(), я приведу пример того, как можно реализовать L(), а затем вы можете попробовать S() и E() самостоятельно.

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

/**
 * This procedure corresponds to the L non-terminal
 * L -> 'end'
 * L -> ';' S L
 */ 
public void L()
{
   if(currentToken == 'end')
   {
      //we found an 'end' token, get the next token in the input stream
      //Notice, there are no other non-terminals or terminals after 
      //'end' in this production so all we do now is return
      //Note: that here we return to the procedure that called L()
      getNextToken();
      return; 
   } 
   else if(currentToken == ';')
   {
      //we found an ';', get the next token in the input stream
      getNextToken();
      //Notice that there are two more non-terminal symbols after ';'
      //in this production, S and L; all we have to do is call those
      //procedures in the order they appear in the production, then return
      S();
      L();
      return;
   }
   else
   {
      //The currentToken is not either an 'end' token or a ';' token 
      //This is an error, raise some exception
      throw new IllegalTokenException(
          "Procedure L() expected an 'end' or ';' token "+
          "but received: " + currentToken);
   }
}

Теперьвы пробуете S() и E() и отправляете обратно.

Также, как указывает Кристофер, в вашей грамматике есть что-то, что называется висящим другим,Имеется в виду, что у вас есть производство, которое начинается с одной и той же вещи до определенного момента:

S -> if E then S 
S -> if E then S else S

Таким образом, возникает вопрос, видит ли ваш парсер токен 'if', какое производство он должен выбрать для обработки ввода?Ответ в том, что он понятия не имеет, какой из них выбрать, потому что в отличие от людей, компилятор не может заглянуть во входной поток для поиска токена «else».Эту простую проблему можно решить, применив правило, известное как левый факторинг, очень похожее на то, как вы бы учли проблему алгебры.

Все, что вам нужно сделать, - это создать новый нетерминальный символ S '(S-prime), чья правая сторона будет содержать фрагменты необычных произведений, поэтому ваши S постановки no становятся:

S  -> if E then S S'
S' -> else S 
S' -> e   
(e is used here to denote the empty string, which basically means there is no   
 input seen)
7 голосов
/ 22 марта 2012

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

S -> if E then S | if E then S else S |  begin S L | print E

рассмотреть

if 5 then begin begin begin begin ...

Когда мы определим эту глупость еще?

также рассмотрим

if 5 then if 4 then if 3 then if 2 then print 2 else ...

Так вот, это else должно было связываться с фрагментом if 5 then? Если нет, то на самом деле это круто, но будьте явными.

Вы можете переписать свою грамматику (возможно, в зависимости от правила else) следующим образом:

S -> if E then S (else S)? | begin S L | print E
L -> end | ; S L
E -> i

Что может или не может быть тем, что вы хотите. Но псевдокод вроде как выпрыгивает из этого.

define S() {
  if (peek()=="if") {
    consume("if")
    E()
    consume("then")
    S()
    if (peek()=="else") {
      consume("else")
      S()
    }
  } else if (peek()=="begin") {
    consume("begin")
    S()
    L()
  } else if (peek()=="print") {
    consume("print")
    E()
  } else {
    throw error()
  }
}

define L() {
  if (peek()=="end") {
    consume("end")
  } else if (peek==";")
    consume(";")
    S()
    L()
  } else {
    throw error()
  }
}

define E() {
  consume_token_i()
}

Для каждого заместителя я создал оператор if, который посмотрел на уникальный префикс. Последнее в любой попытке совпадения всегда является ошибкой. Я использую ключевые слова и вызываю функции, соответствующие производственным правилам, когда сталкиваюсь с ними.

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

Я бы порекомендовал Шаблоны реализации языка от Теренса Парра (парня, который написал antlr, генератор синтаксического анализатора рекурсивного спуска) при рассмотрении подобных проблем. Книга Дракона (Ахо и др., Рекомендуется в комментарии) тоже хороша (по-прежнему, вероятно, доминирующий учебник на курсах по компиляции).

2 голосов
/ 22 марта 2012

В прошлом семестре я преподавал (действительно помогал) секцию синтаксического анализа класса PL. Я очень рекомендую посмотреть слайды разбора с нашей страницы: здесь . В основном, для разбора рекурсивного спуска вы задаете себе следующий вопрос:

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

Ваша грамматика - между прочим - демонстрирует очень распространенную двусмысленность, называемую "висячим другим", которая существует со времен Алгола. В синтаксических анализаторах уменьшения смещения (обычно создаваемых генераторами синтаксических анализаторов) это создает конфликт сдвига / уменьшения, когда вы обычно выбираете произвольное смещение вместо уменьшения, предоставляя вам общий принцип «максимально много». (Таким образом, если вы видите «if (b), то, если (b2) S1 или S2», вы читаете его как «if (b), тогда {if (b2) {s1;} else {s2;}}»)

Давайте выбросим это из вашей грамматики и поработаем с немного более простой грамматикой:

T -> A + T
 |   A - T
 |   A
A -> NUM * A
   | NUM / A
   | NUM

мы также будем предполагать, что NUM - это то, что вы получаете от лексера (то есть, это просто токен). Эта грамматика - LL (1), то есть вы можете проанализировать ее с помощью анализатора рекурсивного спуска, реализованного с использованием наивного алгоритма. Алгоритм работает так:

parse_T() {
  a = parse_A();
  if (next_token() == '+') {
    next_token();  // advance to the next token in the stream
    t = parse_T();
    return T_node_plus_constructor(a, t);
  }
  else if (next_token() == '-') {
    next_token();  // advance to the next token in the stream
    t = parse_T();
    return T_node_minus_constructor(a, t);
  } else {
    return T_node_a_constructor(a);
  }
}
parse_A() {
  num = token(); // gets the current token from the token stream
  next_token();  // advance to the next token in the stream
  assert(token_type(a) == NUM);
  if (next_token() == '*') {
    a = parse_A();
    return A_node_times_constructor(num, a);
  }
  else if (next_token() == '/') {
    a = parse_A();
    return A_node_div_constructor(num, a);
  } else {
    return A_node_num_constructor(num);
  }
}

Вы видите образец здесь:

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

Также обратите внимание, что этот тип анализа обычно не дает того, что вы хотите для арифметики. Синтаксические анализаторы рекурсивного спуска (если вы не используете небольшой трюк с хвостовой рекурсией?) Не приведут к самым левым выводам. В частности, вы не можете написать левые рекурсивные правила, такие как «a -> a - a», где крайняя левая ассоциативность действительно необходима! Вот почему люди обычно используют более интересные генераторы синтаксического анализатора. Но трюк с рекурсивным спуском все еще стоит знать и играть с ним.

...