Python源码分析5 – 语法分析器PyParser
Introduction上一篇文章我们分析了Python是如何对语法文件Grammar进行预处理,生成语法数据,并在运行时生成Acclerators加速语法分析的过程。当分析完这些内容之后,下一步便是分析Python中语法分析的机制。回顾一下Python的整个处理流程:
1. PyTokenizer进行词法分析,把源程序分解为Token
2. PyParser根据Token创建CST
3. CST被转换为AST
4. AST被编译为字节码
5. 执行字节码
语法分析处于整个流程的第二步,其目标是处理token生成CST(Concrete Syntax Tree)。因此,在具体分析PyParser的工作方式之前,我们先看一下什么是CST。
CST (Concrete Syntax Tree)
CST (Concrete Syntax Tree) 和AST (Abstract Syntax Tree) 类似,都是语法分析所获得的中间结果。他们的不同之处在于,CST直接对应语法分析的匹配的过程,是直接生成的,含有大量冗余信息。而AST省略了中间的冗余信息,直接对应实际的语义,也就是分析的结果。用例子说明要清楚一些:
假设有这样一个表达式a,
CST是这样的:(->表示从父结点到子结点)
file_input -> stmt -> simple_stmt -> small_stmt -> expr_stmt -> testlist -> test ->or_test ->and_test ->not_test -> comparison -> expr -> xor_expr -> and_expr -> shift_expr -> arith_expr -> term -> factor -> power -> atom -> (NAME, “a”)
而AST则是:
(stmt_ty, expr_kind) -> (expr_ty, name_kind) ->(“a”)
可以看到CST表述了整个分析a的过程,从file_input一直推导到最后的NAME,每一步推导都成了树的结点,而大部分信息都可以说是无用的。AST的结构要简单和直接的多,直接表明a是一个表达式语句(假定a是一个单独的语句),内容是一个标示符,值为”a”。Python的语法分析生成的是CST而非AST,之后Python会调用PyAst_FromNode将CST转换为AST。
CST的结点称为Node,其结构定义在node.h中:
typedef struct _node {
short n_type;
char *n_str;
int n_lineno;
int n_col_offset;
int n_nchildren;
struct _node *n_child;
} node;
Field
Description
n_type
结点类型,终结符定义在token.h中,而非终结符定义在graminit.h中
n_str
结点所对应的字符串的内容
n_lineno
对应的行号
n_col_offset
列号
n_nchildren
子结点的个数
n_child
子结点数组,动态分配内存
Python提供了下面的函数/宏来操作CST,同样定义在node.h中:
PyAPI_FUNC(node *) PyNode_New(int type);
PyAPI_FUNC(int) PyNode_AddChild(node *n, int type,
char *str, int lineno, int col_offset);
PyAPI_FUNC(void) PyNode_Free(node *n);
/* Node access functions */
#define NCH(n) ((n)->n_nchildren)
#define CHILD(n, i) (&(n)->n_child)
#define RCHILD(n, i) (CHILD(n, NCH(n) + i))
#define TYPE(n) ((n)->n_type)
#define STR(n) ((n)->n_str)
/* Assert that the type of a node is what we expect */
#define REQ(n, type) assert(TYPE(n) == (type))
PyAPI_FUNC(void) PyNode_ListTree(node *);
PyNode_New和PyNode_Delete负责创建和释放node结构:
node *
PyNode_New(int type)
{
node *n = (node *) PyObject_MALLOC(1 * sizeof(node));
if (n == NULL)
return NULL;
n->n_type = type;
n->n_str = NULL;
n->n_lineno = 0;
n->n_nchildren = 0;
n->n_child = NULL;
return n;
}
void
PyNode_Free(node *n)
{
if (n != NULL) {
freechildren(n);
PyObject_FREE(n);
}
}
static void
freechildren(node *n)
{
int i;
for (i = NCH(n); --i >= 0; )
freechildren(CHILD(n, i));
if (n->n_child != NULL)
PyObject_FREE(n->n_child);
if (STR(n) != NULL)
PyObject_FREE(STR(n));
}
NCH/CHILD/RCHILD/TYPE/STR是用来封装对node的成员的访问的。需要提一下的是,CHILD(n, i)是从左边开始算,传入i的是正数,而RCHILD(n, i)则是从右边往左,传入的参数i是负数。
PyNode_AddChild将一个新的子结点加入到子结点数组中。由于结点数量是动态变化的,因此在当前分配的结点数组大小不够的时候,Python会调用realloc重新分配内存。内存分配是一个非常耗时的动作,因此Python在PyNode_AddChild之中用到了和std::vector类似的技巧来尽量减少内存分配的次数,每次增长的时候都会根据某个规则进行RoundUp,而不是需要多少就分配多少。XXXROUNDUP函数负责进行此运算。n<=1时, 返回n。1<n<=128的时候,会RoundUp到4的倍数。n>128, 会调用fancy_roundup来RoundUp到2的幂。
#define XXXROUNDUP(n) ((n) <= 1 ? (n) :\
(n) <= 128 ? (((n) + 3) & ~3) : \
fancy_roundup(n))
有了XXXROUNDUP之后,实现PyNode_AddChild就非常直接了:
int
PyNode_AddChild(register node *n1, int type, char *str, int lineno, int col_offset)
{
const int nch = n1->n_nchildren;
int current_capacity;
int required_capacity;
node *n;
if (nch == INT_MAX || nch < 0)
return E_OVERFLOW;
current_capacity = XXXROUNDUP(nch);
required_capacity = XXXROUNDUP(nch + 1);
if (current_capacity < 0 || required_capacity < 0)
return E_OVERFLOW;
if (current_capacity < required_capacity) {
n = n1->n_child;
n = (node *) PyObject_REALLOC(n,
required_capacity * sizeof(node));
if (n == NULL)
return E_NOMEM;
n1->n_child = n;
}
n = &n1->n_child;
n->n_type = type;
n->n_str = str;
n->n_lineno = lineno;
n->n_col_offset = col_offset;
n->n_nchildren = 0;
n->n_child = NULL;
return 0;
}
值得注意的是该函数并没有记录下当前的最大容量,这个可以通过XXXROUNDUP(nch)计算出来。
PyParser
PyParser所作的事情就是根据token生成CST。整个生成树的过程其实也就是一个遍历语法图的过程。在前一篇文章中我们知道语法图是由多个DFA组成的,而输入的token和当前的所处的状态结点可以决定下一个状态结点。由于PyParser是在多个DFA中遍历,因此当结束了某个DFA的遍历需要回到上一个DFA,这些信息都是由一个专门的栈保存着。PyParser所对应的结构是parser_state,这个结构保存着PyParser的内部状态,如下:
typedef struct {
stackp_stack; /* Stack of parser states */
grammar *p_grammar; /* Grammar to use */
node *p_tree; /* Top of parse tree */
#ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD
unsigned long p_flags; /* see co_flags in Include/code.h */
#endif
} parser_state;
Field
Description
p_stack
PyParser状态栈,每一个状态都对应着某个DFA中的某个状态
p_grammar
语法图的指针
p_tree
CST的根结点
p_flags
PyParser的flags,唯一用到的是CO_FUTURE_WITH_STATEMENT
状态栈的定义如下:
typedef struct {
stackentry *s_top; /* Top entry */
stackentry s_base; /* Array of stack entries */
/* NB The stack grows down */
} stack;
状态栈中的状态如下:
typedef struct {
int s_state; /* State in current DFA */
dfa *s_dfa; /* Current DFA */
struct _node *s_parent; /* Where to add next node */
} stackentry;
Field
Description
s_state
当前DFA中的状态ID
s_dfa
当前DFA指针
s_parent
当前的parent结点。PyParser会把新的结点作为Child加到栈顶状态的s_parent结点中去
PyParser调用下面这些函数来操作栈:
static void s_reset(stack *s);
#define s_empty(s) ((s)->s_top == &(s)->s_base)
static int s_push(register stack *s, dfa *d, node *parent)
#define s_pop(s) (s)->s_top++
这些函数的实现非常简单,不再赘述。
PyParser所支持的“成员”函数如下:
parser_state *PyParser_New(grammar *g, int start);
void PyParser_Delete(parser_state *ps);
int PyParser_AddToken(parser_state *ps, int type, char *str,
int lineno, int col_offset,int *expected_ret);
void PyGrammar_AddAccelerators(grammar *g);
PyParser_New & PyParser_Delete显然是用于创建和销毁PyParser的实例的,和PyTokenizer一致。PyGrammar_AddAccelerators在我的前一篇Python源码研究4中提到过,主要用于处理Python的Grammar数据生成Accelerator加快语法分析的速度。在这些函数中,最为核心的是PyParser_AddToken,这个函数的作用是根据PyTokenizer所获得的token和当前所处的状态/DFA,跳转到下一个状态,并添加到CST中。在parsetok函数中,有如下的代码(省略了大部分):
parser_state *ps;
ps = PyParser_New(g, start);
for (;;) {
char *a, *b;
int type;
type = PyTokenizer_Get(tok, &a, &b);
PyParser_AddToken(ps, (int)type, str, tok->lineno, col_offset, &(err_ret->expected));
}
Parsetok的作用是分析某段代码。可以看到,parsetok会反复调用PyTokenizer_Get获得下一个token,然后将反复将获得的token传给PyParser_AddToken来逐步构造整个CST,当所有token都处理过了之后,整棵树也就建立完毕了。
PyParser API
Python不会直接调用PyParser和PyTokenizer的函数,而是直接调用下面的这些Python API:
PyAPI_FUNC(node *) PyParser_ParseString(const char *, grammar *, int,
perrdetail *);
PyAPI_FUNC(node *) PyParser_ParseFile (FILE *, const char *, grammar *, int,
char *, char *, perrdetail *);
PyAPI_FUNC(node *) PyParser_ParseStringFlags(const char *, grammar *, int,
perrdetail *, int);
PyAPI_FUNC(node *) PyParser_ParseFileFlags(FILE *, const char *, grammar *,
int, char *, char *,
perrdetail *, int);
PyAPI_FUNC(node *) PyParser_ParseStringFlagsFilename(const char *,
const char *,
grammar *, int,
perrdetail *, int);
/* Note that he following function is defined in pythonrun.c not parsetok.c. */
PyAPI_FUNC(void) PyParser_SetError(perrdetail *);
PyAPI_FUNC宏是用于定义公用的Python API,表明这些函数可以被外界调用。在Windows上面Python Core被编译成一个DLL,因此PyAPI_FUNC等价于大家常用的__declspec(dllexport)/__declspec(dllimport)。这些函数把PyParser和PyTokenizer对象的接口和细节包装起来,使用者可以直接调用PyParser_ParseXXXX函数来使用PyParser和PyTokenizer的功能而无需知道PyPaser/PyTokenizer的工作方式,这可以看作是一个典型的Façade模式。以PyParser_ParseFile为例,该函数分析传入的FILE返回生成的CST。其他的函数与此类似,只是分析的对象不同和传入参数的不同。
PyParser_AddToken implementation
在全面了解了PyParser的接口和调用方式之后,我们来看一下PyParser_AddToken的具体实现。PyParser_AddToken会调用3个内部函数来做处理:classify, push, shift
Classify根据type和str,确定对应的Label,实现如下:
static int
classify(parser_state *ps, int type, char *str)
{
grammar *g = ps->p_grammar;
register int n = g->g_ll.ll_nlabels;
if (type == NAME) {
register char *s = str;
register label *l = g->g_ll.ll_label;
register int i;
for (i = n; i > 0; i--, l++) {
if (l->lb_type != NAME || l->lb_str == NULL ||
l->lb_str != s ||
strcmp(l->lb_str, s) != 0)
continue;
#ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD
if (!(ps->p_flags & CO_FUTURE_WITH_STATEMENT)) {
if (s == 'w' && strcmp(s, "with") == 0)
<span style
页:
[1]