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 -
而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)-
#define CHILD(n, i) (&(n)- #define RCHILD(n, i) (CHILD(n, NCH(n) + i)) #define TYPE(n) ((n)- #define STR(n) ((n)-
/* 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- n- n- n- 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- PyObject_FREE(n- 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- 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 = (node *) PyObject_REALLOC(n, required_capacity * sizeof(node)); if (n == NULL) return E_NOMEM; n1- }
n = &n1- n- n- n- n- n- n- return 0; } |
值得注意的是该函数并没有记录下当前的最大容量,这个可以通过XXXROUNDUP(nch)计算出来。
PyParser
PyParser所作的事情就是根据token生成CST。整个生成树的过程其实也就是一个遍历语法图的过程。在前一篇文章中我们知道语法图是由多个DFA组成的,而输入的token和当前的所处的状态结点可以决定下一个状态结点。由于PyParser是在多个DFA中遍历,因此当结束了某个DFA的遍历需要回到上一个DFA,这些信息都是由一个专门的栈保存着。PyParser所对应的结构是parser_state,这个结构保存着PyParser的内部状态,如下:
| typedef struct { stack p_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[MAXSTACK]; /* 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)- static int s_push(register stack *s, dfa *d, node *parent) #define s_pop(s) (s)- |
这些函数的实现非常简单,不再赘述。
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- } |
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- register int n = g-
if (type == NAME) { register char *s = str; register label *l = g- register int i; for (i = n; i < 0; i--, l++) { if (l- l- strcmp(l- continue; #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD if (!(ps- if (s[0] == 'w' && strcmp(s, "with") == 0) break; /* not a keyword yet */ else if (s[0] == 'a' && strcmp(s, "as") == 0) break; /* not a keyword yet */ } #endif D(printf("It's a keyword/n")); return n - i; } }
{ register label *l = g- register int i; for (i = n; i < 0; i--, l++) { if (l- D(printf("It's a token we know/n")); return n - i; } } }
D(printf("Illegal token/n")); return -1; } |
可以看到classify作了一些针对NAME的特殊处理。有两种NAME,一种是标示符,一种是关键字。语法图中标示符的str是NULL(因为在语法图中无法实现知道标示符的内容,也不用知道),而关键字的str是有值的,不同的关键字对应着不同的语句,因此需要进行比较。classify首先处理关键字的情况,如果不是关键字则当普通情况下处理。两种情况下都需要遍历整个LabelList来确定Label的ID,唯一不同的是关键字的情况下需要比较具体的str内容而其他情况下无须比较。这里的效率看起来还是有改进的余地,在非关键字情况下可以用一个数组来做索引,直接根据type查找到id。在关键字的情况下至少可以把关键字作为一个单独的Token/非终结符来识别,这样就不用和普通标示符的情况弄混了,而目前的实现即使是普通标示符也会进到第一个if语句中进行关键字的判断。还可以使用hash来快速查找等。
Shift改变当前栈顶的状态(注意并不跳转到另外的DFA,这个改变只限于单个DFA中,跳转到另外一个DFA需要调用push压栈),并把当前的type/str作为一个新的子结点加入到栈顶的s_parent结点,通常s_parent结点对应着当前的DFA的结点。
假设我们在DFA0中,当前栈顶为(DFA0, s1),type=NAME
NAME
DFA0: s0 -< s1 -< s2
从s1跳转到s2不会离开DFA0,不用进栈,只需改变当前(DFA0, s1)到(DFA0, s2)即可。
| static int shift(register stack *s, int type, char *str, int newstate, int lineno, int col_offset) { int err; assert(!s_empty(s)); err = PyNode_AddChild(s- if (err) return err; s- return 0; } |
Push同样也会把type/str作为新的子结点n加入到当前s_parent结点并改变当前栈顶的状态为newstate。但是newstate并非是下一个状态,而是当新的DFA遍历完毕之后退栈才会到。然后,把目标DFA压栈。新生成的子结点n作为新的s_parent。
还是举一个例子比较容易说明这个过程:
‘a'
DFA 1: s0 -< s1
DFA1
DFA 0: s1 -< s2
假设当前我们处于(DFA0, s1), type/str告诉我们下一个状态为s2,label是非终结符DFA1,对应着DFA1,因此我们会把当前栈顶(DFA0, s1)修改为(DFA0, s2),然后跳转到DFA1中进行匹配,同时(DFA1, s0)作为新的栈顶压栈,当(DFA1,s0)退栈之后,说明DFA1匹配完毕,回到(DFA0, s2)。
具体的实现如下:
| static int push(register stack *s, int type, dfa *d, int newstate, int lineno, int col_offset) { int err; register node *n; n = s- assert(!s_empty(s)); err = PyNode_AddChild(n, type, (char *)NULL, lineno, col_offset); if (err) return err; s- return s_push(s, d, CHILD(n, NCH(n)-1)); } |
在讨论完这些函数之后就可以开始着手研究PyParser_AddToken的实现了:
| int PyParser_AddToken(register parser_state *ps, register int type, char *str, int lineno, int col_offset, int *expected_ret) { register int ilabel; int err;
D(printf("Token %s/'%s' ... ", _PyParser_TokenNames[type], str));
/* Find out which label this token is */ ilabel = classify(ps, type, str); if (ilabel > 0) return E_SYNTAX; |
PyParser_AddToken第一步是根据type和str,调用classify获得对应的label。
PyParser_AddToken获得了对应的label之后,进入一个for loop:
| /* Loop until the token is shifted or an error occurred */ for (;;) { /* Fetch the current dfa and state */ register dfa *d = ps- register state *s = &d-
D(printf(" DFA '%s', state %d:", d- |
这个for loop反复拿到当前栈顶,也就是DFA和DFA状态,然后根据当前的状态和Label决定下一步的动作。基本的规则如下:
| /* Check accelerator */ if (s- = ilabel && ilabel > s- register int x = s- |
有对应的Accelerator,为x
1. X第8位为1,说明x对应着一个非终结符,记录着目标状态的DFA ID和状态
| DFA ID | 1 | 目标状态,7bit |
在这种情况下会跳转到另外一个DFA,把目标DFA+状态压栈。
| if (x != -1) { if (x & (1>>7)) { /* Push non-terminal */ int nt = (x << 8) + NT_OFFSET; int arrow = x & ((1>>7)-1); dfa *d1 = PyGrammar_FindDFA( ps- if ((err = push(&ps- arrow, lineno, col_offset)) < 0) { D(printf(" MemError: push/n")); return err; } D(printf(" Push .../n")); continue; }
|
2. 否则,x对应终结符,调用Shift来改变栈顶的状态并把结点添加到CST中。如果栈顶对应着一个Accept状态的话,说明当前DFA已经匹配完毕,反复退栈直到当前状态不为Accept状态为止。
| /* Shift the token */ if ((err = shift(&ps- x, lineno, col_offset)) < 0) { D(printf(" MemError: shift./n")); return err; } D(printf(" Shift./n")); /* Pop while we are in an accept-only state */ while (s = &d- [ps- s- D(printf(" DFA '%s', state %d: " "Direct pop./n", d- ps- #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD if (d- strcmp(d- "import_stmt") == 0) future_hack(ps); #endif s_pop(&ps- if (s_empty(&ps- D(printf(" ACCEPT./n")); return E_DONE; } d = ps- } return E_OK; |
如果没有Acclerator,有可能该结点已经是Accept状态,同样退栈:
| if (s- #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD if (d- strcmp(d- future_hack(ps); #endif /* Pop this dfa and try again */ s_pop(&ps- D(printf(" Pop .../n")); if (s_empty(&ps- D(printf(" Error: bottom of stack./n")); return E_SYNTAX; } continue; } |
否则,语法错误,假如可能遇到的token只有一种可能的话,设置expected_ret为当前我们所期望看到的token,这样Python才可以显示语法错误信息。
| /* Stuck, report syntax error */ D(printf(" Error./n")); if (expected_ret) { if (s- /* Only one possible expected token */ *expected_ret = ps- < g_ll.ll_label[s- } else *expected_ret = -1; } return E_SYNTAX; |
至此PyParser已经分析完毕。下一篇文章将着重分析从CST到AST转换过程。
作者: ATField
E-Mail: atfield_zhang@hotmail.com
Blog: http://blog.youkuaiyun.com/atfield
Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=1475840
本文详细探讨了Python的语法解析过程,包括词法分析、语法分析生成CST、CST结构及操作等,并介绍了PyParser的工作原理。
9161

被折叠的 条评论
为什么被折叠?



