遍历AST时区分运算符优先级

时间:2015-07-29 14:58:52

标签: javascript parsing compiler-construction abstract-syntax-tree peg

我有一个由目标语言的解析表达式语法生成的AST,它将通过遍历其节点编译为源语言。一个简单的来源 像(10 + 20) * 2一样应该生成以下表示,作为本机ECMAScript对象:

var ast = {
   "type": "Stmt",
   "body": [
      {
         "type": "Expr",
         "expression": {
            "type": "BinaryExpr",
            "operator": "*",
            "left": {
               "type": "BinaryExpr",
               "operator": "+",
               "left": {
                  "type": "Literal",
                  "value": 10
               },
               "right": {
                  "type": "Literal",
                  "value": 20
               }
            },
            "right": {
               "type": "Literal",
               "value": 2
            }
         }
      }
   ]
};

生成的对象清楚地定义了运算符的优先级,并且评估此源非常简单,但是,当您必须处理括号求解时,从中生成代码是一项相当复杂的任务。

通过遍历节点生成代码时,优先级完全丢失。我有一个名为visitor的函数,它是程序的入口点:

function visitor(node) {
  switch (node.type) {
    case "Stmt":
      return parseStmt(node.body);
  }
}

这个简单的语法可以接受多个语句......

function parseStmt(body) {
  var stmtList = Array(body.length);

  for (var i = 0, len = body.length; i < len; i++) {
    stmtList[i] = (function(stmt) {
      switch (stmt.type) {
        case "Expr":
          return parseExpr(stmt.expression);
      }
    })(body[i]);
  }

  return stmtList.join(";\n");
}

......还有两种表达方式:

function parseExpr(expr) {
  switch (expr.type) {
    case "BinaryExpr":
      return parseBinaryExpr(expr);
    case "Literal":
      return parseLiteral(expr);
  }
}

Literal只处理字符串转换......

function parseLiteral(expr) {
  return expr.value.toString();
}

...和BinaryExpr在求解优先级时不明确:

function parseBinaryExpr(expr) {
  var op = {
    left: parseExpr(expr.left),
    right: parseExpr(expr.right)
  };

  switch (expr.operator) {
    case "+":
      return Codegen.OP_ADD(op.left, op.right);
    case "*":
      return Codegen.OP_MUL(op.left, op.right);
  }
}

这里只支持两个数学运算,代码生成在这里:

var Codegen = {
  OP_ADD: function(left, right) {
    return left + " + " + right;
  },
  OP_MUL: function(left, right) {
    return left + " * " + right;
  }
};

当回调visitor(ast)时,我得到10 + 20 * 2,它将评估为10 + (20 * 2)而不是(10 + 20) * 2,并且在二进制表达式的每一侧插入括号将是荒谬的解决方法:(10 + 20) * 2其中:

function parseBinaryExpr(expr) {
  var op = {
    left: "(" + parseExpr(expr.left) + ")",
    right: "(" + parseExpr(expr.right) + ")"
  };
...

如何以一种好的方式解决这种歧义?

2 个答案:

答案 0 :(得分:1)

不是一个简单的优先级表,并查看父表达式来解决它吗?

此外,交换机中还有一个小错误。

var ast = {
   "type": "Stmt",
   "body": [
      {
         "type": "Expr",
         "expression": {
            "type": "BinaryExpr",
            "operator": "*",
            "left": {
               "type": "BinaryExpr",
               "operator": "+",
               "left": {
                  "type": "Literal",
                  "value": 10
               },
               "right": {
                  "type": "Literal",
                  "value": 20
               }
            },
            "right": {
               "type": "Literal",
               "value": 2
            }
         }
      }
   ]
};

var precedence = { "*": 0, "+": 1 };

var Codegen = {
  OP_ADD: function(left, right) {
    return left + " + " + right;
  },
  OP_MUL: function(left, right) {
    return left + " * " + right;
  }
};

function visitor(node) {
  switch (node.type) {
    case "Stmt":
      return parseStmt(node.body);
  }
}

function parseStmt(body) {
  var stmtList = Array(body.length);

  for (var i = 0, len = body.length; i < len; i++) {
    stmtList[i] = (function(stmt) {
      switch (stmt.type) {
        case "Expr":
          return parseExpr(stmt.expression, null);
      }
    })(body[i]);
  }

  return stmtList.join(";\n");
}

function parseExpr(expr, parent) {
  switch (expr.type) {
    case "BinaryExpr":
      return parseBinaryExpr(expr, parent);
    case "Literal":
      return parseLiteral(expr);
  }
}

function parseLiteral(expr) {
  return expr.value.toString();
}

function parseBinaryExpr(expr, parent) {
  var op = {
    left: parseExpr(expr.left, expr),
    right: parseExpr(expr.right, expr)
  };
  var ret = "";
  switch (expr.operator) {
    case "+":
      ret = Codegen.OP_ADD(op.left, op.right); 
      break;
    case "*":
      ret = Codegen.OP_MUL(op.left, op.right); 
      break;
  }
  if (parent && precedence[expr.operator] > precedence[parent.operator]) {
    ret = "(" + ret + ")";
  }
  return ret;
}

visitor(ast);

或者如果在另一个内部嵌套二进制表达式,你总是可以加上一个括号,这样做也可以。

  if (parent) {
    ret = "(" + ret + ")";
  }

只检查父项,因为如果我们已经在二进制表达式中,我们只传递父项。

答案 1 :(得分:0)

我会在CodeGen而不是ParseBinaryExpr中添加括号:

var Codegen = {
  OP_ADD: function(left, right) {
    return "(" + left + " + " + right + ")";
  },
  OP_MUL: function(left, right) {
    return "(" + left + " * " + right + ")";
  }
};

这将导致更少的冗余括号,尽管你仍然会得到很多括号。从积极的方面来看,毫无疑问,结果表达式对应于AST。 (顺便说一下,你需要在代码gen中为括号运算符添加括号。)

通过检查ParseBinaryExpr中的运算符优先级可以避免所有冗余括号 - 也就是说,只有当它的优先级小于二进制表达式的运算符的优先级时,才用括号括起一个参数 - 但这很容易出错并导致微妙的错误。