【问题标题】:Parser/Grammar: 2x parenthesis in nested rules解析器/语法:嵌套规则中的 2x 括号
【发布时间】:2020-09-18 13:08:22
【问题描述】:

尽管我对编译/解析的知识有限,但我还是敢于​​为 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) 这样的简单表达式?据我所知,所有( 都被commonExpr 内的parenExpr 规则所消耗,即它们还必须在commonExpr 之后关闭,以免导致错误并且boolParenExpr 永远不会受到影响。我想我阅读这种语法的经验/直觉不足以理解它。 ABNF 中的一条评论说:“注意 boolCommonExpr 也是一个 commonExpr”。也许这就是谜团的一部分?

显然,单独的 ( 开头不会告诉我它将在哪里关闭:在当前 commonExpr 表达式之后或在 boolCommonExpr 之后更远的地方。我的词法分析器前面有一个 all 标记列表(URL 是非常短的输入)。我想用它来找出我有什么类型的(。好主意?

我宁愿限制输入或进行一些小技巧,也不愿切换到通常更强大的解析器模型。对于像这样的简单表达式翻译,我还想避免使用编译器工具。


编辑 1:rici 回答后的扩展 - 语法重写是否正确?

实际上我是从example for recursive-descent parsers given on Wikipedia 开始的。然后我想更好地适应 OData 标准给出的官方语法更“符合”。但是根据 rici 的建议(以及“内部服务器错误”的评论)重写语法,我倾向于回到维基百科上提供的更易于理解的结构。

适应 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 :-(。关于“() 难度”的最佳读物是在 Let's Build a Compiler (Part VI) 中。其他语言似乎在语法上花了很长时间来解决问题。因为我没有经验语法结构我不再自己做。

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

【问题讨论】:

  • 您可能需要重新编写语法,以便实质上将"(" this ... ")" | "(" that ... ")" | other 重新编写为"(" (this ... | that ... ) ")" | other
  • 感谢“HTTP 500”。根据 rici 和您的建议,我将我的问题扩展到了这个方向。

标签: parsing grammar parentheses recursive-descent ambiguous-grammar


【解决方案1】:

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

具体来说,如果目标是boolCommonExpr,则有两个产生式可以匹配( 前瞻令牌:

boolCommonExpr = ( … 
                 / commonExpr [ eqExpr / neExpr / … ]
                 / boolParenExpr
                 / …
                 ) …
commonExpr     = ( …
                 / parenExpr
                 / …
                 ) …

在大多数情况下,这是一种让语法检测类型违规的错误尝试。 (如果实际上它是一种类型违规。)这是被误导的,因为如果有布尔变量,它就注定会失败,而布尔变量显然存在于这个环境中。由于没有关于变量类型的句法线索,解析器无法确定特定表达式是否格式正确,因此有一个很好的理由可以完全不尝试,特别是如果它会导致解析头痛。更好的解决方案是首先将表达式解析为某种形式的 AST,然后再通过 AST 以检查每个运算符是否具有正确类型的操作数(如果需要,可能插入显式转换运算符)。

除了任何其他优势之外,在单独的通道中进行类型检查可以让您产生更好的错误消息。如果您犯了(某些)类型违规语法错误,那么您可能会让用户对他们的表达式被拒绝的原因感到困惑;相反,如果您注意到比较操作被用作相乘的操作数(并且如果您的语言的语义不允许从 True/False 到 1/0 的自动转换),那么您可能会产生一个目标明确的错误消息(例如,“比较不能用作算术运算符的操作数”)。

将不同的运算符(但不是括号)放入不同的语法变量的一个可能原因是表达语法优先级。这种考虑可能会鼓励您以明确的优先级重写语法。 (如所写,语法假定所有算术运算符具有相同的优先级,这可能会导致 2 + 3 * a 被解析为 (2 + 3) * a,这可能是一个巨大的惊喜。)或者,您可以使用一些简单的优先级感知子解析器表达式。

【讨论】:

  • 感谢您对为什么有人甚至会写出这样的模棱两可的语法的启发。如果您为像 OData 这样的广泛标准发布综合语法,我会假设它是供编译器工具阅读的。但是,这些将如何应对这样的模棱两可?我已按照您的建议重写语法。结果无法在评论中格式化并且太大。你能看看我上面问题的扩展吗?谢谢。
【解决方案2】:

如果您想测试 ABNF 语法的确定性(即 LL(1)),您可以使用 Tunnel Grammar Studio (TGS)。我已经测试了完整的语法,并且有很多冲突,不仅仅是这个范围。如果您能够提取相关规则,您可以使用桌面版本的 TGS 将冲突可视化(在线版本检查器仅带有文本结果)。如果规则不是太多,该演示可以帮助您根据规则创建 LL(1) 语法。

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

TGS 是我从事的一个项目。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-12
    相关资源
    最近更新 更多