ANTLR4源码分析和python式缩进语法的实现

本文系“stuPyd教学编程语言”项目开发过程中产生的成果文档之一,一方面旨在针对目前中国国内对ANTLR4的中文资料缺乏和相对外网应用尚未成熟的境况提供给各位开发者一个成熟的参考案例;另一方面,由于使用java做开发的代码已经比较成熟,因此我们使用python语言,为使用其他语言的各位程序员提供思路。

  • 开发工具:ANTLR4 PyCharm插件;Python 3.7.0解释器;PyCharm Professional
  • 未经允许禁止转载。

一、杂项

在GitHub.com上,国外同行已经根据Python Documentation中给出的文法使用ANTLR4制作了python的语法分析器。在本文所涉及的部分(仅指缩进语法的实现一块)代码逻辑皆参考于此。网址在https://github.com/antlr/grammars-v4/blob/master/python3/Python3.g4 。然而一方面ANTLR支持不同语言(包括C++和Python2和Python3),目前仅在java语言上技术较为成熟;另一方面这方面的中文资料少得可怜,并无搬运。因此本文对源码作出分析并改写为Python,同时详细介绍ANTLR4所生成的词法分析器(Lexer)和语法分析器(Parser)以及相关的类(Token,CommonToken)。
    本文中实现代码放在最后,而将ANTLR源码解析放在前部且占大篇幅,主要原因是网上代码示例有余而原理解析不足,以致笔者在自己实现时遇到了很多困惑,同时由于目标语言不同时ANTLR生成的方法名、成员名也有所不同,导致代码可移植性不强,这也是撰写此文的原因之一,希望读到此文的读者能对此有更深刻的了解。
关于如何在IDE里使用ANTLR插件,以及ANTLR的基础语法请搜素其他网站。我们将可能在后续博文中给出教学,本文暂不涉及。

二、内置函数和ANTLR分析器运行机制

本段主要讲解在完成缩进文法过程中需要用到的类和方法,均为改写Lexer所必须。ANTLR在生成代码时,类和方法的名字多半不会改变,但由于语言特性的原因多少会有些不同。Java用户请参考前文中的源码,C++用户则请按本文介绍的思路和顺序阅读源码做出改写。
1.Token和TokenStream
    ANTLR4产生的语法分析器(以语法起名stuPyd为例,其自动产生的语法分析器名为stuPydParser)继承自Parser类,用于分析源输入文件经词法处理后产生的Token流。举例说明,我们给出各个语法非终结符的定义(例如file_input,INDENT等)并使ANTLR4生成分析器之后,我们可以在其中看到如下成员:

RULE_file_input = 0
RULE_stmt = 1
RULE_simple_stmt = 2
RULE_small_stmt = 3
...
INDENT=38
DEDENT=39

这些即为token的type属性。语法分析所分析的便是由type表示token内容的token流。
    Token是由词法分析器生成的。词法分析结束后生成的token对象输出时如下:

[@-1,0:2='num',<3>,1:0]

其中我们用的到的属性有两个,一个是’num’,即token.text,存放token的原文,另一个是<3>,即token.type,存放词法分析的int型类型结果,语法分析器便在按次序存放的CommonTokenStream类里(默认情况下将Token放于其中将由程序自动完成,我们无需操心。但在INDENT和DEDENT生成的要求下还要考虑其原理的问题。后文有提及)针对type进行分析。
    在Python Document给出的文法中可以看到,涉及代码分层的模块被命名为suite。Suite的推导如下:
suite : NEWLINE INDENT stmt+ DEDENT
    举例说明,在我们设计的stuPyd.g4里,suite用法形如
while_stmt : ‘while’ bool ‘:’suite;
    这就是一个模仿python型缩进语言的典型案例。写出来形如:

while True :Code..Code...
Other code...

其中NEWLINE是指换行符‘\n’等,INDENT指“更多的缩进个数”,DEDENT指缩进退格,用过python的用户可能更能理解其含义。若实现语法分析,他们都必须以Token的形式出现在CommonTokenStream里。但从我们希望达到的效果来看,INDENT和DEDENT并不能够以正则表达式来描述他们,也就是Parser不能通过正常的方式获取INDENT和DEDENT的token。因此我们要对词法分析器嵌入代码来生成这两个token。首先是在.g4文件的grammar语句下面写:

tokens{INDENT,DEDENT}

tokens是ANTLR4内置的关键字,意为对括号内的token符号进行声明,表明他们将会以非自动的方式由用户生成但将不在正文里做正则表达式解释。因此我们在写下tokens之后可以写出上文suite的产生式而避免报错,同时也会在stuPydParser里生成这两个Token的代号,也就是上文中stuPydParser中自动生成的的INDENT和DEDENT属性。接下来就是在代码中生成INDENT和DEDENT了。
    类CommonToken是用来创建新的token实例的,我们可以通过一些方法将他们送进CommonTokenStream。其构造函数如下:

def __init__(self, source:tuple = EMPTY_SOURCE, type:int = None, channel:int=Token.DEFAULT_CHANNEL, start:int=-1, stop:int=-1):

其中,source:tuple这个参数可以用Lexer内置成员填上,在下一节会介绍;type可以随意选择,如上文所示,我们将使用stuPydParser.INDENT或stuPydParser.DEDENT来填补这个参数;channel用以标识token存放位置,这一条不需额外修改。Start和stop将标识该token的文本开始和结束位置,随后text由这两个参数决定。
2.Lexer
    用户生成的词法分析器stuPydLexer继承自Lexer,简单说只是增加了用户定义的关键字和嵌入的代码。因此要理解程序运行机制需要从Lexer开始研究。Lexer里有以下几个属性需要注意:
    1) _input
    在调用lexer时可以为构造函数填入多种输入,包括字节流和文件流,也就是_input即为输入流。在后文使用时主要是通过self._input.LA(1)这一方法。该方法可以在不移动文件指针的情况下获取输入流的下一个字符(char型,或者理解为int型的ascii码也可)。
    2)_tokenFactorySourcePair
    这个无需太多注意。用来填写CommonToken类的tuple参数即可。
    3)_token
    用来临时存储当前识别的token。在一个词法符号识别结束后发送给CommonTokenStream。在类内可以直接调用并修改。
    4)nextToken(self)
    这是一个比较重要的函数。在我们的需求中要对它进行重写。它的功能摘取ANTLR4产生的代码注释:从自身来源中返回一个token。换言之,在输入流中向后匹配一个token。该方法指挥词法分析器跳过当前词法规则以寻找下一个token。当一个词法规则分析完一个被标记skip的token时nextToken() 会自动寻找下一个。注意: 如果在任何token规则结束时token为空,它将仍会创建一个并且将它送到token流。
    在我们重写时,会考虑到它本身的功能以及配套函数emit的协同工作。
    5)emit(self)
    emit,即发送token的函数。函数本体声明如下:

    def emit(self):t = self._factory.create(self._tokenFactorySourcePair, self._type, self._text, self._channel, self._tokenStartCharIndex,self.getCharIndex()-1, self._tokenStartLine, self._tokenStartColumn)self.emitToken(t)return t

它自动根据实例化的Lexer自身属性生成token(这些属性由其他类方法修改,我们不需关心),并发送到自身的“token缓冲区”self._token(这是由emitToken完成的。但请注意此时仅存放在self._token里,并没有发送到CommonTokenStream)。

3.CommonTokenStream以及分析
    CommonTokenStream继承自BufferedTokenStream,在这个类中最重要方法是fetch()。其会循环调用输入源lexer的nextToken方法,对该方法返回的token按次序排列。
    经过对以上源码的阅读和理解,我们就可以理清思路了,下面的代码也更容易理解。我们的方案是:
    0.设置一个缩进栈以存储最近缩进个数;设置一个token栈以保存待发送的token符号(这点在后面做解释)
    1.对NEWLINE后面紧跟的换行符号进行计数(即把生成INDENT的操作写在NEWLINE的嵌入代码里),而对未出现在换行符后的空格不作处理(在语法文件里skip掉),若缩进栈为空则默认栈顶为0.如果空格大于栈顶空格个数,就产生INDENT,并将此时的空格个数压入栈中;如果空格数与栈顶相等,则不作处理调用self.skip();如果空格数小于栈顶,则产生DEDENT并栈顶出栈,继续判断是否相等。
    这其中有一个至关重要的问题也是唯一难点:有可能两层缩进在同一行结束,这意味着分析同个词法符号NEWLINE时很可能不止产生一个token(即使该行只有一个缩进,也面临着产生NEWLINE和INDENT两个token的难题)。这使得CommonTokenStream无法正常工作,因为lexer的_token属性仅能容纳一个token,而它将在CommonTokenStream调用lexer.nextToken()时返回。因此设置token栈的意义在于:通过改写nextToken()和emit(),使emit()发送token到栈中而nextToken()在token栈不为空时返回栈顶元素,在token栈为空时才调用父类的nextToken()去读取下一个token。
    问题解决了,下面是代码实现。

三、代码实现及注释

    # grammar stuPyd;
//decls: waiting for write@lexer::header{from stuPydParser import stuPydParser
from antlr4.Token import CommonToken
}@lexer::members{self.lastToken = Noneself.tokens = []self.indentStack = []def emit(self,t=None):if t == None :s = self._factory.create(self._tokenFactorySourcePair, self._type, self._text, self._channel, self._tokenStartCharIndex,self.getCharIndex()-1, self._tokenStartLine, self._tokenStartColumn)self.emitToken(s)print(s)self.tokens.append(s)return selse:self.emitToken(t)self.tokens.append(t)print(t)return tdef commonToken(self,type,text):stop = self.getCharIndex()-1if len(text)==0 :start = stopelse:start = stop - len(text)+1return CommonToken(self._tokenFactorySourcePair,type,self.DEFAULT_TOKEN_CHANNEL,start,stop)def createDedent(self):dedent = self.commonToken(stuPydParser.DEDENT,'')dedent.line = self.lastToken.linereturn dedent@staticmethod
def getIndentationCount(spaces):count = 0for ch in spaces:if ch == '\t':count +=( 4 - (count%4))elif ch == ' ':count += 1else :pass# print(count)return countdef nextToken(self):# Check if the end-of-file is ahead and there are still some DEDENTS expectedif(self._input.LA(1)==Token.EOF and len(self.indentStack)!=0):# Remove ant trailing EOF tokens from our bufferi = len(self.tokens)-1while i>= 0 :if(self.tokens[i].getType()==Token.EOF):self.tokens.pop(i)i-=1# First emit an extra line break that serves as the end of the stmtself.emit(self.commonToken(stuPydParser.NEWLINE,'\n'))# Now emit as much DEDENT tokens as neededwhile len(self.indentStack)!=0 :self.emit(self.createDedent())self.indentStack.pop()# put the EOF back on the token stream .self.emit(self.commonToken(stuPydParser.EOF,'<EOF>'))next = super().nextToken()if (next.channel == Token.DEFAULT_CHANNEL):# Keep track of the last token on the default channelself.lastToken = nextif len(self.tokens) == 0 :# print(next)return nextelse :temp = self.tokens[0]self.tokens.pop(0)#print(temp)return tempdef atStartOfInput(self):if super().getCharIndex()==0 and super().line==1 :# print('True')return Trueelse:# print('False')return False
}tokens{INDENT,DEDENT}fragment SPACES : [ \t]+;NEWLINE:( {self.atStartOfInput()}?   SPACES| ( '\r'? '\n' | '\r' ) SPACES?){newLine = self.textfor i in newLine :if i == '\r' or i == '\n' or i == '\f':passelse:newLine.replace(str(i),'')spaces = self.text.replace('\r','')spaces = self.text.replace('\n','')spaces = self.text.replace('\f','')next = self._input.LA(1)if(next=='\r' or next == '\n' or next == '\f' or next == '<EOF>'):self.skip()else:self.emit(self.commonToken(self.NEWLINE,newLine))indent = self.getIndentationCount(spaces)previous = 0if len(self.indentStack) != 0 :previous = self.indentStack.pop()self.indentStack.append(previous)# it is equal to indentStack.peek()if indent==previous :# skip indents of the same size as the present indent-sizeself.skip()elif indent > previous :self.indentStack.append(indent)self.emit(self.commonToken(stuPydParser.INDENT,spaces))else:# Possibly emit more than 1 DEDENT tokenwhile len(self.indentStack)!=0 and self.indentStack[len(self.indentStack)-1]>indent:self.emit(self.createDedent())self.indentStack.pop()}
;
BLANK : (SPACES|COMMENT)+ ->skip
;

通过这几个即可实现缩进式语法。不要忘记
Suite:NEWLINE INDENT stmt+ DEDENT
其他的语法细节读者可自行添加。

ANTLR4源码分析和python式缩进语法的实现相关推荐

  1. 【GitHub探索】python调试利器——pysnooper源码分析

    前言 这次又开了个新坑--GitHub探索,主要内容是试水当期GitHub上较火的repo 虽然top榜上各路新手教程跟经典老不死项目占据了大半江山,但清流总是会有的. 第一期就试水一下pysnoop ...

  2. Python微型Web框架Bottle源码分析

    Bottle 是一个快速,简单和轻量级的 WSGI 微型 Web 框架的 Python.它作为单个文件模块分发,除了 Python 标准库之外没有依赖关系. 选择源码分析的版本是 Release 于 ...

  3. zg手册 之 python2.7.7源码分析(1)-- python中的对象

    为什么80%的码农都做不了架构师?>>>    源代码主要目录结构 Demo: python 的示例程序 Doc: 文档 Grammar: 用BNF的语法定义了Python的全部语法 ...

  4. 从flink-example分析flink组件(3)WordCount 流式实战及源码分析

    前面介绍了批量处理的WorkCount是如何执行的 <从flink-example分析flink组件(1)WordCount batch实战及源码分析> <从flink-exampl ...

  5. python树状节点 可拖拽_Python 的 heapq 模块源码分析

    原文链接:Python 的 heapq 模块源码分析 起步 heapq 模块实现了适用于Python列表的最小堆排序算法. 堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系.因为堆排序中的树是 ...

  6. Python 的 heapq 模块源码分析

    作者:weapon 来源:https://zhuanlan.zhihu.com/p/54260935 起步 heapq 模块实现了适用于Python列表的最小堆排序算法. 堆是一个树状的数据结构,其中 ...

  7. 【python】Dpark源码分析

    关于Dpark的PDF: http://velocity.oreilly.com.cn/2011/ppts/dpark.pdf 源码分析: Dpark/Spark中最重要的核心就是RDD(弹性分布式数 ...

  8. STM32下推式磁悬浮装置(三)PID调试与源码分析

    目录 前言 一.源码分析 1.工程驱动 2.PID代码 3.控制函数 二.PID调试 三.整体效果 结语 前言 这是STM32下推式磁悬浮装置的第三篇文章,也是这个项目的最后一篇文章.前面两篇文章介绍 ...

  9. AI作曲基础-Python编程作曲软件篇-FoxDot文档及源码分析-官方教程01

    AI作曲基础-Python编程作曲软件篇-FoxDot文档及源码分析-官方教程01 前言 本系列系列目录放在文尾: 本系列是AI作曲的基础,暂时和AI关系不大,但尤为重要: 借助FoxDot,从文档分 ...

最新文章

  1. [微信官方文档] 小程序-错误码信息与解决方案表
  2. AI领域真正最最最最最稀缺的人才是……会庖丁解牛的那个人
  3. C#中控件Control的Paint事件和OnPaint虚函数的区别
  4. 算法描述怎么写_管件材料描述怎么写
  5. NB-IOT连接移动onenet平台流程
  6. Python 利用pymupdf将pdf转换为图片并拆分,后通过PIL合并生成一张图片
  7. 分布式session的6种解决方案
  8. python自动登录灯塔党建_python 奇淫技巧之自动登录 哔哩哔哩
  9. android设置adb环境变量,如何配置android的adb环境变量
  10. 定义一个函数,返回整形数组中最大值
  11. 勾股数规律(任意三个数能够满足勾股定理需要满足的条件)
  12. 快手短视频怎么同步到头条?
  13. 基于端口号的虚拟主机配置
  14. git 解决push报错:[rejected] master -> master (fetch first)
  15. 小红伞命令行工具ScanCL使用安装
  16. cargo build failed: SSL connect error (schannel: failed to receive handshake, SSL/TLS connection fai
  17. PHP 将大量数据导出到 Excel 的方法
  18. 习题4-5 换硬币 (20分) 将一笔零钱换成5分、2分和1分的硬币,要求每种硬币至少有一枚,有几种不同的换法?
  19. C、C++和C# 到底有什么区别了,纳闷
  20. “数字化供应链的下半场”:从平台战略到生态战略

热门文章

  1. 《程序员面试金典(第6版)》面试题 16.07. 最大数值(移位 + 整形提升)
  2. 【Mysql】Sql分组查询后取每组的前N条记录
  3. 鸿蒙系统配在华为什么手机上,鸿蒙系统什么时候能用 鸿蒙系统哪些手机可以用...
  4. web前端自学还是去培训?
  5. 改装避坑指南: 为什么汽车做完隔音没有效果?
  6. 还在为Android表情开发烦恼吗,快来试试Android Emoji吧
  7. PPT报告直接领,这份51页「大数据决策分析平台搭建方案」真的很值
  8. 武汉php比Java好_关于Java和PHP哪个前景好的比较
  9. Docker容器 - DockerFile发布Java微服务并部署到Docker容器
  10. bat文件安装服务器,bat安装服务器