繁体   English   中英

递归下降解析器:RPN [2] 的中缀

[英]Recursive descent Parser: infix to RPN [2]

这是我继续尝试制作一个递归下降解析器——LL(1)——它接受中缀表达式并输出 RPN。 这是我的第一个问题的链接,@rici 回答得非常出色,我希望我通过这个修改后的实现来公正地回答他。 我的新语法如下(不支持一元运算符):

expr -> term (+|-) term | term
term -> exponent (*|/) exponent | exponent
exponent -> factor ^ factor | factor
factor -> number | ( expr )

在他的回答中,@rici 指出了Norwell 的语法

我们通常将一元否定运算符放在乘法和取幂之间

我试图在这里合作:

expr -> term (+|-) term | term
term -> exponent1 (*|/) exponent1 | exponent1
exponent1 -> (+|-) exponent | exponent 
exponent -> factor ^ factor | factor
factor -> number | ( expr )

对第一个语法进行编码使得不能接受 uary(+/-) 数字,并且只能接受二进制 -/+ 运算符。 该解决方案适用于我尝试过的许多问题(可能是错误的,希望了解更多)。 然而,仔细检查后,第二个失败了,我被迫回到我第一个使用的“hack”。 正如@rici 指出的那样:

顺便说一句,您的 output 不是反向波兰表示法(也不是没有括号的明确表示),因为您在操作数之前是 output 一元运算符。

公平地说,他确实指出添加额外的 0 操作数很好,我认为它会起作用。 但是,如果我这样做13/-5 ,其等效中缀将是13/0-5及其 RPN 13 0 / 5 - 或者也许我误解了他的观点。

最后把钉子钉在棺材里@rici 还指出:

左递归消除将删除左关联运算符和右关联运算符之间的区别

因此,这意味着几乎不可能确定任何运算符的关联性,即所有运算符都相同且没有不同。 此外,这意味着对于简单的 LL(1) 解析器来说,如果不是不可能的话,尝试支持许多左右关联运算符将是非常困难的。

下面是我的 C 代码实现的语法:


#include <stdio.h>
#include <stdlib.h>

void error();
void factor();
void expr();
void term();
void exponent1();
void exponent();
void parseNumber();
void match(int t);

char lookahead;
int position=0;
int main() {
  lookahead = getchar();
  expr();
return 0;
}

void error() {
  printf("\nSyntax error at lookahead %c pos: %d\n",lookahead,position);
  exit(1);
}


void factor() {
 if (isdigit(lookahead)) {
      parseNumber();
     // printf("lookahead at %c",lookahead);
  } else if(lookahead =='('){
      match('(');
      expr();
      match(')');
  }else {
      error();
      }
}

void expr(){
    term();
    while(1){
        if(!lookahead||lookahead =='\n') break;
        if(lookahead=='+'|| lookahead=='-'){
            char token  = lookahead;
            match(lookahead);
            term();
            printf(" %c ", token);
        }else {
            break;
        }
    }
}

void term(){
    exponent1();
    while(1){
        if(!lookahead||lookahead =='\n') break;
        if(lookahead=='/'|| lookahead=='*'){
            char token = lookahead;
            match(lookahead);
            exponent1();
            printf(" %c ", token);
        }else {
            break;
        }
    }
}

void exponent1(){
    if(lookahead=='-'||lookahead=='+'){
        char token  = lookahead;
        match(lookahead);
        //having the printf here:
        printf("%c", token);
        //passes this:
        //  2+6*2--5/3      := 2.00 6.00 2.00  *  + 5.00 3.00  /  -
        // -1+((-2-1)+3)*-2 := -1.00 -2.00 1.00  - 3.00  + -2.00  *  + (not actual RPN @rici mentions)
        //but fails at:
        // -(3/2)           := -3.00 2.00  /
        // -3/2             := -3.00 2.00  /
        exponent();
        // but having the printf here
        //printf("%c ", token);
        // fails this -1+((-2-1)+3)*-2 := 1.00 - 2.00 - 1.00  - 3.00  + 2.00 -  *  +
        // since it is supposed to be
        //  1.00 - -2.00 1.00 - 3.00 + -2.00 * +
        //  but satisfies this:
        // -(3/2) := 3.00 2.00  / -
        // (-3/2) := 3.00 - 2.00  /
    }else {
        exponent();
        //error();
    }
}

void exponent(){
    factor();
        while(1){
        if(!lookahead||lookahead =='\n') break;
        if(lookahead=='^'){
            char token  = lookahead;
            match('^');
            factor();
            printf(" ^ ");
        }else {
            break;
        }
    }
}

void parseNumber() {
  double number = 0;
  if (lookahead == '\0'|| lookahead=='\n') return;
  while (lookahead >= '0' && lookahead <= '9') {
    number = number * 10 + lookahead - '0';
    match(lookahead);
  }
  if (lookahead == '.') {
    match(lookahead);
    double weight = 1;
    while (lookahead >= '0' && lookahead <= '9') {
      weight /= 10;
      number = number + (lookahead - '0') * weight;
      match(lookahead);
    }
  }
  printf("%.2f ", number);
  //printf("\ncurrent look ahead at after exiting parseNumber %c\n",lookahead);
}
void match(int t) {
  if (lookahead == t){
    lookahead = getchar();
    position++;
    }
  else error();

}

那么这是否意味着我应该放弃 LL(1) 解析器而改用 LR 解析器呢? 或者可以增加前瞻数量有帮助,如果有很多路径,那么它可能会缩小范围,减少前瞻的前瞻。 例如:

-(5 ;; 看起来很奇怪

-( 5;; 可能是 - ( exp )

或者

--5 ;; 可能有很多东西

-- 5;; 应该是 -- 运算符和 output 说 #

编辑:

  1. 我认为有一个更大的前瞻将很难协调。 所以也许有类似调车场算法的东西,我喜欢窥视下一个运算符,并根据运算符的优先级,算法将确定 function 调用。 类似于使用实际运行程序的实际堆栈。 因此,弹出将是返回,而推送将是 function 调用。 不知道我怎么能用递归下降来协调它。

  2. 也许窥视的优先级应该决定前瞻长度?

  1. 增加前瞻并没有帮助。

  2. 这是算术表达式的常用 LALR(1) 语法,包括求幂:

     expr -> sum sum -> sum (+|-) product | product product -> product (*|/) prefix | prefix prefix -> (+|-) prefix | exponent exponent -> atom ^ exponent | atom atom -> number | ( expr )

    您可以在整个 Internet 上找到构建语法的 model 示例,尽管您还会发现许多示例,其中始终使用相同的非终结符,并且使用优先声明处理由此产生的歧义。

    请注意exponent和其他二元运算符之间的结构差异。 exponent是右递归的(因为指数是右结合的); 其他的是左递归的(因为其他二元运算符是左关联的)。

  3. 当我说您可以通过添加显式 0 来修复前缀运算符字符的歧义时,我并不是说您应该编辑输入以插入 0。那是行不通的,因为(正如您所注意到的)它得到一元运算符的优先级错误。 我的意思是递归下降 RPN 转换器应该是这样的:

     void parsePrefix(void) { if (lookahead=='-'||lookahead=='+') { char token = lookahead; match(lookahead); fputs("0 ", stdout); parsePrefix(); printf("%c ", token); } else { parseExponent(); } }

    它将 0 精确地输出到 go 所需的位置。

    警告:以下段落是纯粹的意见,不符合 StackOverflow 的政策。 如果这会冒犯你,请不要阅读它。 (在这种情况下,也许你应该跳过这个答案的 rest 。)

    恕我直言,这确实是一个 hack,但 RPN 的使用也是如此。 如果您正在构建一个 AST,您只需构建一个具有单个操作数的 unaryOperator AST 节点。 不会有歧义的问题,因为在评估期间不需要再次解释令牌。 无论出于何种原因,通过通常的编译器理论课程学习 go 的学生似乎都认为 AST 在某种程度上是一种复杂的技术,在必要时应该避免使用,必须不惜一切代价避免左递归,并且有道德编码自己的 LL 解析器而不是仅仅使用标准可用的 LALR 解析器生成器的价值。 我不同意所有这些事情。 特别是,我建议您从创建 AST 开始,因为它会使几乎所有其他事情变得更容易。 此外,如果您想了解解析,请从使用标准工具开始,并专注于编写清晰、自记录的语法,并使用有关您尝试解析的输入的句法结构的信息。 稍后您可以了解解析器生成器的工作原理,如果您真的觉得这很有趣的话。 同样,我绝不会从准确评估sin() function 的泰勒展开式开始教授三角学。 这并没有为学生提供有关如何使用三角函数(例如,旋转一个角度)的任何见解,这无疑是三角学中最重要的部分。 一旦学生对三角学、如何使用它,特别是在典型问题领域中精确计算的要求有了深刻的理解,那么他们可能会想看看泰勒展开式和其他计算技术。 但大多数人会满足于调用sin() ,我认为这很完美。

  4. 如果您真的想使用递归下降解析器,请使用 go。 他们当然可以工作。

    当您将语法编码到可执行程序中时会发生什么,您将慢慢开始偏离可以在其他程序中使用的语法表示,例如语法着色器和 static 分析器。 部分原因是您使用的语法已经丢失了语法的重要方面,包括关联性,而这些功能直接编码到您的解析器代码中。 最终的结果往往是只维护解析器代码,让理论语法腐烂。 当代码本身是“语法”时,它不再可用作您语言语法的实际文档。

    但我并不是说它不能完成。 它肯定是可以做到的,并且在实际使用中有很多很多的解析器都是这样做的。

  5. 调车场算法(以及一般的运算符优先级解析)是一种自下而上的技术,就像 LR 解析一样,它们都不需要递归解析器。 如果出于某种原因你真的想使用递归,你可以使用 Pratt 解析器,但是自底向上解析比递归下降有一个巨大的实际优势:它消除了不可控堆栈溢出的可能性。 所以很难推荐在生产解析器中使用递归下降,除非输入文本被严格控制以避免可能的堆栈溢出攻击。 这可能不适用于未与未经验证的输入一起使用的编译器。 但是你的编译器真的是这样吗? 您是否从未从外国站点下载源代码压缩包然后输入./configure && make all :-)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM