Parsing 解析器/语法:嵌套规则中有2个括号
尽管我在编译/解析方面的知识有限,但我还是敢于为OData$filter表达式构建一个小型递归下降解析器。解析器只需要检查表达式的正确性,并在SQL中输出相应的条件。由于输入和输出具有几乎相同的标记和结构,这相当简单,我的实现实现了我想要的90% 但现在我被括号卡住了,它出现在逻辑表达式和算术表达式的不同规则中。ABNF中完整的OData语法是,所涉及规则的浓缩版本如下:Parsing 解析器/语法:嵌套规则中有2个括号,parsing,grammar,parentheses,recursive-descent,ambiguous-grammar,Parsing,Grammar,Parentheses,Recursive Descent,Ambiguous Grammar,尽管我在编译/解析方面的知识有限,但我还是敢于为OData$filter表达式构建一个小型递归下降解析器。解析器只需要检查表达式的正确性,并在SQL中输出相应的条件。由于输入和输出具有几乎相同的标记和结构,这相当简单,我的实现实现了我想要的90% 但现在我被括号卡住了,它出现在逻辑表达式和算术表达式的不同规则中。ABNF中完整的OData语法是,所涉及规则的浓缩版本如下: boolCommonExpr = ( boolMethodCallExpr / notE
boolCommonExpr = ( boolMethodCallExpr
/ notExpr
/ commonExpr [ eqExpr / neExpr / ltExpr / ... ]
/ boolParenExpr
) [ andExpr / orExpr ]
commonExpr = ( primitiveLiteral
/ firstMemberExpr ; = identifier
/ methodCallExpr
/ parenExpr
) [ addExpr / subExpr / mulExpr / divExpr / modExpr ]
boolParenExpr = "(" boolCommonExpr ")"
parenExpr = "(" commonExpr ")"
这个语法如何匹配像(1等式2)
这样的简单表达式?从我所能看到的一切(
被parenExpr
内部的commonExpr
规则所消耗,也就是说,它们必须在commonExpr
之后关闭,以避免引起错误,并且boolParenExpr
永远不会被击中。我想我阅读这种语法的经验/直觉不足以得到它。ABNF中的一条评论说:“注意boolCommonExpr也是一个commonExpr”,也许这就是谜团的一部分
显然,一个开始的(
本身并不能告诉我它将在哪里结束:在当前的commonExpr
表达式之后,或者在boolCommonExpr
之后更远的地方。我的lexer前面有一个所有标记的列表(URL是非常短的输入)。我想用它来找出的类型(
我有。好主意
我宁愿在输入上有一些限制或者稍微修改一下,也不愿切换到一个通常更强大的解析器模型。对于这样一个简单的表达式转换,我还希望避免使用编译器工具
编辑1:rici回答后的扩展-语法重写正确吗? 事实上,我是从开始的。然后我想更好地适应OData标准给出的官方语法,使其更加“一致”。但根据rici的建议(以及“内部服务器错误”的评论),重写语法,我倾向于回到维基百科上提供的更易于理解的结构 适用于OData$过滤器的布尔表达式,可能如下所示:
boolSequence= boolExpr {("and"|"or") boolExpr} .
boolExpr = ["not"] expression ("eq"|"ne"|"lt"|"gt"|"lt"|"le") expression .
expression = term {("add"|"sum") term} .
term = factor {("mul"|"div"|"mod") factor} .
factor = IDENT | methodCall | LITERAL | "(" boolSequence")" .
methodCall = METHODNAME "(" [ expression {"," expression} ] ")" .
对于布尔表达式来说,上述内容一般有意义吗?它是否基本上等同于上面的原始结构,并且对于递归下降解析器来说是可消化的
@里希:谢谢你对类型检查的详细评论。新语法应该能解决你对算术表达式优先级的担忧
对于所有三个终端(上面语法中的大写),我的lexer都提供一个类型(字符串、数字、日期时间或布尔值)。非终端返回它们生成的类型。通过这一点,我在当前实现中很好地进行了动态类型检查,包括适当的错误消息。希望这也适用于新语法
编辑2:返回原始OData语法 “逻辑”和“算术”
(
之间的区别并不是一个微不足道的问题。为了解决这个问题,即使是N.沃思也使用了一种狡猾的变通方法来保持Pascal语法的简单。因此,在Pascal中有一对额外的()
在和和或表达式中是强制性的。既不直观也不符合OData:-(。我发现的“()困难”的最佳读物是在。其他语言似乎在语法中花费了大量时间来解决这个问题。由于我没有语法构造的经验,我停止了自己的语法构造
我最终实现了原始的OData语法。在运行解析器之前,我回顾了所有标记,以找出哪个(
属于逻辑/算术表达式。URL的潜在长度不是问题。就我个人而言,我只是修改语法,使它只有一种表达式类型,因此只有一种括号类型。我不相信它实际上是正确的;它在LL(1)(或递归下降)中肯定不可用这正是您提到的原因
具体来说,如果目标是boolCommonExpr
,则有两个产品可以匹配(
lookahead令牌:
boolCommonExpr = ( …
/ commonExpr [ eqExpr / neExpr / … ]
/ boolParenExpr
/ …
) …
commonExpr = ( …
/ parenExpr
/ …
) …
在大多数情况下,这是一种误导性的尝试,试图让语法检测到类型冲突(如果实际上是类型冲突)这是错误的,因为如果存在布尔变量,那么它注定会失败,而这种环境中显然存在布尔变量。由于没有关于变量类型的语法线索,解析器无法确定特定表达式是否格式正确,因此有一个很好的理由不尝试,特别是更好的解决方案是首先将表达式解析为某种形式的AST,然后再次传递AST以检查每个运算符是否具有正确类型的操作数(如果需要,还可能插入显式强制转换运算符)
除了任何其他优点外,在单独的过程中执行类型检查可以生成更好的错误消息。如果出现(某些)类型冲突语法错误,则可能会让用户对其表达式被拒绝的原因感到困惑;相反,如果您注意到一个比较操作正被用作乘法操作数,则可能会让用户感到困惑(如果您的语言语义不允许从True/False自动转换为1/0),则可以生成目标明确的错误消息(“例如,比较不能用作算术运算符的操作数”)
将不同运算符(而不是括号)放入不同语法变量的一个可能原因是为了表示语法优先级。这种考虑可能会鼓励您以明确的优先级重写语法。(如前所述,语法假定所有算术运算符具有相同的优先级,这可能会导致2+3*a
被解析为a