Introduction
上次我们分析了Python中执行程序可分为5个步骤:
- Tokenizer进行词法分析,把源程序分解为Token
- Parser根据Token创建CST
- CST被转换为AST
- AST被编译为字节码
- 执行字节码
本文将介绍Python程序执行的第一步,也就是词法分析。词法分析简单来说就是把源程序的字符分解组合成Token。比如sum=0可以分解成3个token,'sum', '=', '0'。程序中的whitespace通常只作为分隔符用,最终会被忽略掉,因此没有出现在token的列表中。不过在Python之中,由于语法规则的关系,Tab/Space需要用来分析程序的缩进,因此Python中对于Whitespace的处理比一般C/C++编译器的处理会要稍微复杂一些。
在Python中词法分析的实现在Parser目录下的tokenizer.h和tokenizer.cpp。Python的其他部分会直接调用tokenizer.h中定义的函数,如下:
extern struct tok_state *PyTokenizer_FromString(const char *);
extern struct tok_state *PyTokenizer_FromFile(FILE *, char *, char *);
extern void PyTokenizer_Free(struct tok_state *);
extern int PyTokenizer_Get(struct tok_state *, char **, char **);
|
这些函数均以PyTokenizer开头。这是Python源代码中的一个约定。虽然Python是用C语言实现的,其实现方式借鉴了很多面对对象的思想。拿词法分析来说,这四个函数均可以看作PyTokenizer的成员函数。头两个函数PyTokenizer_FromXXXX可以看作是构造函数,返回PyTokenizer的instance。PyTokenizer对象内部状态,也就是成员变量,储存在tok_state之中。PyTokenizer_Free可以看作是析构函数,负责释放PyTokenizer,也就是tok_state所占用的内存。PyTokenizer_Get则是PyTokenizer的一个成员函数,负责取得在字符流中下一个Token。这两个函数均需要传入tok_state的指针,和C++中需要隐含传入this指针给成员函数的道理是一致的。可以看到,OO的思想其实是和语言无关的,即使是C这样的结构化的语言,也可以写出面对对象的程序。
tok_state
tok_state等价于PyTokenizer这个class本身的状态,也就是内部的私有成员的集合。部分定义如下:
/* Tokenizer state */
struct tok_state {
/* Input state; buf <= cur <= inp <= end */
/* NB an entire line is held in the buffer */
char *buf;/* Input buffer, or NULL; malloc'ed if fp != NULL */
char *cur;/* Next character in buffer */
char *inp;/* End of data in buffer */
char *end;/* End of input buffer if buf != NULL */
char *start;/* Start of current token if not NULL */
int done;/* E_OK normally, E_EOF at EOF, otherwise error code
/* NB If done != E_OK, cur must be == inp!!! */
FILE *fp;/* Rest of input; NULL if tokenizing a string */
int tabsize;/* Tab spacing */
int indent;/* Current indentation index */
int indstack[MAXINDENT];/* Stack of indents */
int atbol;/* Nonzero if at begin of new line */
int pendin;/* Pending indents (if > 0) or dedents (if < 0) */
char *prompt, *nextprompt;/* For interactive prompting */
int lineno;/* Current line number */
int level;/* () [] {} Parentheses nesting level */
/* Used to allow free continuations inside them */
};
|
|
最重要的是buf, cur, inp, end, start。这些field直接决定了缓冲区的内容:
buf是缓冲区的开始。假如PyTokenizer处于字符串模式,那么buf指向字符串本身,否则,指向文件读入的缓冲区。
cur指向缓冲区中下一个字符。
inp指向缓冲区中有效数据的结束位置。PyTokenizer是以行为单位进行处理的,每一行的内容存入从buf到inp之间,包括/n。一般情况下 ,PyTokenizer会直接从缓冲区中取下一个字符,一旦到达inp所指向的位置,就会准备取下一行。当PyTokenizer处于不同模式下面,具体的行为会稍有不同。
end是缓冲区的结束,在字符串模式下没有用到。
start指向当前token的开始位置,如果现在还没有开始分析token,start为NULL。
PyTokenzer_FromString & PyTokenizer_FromFile
PyTokenizer_FromString & PyTokenizer_FromFile可以说是PyTokenizer的构造函数。从这两个函数的命名可以看出,PyTokenizer支持两种模式:字符串和文件。由于标准输入STDIN也可以看作是文件,因此实际上PyTokenizer支持3种模式:字符串,交互,文件。
PyTokenizer_FromFile的实现和PyTokenizer_FromString的实现大致相同。后者的实现如下:
/* Set up tokenizer for string */
struct tok_state *
PyTokenizer_FromString(const char *str)
{
struct tok_state *tok = tok_new();
if (tok == NULL)
return NULL;
str = (char *)decode_str(str, tok);
if (str == NULL) {
PyTokenizer_Free(tok);
return NULL;
}
/* XXX: constify members. */
tok->buf = tok->cur = tok->end = tok->inp = (char*)str;
return tok;
}
|
直接调用tok_new返回一个tok_state的instance,后面的decode_str负责对str进行解码,然后赋给tok->buf/cur/end/inp。
PyTokenizer_Get
下面我们来分析一下PyTokenizer_Get函数。该函数的作用是在PyTokenizer所绑定的字符流(可以是字符串也可以是文件)中取出下一个token,比如sum=0刚取到了'sum',那么下一个取到的就是'='。一个返回的token由两部分参数描述,一个是表示token类型的int,一个是token的具体内容,也就是一个字符串。Python会把不同token分为若干种类型,这些不同的类型定义在include/token.h里面以宏的形式存在,如NAME,NUMBER,STRING,NEWLINE等。举例来说,'sum'这个token可以表示成(NAME,
'sum')。NAME是类型,表明sum是一个名称(注意请和字符串区分开)。此时Python并不判定该名称是关键字还是标识符,一律统称为NAME。而这个NAME的内容是'sum'。PyTokenizer_Get返回的int便是token的类型,而两个参数char **p_start, char **p_end是输出参数,指向token在PyTokenizer内部缓冲区中的位置。这里采用返回一个p_start和p_end的意图是避免构造一份token内容的copy,而是直接给出token在缓冲区中的开始和结束的位置。这样做显然是为了提高效率。
PyTokenizer_Get的实现如下,直接调用tok_get函数:
Int
PyTokenizer_Get(struct tok_state *tok, char **p_start, char **p_end)
{
int result = tok_get(tok, p_start, p_end);
if (tok->decoding_erred) {
result = ERRORTOKEN;
tok->done = E_DECODE;
}
return result;
}
|
tok_get负责以下几件事情:
1. 处理缩进
缩进的处理只在一行开始的时候。如果tok_state::atbol(at beginning of line)非0,说明当前处于一行的开始,否则不做处理。
/* Get indentation level */
if (tok->atbol) {
register int col = 0;
register int altcol = 0;
tok->atbol = 0;
for (;;) {
c = tok_nextc(tok);
if (c == ' ')
col++, altcol++;
else if (c == '/t') {
col = (col/tok->tabsize + 1) * tok->tabsize;
altcol = (altcol/tok->alttabsize + 1)
* tok->alttabsize;
}
else if (c == '/014') /* Control-L (formfeed) */
col = altcol = 0; /* For Emacs users */
else
break;
}
tok_backup(tok, c);
|
上面的代码负责计算缩进了多少列。由于tab键可能有多种设定,PyTokenizer对tab键有两套处理方案:tok->tabsize保存着"标准"的tab的大小,缺省为8(一般不要修改此值)。Tok->alttabsize保存着另外的tab大小,缺省在tok_new中初始化为1。col和altcol保存着在两种不同tab设置之下的列数,遇到空格+1,遇到/t则跳到下一个tabstop,直到遇到其他字符为止。
if (c == '#' || c == '/n') {
/* Lines with only whitespace and/or comments
shouldn't affect the indentation and are
not passed to the parser as NEWLINE tokens,
except *totally* empty lines in interactive
mode, which signal the end of a command group. */
if (col == 0 && c == '/n' && tok->prompt != NULL)
blankline = 0; /* Let it through */
else
blankline = 1; /* Ignore completely */
/* We can't jump back right here since we still
may need to skip to the end of a comment */
}
|
接下来,如果遇到了注释或者是空行,则不加以处理,直接跳过,这样做是避免影响缩进。唯一的例外是在交互模式下的完全的空行(只有一个换行符)需要被处理,因为在交互模式下空行意味着一组语句将要结束,而在非交互模式下完全的空行是要被直接忽略掉的。
if (!blankline && tok->level == 0) {
if (col == tok->indstack[tok->indent]) {
// 情况1:col=当前缩进,不变
}
else if (col > tok->indstack[tok->indent]) {
// 情况2:col>当前缩进,进栈
tok->pendin++;
tok->indstack[++tok->indent] = col;
tok->altindstack[tok->indent] = altcol;
}
else /* col < tok->indstack[tok->indent] */ {
// 情况3:col<当前缩进,退栈
while (tok->indent > 0 &&
col < tok->indstack[tok->indent]) {
tok->pendin--;
tok->indent--;
}
}
}
|
最后,根据col和当前indstack的栈顶(也就是当前缩进的位置),确定是哪一种情况,具体请参看上面的代码。上面的代码有所删减,去掉了一些错误处理,加上了一点注释。需要说明的是PyTokenizer维护两个栈indstack & altindstack,分别对应col和altcol,保存着缩进的位置,而tok->indent保存着栈顶。
2. 跳过whitespace和注释
代码很简单,在此不做说明。
3. 确定token
反复调用tok_nextc,获得下一个字符,依据字符内容判定是何种token,然后加以返回。具体的过程比较长,但是logic还是比较简单的。
下面举一个处理标识符(变量和关键字)的例子
/* Identifier (most frequent token!) */
if (isalpha(c) || c == '_') {
/* Process r"", u"" and ur"" */
switch (c) {
case 'r':
case 'R':
c = tok_nextc(tok);
if (c == '"' || c == '/'')
goto letter_quote;
break;
case 'u':
case 'U':
c = tok_nextc(tok);
if (c == 'r' || c == 'R')
c = tok_nextc(tok);
if (c == '"' || c == '/'')
goto letter_quote;
break;
}
while (isalnum(c) || c == '_') {
c = tok_nextc(tok);
}
tok_backup(tok, c);
*p_start = tok->start;
*p_end = tok->cur;
return NAME;
}
|
假如当前字符是字母或者是下划线,则开始当作标示符进行分析,否则,继续执行下面的语句,处理其他的可能性。不过还有一种可能性,Python中字符串可以是用r或者u开头,比如r"string", u"string"。r代表raw string,u代表unicode string。一旦遇到了r或者u的情况下,直接跳转到letter_quote标号处,开始作为字符串进行分析。如果不是r/u,反复拿到下一个字符直到下一个字符不是字母,数字或者下划线为止。由于最后一次拿到的字符不属于当前标示符,应该被放到下一次进行分析,因此调用tok_backup把字符c回送到缓冲区中,类似ungetch()。最后,设置好p_start
& p_end,返回NAME。这样,返回的结果表明下一个token是NAME,开始于p_start,结束于p_end。
tok_nextc
tok_nextc负责从缓冲区中取出下一个字符,可以说是整个PyTokenizer的最核心的部分。
/* Get next char, updating state; error code goes into tok->done */
static int
tok_nextc(register struct tok_state *tok)
{
for (;;) {
if (tok->cur != tok->inp) {
// cur没有移动到inp,直接返回*tok->cur++
return Py_CHARMASK(*tok->cur++); /* Fast path */
}
if (tok->fp == NULL) {
// 字符串模式
}
if (tok->prompt != NULL) {
// 交互模式
}
else {
// 磁盘文件模式
}
}
}
|
大部分情况,tok_nextc会直接返回*tok->cur++,直到tok->cur移动到达tok->inp。一旦tok->cur==tok->inp,tok_nextc会读入下一行。根据PyTokenizer处于模式的不同,处理方式会不太一样:
1. 字符串模式
字符串的处理是最简单的一种情况,如下:
char *end = strchr(tok->inp, '/n');
if (end != NULL)
end++;
else {
end = strchr(tok->inp, '/0');
if (end == tok->inp) {
tok->done = E_EOF;
return EOF;
}
}
if (tok->start == NULL)
tok->buf = tok->cur;
tok->line_start = tok->cur;
tok->lineno++;
tok->inp = end;
return Py_CHARMASK(*tok->cur++);
|
尝试获得下一行的末尾处作为新的inp,否则,说明下一行结尾处没有/n换行符(说明这是最后一行)或者当前行就是最后一行。在前者的情况下,inp就是字符串/0的位置,否则,返回EOF。当获得了下一行之后,返回下一个字符Py_CHARMASK(*tok->cur++)。
2. 交互模式
代码如下:
char *newtok = PyOS_Readline(stdin, stdout, tok->prompt);
if (tok->nextprompt != NULL)
tok->prompt = tok->nextprompt;
if (newtok == NULL)
tok->done = E_INTR;
else if (*newtok == '/0') {
PyMem_FREE(newtok);
tok->done = E_EOF;
}
#if !defined(PGEN) && defined(Py_USING_UNICODE)
else if (tok_stdin_decode(tok, &newtok) != 0)
PyMem_FREE(newtok);
#endif
else if (tok->start != NULL) {
size_t start = tok->start - tok->buf;
size_t oldlen = tok->cur - tok->buf;
size_t newlen = oldlen + strlen(newtok);
char *buf = tok->buf;
buf = (char *)PyMem_REALLOC(buf, newlen+1);
tok->lineno++;
if (buf == NULL) {
PyMem_FREE(tok->buf);
tok->buf = NULL;
PyMem_FREE(newtok);
tok->done = E_NOMEM;
return EOF;
}
tok->buf = buf;
tok->cur = tok->buf + oldlen;
tok->line_start = tok->cur;
strcpy(tok->buf + oldlen, newtok);
PyMem_FREE(newtok);
tok->inp = tok->buf + newlen;
tok->end = tok->inp + 1;
tok->start = tok->buf + start;
}
|
首先调用PyOs_Readline,获得下一行。注意newtok所对应的内存是被malloc出来的,最后需要free。由于在交互模式下,第一句话的prompt是>>>,保存在tok->prompt中。从第二句开始提示符是...,保存在tok->nextprompt中,因此需要设置tok->prompt = tok->nextprompt。最后一个else if (tok->start != NULL)的作用是,一旦当读入下一行的时候,当前token还没有结束(一个典型的例子是长字符串"""可以跨越多行),由于buf原来的内容不能丢弃,下一行的内容必须加到buf的末尾,。PyTokenizer的做法是调用realloc改变buf的大小,然后把下一行的内容strcpy到buf的末尾。这样做虽然效率不高,由于一般情况下此种情况发生并不频繁,而且是处于交互模式下,因此性能上面没有问题。
3. 文件模式
文件模式下的处理比上面两种模式都复杂。主要原因是文件模式下一行可能比BUFSIZE大很多,因此一旦BUFSIZE不够容纳一整行的话,必须反复读入,realloc缓冲区buf,然后把刚刚读入的内容append到buf的末尾,直到遇到行结束符为止。如果tok->start != NULL,说明当前正在读入token之中,同样的当前的buf不能丢弃,因此读入的新一行的内容同样也要append到buf的末尾,否则,新一行的内容直接读入到buf中。由于代码比较多,这里就不给出了。
作者: ATField
E-Mail:
atfield_zhang@hotmail.com
Blog: http://blog.csdn.net/atfield
分享到:
相关推荐
Python 语言写的 C 语言的词法分析器,是实验报告的一个实验实现,写的很粗糙,简单看看就好了,不保证最终效果
在Python中,我们可以利用各种方法来实现词法分析器,其中一种常见的方式是通过编写自定义的解析规则来构建词法分析器。本项目提供的“词法分析器Python版本”显然就是这样一个工具,它基于Python语言开发,并且采用...
本项目聚焦于词法分析器和语法分析器的实现,采用Python作为开发语言,对于学习编译原理和Python编程的开发者来说具有很高的实践价值。 词法分析器,也称为扫描器或 tokenizer,是编译器的第一步,它的任务是将源...
本项目是一个基于Python实现的词法分析器,它不仅具备基本的功能,还包含了用户友好的图形用户界面(GUI)。通过这个工具,用户可以输入源代码,然后该分析器会识别并解析出代码中的各种符号和结构。 词法分析器,...
基于python实现词法分析器、语法分析器和语法制导翻译器 基于Python实现词法分析器、语法分析器和语法制导翻译器的项目适合以下人群: 1. 计算机科学与技术专业的学生:这类项目是编译原理课程的经典实践项目,有助...
1. **Python解释器的运行流程**:介绍如何从源代码到字节码,再到机器码的转换过程,包括词法分析、语法解析和编译阶段。 2. **对象系统**:探讨Python中的对象模型,包括对象的创建、生命周期管理、继承和多态性。...
Python词法分析器是编程语言处理的一个重要环节,主要用于解析源代码并将其转换为一系列有意义的标记或符号,这是编译器和解释器的第一步。在这个过程中,我们通常会涉及以下几个核心概念: 1. **词法分析(Lexical...
3. **处理空白和注释**:词法分析器需要忽略源代码中的空格、制表符和单行/多行注释。 4. **处理字符串和字符字面量**:正确识别和处理引号包围的字符串和字符。 5. **错误处理**:当遇到不符合规则的输入时,词法...
词法分析是完成编译程序的第一个阶段的工作。所谓词法分析就是对输入字符串形式的源程序按顺序进行扫描,识别其中的字符串作为输出。词法分析是从左向右扫描每行源程序的符号,拼成单词,换成统一的机内表示形式——...
在编程语言处理领域,语法分析器和词法分析器是至关重要的组成部分,它们主要用于解析源代码,将其转化为计算机可以理解的形式。在这个项目中,我们关注的是一个基于Java实现的语法分析器和词法分析器。Java是一种...
词法分析器,也称为扫描器或词法分析器,是编译器设计中的关键组成部分。在编程语言处理中,词法分析器的作用是将源代码文本分解成一系列有意义的符号,即词法单元(Token),这些词法单元是构成程序的基本元素。...
Java 词法分析器 Java 词法分析器是 Java 程序设计语言的词法分析工具,用于对 Java 源代码进行词法分析。以下是 Java 词法分析器的知识点: 1. Java 词法分析器的组成部分 Java 词法分析器由多个组成部分组成,...
编译原理大作业-词法分析器、语法分析器源码(java实现).zip编译原理大作业-词法分析器、语法分析器源码(java实现).zip编译原理大作业-词法分析器、语法分析器源码(java实现).zip编译原理大作业-词法分析器、...
在这个实验报告中,我们关注的是如何使用Python语言实现一个词法分析器,特别是针对C语言的源代码。 1. **实验目标**: 实验的主要目标是理解和掌握词法分析的原理,以及如何使用Python实现词法分析器。通过这个...
3. **状态机构造**:基于词法规则,构建有限状态自动机( Finite State Automaton, FSA),它决定了词法分析器如何根据输入字符来决定当前应该识别的标记类型。 4. **标记生成**:当匹配到一个完整的模式时,词法...
在编程领域,编译原理是理解计算机语言处理过程的关键部分,它涉及到...通过`WordThink3.java`和`SyntaxThink2.java`这两个文件,我们可以学习到如何自定义词法分析器和语法分析器,以及如何在Java中实现这些关键功能。
3. **状态机**:词法分析器的核心通常是一个有限状态自动机(Finite State Machine, FSM)。每个状态代表词法规则的一部分,随着输入字符的改变,词法分析器会从一个状态转移到另一个状态,直到找到一个完整的标记。...
语法分析器接着词法分析器的工作,它负责解析由词法分析器生成的标记流,并根据语法规则构建抽象语法树(AST)。这一步骤是基于上下文无关文法(Context-Free Grammar,CFG)进行的,它定义了语言的结构和规则。例如...
词法分析器的设计与实现 词法分析器是编译器的组成部分,负责将源代码分解成单词符号,以便后续的语法分析和语义分析。设计和实现词法分析器是编译原理和编译技术的重要组成部分。 正规式 正规式是描述语言结构的...