0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

pgen解析器的诸多缺陷,并介绍了PEG解析器的优点

WpOh_rgznai100 来源:lq 2019-08-02 09:19 次阅读

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

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

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

我现在感兴趣于 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_statementexpr:expr'+'term|expr'-'term|termterm:term'*'atom|term'/'atom|atomatom:NAME|NUMBER|'('expr')'assignment:target'='exprtarget:NAMEif_statement:'if'expr':'statement

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

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

其中一个原因是某些规则(如expr和term)是左递归的,而 pgen 还不足以聪明地解析。这通常需要通过重写规则来解决,例如(在保持其它规则不变的情况下):

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

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

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

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

由于前向的单一标记符,解析器无法确定它查看的是一个表达式的开头,还是一个赋值。在一个语句的开头,解析器需要根据它看到的第一个标记符,来决定它要查看的statement的可选内容。(为什么呢?pgen 的自动解析器就是这样工作的。)

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

answer=42

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

但是expr与assignment都能以NAME标记符开头,因此就会引起歧义(ambiguous),pgen 会拒绝我们的语法。

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

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

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

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

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

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

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

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

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

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

call:atom'('arguments')'arguments:arg(','arg)*arg:posarg|kwargposarg:exprkwarg: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
    +关注

    关注

    51

    文章

    4657

    浏览量

    83380
  • 语法
    +关注

    关注

    0

    文章

    40

    浏览量

    9666

原文标题:Python之父发文,将重构现有核心解析器

文章出处:【微信号:rgznai100,微信公众号:rgznai100】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何在DSADC中使用外部生成载波而不是aurix生成的激发波的例子?

    你好,有没有关于如何在 DSADC 中使用外部生成载波而不是 aurix 生成的激发波的例子? 背景:当定子和转子角度为 90 度时,我试图通过尝试馈送来自函数生成器的信号来模拟解析器位置。 由于
    发表于 01-22 07:37

    TC387 rdc(解析器)代码不起作用的原因?

    我正在解码解析器信号,我正在使用为 TC38x 电机控制软件下载的 edsadc 和 rdc 库。 当我馈送解析器信号时,我看不到任何转换结果,因为它在 1 到 65536 值之间滑动。 正如你在
    发表于 01-22 06:17

    一种扩展Spring控制反转的绝妙方法

    类型等于JSON,我就用JSON解析器,那如果新加一个类型的解析器,是不是调用的客户端还要修改呢?这显然太耦合了,本文就介绍一种方法,服务定位模式Service Locator Pattern来解决,它帮助我们消除紧耦合实现及其
    的头像 发表于 01-10 09:41 115次阅读

    MySQL执行过程:如何进行sql 优化

    (1)客户端发送一条查询语句到服务器; (2)服务器先查询缓存,如果命中缓存,则立即返回存储在缓存中的数据; (3)未命中缓存后,MySQL 通过关键字将 SQL 语句进行解析,并生成一颗对应的解析树,MySQL 解析器将使用
    的头像 发表于 12-12 10:19 178次阅读
    MySQL执行过程:如何进行sql 优化

    安全挖掘快速多用途工具

    dnsx是一个快速的多用途DNS工具包,设计用于通过retryabledns库运行各种探测。它支持多个DNS查询、用户提供的解析器、DNS 通配符过滤(如shuffledns等)。
    的头像 发表于 11-30 16:22 222次阅读
    安全挖掘快速多用途工具

    LLM作用下的成分句法分析基础研究

    采用伯克利神经解析器(Berkeley Neural Parser)作为方法的基础。该解析器是一种基于图表的方法,采用自注意力编码器和图表解码器,利用预训练的嵌入作为输入来增强解析过程。由于融合
    的头像 发表于 11-10 10:47 178次阅读
    LLM作用下的成分句法分析基础研究

    更低内存占用的通用Json库-RyanJson

    RyanJson是一个小巧的c语言json解析器,包含json文本文件解析 / 生成,专门针对内存占用进行优化,相比cJSON内存占用减少30% - 60%,运行速度和cJSON差不多。
    的头像 发表于 08-24 17:23 729次阅读
    更低内存占用的通用Json库-RyanJson

    TSMaster小功能—Python小程序如何导入外部库

    今天给大家介绍TSMaster功能之Python小程序如何导入外部库。通过在TSMaster默认的解析器路径下导入外部库来介绍,以便我们去使用Python外部库。TSMaster默认Python
    的头像 发表于 08-14 10:06 646次阅读
    TSMaster小功能—Python小程序如何导入外部库

    一文走进SQL编译-语义解析

    SQL 引擎主要由三大部分构成:解析器、优化器和执行器。
    的头像 发表于 06-18 10:46 431次阅读
    一文走进SQL编译-语义<b class='flag-5'>解析</b>

    嗅探spi flash和esp8266,如何知道需要读取多少字节?

    flash 的数据表嗅探的协议另一方面,我弄清楚它是如何工作的,我也在写一个小解析器来显示更易读的通信 但我无法理解快速读取双 IO (0xBB) 如何知道需要读取多少字节。几个 0xBB 返回 32 位,但后来 0xBB 返回 64 位 在哪里设置读取的大小?
    发表于 05-31 08:22

    有可能在LS1028a ENETC上的MAC地址之前添加一个偏移量吗?

    我们有可能在 LS1028a ENETC 上的 MAC 地址之前添加一个偏移量吗? 第 2 层偏移量。 解析器预期在以太网 DA 的第一个字节之前看到的帧开头的八位字节对的数量。
    发表于 05-25 07:11

    是否有可用的ESP AT命令的C/C++库?

    我想知道是否有可用的 ESP AT 命令的 C/C++ 库?!就像一个简单的 AT 命令解析器,我可以将其集成到我的 MCU 固件中,因为我计划将 ESP 模块用作我的主机 MCU 的从属 wifi
    发表于 05-15 06:47

    Spring项目中用这种模式更方便

    ,比如类型等于JSON,我就用JSON解析器,那如果新加一个类型的解析器,是不是调用的客户端还要修改呢?这显然太耦合了,本文就介绍一种方法,服务定位模式`Service Locator Pattern`来解决,它帮助
    的头像 发表于 05-11 10:39 283次阅读
    Spring项目中用这种模式更方便

    为什么AN12218SW引导加载程序无法解析生成的.srec文件?

    当将提供的 S32K148 引导加载程序应用程序与 S32DS 生成的 .srec 文件一起使用时,它无法写入未对齐的短语。 请 NXP:修复 Bootloader 中的 srecord 解析器或修复生成的 .srec 文件
    发表于 05-06 06:37

    shell脚本基础知识

    shell脚本是一个文件,里面存放的是特定格式的指令,系统可以使用脚本解析器翻译或解析指令并执行(无需编译),shell脚本的本质是shell命令的有序集合
    的头像 发表于 04-17 15:00 673次阅读