python tokenize_Python语法处理(1)——Tokenizer

今天主要来看Token和tokenizer。 主要涉及Parser文件夹下的token.c,tokenizer.c,tokenizer.h。

前排提醒:不要学Python这么写Tokenizer。至少不要像Python的这个一样goto和hack满天飞。

Python在实现自己的Parser时并没有使用类似flex或lex之类的词法检查生成器,以及yacc或bison之类的LALR Parser 生成器,而是选择自己写了一个tokenizer和自己的一个LL(1) Parser。这篇文章的重点在Tokenizer。

在第一篇文章里提到了Grammar的替换,其实token.c就是利用Grammar/Tokens和Tools/scripts/generate_token.py生成出来的,主要处理那些特殊符号和操作符。

但剩余的词法规则几乎都是靠tokenizer.c手工写成的。

相较于传统的编译器前端的tokenizer,python的tokenizer有一些别的作用,比如检查文件编码,编码内部转换等。主要也是因为支持UTF-8,UTF-16等编码,这些编码检查也是错误处理的一部分。

真正进入tokenizer的部分,首先会看到struct tok_state,具体定义在tokenizer.h中。可以看到其主要部分有:Buffer相关,包括当前位置,buffer终点,buffer内容终点,如果来源是文件的话还有文件指针等等

当前状态,包括错误状态,缩进层数,括号层数,行号等等

交互模式下的提示符

编码相关

async相关

有一说一,你个tokenizer保留async的状态有点诡异啊……

这个buffer由以下几个指针构成:buf:开始位置

cur:下一个字符

inp:最后一个字符

end:buffer的末尾

在正常情况下,这几个指针应当从小到大排开。

tokenizer的最主要的功能就是提供下一个token。而最核心的部分是读取下一个字符。这两个步骤也分别对应tok_get和tok_nextc。

先来看tok_nextc。洋洋洒洒近两百行,其实主要是在处理各种各样的潜在错误。buffer没用完?直接读下一个字符。

有错误?返回EOF。

在读字符串?往inp后面接着读一行

在交互模式下?让用户再输入一行

(代码来自文件)读下一行

上面的情况3、4、5读进来的下一行也有可能有各种各样的问题,比如说下一行为空或者遇到EOF,总之有各种各样的问题需要解决。就算没遇到什么问题,也要维护tokenizer的内部状态,甚至有的时候还要手动处理字符串对象的引用计数。

tok_get更加长,六百多行……

首先先搞明白,p_start和p_end这两个参数是用来给tok_get写的,标出这个token的范围的,实际上真的传进来的用来计算的参数只有tok。

进入函数第一件事把p_start和p_end清空。如果现在在新的一行的开头就先尝试着获取当前的缩进等级,并根据情况抛IndentError(比方说层数超上限了,或者缩进混用了,或者空格数量不统一了)。(别的语言可以把Whitespace略过,但这里是Python,一行开头的空格不能跳)

随后根据缩进层数的变化,根据情况返回INDENT或DEDENT。缩进层数的变化被抽象成了各自独立的token(可以理解为一对括号……嗯没错,可以理解为Python的缩进在Tokenize的时候已经被重写成括号了)。随后甚至还要处理type comment意外终止async函数体的问题……

啊放下这些奇怪的问题不管,我们继续往下看。

之后就是处理type comment。在ast库中可能会把type_comments设为True。这时type comment就不会被忽略了。

处理完EOF之后就是各种类型的token的手工处理了。

高能预警,这个东西写的真的……大家手工写完这么复杂的东西一定记得检查有没有漏情况。

先是一套针对bruf这四个字母的检查。为啥?当然是因为他们可以作为字符串前缀啊。如果看到双引号或者单引号,跳转到专门处理字符串的地方。

我们先假装不是字符串往下看。因为async和await需要特判,所以要先处理一下。这里有段很骚的注释,我给大家分享一下:

/* The next token is going to be 'def', so instead ofreturning a plain NAME token, return ASYNC. */

哇,上下文敏感tokenizer。

之后是对换行的手工判断,可能是NL也可能是NEWLINE。

随后是点。有可能是小数点,有可能是ELLIPSIS,也有可能是一个普通的DOT。

然后是数字开头的token。数字也要分各种奇奇怪怪的情况。比方说不能以0开头后接数字,因为以0开头意味着后面一个字母是表进制的,或者后接小数点。Python支持x(十六进制)o(八进制)和b(二进制)以及下划线分割(从Python 3.6开始)。还要考虑.(小数点)e(科学计数法)和j(虚数)。

插一句,以0开头后接数字是在PEP 3127里禁止的,主要是为了防止编程新手困惑,具体可以去看看这个PEP对相关问题的讨论。

接着看字符串。字符串会被原样传给parser,不做转义处理,开头的那些字母(bruf)会被一并传给parser。

随后是"\\\n"这种情况的特判,三字符和二字符特殊运算符的检查(token.c的东西)和括号匹配。最后是单字符特殊符号。

可以看到,想在Python里新增一种语法,在修改tokenizer这个层面上看是一件可能还行但也可能很麻烦的事情。对于海象运算符:=可能只是在Grammar/Tokens里加上COLONEQUAL然后重新生成一遍相关文件的事情,但是加入Async/Await却可能产生各种意想不到的问题,然后加上各种惊为天人的patch。

考虑到各种C-API的兼容问题,估计这个东西也不会改了(都被打上static了还改个啥),而是拿别的函数包一包,调用那些看着还行的接口。现在看着这东西可能还能苟段时间,调理也姑且还算清晰,但谁知道会不会暴雷呢?就像加入async的时候那样。

要写个结论的话……自己别这么写,嗯。

下一篇文章大概是关于Parser的一些周围设施,比如语法树节点以及Parser如何和Tokenizer对接等话题。

你可能感兴趣的:(python,tokenize)