2022年 11月 9日

python实现语法分析器_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 {

shortn_type;

char*n_str;

intn_lineno;

intn_col_offset;

intn_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[i])

#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。1128, 会调用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[n1->n_nchildren++];

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 longp_flags;/* see co_flags in Include/code.h */

#endif

} parser_state;