当前位置:Gxlcms > Python > 设计类Python编译器时如何处理tab和space缩进?

设计类Python编译器时如何处理tab和space缩进?

时间:2021-07-01 10:21:17 帮助过:39人阅读

回复内容:

Python的做法大体是在tokenizer里面做hack,大体思路就是解析行首有多少个space,再依靠缩进的历史纪录发射INDENT/DEDENT token.

Python为缩进定义了两种额外的token类型,INDENT和DEDENT,你可以认为类似C的{, }. Tokenzier会在扫描字符流的同时注意当前缩进层级的变化,从而在适当的时候发射出INDENT和DEDENT token. Python的token类型定义见Include/token.h [projects] Contents of /python/trunk/Include/token.h

将题主贴出的那段代码送入这样的tokenizer解析,得到的token序列应该是这样的(注意被插入的INDENT和DEDENT)
token [NAME]: func
token [NAME]: fab
token [LPAR]: (
token [NAME]: number
token [RPAR]: )
token [COLON]: :
token [NEWLINE]: 
token [INDENT]: 
token [NAME]: if
token [LPAR]: (
token [NAME]: number
token [EQEQUAL]: ==
token [NUMBER]: 1
token [RPAR]: )
token [COLON]: :
token [NEWLINE]: 
token [INDENT]: 
token [NAME]: return
token [NUMBER]: 1
token [NEWLINE]: 
token [DEDENT]: 
token [NAME]: if
token [LPAR]: (
token [NAME]: number
token [EQEQUAL]: ==
token [NUMBER]: 2
token [RPAR]: )
token [COLON]: :
token [NEWLINE]: 
token [INDENT]: 
token [NAME]: return
token [NUMBER]: 2
token [NEWLINE]: 
token [DEDENT]: 
token [NAME]: return
token [NAME]: fab
token [LPAR]: (
token [NAME]: number
token [MINUS]: -
token [NUMBER]: 1
token [RPAR]: )
token [PLUS]: +
token [NAME]: fab
token [LPAR]: (
token [NAME]: number
token [MINUS]: -
token [NUMBER]: 2
token [RPAR]: )
token [NEWLINE]: 
token [DEDENT]: 
关键词: layout sensitive parsing, off-side rule

@Kontinuation 讲的 lexer 插入伪 INDENT / DEDENT token 的方式是主流做法, 不过维护一个状态变量会对实现 scannerless parser, online parser 或者 parallel parser 带来一些困难.

另一个做法是在语法描述中添加 layout constraint --- 当然你的分析器生成程序要支持对语法添加约束, 例如 Antlr 可以用 {boolean-expr}? 指定 semantic predicate. 然后解析 python 函数定义的语法可以像这么写 (伪代码):

funcdef: 'def' NAME parameters ':' funcdef_body {
           $5.line > $1.line && $5.col > $1.col
         }?
       ;

我有一个想法。

首先确定源代码的缩进是由空格控制还是制表符控制,并在之后使用一致的方式进行处理。为了便于说明,下面不妨假设我们需要处理的代码都是由制表符控制缩进的。

在词法解析的阶段,需要把制表符输出为一个token,比如叫tabToken。然后对于每个语句,就有这样一个特点:在换行符的后边的tabToken数目就是代码的层级数(特殊情况另说)。根据这个原理,处理的时候就可以在层级数变化的时候,插入花括号token:当层级数变大的时候,插入左花括号token;变小的时候,插入右花括号token。

那么,我们就可以使用处理C代码的方法来处理Python代码了。

我思考过这个问题: Cirru 解析缩进的方案
最后想的方案是用一个变量记录上一次缩进的层级, 后面每解析一行做一次比对, 并刷新记录,
我只考虑了空格, 而且语法也限定在很简单的情况.

大多数基于缩进的语言都是同时支持 Tab 跟空格的, 我好像只看到 Nim 只支持空格的.
如果是发布给他人使用, 照顾不同的使用习惯是有必要考虑的. 先说结论:

正如 @饭米 和 @薛银松 所说,处理这种基于缩进的语言,首先把 Tab 按照统一的规则替换成空格,然后根据空格的层级,解析生成 INDENT 和 DEDENT 两种 Token,其实也就相当于把空格根据上下文缩进替换成 { 和 },然后缩进错误神马的,用一个栈来存,INDENT 就进栈,DEDENT 就弹栈,大概这样

然后说说如何解决的

首先查阅 Python 官方文档(2. Lexical analysis),关于 Lexical Analysis 这块的 Indentation 处理是这么说的

Leading whitespace (spaces and tabs) at the beginning of a logical line is used to compute the indentation level of the line, which in turn is used to determine the grouping of statements.

First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight (this is intended to be the same rule as used by Unix). The total number of spaces preceding the first non-blank character then determines the line’s indentation. Indentation cannot be split over multiple physical lines using backslashes; the whitespace up to the first backslash determines the indentation.


然后放狗搜索 StackOverflow,发现有很多类似问题,比如下面这个

Parsing "off-side" (indentation-based) languages

然后下面这个链接给出了一个示例

c++ - Indentation control while developing a small python like language

最后,还是直接参考 Python 源码比较准确,相关代码位于 Python-2.7.9/Parser/tokenizer.c 的 1209 行到 1359 行

/* Get next token, after space stripping etc. */

static int
tok_get(register struct tok_state *tok, char **p_start, char **p_end)
{
    register int c;
    int blankline;

    *p_start = *p_end = NULL;
  nextline:
    tok->start = NULL;
    blankline = 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);
        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]) {
                /* No change */
                if (altcol != tok->altindstack[tok->indent]) {
                    if (indenterror(tok))
                        return ERRORTOKEN;
                }
            }
            else if (col > tok->indstack[tok->indent]) {
                /* Indent -- always one */
                if (tok->indent+1 >= MAXINDENT) {
                    tok->done = E_TOODEEP;
                    tok->cur = tok->inp;
                    return ERRORTOKEN;
                }
                if (altcol <= tok->altindstack[tok->indent]) {
                    if (indenterror(tok))
                        return ERRORTOKEN;
                }
                tok->pendin++;
                tok->indstack[++tok->indent] = col;
                tok->altindstack[tok->indent] = altcol;
            }
            else /* col < tok->indstack[tok->indent] */ {
                /* Dedent -- any number, must be consistent */
                while (tok->indent > 0 &&
                    col < tok->indstack[tok->indent]) {
                    tok->pendin--;
                    tok->indent--;
                }
                if (col != tok->indstack[tok->indent]) {
                    tok->done = E_DEDENT;
                    tok->cur = tok->inp;
                    return ERRORTOKEN;
                }
                if (altcol != tok->altindstack[tok->indent]) {
                    if (indenterror(tok))
                        return ERRORTOKEN;
                }
            }
        }
    }

    tok->start = tok->cur;

    /* Return pending indents/dedents */
    if (tok->pendin != 0) {
        if (tok->pendin < 0) {
            tok->pendin++;
            return DEDENT;
        }
        else {
            tok->pendin--;
            return INDENT;
        }
    }

 again:
    tok->start = NULL;
    /* Skip spaces */
    do {
        c = tok_nextc(tok);
    } while (c == ' ' || c == '\t' || c == '\014');

    /* Set start of current token */
    tok->start = tok->cur - 1;

    /* Skip comment, while looking for tab-setting magic */
    if (c == '#') {
        static char *tabforms[] = {
            "tab-width:",                       /* Emacs */
            ":tabstop=",                        /* vim, full form */
            ":ts=",                             /* vim, abbreviated form */
            "set tabsize=",                     /* will vi never die? */
        /* more templates can be added here to support other editors */
        };
        char cbuf[80];
        char *tp, **cp;
        tp = cbuf;
        do {
            *tp++ = c = tok_nextc(tok);
        } while (c != EOF && c != '\n' &&
                 (size_t)(tp - cbuf + 1) < sizeof(cbuf));
        *tp = '\0';
        for (cp = tabforms;
             cp < tabforms + sizeof(tabforms)/sizeof(tabforms[0]);
             cp++) {
            if ((tp = strstr(cbuf, *cp))) {
                int newsize = atoi(tp + strlen(*cp));

                if (newsize >= 1 && newsize <= 40) {
                    tok->tabsize = newsize;
                    if (Py_VerboseFlag)
                        PySys_WriteStderr(
                        "Tab size set to %d\n",
                        newsize);
                }
            }
        }
        while (c != EOF && c != '\n')
            c = tok_nextc(tok);
    }
个人感觉可以把tab和换行符当成token处理。\n\t* 这样,记录\t的数量,这样处理每一个statement都可以方便地知道在哪一层缩进了。

人气教程排行