使用解析器组合器解析简单的数学表达式

时间:2019-12-28 07:05:52

标签: javascript typescript parser-combinators

我正在使用parsimmon来解析一个简单的数学表达式,但是我无法解析一个遵循操作顺序的简单数学表达式(即*/的优先级高于+- )。

即使您不熟悉此库,也请帮助我解决优先级问题,而无需左递归和无限递归。

谢谢。

我使用了TypeScript:

"use strict";

// Run me with Node to see my output!

import * as P from "parsimmon";
import {Parser} from "parsimmon";
// @ts-ignore
import util from "util";


///////////////////////////////////////////////////////////////////////

// Use the JSON standard's definition of whitespace rather than Parsimmon's.
let whitespace = P.regexp(/\s*/m);

// JSON is pretty relaxed about whitespace, so let's make it easy to ignore
// after most text.
function token(parser: Parser<string>) {
  return parser.skip(whitespace);
}

// Several parsers are just strings with optional whitespace.
function word(str: string) {
  return P.string(str).thru(token);
}

let MathParser = P.createLanguage({
  expr: r => P.alt(r.sExpr2, r.sExpr1, r.number),
  sExpr1: r => P.seqMap(r.iExpr, P.optWhitespace, r.plusOrMinus, P.optWhitespace, r.expr, (a, s1, b, s2, c) => [a, b, c]),
  sExpr2: r => P.seqMap(r.iExpr, P.optWhitespace, r.multiplyOrDivide, P.optWhitespace, r.expr, (a, s1, b, s2, c) => [a, b, c]),
  iExpr: r => P.alt(r.iExpr, r.number),    // Issue here! this causes infinite recursion
  // iExpr: r => r.number  // this will fix infinite recursion but yields invalid parse

  number: () =>
    token(P.regexp(/[0-9]+/))
      .map(Number)
      .desc("number"),

  plus: () => word("+"),
  minus: () => word("-"),
  plusOrMinus: r => P.alt(r.plus, r.minus),

  multiply: () => word("*"),
  divide: () => word("/"),
  multiplyOrDivide: r => P.alt(r.multiply, r.divide),

  operator: r => P.alt(r.plusOrMinus, r.multiplyOrDivide)
});

///////////////////////////////////////////////////////////////////////

let text = "3 / 4 - 5 * 6 + 5";

let ast = MathParser.expr.tryParse(text);
console.log(util.inspect(ast, {showHidden: false, depth: null}));

This is my repo

2 个答案:

答案 0 :(得分:1)

当前,您的解析器实现的语法如下(忽略空白):

expr: sExpr2 | sExpr1 | number
sExpr1: iExpr plusOrMinus expr
sExpr2: iExpr multiplyOrDivide expr
// Version with infinite recursion:
iExpr: iExpr | number
// Version without infinite recursion:
iExpr: number

很容易看到iExpr: iExpr是左递归的产物,并且是无限递归的原因。但是,即使Parsimmon可以处理左递归,在该生产中也没有任何意义。如果它没有弄乱解析器,那么它根本什么也不会做。就像方程式x = x不会传达任何信息一样,产生式x: x不会使语法匹配之前不匹配的任何内容。从本质上讲,它是一种无操作,但是却打破了无法处理左递归的解析器。因此,删除它绝对是正确的解决方案。

修复该问题后,您现在得到错误的解析树。具体来说,您将获得解析树,就好像所有运算符都具有相同的优先级和右关联性一样。为什么?因为所有运算符的左侧都是iExpr,并且只能匹配单个数字。因此,您将始终拥有叶子作为操作员节点的左子节点,而树将始终向右生长。

可以正确地写出正确解析左联想运算符的语法,如下所示:

expr: expr (plusOrMinus multExpr)?
multExpr: multExpr (multiplyOrDivide primaryExpr)?
primaryExpr: number | '(' expr ')'

({| '(' expr ')'部分仅在您希望允许使用括号的情况下才需要)

这将导致正确的解析树,因为无法将乘法或除法作为子级进行无括号加减运算,并且如果有多个相同优先级的运算符应用,例如1 - 2 - 3 ,则外部减法会将内部减法包含为其左子元素,从而正确地将运算符视为左关联。

现在的问题是该语法是递归的,因此无法在Parsimmon中使用。最初的想法可能是像这样将左递归更改为右递归:

expr: multExpr (plusOrMinus expr)?
multExpr: primaryExpr (multiplyOrDivide multExpr)?
primaryExpr: number | '(' expr ')'

但是问题在于,现在1 - 2 - 3错误地关联到右侧而不是左侧。相反,常见的解决方案是完全删除递归(当然,从primaryExpr返回到expr的递归除外),然后将其替换为重复:

expr: multExpr (plusOrMinus multExpr)*
multExpr: primaryExpr (multiplyOrDivide primaryExpr)*
primaryExpr: number | '(' expr ')'

在Parsimmon中,您可以使用sepBy1来实现。因此,现在有了一个左操作数,而不是一个左操作数,一个运算符和一个右操作数,然后在数组中任意地有许多对操作数和操作数对。您可以通过简单地在for循环中遍历数组来从中创建左棵树。

答案 1 :(得分:0)

如果您想学习如何处理左递归,可以从https://en.wikipedia.org/wiki/Parsing_expression_grammar开始 或更确切地说https://en.wikipedia.org/wiki/Parsing_expression_grammar#Indirect_left_recursion

然后在线阅读有关PEG的更多信息。 但基本上,一种标准方法是使用循环:

Expr    ← Sum
Sum     ← Product (('+' / '-') Product)*
Product ← Value (('*' / '/') Value)*
Value   ← [0-9]+ / '(' Expr ')'

您可以在任何地方找到这种语法的示例。

如果您想坚持使用更令人满意的左递归语法,则可以阅读有关packrat解析器的信息。并为自己找到另一个解析器。因为我不确定,但是parsimmon似乎不是其中之一。

如果您只是一个有效的代码,则可以转到https://repl.it/repls/ObviousNavyblueFiletype

我已经使用parsimmon API实现了上述语法

Expr        : r => r.AdditiveExpr,
AdditiveExpr: r => P.seqMap(
  r.MultExpr, P.seq(r.plus.or(r.minus), r.MultExpr).many(),
  left_association
),
MultExpr    : r => P.seqMap(
  r.UnaryExpr, P.seq(r.multiply.or(r.divide), r.UnaryExpr).many(),
  left_association
),
UnaryExpr   : r => P.seq(r.minus, r.UnaryExpr).or(r.PrimaryExpr),
PrimaryExpr : r => P.seqMap(
  r.LPAREN, r.Expr, r.RPAREN, // without parens it won't work
  (lp,ex,rp) => ex // to remove "(" and ")" from the resulting AST
).or(P.digits),

plus        : () => P.string('+').thru(p => p.skip(P.optWhitespace)),
minus       : () => P.string('-').thru(p => p.skip(P.optWhitespace)),
multiply    : () => P.string('*').thru(p => p.skip(P.optWhitespace)),
divide      : () => P.string('/').thru(p => p.skip(P.optWhitespace)),

我们还需要使用括号,否则为什么需要递归? Value ← [0-9]+就足够了。如果没有括号,则无需在语法内引用Expr。如果这样做的话,它将不会消耗任何输入,它将毫无意义,并且将无限期递归。

所以我们还要添加:

LPAREN      : () => P.string('(').thru(p => p.skip(P.optWhitespace)),
RPAREN      : () => P.string(')').thru(p => p.skip(P.optWhitespace)),

为了完整性,我还添加了一元表达式。

现在最有趣的部分是生产功能。没有它们的结果,让我们说:

(3+4+6+(-7*5))

将如下所示:

[[["(",[["3",[]],[["+",["4",[]]],["+",["6",[]]],["+",[["(",[[["-","7"],[["*","5"]]],[]],")"],[]]]]],")"],[]],[]]

与他们在一起,将会是:

[[[["3","+","4"],"+","6"],"+",[["-","7"],"*","5"]]]

好多了。

因此对于左联想运算符,我们将需要以下条件:

// input (expr1, [[op1, expr2],[op2, expr3],[op3, expr4]])
// -->
// output [[[expr1, op1, expr2], op2, expr3], op3, expr4]
const left_association  = (ex, rest) => {
  // console.log("input", ex, JSON.stringify(rest))
  if( rest.length === 0 ) return ex
  if( rest.length === 1 ) return [ex, rest[0][0], rest[0][1]]
  let node = [ex]
  rest.forEach(([op, ex]) => {
    node[1] = op;
    node[2] = ex;
    node = [node]
  })
  // console.log("output", JSON.stringify(node))
  return node
}
相关问题