如何为解析器指定语法?


12

我已经进行了很多年的编程,但是仍然需要花费我很长时间的一项任务是为解析器指定语法,即使付出了如此多的努力,我也永远无法确定我想出的语法是否很好(以任何合理的方式衡量“良好”)。

我不希望有一种算法可以自动执行指定语法的过程,但是我希望有一些结构化问题的方法可以消除当前方法的大部分猜测和反复试验。

我的第一个念头是阅读解析器,而我已经做了一些,但是我在该主题上阅读的所有内容都将语法视为既定的(或者琐碎到可以通过检查指定的语法),并专注于将这个语法翻译成解析器的问题。我对之前的问题很感兴趣:首先如何指定语法。

我主要对指定一个正式代表具体示例(正例和负例)的语法的问题感兴趣。这与设计新语法的问题不同。感谢Macneil指出了这一区别。

我从来没有真正欣赏过语法和语法之间的区别,但是现在我开始明白它了,我可以通过说我主要对指定语法来强制执行一个语法的问题来加强我的第一说明。预定义的语法:就我而言,这种语法的基础通常是肯定和否定示例的集合。

如何为解析器指定语法?是否有一本书或参考资料是事实上的标准,用于描述最佳实践,设计方法以及有关为解析器指定语法的其他有用信息?在阅读解析器语法时,我应该重点注意什么?


1
我对您的问题进行了一些编辑,以关注您的实际问题。该站点正是您可以提出有关语法和解析器的问题并获得专家解答的地方。如果有值得关注的外部资源,它们会自然而然地出现在直接帮助您的答案中。
亚当李尔

8
@kjo不幸的是。如果您要提供的只是参考文献列表,则说明您无法充分利用Stack Exchange。您的元问题未按预期使用该网站。在Stack Exchange上几乎不建议使用列表问题,因为它们不太适合问答模型。我强烈建议您将思维方式转向提出具有答案的问题,而不是具有项目,想法或观点的问题
亚当李尔

3
@kjo当然是一个问题,但不是在Stack Exchange上问的正确问题。SE不是在这里建立参考列表。这里参考。请仔细阅读我在评论中链接到的元信息,以获取更详细的解释。
亚当李尔

5
@kjo:请不要气disc!Anna的编辑保留了您问题的核心和核心,她通过使您的问题更像我们对Programmers.SE的期望形式来帮助您解决了问题。我知道您找不到确切的参考文献,但能够提供答案。[OTOH,如果我知道这样的参考文献,我肯定会包括在内。]我们想鼓励像我这样的更多答案,因为在这种特定情况下,我不相信您所寻求的参考文献,只是与他人交谈的经验。
Macneil

4
@kjo我已经回滚到Anna的编辑,并尝试根据我们对书本推荐的指导,纳入一个特定的规范参考要求:提供的答案中有很多很好的信息,并且通过使本书的范围无效而使它们无效。仅寻找一本书的问题将是浪费。现在,如果我们都能停止编辑大战,那就太好了。

Answers:


12

从样本文件中,您将需要根据要从这些例子中得出的结论来做出决策。假设您有以下三个示例:(每个都是一个单独的文件)

f() {}
f(a,b) {b+a}
int x = 5;

您可以简单地指定两个将接受这些样本的语法:

平凡的语法一:

start ::= f() {} | f(a,b) {b+a} | int x = 5;

平凡的语法二:

start ::= tokens
tokens ::= token tokens | <empty>
token ::= identifier | literal | { | } | ( | ) | , | + | = | ;

第一个很简单,因为它接受三个样本。第二个是微不足道的,因为它接受所有可能使用这些令牌类型的东西。[在此讨论中,我假设您不太关心令牌生成器的设计:假设标识符,数字和标点符号作为令牌很简单,您可以从任何脚本语言中借用任何令牌集。无论如何。]

因此,您需要遵循的过程是从高级别开始,然后确定“我要允许每个实例有多少个?” 如果语法构造可以重复任意多次(例如method在类中使用s),那么您将希望使用以下形式的规则:

methods ::= method methods | empty

EBNF中最好将其表示为:

methods ::= {method}

当您只需要零个或一个实例(这意味着该构造是可选的,如extendsJava类的子句),或者您想要允许一个或多个实例(如声明中的变量初始值设定项)时,这可能会很明显。)。您需要注意一些问题,例如要求在元素之间使用分隔符(如,在参数列表中),在每个元素之后要求使用终止符(如使用;to split语句)或不需要使用分隔符或终止符(视情况而定)与类中的方法)。

如果您的语言使用算术表达式,则很容易从现有语言的优先规则中进行复制。最好坚持使用众所周知的东西,例如C的表达式规则,而不是追求异国情调,但前提是所有其他条件都相等。

除了优先级问题(相互解析的内容)和重复性问题(每个元素应该出现多少个,如何将它们分开?)之外,您还需要考虑顺序:必须总是在某些事物之前出现某些事物吗?如果包括一件事,是否应该排除另一件事?

此时,您可能会倾向于语法上强制执行某些规则,例如,如果Person指定了的年龄,则您也不想再指定其生日。尽管您可以构造语法,但是在解析完所有内容之后,您可能会发现通过“语义检查”通过可以更轻松地实施此语法。我认为这样可以使语法更简单,并且在违反规则时可以提供更好的错误消息。


1
+1以获得更好的错误消息。您的语言的大多数用户都不会是专家,无论他们是10还是1000万。解析理论忽略了这个方面太久了。
MSalters 2011年

10

在哪里可以学习如何为解析器指定语法?

对于大多数解析器生成器,它通常是一些变种巴科斯-诺尔<nonterminal> ::= expression格式。我将假设您正在使用类似的东西,而不是尝试手动构建解析器。如果您可以为语法提供格式的解析器(已经在下面提供了示例问题),那么指定语法不是您的问题。

我认为您要面对的是从一组样本中区分语法,这实际上是比模式识别更多的模式识别。如果您必须求助于此,则意味着提供数据的人都无法提供其语法,因为他们的数据格式不好。如果您可以选择后退并告诉他们提供正式定义,请执行此操作。如果您可能根据推定的语法接受错误的输入或拒绝良好的输入来对解析器的后果负责,那么他们给您一个模糊的问题是不公平的。

...我永远不确定我提出的语法是好的(通过任何合理的“好的”度量)。

在您所处的情况下,“良好”必须表示“分析积极因素,拒绝消极因素”。如果没有输入文件语法的任何其他正式规范,则样本是您唯一的测试用例,您将无法做得更好。您可以放下脚步,说只有积极的榜样才是好的,而拒绝其他任何事情,但这可能不符合您要实现的目标。

在较差的情况下,测试语法就像测试其他任何东西:您必须想出足够的测试用例来练习非终结符的所有变体(如果由词法分析器生成,则还包括终结符)。


样本问题

编写语法以解析包含以下规则定义的列表的文本文件:

  • 一个清单由零个或多个事物
  • 一个事情由一个的标识符,一个开放的梅开二度,一个项目清单和一个右括号。
  • _item_list_包含零个或多个项目
  • 一个项目 constsis一个的识别符,一个等号,另一个标识符和分号。
  • 一个标识符是一个或多个字符AZ,AZ,0-9或下划线的序列。
  • 空格被忽略。

输入示例(全部有效):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }

2
并确保使用“ Backus-Naur的某些变体”,而不是BNF本身。BNF可以表达一种语法,但是它使许多非常常见的概念(例如列表)变得复杂得多。有许多改进的版本,例如EBNF,可以改进这些问题。
梅森惠勒

7

Macneil和Blrfl的答案很棒。我只想添加一些有关流程开始的评论。

一个语法仅仅是代表一种方法程序。因此,您的语言语法应由您对以下问题的答案确定:什么是程序?

您可能会说程序是类的集合。好,那给了我们

program ::= class*

作为起点。否则您可能不得不写

program ::= ε
         |  class program

现在,什么是课程?它有个名字;可选的超类规范;以及一堆构造函数,方法和字段声明。此外,您还需要某种将类分组为单个(明确)单元的方法,这应该涉及可用性的一些让步(例如,使用保留字标记它class)。好的:

class ::= "class" identifier extends-clause? "{" class-member-decl * "}"

您可以选择一种表示法(“具体语法”)。或者,您也可以轻松决定:

class ::= "(" "class" identifier extends-clause "(" class-member-decl* ")" ")"

要么

class ::= "class" identifier "=" "CLASS" extends-clause? class-member-decl* "END"

您可能已经隐式做出了此决定,尤其是在有示例的情况下,但我只想强调一下这一点:语法的结构由它表示的程序的结构确定。这就是让您超越Macneil答案中的“琐碎语法”的原因。但是,示例程序仍然非常重要。它们有两个目的。首先,它们可以帮助您从抽象的角度弄清楚什么是程序。其次,它们可以帮助您确定应使用哪种具体语法来表示语言的结构。

一旦确定了结构,就应该回过头来处理诸如允许空格和注释,修正歧义之类的问题。这些都很重要,但它们在总体设计中是次要的,并且高度依赖于解析您正在使用的技术。

最后,请勿尝试在语法中表示有关您的语言的所有内容。例如,您可能希望禁止某些类型的无法访问的代码(例如,return在Java中,在后面的语句)。您可能不应该尝试将其塞入语法中,因为您会错过一些东西((,return大括号中的内容,或者如果if语句的两个分支都返回该怎么办?),或者您会使语法过于复杂管理。这是上下文相关的约束;将其写为单独的通行证。上下文相关约束的另一个非常常见的示例是类型系统。1 + "a"如果您足够努力,则可以拒绝语法中的表达式,但不能拒绝1 + xx字符串类型为)。所以避免语法中的半熟限制,并作为一个单独的通道正确进行处理。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.