根据语法编写词法分析器时应遵循的步骤是什么?


13

在阅读有关语法,词法分析器和语法分析器的澄清问题的答案时,答案指出:

BNF语法包含词法分析和解析所需的所有规则。

这对我来说有点奇怪,因为直到现在,我一直认为词法分析器根本不基于语法,而解析器则很大程度上基于语法。在阅读了许多有关编写词法分析器的博客文章之后,我得出了这个结论,而从来没有人使用1 EBNF / BNF作为设计基础。

如果词法分析器以及解析器都基于EBNF / BNF语法,那么如何使用该方法创建词法分析器呢?也就是说,如何使用给定的EBNF / BNF语法构造词法分析器?

我见过很多,很多的职位,应对写的用EBNF / BNF作为指导或蓝图解析器,但我已经遇到没有到目前为止,显示与词法分析器的设计相当。

例如,采用以下语法:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

一个人如何创建基于语法的词法分析器?我可以想象如何从这样的语法编写解析器,但是我无法掌握使用词法分析器进行解析的概念。

是否有某些规则或逻辑用于完成诸如编写解析器之类的任务?坦率地说,我开始怀疑词法分析器设计是否完全使用EBNF / BNF语法,更不用说基于一个了。


1 扩展Backus–Naur形式Backus–Naur形式

Answers:


18

词法分析器只是简单的解析器,用作主解析器的性能优化。如果我们有一个词法分析器,则词法分析器和解析器一起工作以描述完整的语言。没有单独的词法分析阶段的解析器有时称为“无扫描器”。

如果没有词法分析器,则解析器将不得不在逐个字符的基础上进行操作。由于解析器必须存储有关每个输入项的元数据,并且可能必须针对每个输入项状态预先计算表,因此对于大的输入大小,这将导致不可接受的内存消耗。特别是,在抽象语法树中,我们不需要每个字符都有单独的节点。

由于逐个字符的文本非常含糊,因此这也将导致更多的含糊不清的内容。想象一个规则R → identifier | "for " identifier。其中标识符由ASCII字母组成。如果我想避免歧义,现在我需要提前4个字符来确定应该选择哪个替代方案。使用词法分析器,解析器只需要检查它是否具有IDENTIFIER或FOR令牌-1令牌超前。

两级语法。

词法分析器通过将输入字母转换为更方便的字母来工作。

无扫描器的语法分析器描述一种语法(N,Σ,P,S),其中非终结符N是语法规则的左侧,字母Σ是例如ASCII字符,乘积P是语法中的规则,开始符号S是解析器的顶级规则。

现在,词法分析器定义了记号a,b,c,…的字母。这允许主解析器将这些标记用作字母:Σ= {a,b,c,…}。对于词法分析器,这些标记是非终结符,并且起始规则S L为S L →ε|。S | b S | c S | …,即:令牌的任何序列。词法分析器语法中的规则是产生这些标记所必需的所有规则。

性能优势来自将词法分析器的规则表达为常规语言。与上下文无关的语言相比,它们的解析效率更高。特别是,可以在O(n)空间和O(n)时间中识别常规语言。实际上,代码生成器可以将这种词法分析器转换为高效的跳转表。

从语法中提取标记。

举例说明:digitstring规则在每个字符级别上表达。我们可以将它们用作令牌。其余语法保持不变。这是词法分析器语法,被编写为右线性语法,以明确其规律性:

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

但是由于它是常规的,所以我们通常会使用正则表达式来表达令牌语法。以下是使用.NET字符类排除语法和POSIX charclasss编写的上述令牌定义(正则表达式):

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

然后,主解析器的语法包含词法分析器未处理的其余规则。就您而言,这就是:

input = digit | string ;

当无法轻松使用词法分析器时。

在设计语言时,我们通常会注意将语法清晰地分为词法分析器级别和解析器级别,并且词法分析器级别描述常规语言。这并不总是可能的。

  • 嵌入语言时。某些语言允许您将代码插入到字符串中:"name={expression}"。表达式语法是上下文无关语法的一部分,因此不能通过正则表达式进行标记。为了解决这个问题,我们要么将解析器与词法分析器重新组合,要么引入诸如的其他标记STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END。字符串的语法规则可能类似于:String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END。当然,表达式可能包含其他字符串,这将导致我们遇到下一个问题。

  • 令牌可以互相包含的时间。在类似C的语言中,关键字与标识符是无法区分的。这在词法分析器中通过优先于关键字而不是标识符来解决。这样的策略并非总是可行的。想象一下一个配置文件Line → IDENTIFIER " = " REST,其中,其余部分是直到行尾的任何字符,即使其余部分看起来像标识符。示例行为a = b c。这个词法分析器真的很笨,不知道令牌可能以什么顺序出现。因此,如果我们将IDENTIFIER优先于REST,则词法分析器会给我们IDENT(a), " = ", IDENT(b), REST( c)。如果我们将REST优先于IDENTIFIER,则词法分析器只会给我们REST(a = b c)

    为了解决这个问题,我们必须将词法分析器与解析器重新组合。通过使词法分析器变得懒惰,可以在某种程度上保持这种分离:每次解析器需要下一个标记时,它都会向词法分析器请求下一个标记,并告知词法分析器可接受的标记集。实际上,我们正在为每个位置的词法语法创建新的顶级规则。在这里,这将导致调用nextToken(IDENT), nextToken(" = "), nextToken(REST),并且一切正常。这要求解析器知道每个位置的完整可接受令牌的集合,这意味着像LR这样的自底向上解析器。

  • 当词法分析器必须保持状态时。例如,Python语言不是用花括号来定界代码块,而是用缩进来定界。有多种方法可以处理语法中对布局敏感的语法,但是这些技术对于Python来说是过大的。相反,词法分析器检查每行的缩进,如果找到新的缩进块,则发出INDENT令牌,如果该块已结束,则发出DEDENT令牌。这简化了主要语法,因为它现在可以假装那些标记像花括号一样。但是,词法分析器现在需要维护状态:当前缩进。这意味着词法分析器在技术上不再描述常规语言,而是实际上是上下文相关的语言。幸运的是,这种差异在实践中并不重要,Python的词法分析器仍可以在O(n)时间内工作。


非常好的答案@amon,谢谢。我将需要一些时间来完全消化它。但是我当时想知道有关您的答案的一些事情。在第八段周围,您将展示如何将示例EBNF语法修改为解析器规则。您显示的语法也会被解析器使用吗?还是解析器还有单独的语法?
基督教教务长

@Engineer我做了一些编辑。解析器可以直接使用您的EBNF。但是,我的示例显示了语法的哪些部分可以由单独的词法分析器处理。其他任何规则仍将由主解析器处理,但在您的示例中,这仅仅是input = digit | string
阿蒙2016年

4
无扫描仪解析器的最大优点是它们更易于编写;的,极端的例子是解析器组合库,在那里你什么也不做,但撰写的解析器。对于诸如ECMAScript嵌入HTML嵌入PHP以及SQL嵌入模板语言在顶部或Ruby实例嵌入Markdown的案例,撰写解析器很有趣嵌入Ruby文档说明或类似的内容。
约尔格W¯¯米塔格

最后一点是非常重要的,但我认为您的写作方式具有误导性。的确,基于缩进的语法无法轻松使用词法分析器,但是在这种情况下,无扫描器的解析甚至更加困难。因此,如果您拥有这种语言,那么您实际上使用词法分析器,并用相关状态进行扩充。
user541686 '19

@Mehrdad Python样式的词法分析器驱动的缩进/缩进标记只能用于非常简单的对缩进敏感的语言,并且通常不适用。属性语法是一种更通用的替代方法,但是标准工具中缺少对它们的支持。想法是,我们用缩进注释每个AST片段,并对所有规则添加约束。通过组合器解析可以轻松添加属性,这也使得轻松进行无扫描器解析成为可能。
阿蒙
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.