热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

php中文网(www.php.cn)提供了最全的编程技术基础教程,介绍了HTML、CSS、Javascript、Python,Java,Ruby,C,PHP,MySQL等各种编程语言的基础知识。同时本站中也提供了大量的在线实例,通过实例,您可以更好的学习编程。..

回复内容:

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都可以方便地知道在哪一层缩进了。
推荐阅读
  • BashShell作为Linux的指定合作伙伴我们已经再熟悉不过了,使用Bash可以快速编写简单的脚本方便我们的日常比如善用vim,awk和sed三剑客,也可以创建十分复杂的逻辑, ... [详细]
  • 经过前面的一系列工作后,GO的语言环境已经搭建完成。我们也初步体验了GO提供的大部分命令。但在正式进入开发之前,还有件工作要做,那就是选择一款适合自己的IDE。为什么使用IDE“程 ... [详细]
  • 安装mysqlclient失败解决办法
    本文介绍了在MAC系统中,使用django使用mysql数据库报错的解决办法。通过源码安装mysqlclient或将mysql_config添加到系统环境变量中,可以解决安装mysqlclient失败的问题。同时,还介绍了查看mysql安装路径和使配置文件生效的方法。 ... [详细]
  • 本文介绍了在Mac上配置环境变量,实现Python3的命令行调用的步骤。首先通过官网下载或使用brew安装Python3,并找到安装路径。然后将该路径添加到环境变量中,可以通过编辑.bash_profile文件或执行source命令来实现。配置完成后,即可在命令行中直接调用Python3。 ... [详细]
  • Python项目实战10.2:MySQL读写分离性能优化
    本文介绍了在Python项目实战中进行MySQL读写分离的性能优化,包括主从同步的配置和Django实现,以及在两台centos 7系统上安装和配置MySQL的步骤。同时还介绍了创建从数据库的用户和权限的方法。摘要长度为176字。 ... [详细]
  • 对于一般的扩展包,我们一般直接pipinstallxxx即可安装,但是unrar直接安装后,发现并不能通过Python程序实现解压的功能& ... [详细]
  • 【Python 爬虫】破解按照顺序点击验证码(非自动化浏览器)
    #请求到验证码base64编码json_img_datajson_raw.get(Vimage)#获取到验证码编码 #保存验证码图片到本地defbase64_to_img(bstr ... [详细]
  • python3.7 安装pip3_python3的pip3安装
    ---恢复内容开始---pip3的安装需要对应一整套python的编译工具库,所以安装好的pip3是这个样子:inearAi:~$pip3-Vpi ... [详细]
  • 【技术分享】如何通过恶意插件在Atom中植入后门
    【技术分享】如何通过恶意插件在Atom中植入后门 ... [详细]
  • Haskell学习笔记:探索函数式编程之美
    使用教材:《learnyouaHaskell》,中文1-8章戳我准备工作。编译器&调试器:安装HaskellPlatform, ... [详细]
  • 从2000年毕业,我做这行已经超过十二年了。最初接触计算机程序,还是DOS下的QBASIC,那已经离现在有接近二十年了。最初, ... [详细]
  • 前一段时间用Emacs,但我键盘的Ctrl键比较难按,但Emacs又是Ctrl到死的,老CtrlCtrl的按,手指受不了,而且我还没有到那种Emacs发烧友的程度:把Ctrl和CapsLock键换 ... [详细]
  • PHP连接MySQL的2种方法小结以及防止乱码【PHP】
    后端开发|php教程PHP,MySQL,乱码后端开发-php教程PHP的MySQL配置报错信息:ClassmysqlinotfoundinAnswer:1.在confphp.ini ... [详细]
  • 部署Maven环境
    JDK:在maven3.3以上的版本需要JDK版本1.7,内存:没有最低限制。磁盘:1G可用磁盘空间。操作系统: ... [详细]
  • 基于SSL的mysql服务器的主从架构实现说明:本文选用172.16.22.1作为主服务器,172.16.22.3作为从服务器从服务器的mysql软件版 ... [详细]
author-avatar
步履乘风
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有