解析器/语法:嵌套规则中的2括号

问题描述

尽管我对编译/解析的了解有限,但我还是敢为OData $ filter表达式构建一个小型递归下降解析器。解析器仅需要检查表达式的正确性并在sql输出相应的条件。由于输入和输出具有几乎相同的标记和结构,因此这非常简单,我的实现完成了我想要的90%。

但是现在我陷入了括号,括号出现在逻辑和算术表达式的单独规则中。 ABNF中完整的OData语法为here,其中涉及的规则的精简版本是:

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 eq 2)之类的简单表达式匹配?从我的观察中,所有(都被parenExpr内的规则commonExpr占用,即它们还必须在commonExpr之后关闭,以免引起错误,并且boolParenExpr永远不会受到打击。我想我在阅读这种语法方面的经验/直觉不足以理解它。 ABNF中的注释说:“请注意boolCommonExpr也是commonExpr”。也许这就是谜团的一部分?

很显然,仅(开头不会告诉我它将要关闭的位置:在当前commonExpr表达式之后或在boolCommonExpr之后。我的词法分析器前面有一个所有标记的列表(URL是很短的输入)。我当时想用它来找出我拥有的(类型。好主意吗?

与切换到通常更强大的解析器模型相比,我宁愿在输入方面有所限制或稍作改动。对于像这样的简单表达式翻译,我也想避免使用编译器工具。


编辑1:由rici回答后的扩展名-语法重写正确吗?

实际上,我是从example for recursive descend parsers given on Wikipedia开始的。然后,我将更好地适应OData标准给出的官方语法,使其更加“符合要求”。但是,根据rici的建议(以及“内部服务器错误”的注释)来重写语法,我倾向于回到Wikipedia上提供的更易理解的结构。 适应OData $ filter的布尔表达式,它可能看起来像这样:

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} ] ")" .

以上对于布尔表达式是否大体上有意义,它是否基本上等同于上面的原始结构并且对于递归下降解析器而言是可消化的?

@rici:感谢您对类型检查的详细说明。新语法应该可以解决您对算术表达式中的优先级的担心。

对于所有三个终端(上面语法中的大写),我的词法分析器提供一种类型(字符串,数字,日期时间或布尔值)。非终端返回它们产生的类型。这样,我就很好地管理了当前实现中的类型检查,包括不错的错误消息。希望这也适用于新语法。


编辑2:返回原始OData语法

“逻辑”和“算术”之间的区别(这并非微不足道。为了解决这个问题,N.Wirth甚至使用了一种狡猾的解决方法来使Pascal的语法保持简单。结果,在Pascal中,额外的一对()在andor表达式周围是强制性的。既不直观也不符合OData::( ..我发现的关于“()难度”的最佳读物是在{{ 3}}。其他语言在语法上似乎可以解决问题,因为我没有语法构建的经验,所以我不再自己做。

我最终实现了原始的OData语法。在运行解析器之前,我向后遍历所有标记以找出哪个(属于逻辑/算术表达式。URL的潜在长度不是问题。

解决方法

就个人而言,我只是修改语法,使其仅具有一种类型的表达式,因此也只有一种类型的括号。我不相信OData grammar实际上是正确的;正是由于您提到的原因,它肯定无法在LL(1)(或递归下降)解析器中使用。

具体来说,如果目标是int J420ToARGB(const uint8_t* src_y,int src_stride_y,const uint8_t* src_u,int src_stride_u,const uint8_t* src_v,int src_stride_v,uint8_t* dst_argb,int dst_stride_argb,int width,int height); ,则有两个产品可以与boolCommonExpr前瞻令牌匹配:

(

在大多数情况下,这是使语法检测到类型冲突的错误尝试。 (如果实际上是类型冲突。)它被误导了,因为如果存在布尔变量(显然是在这种环境中),那么注定会失败。由于没有关于变量类型的语法线索,因此解析器无法确定特定表达式的格式是否正确,因此有一个很好的理由,那就是根本不要尝试,尤其是如果它引起解析麻烦。更好的解决方案是先将表达式解析为某种形式的AST,然后再次遍历AST,以检查每个运算符是否具有正确类型的操作数(并在必要时插入显式强制转换运算符)。>

除其他优点外,在单独的传递中进行类型检查可以使您产生更好的错误消息。如果您犯了(某些)类型冲突语法错误,则可能使用户感到困惑,为什么他们的表达式被拒绝;相反,如果您注意到比较操作被用作要进行乘法运算的操作数(并且如果您的语言的语义不允许从True / False到1/0的自动转换),则可能会产生针对性强的错误消息(例如,“比较不能用作算术运算符的操作数”。)

将不同的运算符(而不是括号)放入不同的语法变量的一个可能原因是表达语法优先级。这种考虑可能会鼓励您以显式优先顺序重写语法。 (如所写,语法假定所有算术运算符都具有相同的优先级,这大概会导致boolCommonExpr = ( … / commonExpr [ eqExpr / neExpr / … ] / boolParenExpr / … ) … commonExpr = ( … / parenExpr / … ) … 被解析为2 + 3 * a,这可能是一个很大的惊喜。)或者,您可以使用一些简单的方法优先级优先的表达式子解析器。

,

如果您要测试ABNF语法的确定性(即LL(1)),则可以使用隧道语法工作室(TGS)。我已经测试了完整的语法,不仅有这个范围,还有很多冲突。如果您能够提取相关规则,则可以使用桌面版TGS来可视化冲突(在线版本检查器仅具有文本结果)。如果规则太多,演示程序可能会帮助您根据规则创建LL(1)语法。

如果您提取了所有需要的规则,并将它们添加到您的问题中,我可以为您运行它,并告诉您它是LL(1)。请注意,语法不完全采用ABNF元语法,因为区分大小写的字符串的大小写区分用'键入。根据定义,ABNF(RFC 5234)不区分大小写,因为RFC 7405在实际字符串之前用%s%i(敏感和不敏感)前缀定义了敏感度。默认情况(不带前缀)仍然表示不敏感。这意味着在TGS中进行测试之前,您必须用'...'替换此无效的%s"..."字符串。

TGS是我正在从事的项目。