• 编译原理:剖析python编译阶段


    Python编译器

    GDB跟踪python编译器的执行过程,在tokenizer.c的tok_get()函数中打一个断点,通过GDB查看python的运行,使用bt命令打印输出,结果如下图所示
    image
    image
    image
    image

    整理后可得到:
    image

    该过程就是运行python并执行到词法分析环节的一个执行路径:

    • 1.首先是 python.c,这个文件很短,只是提供了一个 main() 函数。你运行 python 命令的时候,就会先进入这里。

    • 2.接着进入 Modules/main.c 文件,这个文件里提供了运行环境的初始化等功能,它能执行一个 python 文件,也能启动 REPL 提供一个交互式界面。

    • 3.之后是 Python/pythonrun.c 文件,这是 Python 的解释器,它调用词法分析器、语法分析器和字节码生成功能,最后解释执行。

    • 4.再之后来到 Parser 目录的 parsetok.c 文件,这个文件会调度词法分析器和语法分析器,完成语法分析过程,最后生成 AST。

    • 5.最后是 toknizer.c,它是词法分析器的具体实现。

    REPL为Read-Evaluate-Print-Loop的所写,即通过一个交互界面接受输入并回显结果



    词法分析

    词法分析方法

    词法分析的任务就是:输入字符串,输出Token串,词法分析在英文中一般叫做Tokenizer
    具体实现:有个计算模型,叫做有限自动机(Finite-state Automaton,FSA),或者叫做有限状态自动机(Finite-state Machine,FSM)

    有限自动机它的状态数量是有限的,当它受到一个新字符的时候,会导致状态的转移。
    比如:下面的状态机能够区分标识符和数字字面量:
    image
    在这样一个状态机里,用单线圆圈表示临时状态,双线圆圈表示接受状态。接受状态就是一个合格的 Token,比如上图中的状态 1(数字字面量)和状态 2(标识符)。当这两个状态遇到空白字符的时候,就可以记下一个 Token,并回到初始态(状态 0),开始识别其他 Token。

    可看到,词法分析的过程,就是对一个字符串进行模式匹配的过程
    字符串模式匹配的工具 -- 正则表达式工具
    如:

    ps -ef | grep 's[a-h]'
    

    s[a-h]用来描述匹配规则,实现匹配所有包含"sa","sb",...,"sh"的字符串

    相同原理,正则表达式也可以用来描述词法规则,这种描述方法叫做:正则文法(Regular Grammar)
    比如:数字字面量和标识符的正则文法描述如下:

    IntLiteral : [0-9]+;          //至少有一个数字
    Id : [A-Za-z][A-Za-z0-9]*;    //以字母开头,后面可以是字符或数字
    

    可看到正则文法的格式为:Token类型:正则表达式,每个词法规则都采用这种格式,用于匹配一种Token

    词法规则里面要有优先级,比如排在前面的词法规则优先级更高。
    一个能够识别int关键字和标识符的有限自动机图示:
    image

    从正则表达式生成有限自动机

    上面已经了解了如何构造有限自动机以及如何处理词法规则的冲突,这样就可以实现手写词法分析器。但手写词法分析器的步骤太过繁琐,可只写出词法规则,自动生成对应的有限自动机

    词法分析器生成工具lex(及GNU版本的flex)也能够基于规则自动生成词法分析器。

    具体的实现思路如下:
    把一个正则表达式翻译成NFA,然后把NFA转换成DFA

    • DFA: Deterministic Finite Automation,即确定的有限自动机,特点就是:该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换
    • NFA: Nondeterministic Finite Automaton, 即不确定的有限自动机,特点就是:该状态机中存在某写状态,针对某些输入,不能做一个去诶的能够的转换
      这就有可以细分成两种情况:
    1. 对于一个输入,它有来能够个状态可以转换

    2.存在ε转换的情况,也就是没有任何字符输入的情况下,NFA也可以从一个状态迁移到另一个状态
    如:"a[a-zA-Z0-9]bc" 这个正则表达式,对字符串的要求是以a开头,以bc结尾,a和bc之间可以有任意多个字母或数字,如下图所示,状态1的节点输入b时,有两条路经可以选择:一条是迁移到状态2,另一条仍然保持在状态1,因此这一个有限自动机是一个NFA
    一个NFA例子--识别"a[a-zA-Z0-9]
    bc" 的自动机
    image
    NFA引入\(\varepsilon\)转换的画法如下图所示:
    另一个 NFA 的例子,同样能识别“a[a-zA-Z0-9]*bc”,其中有ε转换
    image

    无论是NFA还是DFA都等价于正则表达式,即所有的正则表达式都能转换成NFA或DFA;而所有的NFA或DFA,也都能转换成正则表达式

    一个正则表达式可以翻译成一个NFA,它的翻译方法如下:

    • 识别字符i的NFA

    当接受字符 i 的时候,引发一个转换,状态图的边上标注 i。其中,第一个状态(i,initial)是初始状态,第二个状态 (f,final) 是接受状态。
    image

    • 转换“s|t”这样的正则表达式
      它这个意思是或是s,或是t,两者二选一。s和t本身是两个自表达式,可以增加两个新的状态:开始状态和接受状态,然后,用\(\varepsilon\)转换分别连接代表s和t的子图。
      如下图所示--识别s|t的NFA
      image

    • 转换“st”这样的正则表达式
      s 之后接着出现 t,转换规则是把 s 的开始状态变成 st 整体的开始状态,把 t 的结束状态变成 st 整体的结束状态,并且把 s 的结束状态和 t 的开始状态合二为一。这样就把两个子图衔接了起来,走完 s 接着走 t。
      识别st的NFA如下图所示:
      image

    • 对于“?”“”和“+”这样的符号,它们的意思是可以重复 0 次、0 到多次、1 到多次,转换时要增加额外的状态和边
      识别"s
      "的NFA图:
      image
      可看到从i直接到f,也就是对s匹配0次,也可以在s的起止节点上循环多次。

    "s+",那就是s至少经过一次:
    image

    通过这样的转换,所有的正则表达式都可以转换为一个NFA

    基于NFA可以写一个正则表达式公式:
    举例:能够识别int关键字/标识符和数字字面量的正则表达式,这个表达式首先被表示为一个内部的树状数据结构,然后可以转换为NFA

    int | [a-zA-Z][a-zA-Z0-9]* | [0-9]*
    

    下面的输出结果中列出了所有的状态,以及每个状态到其他状态的转换,比如“0 ε -> 2”的意思是从状态 0 通过 ε 转换,到达状态 2 :

    NFA states:
    0  ε -> 2
      ε -> 8
      ε -> 14
    2  i -> 3
    3  n -> 5
    5  t -> 7
    7  ε -> 1
    1  (end)
      acceptable
    8  [a-z]|[A-Z] -> 9
    9  ε -> 10
      ε -> 13
    10  [0-9]|[a-z]|[A-Z] -> 11
    11  ε -> 10
      ε -> 13
    13  ε -> 1
    14  [0-9] -> 15
    15  ε -> 14
      ε -> 1
    

    可以图示的方式展示输出的结果:
    image

    有了NFA后,就要利用它来识别某个字符串,在做正则表达式的匹配过程中,存在这大量的回溯操作,效率比较低,那能否将NFA转成DFA,让字符串的匹配过程更简单呢? 这样整个过程就是一个自动化的过程,从正则表达式到NFA,再到DFA
    这个方法就是子集构造法

    image

    python的词法分析功能

    了解了词法分析的实现的实现原理,来看着python的词法分析是如何做的
    在查阅了tokenizer.c的tok_get()函数后,它也是通过有限自动机将字符串变成Token

    python源码词法分析功能剖析

    python词法分析的实现在Parser目录下的tokenizer.h和tokenizer.c。python的其他部分会直接调用tokenizer.h中定义的函数,如下:
    image

    这些函数均以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本身的状态,也就是内部的私有成员集合,定义如下:
    image
    其中最重要的是buf,cur,inp,end,start,这些字段决定了缓冲区的内容

    • buf:缓冲区的开始。假如PyTokenizer处于字符串模式,那么buf指向字符串本身,否则,指向文件读入的缓冲区。
    • cur:指向缓冲区中下一个字符。
    • inp:指向缓冲区中有效数据的结束位置。PyTokenizer是以行为单位进行处理的,每一行的内容存入从buf到inp之间,包括/n。一般情况下 ,PyTokenizer会直接从缓冲区中取下一个字符,一旦到达inp所指向的位置,就会准备取下一行。当PyTokenizer处于不同模式下面,具体的行为会稍有不同。
    • end:缓冲区的结束,在字符串模式下没有用到。
    • start:指向当前token的开始位置,如果现在还没有开始分析token,start为NULL。

    PyTokenizer_FromFile & PyTokenizer_FromString & PyTokenizer_FromUTF8

    这三种的实现大致相同,以PyTokenizer_FromString为例:

    /* Set up tokenizer for string */
    
    struct tok_state *
    PyTokenizer_FromString(const char *str, int exec_input)
    {
        struct tok_state *tok = tok_new();
        if (tok == NULL)
            return NULL;
        str = decode_str(str, exec_input, 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函数的实现:

    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;
    }
    

    PyTokenizer_Get返回值的int便是token的类型
    两个参数char **p_start, char **p_end为输出参数,指向token在pyTokenizer内部缓冲区的位置。

    这里采用返回一个p_start和p_end的意图是避免构造一份token内容的copy,而是直接给出token在缓冲区中的开始和结束的位置,以提高效率

    PyTokenizer_Get该函数的作用是在PyTokenizer所绑定的字符流(可以是字符串也可以而是文件)中取出一个token,比如sum=0,取到sum,那下一个取到的就是=

    一个返回的token由两部分参数描述:

    • 1.表示token类型的int
    • 2.btoken的具体内容,就是一个字符串

    python会把不同的token分成若干种类型,该类型定义在include/token.h中以宏的形式存在,如NAME,NUMBER,STRING等
    举例:
    "sum"这个token可以表示出(NAME,"sum")
    NAME是类型,表示sum是一个名称(注意和字符串区分开)

    此时python并不会判定该名称是关键字还是标识符,统一称为NAME

    tok_get函数

    PyTokenizer_Get函数实现中,核心代码就是tok_get函数
    tok_get函数主要负责做了以下几件事:

    • 1.处理缩进
      缩进的处理只在一行开始的时候。如果tok_state::atbol(at beginning of line)非0,说明当前处于一行的开始,否则不做处理。
        /* Get indentation level */
        if (tok->atbol) {
            int col = 0;
            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 / ALTTABSIZE + 1) * 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 if (tok->prompt != NULL && tok->lineno == 1) {
                    /* In interactive mode, if the first line contains
                       only spaces and/or a comment, let it through. */
                    blankline = 0;
                    col = altcol = 0;
                }
                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 */
            }
    

    最后,根据col和当前indstack的栈顶(也就是当前缩进的位置),确定是哪一种情况,具体请参看上面的代码。上面的代码有所删减,去掉了一些错误处理,加上了一点注释。
    注: PyTokenizer维护两个栈indstack & altindstack,分别对应col和altcol,保存着缩进的位置,而tok->indent保存着栈顶。

    		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--;
                    }
                }
            }
    

    确定token

    反复调用tok_nextc,获得下一个字符,依据字符内容判定是何种token,然后加以返回。

    /* Identifier (most frequent token!) */
        nonascii = 0;
        if (is_potential_identifier_start(c)) {
            /* Process the various legal combinations of b"", r"", u"", and f"". */
            int saw_b = 0, saw_r = 0, saw_u = 0, saw_f = 0;
            while (1) {
                if (!(saw_b || saw_u || saw_f) && (c == 'b' || c == 'B'))
                    saw_b = 1;
                /* Since this is a backwards compatibility support literal we don't
                   want to support it in arbitrary order like byte literals. */
                else if (!(saw_b || saw_u || saw_r || saw_f)
                         && (c == 'u'|| c == 'U')) {
                    saw_u = 1;
                }
                /* ur"" and ru"" are not supported */
                else if (!(saw_r || saw_u) && (c == 'r' || c == 'R')) {
                    saw_r = 1;
                }
                else if (!(saw_f || saw_b || saw_u) && (c == 'f' || c == 'F')) {
                    saw_f = 1;
                }
                else {
                    break;
                }
                c = tok_nextc(tok);
                if (c == '"' || c == '\'') {
                    goto letter_quote;
                }
            }
    		
    ......
    	
            *p_start = tok->start;
            *p_end = tok->cur;
    
    ......
            return NAME;
        }
    

    saw_x标识x是否出现过,用来支持这种场景: ur"" and ru"" are not supported
    假如当前字符是字母或者是下划线,则开始当作标示符进行分析,否则,继续执行下面的语句,处理其他的可能性。
    python中的字符串可以是用r/u/f开头,如r"string", u"string",f"string",r代表raw string, u代表unicode string,f代表格式化字符串常量

    一旦遇到了r或者u的情况下,直接跳转到letter_quote标号处,开始作为字符串进行分析。

    由于最后一次拿到的字符不属于当前标示符,应该被放到下一次进行分析,因此调用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处于模式的不同,处理方式会不太一样

    python词法分析直观展示

    image
    其中第二列是Token的类型,第三列是Token对应的字符串。各种Token类型的定义可以在 Grammar/Tokens 文件中找到
    image



    语法分析

    语法分析方法

    语法分析的核心知识点:两个基本功和两种算法思路

    • 两个基本功:第一,必须能够阅读和书写语法规则,也就是掌握上下文无关文法;第二,必须掌握递归下降算法
    • 两种算法思路:一种是自定向下的语法分析,另一种是自地向上的语法分析

    上下文无关文法(Contex-Free Grammar)

    在开始语法分析之前,需要解决的第一个问题:如何表达语法规则?
    以下面程序为例,里面用到了变量声明语句,加法表达式,看下语法规则怎么写。

     int a = 2;
     int b = a + 3;
     return b;
    
    • 第一种写法如下,跟词法规则差不多,左边表示规则名称,右边是正则表达式
    start:blockStmts ;               //起始
    block : '{' blockStmts '}' ;      //语句块
    blockStmts : stmt* ;              //语句块中的语句
    stmt = varDecl | expStmt | returnStmt | block;   //语句
    varDecl : type Id varInitializer? ';' ;         //变量声明
    type : Int | Long ;                              //类型
    varInitializer : '=' exp ;                       //变量初始化
    expStmt : exp ';' ;                              //表达式语句
    returnStmt : Return exp ';' ;                    //return语句
    exp : add ;                                      //表达式       
    add : add '+' mul | mul;                         //加法表达式
    mul : mul '*' pri | pri;                         //乘法表达式
    pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 
    

    在语法规则中,冒号左边的叫做非终结符(Non-terminal),又叫变元(Variable)。 非终结符可以按照右边的正则表达式来逐步展开,直到最后都变成了标识符,字面量,运算符等这些不可展开的符号,就是终结符(Terminal)终结符其实就是词法分析中形成的Token

    像这样左边是非终结符,右边是正则表达式的书写语法规则的方式,就叫做扩展巴科斯范式(EBNF)

    • 第二种写法,产生式(Production Relu),又叫做替换规则(Substitution Rule)
      产生式的左边是非终结符(变元),可以用右边的部分替代,中间通常用箭头链接
    add -> add + mul
    add -> mul
    mul -> mul * pri
    mul -> pri
    

    有个偷懒的写法,就是把同一个变元的多个产生式写在一起,用竖线分割(但此时,如果产生式里原本就要用到"|"总结符,那就要用引号来及你想那个区分)

    add -> add + mul | mul
    mul -> mul * pri | pri
    

    在产生式中,不用"" 和 "+" 来表示重复,而是引入" ε"(空字符串),所以“blockStmts : stmt”可以写成下面这个样子:

    blockStmts -> stmt blockStmts | ε
    

    总结起来,语法规则是由4个组成部分组成

    • 一个有穷的非终结符(或变元)的集合;

    • 一个有穷的终结符的集合;

    • 一个有穷的产生式集合;

    • 一个起始非终结符(变元)。

    那么符合这四个特点的文法规则,就叫做上下文无关文法(Context-Free Grammar,CFG)

    上下文无关文法和词法分析中用到的正则文法是否有一定的关系?
    答案是有的,正则文法是上下文无关文法的一个子集。其实,这个正则文法也可以写成产生式的格式。比如,数字字面量(正则表达式为:"[0-9]+")可以写成:

    IntLiteral -> Digit IntLiteral1
    IntLiteral1 -> Digit IntLiteral1 
    IntLiteral1 -> ε
    Digit -> [0-9]
    

    但在上下文无关文法中,产生式的右边可以放置任意的终结符和非终结符,而正则文法只是其中的一个子集,叫做线性文法(Linear Grammar)。它的特点就是产生式的右边部分最多 只有一个非终结符,比如X->aYb,其中a和b是终结符
    正则文法是上下文无关文法的子集:
    image

    上下文相关文法:存在上下文相关的,比如,在高级语言中,本地变量必须先声明,才能在后面使用,这种制约关系就是上下文相关的

    在语法分析阶段,并不关注上下文之间的依赖关系,这样使得语法分析的任务更加简单。至于上下文相关的情况,就交给语义分析阶段去处理了

    接下来,就要根据语法规则,编写语法分析程序,把Token串转化为AST

    梯度下降算法(Recursive Descent Parsing)

    基本思路就是按照语法规则去匹配Token串

    举例:
    变量声明语句的规则如下:

    varDecl : types Id varInitializer? ';' ;        //变量声明
    varInitializer : '=' exp ;                       //变量初始化
    exp : add ;                                      //表达式       
    add : add '+' mul | mul;                         //加法表达式
    mul : mul '*' pri | pri;                         //乘法表达式
    pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式
    

    写成产生式格式,如下:

    varDecl -> types Id varInitializer ';'
    varInitializer -> '=' exp
    varInitializer -> ε
    exp -> add
    add -> add + mul
    add -> mul
    mul -> mul * pri
    mul -> pri
    pri -> IntLiteral
    pri -> Id
    pri -> ( exp )
    

    基于这个规则做解析的算法如下:

    匹配一个数据类型(types)
    匹配一个标识符(Id),作为变量名称
    匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
       匹配一个等号
       匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
       创建一个varInitializer对应的AST节点并返回
    如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
    匹配一个分号
    创建一个varDecl对应的AST节点并返回
    

    用上述算法解析"int a = 2",就会生成下面的AST:
    image

    总结起来,递归下降算法的特点如下:

    • 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。

    • 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式

    • 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)

    递归下降非常容易理解,能够有效处理多种语法规则,但它有两个缺点:

    • 第一个缺点:就是著名的左递归(Left Recursion)问题。

    什么是左递归问题呢?
    比如,在匹配算术表达式时,产生式的第一项是就是非终结符add,那按照算法,要下降一层,继续匹配add.这个过程会一直持续下去,无限递归。
    image
    所以递归下降算法是无法处理左递归问题的。那改成有递归,也就是把add这个递归项放在右边 -- 虽然规避了左递归的问题,但同时又导致了结合性的问题
    那怎么办呢?
    把递归调用转换成循环

    在EBNF格式中,允许使用"" 号和"+"号表示重复:
    image
    对于('+'mul)
    这部分,可以写成一个循环,在这个循环里,可以根据结合性的要求,手工生成正确的AST,它的伪代码如下:

    左子节点 = 匹配一个mul
    while(下一个Token是+){
      消化掉+
      右子节点 = 匹配一个mul
      用左、右子节点创建一个add节点
      左子节点 = 该add节点
    }
    

    创建正确的AST如下图所示:
    image

    • 第二个缺点:就是当产生式匹配失败的时候,必须要“回溯”,这就可能导致浪费

    LL算法:计算First和Follow集合

    LL算法家族可自动计算出选择不同产生式的依据

    LL算法的要点:计算First和Follow集合
    First集合是每个产生式开头可能出现的Token集合,像stmt有三个产生式,它的First集合如下:
    image
    而 stmt 的 First 集合,就是三个产生式的 First 集合的并集,也是 Int Long IntLiteral Id ( Return。

    总体来说,针对对非终结符x,它的First集合的计算规则如下:

    • 如果产生式以终结符开头,那么把这个终结符加入 First(x);
    • 如果产生式以非终结符 y 开头,那么把 First(y) 加入 First(x);
    • 如果 First(y) 包含ε,那要把下一个项的 First 集合也加入进来,以此类推;
    • 如果 x 有多个产生式,那么 First(x) 是每个产生式的并集。

    在计算First集合时,具体采用"不动点法" ,可参考实例程序FirstFollowSet类的 CalcFirstSets() 方法,运行示例程序能打印各个非终结符的 First 集合。

    有种特殊的情况需要考虑,那就是对于某个非终结符,它自身会产生的ε情况
    比如,示例文法中的blockStmts,它是可能产生ε的,也就是块中一个语句都没有。

    block : '{' blockStmts '}' ;                 //语句块
    blockStmts : stmt* ;                         //语句块中的语句
    stmt = varDecl | expStmt | returnStmt;       //语句
    

    语法解析器在这个时候预读的下一个 Token 是什么呢?是右花括号。这证明 blockStmts 产生了ε,所以才读到了后续跟着的花括号。

    对于某个非终结符后面可能跟着的 Token 的集合,我们叫做 Follow 集合。如果预读到的 Token 在 Follow 中,那么我们就可以判断当前正在匹配的这个非终结符,产生了ε。

    Follow的算法规则如下:(以非终结符 x为例)

    • 扫描语法规则,看看 x 后面都可能跟着哪些符号;

    • 对于后面跟着的终结符,都加到 Follow(x) 集合中去;

    • 如果后面是非终结符 y,就把 First(y) 加 Follow(x) 集合中去;

    • 最后,如果 First(y) 中包含ε,就继续往后找;

    • 如果 x 可能出现在程序结尾,那么要把程序的终结符 $ 加入到 Follow(x) 中去。

    这样在计算了 First 和 Follow 集合之后,你就可以通过预读一个 Token,来完全确定采用哪个产生式。这种算法,就叫做 LL(1) 算法。

    LL(1) 中的第一个 L,是 Left-to-right 的缩写,代表从左向右处理 Token 串。第二个 L,是 Leftmost 的缩写,意思是最左推导, LL(1)中的1,指的是预读一个Token

    最左推导:就是它总是先把产生式中最左侧的非终结符展开完毕后,再展开下一个,相当于对AST从左子节点开始的深度优先遍历

    LR算法:移进和规约

    前面讲的递归下降和LL算法,都是自顶向下的算法,自底向上的其中代表就是LR算法
    LR含义解读: L 还是代表从左到右读入 Token,而 R 是最右推导(Rightmost)的意思

    自顶向下的算法,是从根节点逐层往下分解,形成最后的AST;而LR算法的原理则是从底下先拼凑出AST的一些局部拼图,并逐步组装成一颗完整的AST,所以,其中的关键在于如何"拼凑"

    假设采用下面的上下文无关文法,推演一个实例,具体语法规则如下:
    image
    如果用来解析"2+3*5",最终会形成下面的AST:
    image

    那算法是如何从底部拼凑出来这棵AST呢?
    LR 算法和 LL 算法一样,也是从左到右地消化掉 Token。

    在第 1 步,它会取出“2”这个 Token,放到一个栈里,这个栈是用来组装 AST 的工作区。同时,它还会预读下一个 Token,也就是“+”号,用来帮助算法做判断。

    在下面的示意图里,画了一条橙色竖线,竖线的左边是栈,右边是预读到的一个 Token。在做语法解析的过程中,竖线会不断地往右移动,把 Token 放到栈里,这个过程叫做“移进”(Shift)
    第一步,移进一个Token:
    image
    注意:在上图中使用虚线推测了AST的其他部分。也就是如果第一个Token遇到的是整型字面量,而后面跟着一个+号,那么这两个Token就决定了它们必然是这棵推测出来的AST的一部分。而图中右边就是它的推导过程,其中的每个步骤,都是用了一个产生式加了一个点(如".add"),这个点相当于图中左边的橙色竖线

    可根据这棵假想的AST,即依据推想的推导过程,给它反推回去 ,把Int还原成pri,这个还原过程就叫做"规约(Reduce)"。工作区里的元素也随之更新成pri

    第2步:Int规约为pri
    image

    按照这样的思路,不断地移进和规约,这棵 AST 中推测出来的节点会不断地被证实。而随着读入的 Token 越来越多,这棵 AST 也会长得越来越高,整棵树变得更大。下图是推导过程中间的一个步骤。
    移进和规约过程中的一个步骤:
    image

    最后,整个AST构造完毕,而工作区里也就只剩下一个Start节点。
    最后一步,add规约为start
    image

    2+3*5”最右推导的过程写在了下面,而如果你从最后一行往前一步步地看,它恰好就是规约的过程。
    image

    如果你见到 LR(k),那它的意思就是会预读 k 个 Token,我们在示例中采用的是 LR(1)

    注:

    相对于 LL 算法,LR 算法的优点是能够处理左递归文法。但它也有缺点,比如不利于输出全面的编译错误信息。因为在没有解析完毕之前,算法并不知道最后的 AST 是什么样子,所以也不清楚当前的语法错误在整体 AST 中的位置。

    image

    Python的语法分析功能

    继续使用GDB跟踪执行过程,会在parser.c中找到语法分析的相关逻辑:
    image

    先来看下,Grammar文件,这个是用EBNF语法编写的Python语法规则文件。

    //声明函数
    funcdef: 'def' NAME parameters ['->' test] ':' [TYPE_COMMENT] func_body_suite
    //语句
    simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
    small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
                 import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
    

    该规则文件,实际上python的编译器本身并不使用它,它是给一个pgen的工具程序(Parser/pgen)使用的,
    这个程序能够基于语法规则生成解析表(Parse Table),供语法分析程序使用。

    有了 pgen 这个工具,你就可以通过修改规则文件来修改 Python 语言的语法,比如,你可以把函数声明中的关键字“def”换成“function”,这样你就可以用新的语法来声明函数。

    在Makefile.pre.in文件中可找到该工具的编译命令,最后生成:Lib/keyword.py文件:
    image

    pgen能给生成新的语法解析器(在parser.c的注释中讲解了它的工作原理):它是把EBNF转化为一个NFA,然后再把这个NFA转化为DFA,基于这个DFA,在读取Token的时候,编译器就知道如何做状态迁移,并生成解析树

    Python 用的是 LL(1) 算法。LL(1) 算法的特点:针对每条语法规则,最多预读一个 Token,编译器就可以知道该选择哪个产生式。这其实就是一个 DFA,从一条语法规则,根据读入的 Token,迁移到下一条语法规则

    通过一个例子,来看下一个python的语法分析特点,语法规则如下:

    add: mul ('+' mul)* 
    mul: pri ('*' pri)* 
    pri: IntLiteral | '(' add ')'
    

    其对应DFA如下:
    add: mul ('+' mul)*对应的DFA:
    image

    mul: pri ('' pri)对应的DFA:
    image

    pri: IntLiteral | '(' add ')'对应的DFA:
    image

    parser.c 用了一个通用的函数去解析所有的语法规则,它所依据的就是为每个规则所生成的 DFA。
    主要的实现逻辑是在** parser.c ** 的PyParser_AddToken()函数里

    为了便于理解,模仿 Python 编译器,用上面的文法规则解析了一下“2+3*4+5”,并把整个解析过程画成图。

    在解析的过程,用了一个栈作为一个工作区,来保存当前解析过程中使用的 DFA。

    第1步,匹配add规则。 把add对应的DFA压到栈里,此刻DFA处于状态0,这时候预读了一个Token,是字面量2
    image

    第2步,根据add的DFA,走mul-1这条边,去匹配mul规则。 这时把mul对应的DFA入栈。在示意图中,栈从上往下延伸的。
    image

    第3步,根据mul的DFA,走pri-1这条边,去匹配pri规则。 这时把pri对应的DFA入栈。
    image

    第4步,根据pri的DFA,因为预读的Token是字面量2,所以移进这个字面量,并迁移到状态3。同时,为字面量2建立解析树的节点。 这个时候,又会预读下一个Token,'+'号
    image

    第 5 步,从栈里弹出 pri 的 DFA,并建立 pri 节点。 因为成功匹配了一个 pri,所以 mul 的 DFA 迁移到状态 1。
    image

    第 6 步,因为目前预读的 Token 是'+'号,所以 mul 规则匹配完毕,把它的 DFA 也从栈里弹出。 而 add 对应的 DFA 也迁移到了状态 1。
    image

    第 7 步,移进'+'号,把 add 的 DFA 迁移到状态 2,预读了下一个 Token:字面量 3。 这个 Token 是在 mul 的 First 集合中的,所以就走 mul-2 边,去匹配一个 mul。
    image

    按照这个思路继续做解析,直到最后,可以得到完整的解析树:
    image

    总结起来,Python 编译器采用了一个通用的语法分析程序,以一个作为辅助的数据结构,来完成各个语法规则的解析工作。当前正在解析的语法规则对应的 DFA,位于栈顶。一旦当前的语法规则匹配完毕,那语法分析程序就可以把这个 DFA 弹出,退回到上一级的语法规则。

    经过上面的语法分析,形成的结果叫做解析树(Parse Tree), 又可叫做CST(Concrete Syntax Tree,具体语法树),和AST(抽象语法树)是相对的:一个具体,一个抽象
    它们两个的区别在于:CST精确地反映了语法规则的推导过程,而AST则更准确地表达了程序的结构。如果说CST是"形似",那AST就是"神似"。

    前面例子形成的CST的特点:
    image
    首先,加法是个二元运算符,但在这里 add 节点下面对应了两个加法运算符,跟原来加法的语义不符。第二,很多节点都只有一个父节点,这个其实可以省略,让树结构更简洁。

    所期待的AST是这个样子的:
    image

    python的语法分析生成的是CST, 而不是AST,之后Python会调用PyAst_FromNode将CST转换成AST.

    CST结构与方法

    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;
        int                 n_end_lineno;
        int                 n_end_col_offset;
    } node;
    

    image

    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,
                                    int end_lineno, int end_col_offset);
    PyAPI_FUNC(void) PyNode_Free(node *n);
    #ifndef Py_LIMITED_API
    PyAPI_FUNC(Py_ssize_t) _PyNode_SizeOf(node *n);
    #endif
    
    /* 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)
    #define LINENO(n)       ((n)->n_lineno)
    
    /* 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 *);
    void _PyNode_FinalizeEndPos(node *n);  // helper also used in parsetok.c
    

    PyNode_NewPyNode_Free负责创建和释放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_end_lineno = 0;
        n->n_end_col_offset = -1;
        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 ? (int)_Py_SIZE_ROUND_UP((n), 4) :    \
                   fancy_roundup(n))
    
    /* Round up size "n" to be a multiple of "a". */
    #define _Py_SIZE_ROUND_UP(n, a) (((size_t)(n) + \
            (size_t)((a) - 1)) & ~(size_t)((a) - 1))
    

    使用XXXROUNDUP函数计算出来当前的最大容量
    分配完空间后,就可以开始赋值了

       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_end_lineno = end_lineno;  // this and below will be updates after all children are added.
        n->n_end_col_offset = end_col_offset;
        n->n_nchildren = 0;
        n->n_child = NULL;
    

    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;
    

    image

    状态栈的定义如下:

    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 */
        const dfa       *s_dfa;         /* Current DFA */
        struct _node    *s_parent;      /* Where to add next node */
    } stackentry;
    

    image

    PyParser可调用这些函数来操作栈:

    /* STACK DATA TYPE */
    
    static void s_reset(stack *);
    
    #define s_empty(s) ((s)->s_top == &(s)->s_base[MAXSTACK])
    
    static int
    s_push(stack *s, const 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 end_lineno, int end_col_offset,
                          int *expected_ret);
    void PyGrammar_AddAccelerators(grammar *g);
    

    PyParser_New & PyParser_Delete显然是用于创建和销毁PyParser的实例的,和PyTokenizer一致。
    PyGrammar_AddAccelerators主要用于处理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不会直接调用PyParserPyTokenizer的函数,而是直接调用下面的这些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_AddToken会调用3个内部函数来做处理:classify, push, shift

    • Classify根据type和str,确定对应的Label,实现如下:
    
    static int
    classify(parser_state *ps, int type, const char *str)
    {
        grammar *g = ps->p_grammar;
        int n = g->g_ll.ll_nlabels;
    
        if (type == NAME) {
            const label *l = g->g_ll.ll_label;
            int i;
            for (i = n; i > 0; i--, l++) {
                if (l->lb_type != NAME || l->lb_str == NULL ||
                    l->lb_str[0] != str[0] ||
                    strcmp(l->lb_str, str) != 0)
                    continue;
    #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD
    #if 0
                /* Leaving this in as an example */
                if (!(ps->p_flags & CO_FUTURE_WITH_STATEMENT)) {
                    if (str[0] == 'w' && strcmp(str, "with") == 0)
                        break; /* not a keyword yet */
                    else if (str[0] == 'a' && strcmp(str, "as") == 0)
                        break; /* not a keyword yet */
                }
    #endif
    #endif
                D(printf("It's a keyword\n"));
                return n - i;
            }
        }
    
        {
            const label *l = g->g_ll.ll_label;
            int i;
            for (i = n; i > 0; i--, l++) {
                if (l->lb_type == type && l->lb_str == NULL) {
                    D(printf("It's a token we know\n"));
                    return n - i;
                }
            }
        }
    
        D(printf("Illegal token\n"));
        return -1;
    }
    

    classify作了一些针对NAME的特殊处理。有两种NAME,一种是标示符,一种是关键字

    • Shift改变当前栈顶的状态(注意并不跳转到另外的DFA,这个改变只限于单个DFA中,跳转到另外一个DFA需要调用push压栈),并把当前的type/str作为一个新的子结点加入到栈顶的s_parent结点,通常s_parent结点对应着当前的DFA的结点。
      假设我们在DFA0中,当前栈顶为(DFA0, s1),type=NAME
      image

    从s1跳转到s2不会离开DFA0,不用进栈,只需改变当前(DFA0, s1)到(DFA0, s2)即可。

    static int
    shift(stack *s, int type, char *str, int newstate, int lineno, int col_offset,
          int end_lineno, int end_col_offset)
    {
        int err;
        assert(!s_empty(s));
        err = PyNode_AddChild(s->s_top->s_parent, type, str, lineno, col_offset,
                              end_lineno, end_col_offset);
        if (err)
            return err;
        s->s_top->s_state = newstate;
        return 0;
    }
    
    • Push同样也会把type/str作为新的子结点n加入到当前s_parent结点并改变当前栈顶的状态为newstate。但是newstate并非是下一个状态,而是当新的DFA遍历完毕之后退栈才会到。然后,把目标DFA压栈。新生成的子结点n作为新的s_parent。
      image
      假设当前我们处于(DFA0, s1), type/str告诉我们下一个状态为s2,label是非终结符DFA1,对应着DFA1,因此我们会把当前栈顶(DFA0, s1)修改为(DFA0, s2),然后跳转到DFA1中进行匹配,同时(DFA1, s0)作为新的栈顶压栈,当(DFA1,s0)退栈之后,说明DFA1匹配完毕,回到(DFA0, s2)。
    static int
    push(stack *s, int type, const dfa *d, int newstate, int lineno, int col_offset,
         int end_lineno, int end_col_offset)
    {
        int err;
        node *n;
        n = s->s_top->s_parent;
        assert(!s_empty(s));
        err = PyNode_AddChild(n, type, (char *)NULL, lineno, col_offset,
                              end_lineno, end_col_offset);
        if (err)
            return err;
        s->s_top->s_state = newstate;
        return s_push(s, d, CHILD(n, NCH(n)-1));
    }
    

    接下来来看下:PyParser_AddToken函数的实现

    int
    PyParser_AddToken(parser_state *ps, int type, char *str,
                      int lineno, int col_offset,
                      int end_lineno, int end_col_offset,
                      int *expected_ret)
    {
        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 */
            const dfa *d = ps->p_stack.s_top->s_dfa;
            state *s = &d->d_state[ps->p_stack.s_top->s_state];
    
            D(printf(" DFA '%s', state %d:",
                d->d_name, ps->p_stack.s_top->s_state));
    

    这个for loop反复拿到当前栈顶,也就是DFA和DFA状态,然后根据当前的状态和Label决定下一步的动作。基本的规则如下:

    /* Check accelerator */
            if (s->s_lower <= ilabel && ilabel < s->s_upper) {
                int x = s->s_accel[ilabel - s->s_lower];
    

    有对应的Accelerator,为x

    X第8位为1,说明x对应着一个非终结符,记录着目标状态的DFA ID和状态
    image

    在这种情况下会跳转到另外一个DFA,把目标DFA+状态压栈。

    if (x != -1) {
                    if (x & (1<<7)) {
                        /* Push non-terminal */
                        int nt = (x >> 8) + NT_OFFSET;
                        int arrow = x & ((1<<7)-1);
                        if (nt == func_body_suite && !(ps->p_flags & PyCF_TYPE_COMMENTS)) {
                            /* When parsing type comments is not requested,
                               we can provide better errors about bad indentation
                               by using 'suite' for the body of a funcdef */
                            D(printf(" [switch func_body_suite to suite]"));
                            nt = suite;
                        }
                        const dfa *d1 = PyGrammar_FindDFA(
                            ps->p_grammar, nt);
                        if ((err = push(&ps->p_stack, nt, d1,
                            arrow, lineno, col_offset,
                            end_lineno, end_col_offset)) > 0) {
                            D(printf(" MemError: push\n"));
                            return err;
                        }
                        D(printf(" Push '%s'\n", d1->d_name));
                        continue;
                    }
    

    否则,x对应终结符,调用Shift来改变栈顶的状态并把结点添加到CST中。如果栈顶对应着一个Accept状态的话,说明当前DFA已经匹配完毕,反复退栈直到当前状态不为Accept状态为止。

     /* Shift the token */
                  if ((err = shift(&ps->p_stack, type, str,
                                  x, lineno, col_offset,
                                  end_lineno, end_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->d_state
                                  [ps->p_stack.s_top->s_state],
                      s->s_accept && s->s_narcs == 1) {
                      D(printf("  DFA '%s', state %d: "
                               "Direct pop.\n",
                               d->d_name,
                               ps->p_stack.s_top->s_state));
    #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD
    #if 0
                      if (d->d_name[0] == 'i' &&
                          strcmp(d->d_name,
                             "import_stmt") == 0)
                          future_hack(ps);
    #endif
    #endif
                      s_pop(&ps->p_stack);
                      if (s_empty(&ps->p_stack)) {
                          D(printf("  ACCEPT.\n"));
                          return E_DONE;
                      }
                      d = ps->p_stack.s_top->s_dfa;
                  }
                  return E_OK;
    

    如果没有Acclerator,有可能该结点已经是Accept状态,同样退栈:

    if (s->s_accept) {
    #ifdef PY_PARSER_REQUIRES_FUTURE_KEYWORD
    #if 0
                if (d->d_name[0] == 'i' &&
                    strcmp(d->d_name, "import_stmt") == 0)
                    future_hack(ps);
    #endif
    #endif
                /* Pop this dfa and try again */
                s_pop(&ps->p_stack);
                D(printf(" Pop ...\n"));
                if (s_empty(&ps->p_stack)) {
                    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->s_lower == s->s_upper - 1) {
                    /* Only one possible expected token */
                    *expected_ret = ps->p_grammar->
                        g_ll.ll_label[s->s_lower].lb_type;
                }
                else
                    *expected_ret = -1;
            }
            return E_SYNTAX;
    

    至此,PyParser的语法分析就结束了,接下来就是要将CST转换成AST以及字节码

    那么,Python 是如何把 CST 转换成 AST 的呢?这个过程分为两步。

    • 首先,Python 采用了一种叫做 ASDL 的语言,来定义了 AST 的结构。ASDL是“抽象语法定义语言(Abstract Syntax Definition Language)”的缩写,它可以用于描述编译器中的 IR 以及其他树状的数据结构。

    这个定义文件是 Parser/Python.asdl。CPython 编译器中包含了两个程序(Parser/asdl.py 和 Parser/asdl_c.py)来解析 ASDL 文件,并生成 AST 的数据结构。最后的结果在 Include/Python-ast.h 文件中。

    在Makefile.pre.in文件中可看到,Include/Python-ast.h正是通过 Parser/asdl_c.py -h解析生成的

    .PHONY=regen-ast
    regen-ast:
    	# Regenerate Include/Python-ast.h using Parser/asdl_c.py -h
    	$(MKDIR_P) $(srcdir)/Include
    	$(PYTHON_FOR_REGEN) $(srcdir)/Parser/asdl_c.py \
    		-h $(srcdir)/Include/Python-ast.h.new \
    		$(srcdir)/Parser/Python.asdl
    	$(UPDATE_FILE) $(srcdir)/Include/Python-ast.h $(srcdir)/Include/Python-ast.h.new
    	# Regenerate Python/Python-ast.c using Parser/asdl_c.py -c
    	$(MKDIR_P) $(srcdir)/Python
    	$(PYTHON_FOR_REGEN) $(srcdir)/Parser/asdl_c.py \
    		-c $(srcdir)/Python/Python-ast.c.new \
    		$(srcdir)/Parser/Python.asdl
    	$(UPDATE_FILE) $(srcdir)/Python/Python-ast.c $(srcdir)/Python/Python-ast.c.new
    

    Include/Python-ast.h定义了AST所用到的类型,以stmt_ty类型为例:

    enum _stmt_kind {FunctionDef_kind=1, AsyncFunctionDef_kind=2, ClassDef_kind=3,
                      Return_kind=4, Delete_kind=5, Assign_kind=6,
                      AugAssign_kind=7, AnnAssign_kind=8, For_kind=9,
                      AsyncFor_kind=10, While_kind=11, If_kind=12, With_kind=13,
                      AsyncWith_kind=14, Raise_kind=15, Try_kind=16,
                      Assert_kind=17, Import_kind=18, ImportFrom_kind=19,
                      Global_kind=20, Nonlocal_kind=21, Expr_kind=22, Pass_kind=23,
                      Break_kind=24, Continue_kind=25};
    struct _stmt {
        enum _stmt_kind kind;
        union {
            struct {
                identifier name;
                arguments_ty args;
                asdl_seq *body;
                asdl_seq *decorator_list;
                expr_ty returns;
                string type_comment;
            } FunctionDef;
    
            struct {
                identifier name;
                arguments_ty args;
                asdl_seq *body;
                asdl_seq *decorator_list;
                expr_ty returns;
                string type_comment;
            } AsyncFunctionDef;
    
            struct {
                identifier name;
                asdl_seq *bases;
                asdl_seq *keywords;
                asdl_seq *body;
                asdl_seq *decorator_list;
            } ClassDef;
    
    	// . . . 过长,省略
    
        } v;
        int lineno;
        int col_offset;
        int end_lineno;
        int end_col_offset;
    };
    
    typedef struct _stmt *stmt_ty; 
    

    stmt_ty是语句结点类型,实际上就是_stmt结构的指针,_stmt结构比较长,但有着很清晰的Pattern:

    • 第一个Field为kind,代表语句的类型,_stmt_kind定义了_stmt的所有可能的语句类型,从函数定义语句,类定义语句直到Continue语句共有25种类型。
    • 第二个Field为union v,每个成员都是struct,分别对应_stmt_kind中的一种类型,如_stmt.v.FunctionDef对应了_stmt_kind枚举中的FunctionDef_Kind,也就是说,当_stmt.kind == FunctionDef_Kind时,_stmt.v.FunctionDef中保存的就是对应的函数定义语句的具体内容。
    • 剩下的就是其他数据,如lineno,co_offset等

    大部分的AST结点类型都是按照类似的pattern来定义的。除此之外,还有一个种比较简单的AST类型如operator_ty,expr_context_ty等,由于这些类型仍以_ty结尾,因此也可以认为是AST的结点,但实际上,这些类型只是简单的枚举类型,并非指针。因此在以后的文章中,并不把此类AST类型作为结点看待,而是作为简单的枚举处理。

    由于每个AST类型会在union中引用其他的AST,这样层层引用,最后便形成了一颗AST树,试举例如下:
    image
    这颗AST树代表的是单条语句a+1。

    与AST类型对应,在python_ast.h/c中定义了大量用于创建AST结点的函数,可以看作是AST结点的构造函数。以BinOp为例

    expr_ty
    BinOp(expr_ty left, operator_ty op, expr_ty right, int lineno, int col_offset,
          int end_lineno, int end_col_offset, PyArena *arena)
    {
        expr_ty p;
        if (!left) {
            PyErr_SetString(PyExc_ValueError,
                            "field left is required for BinOp");
            return NULL;
        }
        if (!op) {
            PyErr_SetString(PyExc_ValueError,
                            "field op is required for BinOp");
            return NULL;
        }
        if (!right) {
            PyErr_SetString(PyExc_ValueError,
                            "field right is required for BinOp");
            return NULL;
        }
        p = (expr_ty)PyArena_Malloc(arena, sizeof(*p));
        if (!p)
            return NULL;
        p->kind = BinOp_kind;
        p->v.BinOp.left = left;
        p->v.BinOp.op = op;
        p->v.BinOp.right = right;
        p->lineno = lineno;
        p->col_offset = col_offset;
        p->end_lineno = end_lineno;
        p->end_col_offset = end_col_offset;
        return p;
    }
    

    此函数只是根据传入的参数做一些简单的错误检查,分配内存,初始化对应的expr_ty类型,并返回指针。

    asdl_seq & asdl_int_seq

    stmt_ty定义中,可以发现其中大量的用到了adsl_seq类型。

    adsl_seq & adsl_int_seq简单来说就是一个动态构造出来的定长数组。

    adsl_seq是void *的数组:

    typedef struct {
        Py_ssize_t size;
        void *elements[1];
    } asdl_seq;
    

    asdl_int_seq是int类型的数组:

    typedef struct {
        Py_ssize_t size;
        int elements[1];
    } asdl_int_seq;
    

    sizePy_ssize_t类型的,其实它就是一个long int 类型的变量,elements为数组的元素。定义elements数组长度为1,而在动态分配内存时则是按照实际长度sizeof(adsl_seq)+sizeof(void *) * (size - 1)来分配的:

    asdl_seq *
    _Py_asdl_seq_new(Py_ssize_t size, PyArena *arena)
    {
        asdl_seq *seq = NULL;
        size_t n;
    
        /* check size is sane */
        if (size < 0 ||
            (size && (((size_t)size - 1) > (SIZE_MAX / sizeof(void *))))) {
            PyErr_NoMemory();
            return NULL;
        }
        n = (size ? (sizeof(void *) * (size - 1)) : 0);
    
        /* check if size can be added safely */
        if (n > SIZE_MAX - sizeof(asdl_seq)) {
            PyErr_NoMemory();
            return NULL;
        }
        n += sizeof(asdl_seq);
    
        seq = (asdl_seq *)PyArena_Malloc(arena, n);
        if (!seq) {
            PyErr_NoMemory();
            return NULL;
        }
        memset(seq, 0, n);
        seq->size = size;
        return seq;
    }
    

    这样既可以动态分配数组元素,也可以很方便的用elements来访问数组元素。

    用以下的宏和函数可以操作adsl_seq / adsl_int_seq :

    asdl_seq *_Py_asdl_seq_new(Py_ssize_t size, PyArena *arena);
    asdl_int_seq *_Py_asdl_int_seq_new(Py_ssize_t size, PyArena *arena);
    
    #define asdl_seq_GET(S, I) (S)->elements[(I)]
    #define asdl_seq_LEN(S) ((S) == NULL ? 0 : (S)->size)
    #ifdef Py_DEBUG
    #define asdl_seq_SET(S, I, V) \
        do { \
            Py_ssize_t _asdl_i = (I); \
            assert((S) != NULL); \
            assert(0 <= _asdl_i && _asdl_i < (S)->size); \
            (S)->elements[_asdl_i] = (V); \
        } while (0)
    #else
    #define asdl_seq_SET(S, I, V) (S)->elements[I] = (V)
    #endif
    

    注:
    adsl_seq / adsl_int_seq均是从PyArena中分配出

    • 在有了 AST 的数据结构以后,第二步,是把 CST 转换成 AST,这个工作是在 Python/ast.c 中实现的,入口函数是 PyAST_FromNode()。

    PyAST_FromNode函数的大致代码如下:
    此函数会深度遍历整棵CST,过滤掉CST中的多余信息,只是将有意义的CST子树转换成AST结点构造出AST树。

    
    mod_ty
    PyAST_FromNodeObject(const node *n, PyCompilerFlags *flags,
                         PyObject *filename, PyArena *arena)
    {
        int i, j, k, num;
        asdl_seq *stmts = NULL;
        asdl_seq *type_ignores = NULL;
        stmt_ty s;
        node *ch;
        struct compiling c;
        mod_ty res = NULL;
        asdl_seq *argtypes = NULL;
        expr_ty ret, arg;
    
        c.c_arena = arena;
        /* borrowed reference */
        c.c_filename = filename;
        c.c_normalize = NULL;
        c.c_feature_version = flags ? flags->cf_feature_version : PY_MINOR_VERSION;
    
        if (TYPE(n) == encoding_decl)
            n = CHILD(n, 0);
    
        k = 0;
        switch (TYPE(n)) {
            case file_input:
                stmts = _Py_asdl_seq_new(num_stmts(n), arena);
                if (!stmts)
                    goto out;
                for (i = 0; i < NCH(n) - 1; i++) {
                    ch = CHILD(n, i);
                    if (TYPE(ch) == NEWLINE)
                        continue;
                    REQ(ch, stmt);
                    num = num_stmts(ch);
                    if (num == 1) {
                        s = ast_for_stmt(&c, ch);
                        if (!s)
                            goto out;
                        asdl_seq_SET(stmts, k++, s);
                    }
                    else {
                        ch = CHILD(ch, 0);
                        REQ(ch, simple_stmt);
                        for (j = 0; j < num; j++) {
                            s = ast_for_stmt(&c, CHILD(ch, j * 2));
                            if (!s)
                                goto out;
                            asdl_seq_SET(stmts, k++, s);
                        }
                    }
                }
    
                /* Type ignores are stored under the ENDMARKER in file_input. */
                ch = CHILD(n, NCH(n) - 1);
                REQ(ch, ENDMARKER);
                num = NCH(ch);
                type_ignores = _Py_asdl_seq_new(num, arena);
                if (!type_ignores)
                    goto out;
    
                for (i = 0; i < num; i++) {
                    string type_comment = new_type_comment(STR(CHILD(ch, i)), &c);
                    if (!type_comment)
                        goto out;
                    type_ignore_ty ti = TypeIgnore(LINENO(CHILD(ch, i)), type_comment, arena);
                    if (!ti)
                       goto out;
                   asdl_seq_SET(type_ignores, i, ti);
                }
    
                res = Module(stmts, type_ignores, arena);
                break;
            case eval_input: {
    			...
            case single_input:
             ...
                break;
            case func_type_input:
             ...
                break;
            default:
                PyErr_Format(PyExc_SystemError,
                             "invalid node %d for PyAST_FromNode", TYPE(n));
                goto out;
        }
     out:
        if (c.c_normalize) {
            Py_DECREF(c.c_normalize);
        }
        return res;
    }
    
    mod_ty
    PyAST_FromNode(const node *n, PyCompilerFlags *flags, const char *filename_str,
                   PyArena *arena)
    {
        mod_ty mod;
        PyObject *filename;
        filename = PyUnicode_DecodeFSDefault(filename_str);
        if (filename == NULL)
            return NULL;
        mod = PyAST_FromNodeObject(n, flags, filename, arena);
        Py_DECREF(filename);
        return mod;
    }
    

    PyAst_FromNode根据N的类型作了不同处理,以file_input为例,file_input的产生式(在Grammar文件中定义)如下:
    file_input: (NEWLINE | stmt)* ENDMARKER
    对应的PyAST_FromNode的代码做了如下事情:

    • num_stmts(n)计算出所有顶层语句的个数,并创建出合适大小的adsl_seq结构以存放这些语句
    • 对file_input结点的所有子节点做如下处理:
    • 忽略掉NEW_LINE,换行无需处理
    • REQ(ch, stmt);断言ch的类型必定为stmt,从产生式可以得此结论
    • 计算子结点stmt 的语句条数num:

    num == 1,说明stmt对应单条语句调用ast_for_stmt遍历stmt对应得CST子树,生成对应的AST子树,并调用adsl_seq_SET设置到数组之中。这样AST的根结点mod_ty便可以知道有哪些顶层的语句(stmt),这些语句结点便是根结点mod_ty的子结点。
    num != 1,说明stmt对应多条语句。根据Grammar文件中定义的如下产生式可以推知此时ch的子结点必然为simple_stmt。

    stmt: simple_stmt | compound_stmt
    simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
    small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
                 import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
    expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                         [('=' (yield_expr|testlist_star_expr))+ [TYPE_COMMENT]] )
    annassign: ':' test ['=' (yield_expr|testlist_star_expr)]
    

    由于simple_stmt的定义中small_stmt和’;’总是成对出现,因此index为偶数的CST结点便是所需的单条顶层语句的结点,对于每个这样的结点调用adsl_seq_SET设置到数组之中

    • 最后,调用Module函数从stmts数组生成mod_ty结点,也就是AST的根结点

    上面的过程中用到了两个关键函数:num_stmts和ast_for_stmt

    • 先来看num_stmts函数
    
    static int
    num_stmts(const node *n)
    {
        int i, l;
        node *ch;
    
        switch (TYPE(n)) {
            case single_input:
                if (TYPE(CHILD(n, 0)) == NEWLINE)
                    return 0;
                else
                    return num_stmts(CHILD(n, 0));
            case file_input:
                l = 0;
                for (i = 0; i < NCH(n); i++) {
                    ch = CHILD(n, i);
                    if (TYPE(ch) == stmt)
                        l += num_stmts(ch);
                }
                return l;
            case stmt:
                return num_stmts(CHILD(n, 0));
            case compound_stmt:
                return 1;
            case simple_stmt:
                return NCH(n) / 2; /* Divide by 2 to remove count of semi-colons */
            case suite:
            case func_body_suite:
                /* func_body_suite: simple_stmt | NEWLINE [TYPE_COMMENT NEWLINE] INDENT stmt+ DEDENT */
                /* suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT */
                if (NCH(n) == 1)
                    return num_stmts(CHILD(n, 0));
                else {
                    i = 2;
                    l = 0;
                    if (TYPE(CHILD(n, 1)) == TYPE_COMMENT)
                        i += 2;
                    for (; i < (NCH(n) - 1); i++)
                        l += num_stmts(CHILD(n, i));
                    return l;
                }
            default: {
                char buf[128];
    
                sprintf(buf, "Non-statement found: %d %d",
                        TYPE(n), NCH(n));
                Py_FatalError(buf);
            }
        }
        Py_UNREACHABLE();
    }
    

    此函数比较简单,根据结点类型和产生式递归计算顶层语句的个数。所谓顶层语句,也就是把复合语句(compound_stmt)看作单条语句,复合语句中的内部的语句不做计算,当然普通的简单语句(small_stmt) 也是算1条语句。下面根据不同结点类型分析此函数:

    • Single_input

    代表单条交互语句,对应的产生式:single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
    如果single_input的第一个子结点为NEW_LINE,说明无语句,返回0,否则说明是simple_stmt或者compound_stmt NEWLINE,可以直接递归调用num_stmts处理

    • File_input

    代表整个代码文件,对应的产生式:file_input: (NEWLINE | stmt)* ENDMARKER
    只需要反复对每个子结点调用num_stmts既可。

    • Compound_stmt

    代表复合语句,对应的产生式:compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef
    compound_stmt只可能有单个子结点,而且必然代表单条顶层的语句,因此无需继续遍历,直接返回1既可。

    • Simple_stmt

    代表简单语句(非复合语句)的集合,对应的产生式:simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
    可以看到顶层语句数=子结点数/2 (去掉多余的分号和NEWLINE)

    • Suite

    代表复合语句中的语句块,也就是冒号之后的部分(如:classdef: 'class' NAME ['(' [testlist] ')'] ':' suite),类似于C/C++大括号中的内容,对应的产生式如下:suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
    子结点数为1,说明必然是simple_stmt,可以直接调用num_stmts处理,否则,说明是多个stmt的集合,遍历所有子结点调用num_stmts并累加既可

    可以看到,num_stmts基本上是和语句有关的产生式是一一对应的。

    接下来分析ast_for_stmts的内容:

    
    static stmt_ty
    ast_for_stmt(struct compiling *c, const node *n)
    {
        if (TYPE(n) == stmt) {
            assert(NCH(n) == 1);
            n = CHILD(n, 0);
        }
        if (TYPE(n) == simple_stmt) {
            assert(num_stmts(n) == 1);
            n = CHILD(n, 0);
        }
        if (TYPE(n) == small_stmt) {
            n = CHILD(n, 0);
            /* small_stmt: expr_stmt | del_stmt | pass_stmt | flow_stmt
                      | import_stmt | global_stmt | nonlocal_stmt | assert_stmt
            */
            switch (TYPE(n)) {
                case expr_stmt:
                    return ast_for_expr_stmt(c, n);
                case del_stmt:
                    return ast_for_del_stmt(c, n);
                case pass_stmt:
                    return Pass(LINENO(n), n->n_col_offset,
                                n->n_end_lineno, n->n_end_col_offset, c->c_arena);
                case flow_stmt:
                    return ast_for_flow_stmt(c, n);
                case import_stmt:
                    return ast_for_import_stmt(c, n);
                case global_stmt:
                    return ast_for_global_stmt(c, n);
                case nonlocal_stmt:
                    return ast_for_nonlocal_stmt(c, n);
                case assert_stmt:
                    return ast_for_assert_stmt(c, n);
                default:
                    PyErr_Format(PyExc_SystemError,
                                 "unhandled small_stmt: TYPE=%d NCH=%d\n",
                                 TYPE(n), NCH(n));
                    return NULL;
            }
        }
        else {
            /* compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt
                            | funcdef | classdef | decorated | async_stmt
            */
            node *ch = CHILD(n, 0);
            REQ(n, compound_stmt);
            switch (TYPE(ch)) {
                case if_stmt:
                    return ast_for_if_stmt(c, ch);
                case while_stmt:
                    return ast_for_while_stmt(c, ch);
                case for_stmt:
                    return ast_for_for_stmt(c, ch, 0);
                case try_stmt:
                    return ast_for_try_stmt(c, ch);
                case with_stmt:
                    return ast_for_with_stmt(c, ch, 0);
                case funcdef:
                    return ast_for_funcdef(c, ch, NULL);
                case classdef:
                    return ast_for_classdef(c, ch, NULL);
                case decorated:
                    return ast_for_decorated(c, ch);
                case async_stmt:
                    return ast_for_async_stmt(c, ch);
                default:
                    PyErr_Format(PyExc_SystemError,
                                 "unhandled compound_stmt: TYPE=%d NCH=%d\n",
                                 TYPE(n), NCH(n));
                    return NULL;
            }
        }
    }
    

    可以看到,ast_for_stmt基本上是根据stmt的产生式来遍历CST的,stmt的产生式为stmt:

    • simple_stmt | compound_stmt,对应了if语句的两条分支。之后,根据子结点simple_stmt或者compound_stmt的具体type,调用不同的ast_for_xxx函数来遍历CST,生成对应的AST结点。
      这整个是一个递归下降的遍历分析的过程。其实很多编译器的语法分析是直接用递归下降生成AST实现的,而Python则稍有不同,先是用生成的代码生成CST,然后再用手写的递归下降分析法遍历CST生成AST,本质一样,不过Python的做法可以减少手写的工作量,只需分析CST,无需考虑词法分析的内容,当然增加的工作量是构造一个生成器从Grammar生成对应的分析代码。总的来说,还是有一定好处的,维护的代码会简单一些。

    在递归下降遍历的过程中,一旦遇到的CST可以生成对应的AST,则会调用对应的AST类型的创建函数来返回对应的AST。这个过程在下面的ast_for_factor中可以看到(优化代码为了清晰起见已去掉):

    
    static expr_ty
    ast_for_factor(struct compiling *c, const node *n)
    {
        expr_ty expression;
    
        expression = ast_for_expr(c, CHILD(n, 1));
        if (!expression)
            return NULL;
    
        switch (TYPE(CHILD(n, 0))) {
            case PLUS:
                return UnaryOp(UAdd, expression, LINENO(n), n->n_col_offset,
                               n->n_end_lineno, n->n_end_col_offset,
                               c->c_arena);
            case MINUS:
                return UnaryOp(USub, expression, LINENO(n), n->n_col_offset,
                               n->n_end_lineno, n->n_end_col_offset,
                               c->c_arena);
            case TILDE:
                return UnaryOp(Invert, expression, LINENO(n), n->n_col_offset,
                               n->n_end_lineno, n->n_end_col_offset,
                               c->c_arena);
        }
        PyErr_Format(PyExc_SystemError, "unhandled factor: %d",
                     TYPE(CHILD(n, 0)));
        return NULL;
    }
    

    Factor对应的产生式如下:**factor: ('+'|'-'|'~') factor | power **

    因此,对应的ast_for_factor的代码也遵循产生式的定义,先调用ast_for_expr分析factor/power对应的CST子树,再根据第一个子结点是+-~分别调用UnaryOp使用不同参数生成对应的AST子树。注意分析factor / power的时候用的是ast_for_expr,一是因为factor可能有左递归,而ast_for_expr会在case factor的时候处理左递归,二是因为ast_for_expr已经可以处理factor和power了,无需多写代码。





    参考:
    词法分析:用两种方式构造有限自动机
    语法分析:两个基本功和两种算法思路
    Python编译器(一):如何用工具生成编译器?
    Python源码分析3 – 词法分析器PyTokenizer
    Python源码分析5 – 语法分析器PyParser
    Python源码分析6 – 从CST到AST的转化

  • 相关阅读:
    Git push 出错 [The remote end hung up unexpectedly]
    [Git高级教程(二)] 远程仓库版本回退方法
    git分支与版本管理、版本回退、冲突解决记录
    上传本地代码到gitHub过程详解
    如何用git将项目代码上传到github
    Git pull 强制覆盖本地文件
    Git忽略规则.gitignore梳理
    composer本地安装文档
    服务器通过微信公众号Token验证测试的代码(Python版)
    转载自lanceyan: 一致性hash和solr千万级数据分布式搜索引擎中的应用
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16651697.html
Copyright © 2020-2023  润新知