好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

回复内容: 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都可以方便地知道在哪一层缩进了。

查看更多关于设计类Python编译器时如何处理tab和space缩进?的详细内容...

  阅读:50次

CopyRight:2016-2025好得很程序员自学网 备案ICP:湘ICP备09009000号-16 http://haodehen.cn
本站资讯不构成任何建议,仅限于个人分享,参考须谨慎!
本网站对有关资料所引致的错误、不确或遗漏,概不负任何法律责任。
本网站刊载的所有内容(包括但不仅限文字、图片、LOGO、音频、视频、软件、程序等)版权归原作者所有。任何单位或个人认为本网站中的内容可能涉嫌侵犯其知识产权或存在不实内容时,请及时通知本站,予以删除。

网站内容来源于网络分享,如有侵权发邮箱到:kenbest@126.com,收到邮件我们会即时下线处理。
网站框架支持:HDHCMS   51LA统计 百度统计
Copyright © 2018-2025 「好得很程序员自学网
[ SiteMap ]