递归下降解析器实现

时间:2012-03-21 23:47:38

标签: parsing recursion recursive-descent

我正在寻找一些递归下降解析器的伪代码。现在,我没有这种编码的经验。我在线阅读了一些例子,但它们只适用于使用数学表达式的语法。这是我基于解析器的语法。

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 个答案:

答案 0 :(得分:17)

基本上在递归下降解析中,语法中的每个非终端都被转换为一个过程,然后在每个过程中检查你正在查看的当前令牌是否与你期望在右侧看到的那个匹配。与程序对应的非终端符号,如果确实如此,则继续应用生产,如果没有,则表示您遇到错误,必须采取一些措施。

因此,如上所述,您将拥有以下程序: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'标记,那么这会引发一个问题,哪个生产应该选择处理输入?答案是它不知道选择哪一个,因为与人类不同,编译器无法向前看输入流以搜索“其他”令牌。这是一个简单的问题,可以通过应用称为Left-Factoring的规则来解决,这与你如何考虑代数问题非常相似。

您所要做的就是创建一个新的非终端符号S'(S-prime),其右侧将保存不常见的作品,因此您的S作品不会变为:

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)

答案 1 :(得分:7)

这不是最简单的语法,因为你的第一个制作规则有无限量的预测:

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片段?如果没有,那实际上很酷,但要明确。

您可以将您的语法(可能,取决于其他规则)等效地重写为:

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语句。任何匹配尝试的最后一个总是错误。当我遇到它时,我使用关键字并调用与生产规则相对应的函数。

从伪代码转换为真实代码并不是太复杂,但这并非易事。那些偷看和消耗可能实际上并不是在字符串上操作。在令牌上操作要容易得多。简单地走一个句子并消费它与解析它并不完全相同。你需要做一些事情,因为你消耗了碎片,可能会建立一个解析树(这意味着这些函数可能返回一些东西)。抛出错误可能在高级别上是正确的,但是您想要在错误中添加一些有意义的信息。而且,如果你需要前瞻,事情会变得更加复杂。

在讨论这些问题时,我会推荐Terence Parr(编写antlr,一个递归下降解析器生成器的人)的语言实现模式。龙书(Aho,等人,在评论中推荐)也很好(它仍然可能是编译器课程中的主要教科书)。

答案 2 :(得分:2)

我上学期教授(真的只是帮助过)PL课程的解析部分。我真的建议从我们的页面查看解析幻灯片:here。基本上,对于递归下降解析,你会问自己以下问题:

  

我已经解析了一点非终结符号,现在我正处于可以选择接下来应该解析的内容的位置。我接下来会看到我将决定什么是非终结的。

你的语法 - 顺便说一句 - 表现出一种非常普遍的模糊性,称为“悬挂其他”,自Algol时代以来一直存在。在shift reduce解析器中(通常由解析器生成器生成),这会产生移位/缩小冲突,您通常选择任意移位而不是reduce,从而为您提供常见的“maximal much”主体。 (所以,如果你看到“如果(b)那么是否(b2)S1否则S2”你把它读作“if(b)then {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);
  }
}

你看到这里的模式:

  

对于语法中的每个非终结符:解析第一个事物,然后查看您需要查看的内容以确定应该解析 next 的内容。继续这个,直到你完成!

另外,请注意,这种类型的解析通常不会产生您想要的算术。递归下降解析器(除非你使用尾递归的小技巧?)将不会产生最左边的派生。特别是,你不能写左递归规则,如“a - > a - a”,其中最左边的关联性真的是必要的!这就是人们通常使用更好的解析器生成器工具的原因。但递归下降技巧仍然值得了解和玩弄。