Python之父发文吐槽现有解析器,考虑将它替换掉

花下猫语: Guido van Rossum 是 Python 的创造者,虽然他如今放弃了“终身仁慈独裁者”的职位,但却成为了指导委员会的五位成员之一,其一举一动依然备受瞩目。近日,他开通了 Medium 帐号,并发表了第一篇文章,透露出要替换 Python 的核心部件(解析器)的想法。这篇文章分析了当前的 pgen 解析器的诸多缺陷,并介绍了 PEG 解析器的优势,使人振奋。这项改造工做仍在进行中,Guido 说他还会写更多相关的文章,咱们就拭目以待吧。算法

本文原创并首发于公众号【Python猫】,未经受权,请勿转载。express

原文地址: https://mp.weixin.qq.com/s/yq...并发


原题 | PEG Parsers函数

做者 | Guido van Rossum(Python之父)学习

译者 | 豌豆花下猫(“Python猫”公众号做者)测试

原文 | https://medium.com/@gvanrossum_83706/peg-parsers-7ed72462f97c优化

声明 | 翻译是出于交流学习的目的,欢迎转载,但请保留本文出处,请勿用于商业或非法用途。ui

几年前,有人问 Python 是否会转换用 PEG 解析器(或者是 PEG 语法,我不记得确切内容、谁说的、何时说的)。我稍微看过这个主题,但没有头绪,就放弃了。atom

最近,我学了不少关于 PEG(Parsing Expression Grammars)的知识,现在我认为它是个有趣的替代品,正好替换掉我在 30 年前刚开始创造 Python 时自制的(home-grown)语法分析生成器(parser generator)(那个语法分析生成器,被称为“pgen”,是我为 Python 写下的第一段代码)。spa

我如今感兴趣于 PEG,缘由是对 pgen 的局限性感到有些恼火了。

它使用了我本身写的 LL(1) 解析的变种——我不喜欢能够产生空字符串的语法规则,因此我禁用了它,进而稍微地简化了生成解析表的算法。

同时,我还发明了一套相似 EBNF 的语法符号(译注:Extended Backus-Naur Form,BNF 的扩展,是一种形式化符号,用于描述给定语言中的语法),至今仍很是喜欢。

如下是 pgen 令我感到烦恼的一些问题。

LL(1) 名字中的 “1” 代表它只使用单一的前向标记符(a single token lookahead),而这限制了咱们编写漂亮的语法规则的能力。例如,一个 Python 语句(statement)既能够是表达式(expression),又能够是赋值(assignment)(或者是其它东西,但那些都以 if 或 def 这类专用的关键字开头)。

咱们但愿使用 pgen 表示法来编写以下的语法。(请注意,这个示例描述了一种玩具语言(toy language),它是 Python 的一个微小的子集,就像传统中的语言设计同样。)

statement: assignment | expr | if_statement
expr: expr '+' term | expr '-' term | term
term: term '*' atom | term '/' atom | atom
atom: NAME | NUMBER | '(' expr ')'
assignment: target '=' expr
target: NAME
if_statement: 'if' expr ':' statement

关于这些符号,解释几句:NAMENUMBER 是标记符(token),预约义在语法以外。引号中的字符串如 '+' 或 'if' 也是标记符。(我之后会讲讲标记符。)语法规则以其名称开头,跟在后面的是 : 号,再后面则是一个或多个以 | 符号分隔的可选内容(alternatives)。

但问题是,若是你这样写语法,解析器不会起做用,pgen 将会罢工。

其中一个缘由是某些规则(如 exprterm)是左递归的,而 pgen 还不足以聪明地解析。这一般须要经过重写规则来解决,例如(在保持其它规则不变的状况下):

expr: term ('+' term | '-' term)*
term: atom ('*' atom | '/' atom)*

这就揭示了 pgen 的一部分 EBNF 能力:你能够在括号内嵌套可选内容,而且能够在括号后放 * 来建立重复,因此这里的 expr 规则就意味着:它是一个术语(term),跟着零个或多个语句块,语句块内是加号跟术语,或者是减号跟术语。

这个语法兼容了第一个版本的语言,但它并无反映出语言设计者的本意——尤为是它并无代表运算符是左绑定的,而这在你尝试生成代码时很是重要。

可是在这种玩具语言(以及在 Python)中,还有另外一个烦人的问题。

因为前向的单一标记符,解析器没法肯定它查看的是一个表达式的开头,仍是一个赋值。在一个语句的开头,解析器须要根据它看到的第一个标记符,来决定它要查看的 statement 的可选内容。(为何呢?pgen 的自动解析器就是这样工做的。)

假设咱们的程序是这样的:

answer = 42

这句程序会被解析成三个标记符:NAME (值是 answer),‘=’ 和 NUMBER (值为 42)。在程序开始时,咱们拥有的惟一的前向标记符是 NAME 。此时,咱们试图知足的规则是 statement (这个语法的起始标志)。此规则有三个可选内容:exprassignment 以及 if_statement 。咱们能够排除if_statement ,由于前向标记符不是 “if”。

可是 exprassignment 都能以 NAME 标记符开头,所以就会引发歧义(ambiguous),pgen 会拒绝咱们的语法。

(这也不彻底正确,由于语法在技术上并不会致使歧义;但咱们先无论它,由于我想不到更好的词来表达。那么 pgen 是如何作决定的呢?它会为每条语法规则计算出一个叫作 FIRST 组的东西,若是在给定的点上,FIRST 组出现了重叠选项,它就会抱怨)(译注:抱怨?应该指的是解析不下去,前文译做了罢工)。

那么,咱们可否为解析器提供一个更大的前向缓冲区,来解决这个烦恼呢?

对于咱们的玩具语言,第二个前向标记符就足够了,由于在这个语法中,assignment 的第二个标记符必须是 “=”。

可是在 Python 这种更现实的语言中,你可能须要一个无限的前向缓冲,由于在 “=” 标记符左侧的东西可能极其复杂,例如:

table[index + 1].name.first = 'Steven'

在 “=” 标记符以前,它已经用了 10 个标记符,若是想挑战的话,我还能够举出任意长的例子。为了在 pgen 中解决它,咱们的方法是修改语法,并增长一个额外的检查,令它能接收一些非法的程序,但若是检查到对左侧的赋值是无效的,则会抛出一个 SyntaxError

对于咱们的玩具语言,这可归结成以下写法:

statement: assignment_or_expr | if_statement
assignment_or_expr: expr ['=' expr]

(方括号表示了一个可选部分。)而后在随后的编译过程当中(好比,在生成字节码时),咱们会检查是否存在 “=”,若是存在,咱们再检查左侧是否有 target 语法。

在调用函数时,关键字参数也有相似的麻烦。咱们想要写成这样(一样,这是 Python 的调用语法的简化版本):

call: atom '(' arguments ')'
arguments: arg (',' arg)*
arg: posarg | kwarg
posarg: expr
kwarg: NAME '=' expr

可是前向的单一标记符没法告诉解析器,一个参数的开头中的 NAME 究竟是 posarg 的开头(由于 expr 可能以 NAME 开头)仍是 kwarg 的开头。

一样地,Python 当前的解析器在解决这个问题时,是经过特别声明:

arg: expr ['=' expr]

而后在后续的编译过程当中再解决问题。(咱们甚至出了点小错,容许了像 foo((a)=1) 这样的东西,给了它跟 foo(a=1) 相同的含义,直到 Python 3.8 时才修复掉。)

那么,PEG 解析器是如何解决这些烦恼的呢?

经过使用无限的前向缓冲!PEG 解析器的经典实现中使用了一个叫做“packrat parsing”(译注:PackRat,口袋老鼠)的东西,它不只会在解析以前将整个程序加载到内存中,并且还能容许解析器任意地回溯。

虽然 PEG 这个术语主要指的是语法符号,可是以 PEG 语法生成的解析器是能够无限回溯的递归降低(recursive-descent)解析器,“packrat parsing”经过记忆每一个位置所匹配的规则,来使之生效。

这使一切变得简单,然而固然也有成本:内存。

三十年前,我有充分的理由来使用单一前向标记符的解析技术:内存很昂贵。LL(1) 解析(以及其它技术像 LALR(1),因 YACC 而著名)使用状态机和堆栈(一种“下推自动机”)来有效地构造解析树。

幸运的是,运行 CPython 的计算机比 30 年前有了更多的内存,将整个文件存在内存中确实已再也不是一个负担。例如,我能在标准库中找到的最大的非测试文件是 _pydecimal.py ,它大约有 223 千字节(译注:kilobytes,即 KB)。在一个 GB 级的世界里,这基本不算什么。

这就是令我再次研究解析技术的缘由。

可是,当前 CPython 中的解析器还有另外一个 bug 个人东西。

编译器都是复杂的,CPython 也不例外:虽然 pgen-驱动的解析器输出的是一个解析树,可是这个解析树并不直接用做代码生成器的输入:它首先会被转换成抽象语法树(AST),而后再被编译成字节码。(还有更多细节,但在这我不关注。)

为何不直接从解析树编译呢?这其实正是它最先的工做方式,可是大约在 15 年前,咱们发现编译器由于解析树的结构而变得复杂了,因此咱们引入了一个单独的 AST,还引入了一个将解析树翻译成 AST 的环节。随着 Python 的发展,AST 比解析树更稳定,这减小了编译器出错的可能。

AST 对于那些想要检查(inspect)Python 代码的第三方代码,也更加容易,它还经过被大众欢迎的 ast 模块而公开。这个模块还容许你从头构建 AST 节点,或是修改现有的 AST 节点,而后你能够将新的节点编译成字节码。

后一项能力支撑起了一整个为 Python 语言添加扩展的家庭手工业(译注:ast 模块为 Python 的三方扩展提供了便利)。(借助 parser 模块,解析树一样能面向 Python 的用户开放,但它使用起来太麻烦了,所以相比于 ast 模块,它就过期了。)

综上所述,我如今的想法是看看可否为 CPython 创造一个新的解析器,在解析时,使用 PEG 与 packrat parsing 来直接构建 AST,从而跳过中间解析树结构,并尽量地节省内存,尽管它会使用无限的前向缓冲。

我还没进展到这个地步,但已经有了一个原型,能够将一个 Python 的子集编译成一个 AST,其速度与当前 CPython 的解析器大体至关。只不过,它占用的内存更多,因此我预计在将它扩展到整个语言时,将会下降 PEG 解析器的速度。

可是,我还没去优化它,因此仍是挺有但愿的。

转换成 PEG 的最后一个好处是它为语言的将来演化提供了更大的灵活性。

过去有人曾说,pgen 的 LL(1) 缺陷帮助了 Python 保持语法的简单。这颇有道理,但咱们还有不少适当的流程,能够防止语言不受控制地膨胀(主要是 PEP 流程,在很是严格的向后兼容性要求以及新的治理结构的帮助下)。因此我并不担忧。

我还有不少内容要写,关于 PEG 解析以及个人具体实现,可是要等我整理好代码后,在后续的文章中再去写了。

公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写做、优质英文推荐与翻译等等,欢迎关注哦。

相关文章
相关标签/搜索