【问题标题】:Recursive descent parser implementation递归下降解析器实现
【发布时间】:2017-04-03 22:00:22
【问题描述】:

我希望编写一些递归下降解析器的伪代码。现在,我对这种类型的编码没有经验。我在网上阅读了一些示例,但它们仅适用于使用数学表达式的语法。这是我作为解析器基础的语法。

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);
    }
}

【问题讨论】:

标签: parsing recursion recursive-descent


【解决方案1】:

基本上在递归下降中,语法中的每个非终结符都会被翻译成一个过程,然后在每个过程中检查你正在查看的当前标记是否与你期望在右侧看到的相符与过程对应的非终结符,如果是则继续应用产生式,如果不是则您遇到错误并且必须采取一些措施。

因此,在您上面提到的情况下,您将有程序:S()L()E(),我将举例说明如何实现L(),然后您可以尝试执行@ 987654325@ 和 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(),然后回帖。

正如 Kristopher 指出的那样,你的语法有一个叫做 dangling else 的东西,这意味着你有一个从相同的东西开始的产品:

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

因此,如果您的解析器看到“if”标记,那么这就引出了一个问题,它应该选择哪个产品来处理输入?答案是它不知道该选择哪一个,因为与人类不同,编译器无法提前查看输入流来搜索“else”标记。这是一个简单的问题,可以通过应用称为左因子的规则来解决,这与代数问题的因子非常相似。

你所要做的就是创建一个新的非终结符号 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)

【讨论】:

  • 我已经更新了我的答案,但我还没有修复悬空的其他问题。创建新的非终端是解决此问题的唯一方法吗?不知道能不能改语法
  • @user1072706 递归下降解析器需要具有预测性,这意味着它需要知道去哪里,在每个场景中应用哪个生产;它没有选项说'哎呀,我不应该在这里,我需要备份调用堆栈'。对于这个,你可以检查一下 else 是否存在,因为它们在同一个非终端中,但更好的解决方案是修改语法。您在上面完成的方式应该可以正常工作。
【解决方案2】:

这不是最容易开始的语法,因为您对第一条生产规则有无限量的前瞻:

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

考虑

if 5 then begin begin begin begin ...

我们什么时候确定这个愚蠢的else?

还有,考虑

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 语句。任何匹配尝试的最终 else 始终是错误的。我消费关键字,遇到生产规则就调用对应的函数。

从伪代码转换为真实代码并不太复杂,但也不是微不足道的。那些偷看和消费可能实际上并没有对字符串进行操作。对令牌进行操作要容易得多。简单地走一个句子并使用它与解析它并不完全相同。当你使用这些片段时,你会想做一些事情,可能会构建一个解析树(这意味着这些函数可能会返回一些东西)。并且抛出一个错误在高层次上可能是正确的,但是您希望将一些有意义的信息放入错误中。此外,如果您确实需要前瞻,事情会变得更加复杂。

在查看此类问题时,我会推荐 Terence Parr(编写 antlr,递归下降解析器生成器的人)的 Language Implementation Patterns。 Dragon Book(Aho 等人在评论中推荐)也很好(它可能仍然是编译器课程中的主要教科书)。

【讨论】:

    【解决方案3】:

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

    我已经解析了一些非终结符,现在我可以选择接下来要解析的内容。我接下来看到的将决定我所处的非终端。

    顺便说一句,您的语法表现出一种非常常见的歧义,称为“dangling else”,这种歧义从大陵五时代就已经存在。在 shift reduce 解析器(通常由解析器生成器生成)中,这会产生 shift / reduce 冲突,您通常会选择任意 shift 而不是 reduce,从而为您提供常见的“maximal much”原则。 (因此,如果您看到“if (b) then if (b2) S1 else 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);
      }
    }
    

    你看到这里的模式了吗:

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

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

    【讨论】:

    • 可以使用递归下降生成解析树,然后改变遍历的顺序来处理算术。
    • @HunterMcMillen,完全正确,这已经完成,但有时对于更复杂的结构来说这是一个令人讨厌的 hack,并且当您可以使用解析器生成器时,通常会建议您这样做。
    • YACC 和 Bison 是你的朋友,我同意。当我学习这些东西时,我们遇到了与上面提到的解析算术相同的问题,我们的教授使用访问者模式创建了一个非常优雅的解决方案。
    • 对,这完全对应于函数式语言中您进行处理,然后使用尾递归函数进行后处理的情况。您还可以通过一些小技巧一次性实现它们,这使您可以使用尾递归解析器获得最左边的推导。 drdobbs.com/cpp/184406384?pgno=1 这是我在帖子中提到的技术。 (在道德上它非常相似......)
    • @KristopherMicinski 在if (next_token() == '+')next_token(); // advance to the next token in the stream 中的next_token() 函数是不同的,对吧?第一个next_token表示在令牌队列中窥视一个令牌,第二个next_token表示advance_token,表示从令牌队列中删除窥视令牌,并为窥视令牌设置一个新值。有时,我认为“下一个”这个词令人困惑,因为在 Hunter McMillen 的回答中,这仅表示“CurrentToken”,而在 ccoakley 的回答中,这表示“偷看的令牌”,对吧?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-07
    • 1970-01-01
    • 2012-05-21
    • 2016-03-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多