使用Python编写一个最基础的代码解释器的要点解析

发表于 5年以前  | 总阅读数:401 次

一直以来都对编译器和解析器有着很大的兴趣,也很清楚一个编译器的概念和整体的框架,但是对于细节部分却不是很了解。我们编写的程序源代码实际上就是一串字符序列,编译器或者解释器可以直接理解并执行这个字符序列,这看起来实在是太奇妙了。本文会用Python实现一个简单的解析器,用于解释一种小的列表操作语言(类似于python的list)。其实编译器、解释器并不神秘,只要对基本的理论理解之后,实现起来也比较简单(当然,一个产品级的编译器或解释器还是很复杂的)。
这种列表语言支持的操作:


    veca = [1, 2, 3]  # 列表声明 
    vecb = [4, 5, 6] 
    print 'veca:', veca   # 打印字符串、列表,print expr+ 
    print 'veca * 2:', veca * 2   # 列表与整数乘法 
    print 'veca + 2:', veca + 2   # 列表与整数加法 
    print 'veca + vecb:', veca + vecb  # 列表加法 
    print 'veca + [11, 12]:', veca + [11, 12]   
    print 'veca * vecb:', veca * vecb  # 列表乘法 
    print 'veca:', veca 
    print 'vecb:', vecb 

对应输出:


    veca: [1, 2, 3] 
    veca * 2: [2, 4, 6] 
    veca + 2: [1, 2, 3, 2] 
    veca + vecb: [1, 2, 3, 2, 4, 5, 6] 
    veca + [11, 12]: [1, 2, 3, 2, 11, 12] 
    veca * vecb: [4, 5, 6, 8, 10, 12, 12, 15, 18, 8, 10, 12] 
    veca: [1, 2, 3, 2] 
    vecb: [4, 5, 6] 

编译器和解释器在处理输入字符流时,基本上和人理解句子的方式是一致的。比如:


    I love you. 

如果初学英语,首先需要知道各个单词的意思,然后分析各个单词的词性,符合主-谓-宾的结构,这样才能理解这句话的意思。这句话就是一个字符序列,按照词法划分就得到了一个词法单元流,实际上这就是词法分析,完成从字符流到词法单元流的转化。分析词性,确定主谓宾结构,是按照英语的语法识别出这个结构,这就是语法分析,根据输入的词法单元流识别出语法解析树。最后,再结合单词的意思和语法结构最后得出这句话的意思,这就是语义分析。编译器和解释器处理过程类似,但是要略微复杂一些,这里只关注解释器:

2016712182247047.jpg \(496×197\)

我们只是实现一个很简单的小语言,所以不涉及到语法树的生成,以及后续复杂的语义分析。下面我就来看看词法分析和语法分析。
词法分析和语法分析分别由词法解析器和语法解析器完成。这两种解析器具有类似的结构和功能,它们都是以一个输入序列为输入,然后识别出特定的结构。词法解析器从源代码字符流中解析出一个一个的token(词法单元),而语法解析器识别出子结构和词法单元,然后进行一些处理。可以通过LL(1)递归下降解析器实现这两种解析器,这类解析器完成的步骤是:预测子句的类型,调用解析函数匹配该子结构、匹配词法单元,然后按照需要插入代码,执行自定义操作。
这里对LL(1)做简要介绍,语句的结构通常用树型结构表示,称为解析树,LL(1)做语法解析就依赖于解析树。比如:x = x +2;

2016712182509962.png \(187×248\)
在这棵树中,像x、=和2这样的叶节点,称为终结节点,其他的叫做非终结节点。LL(1)解析时,不需要创建具体的树型数据结构,可以为每个非终结节点编写解析函数,在遇到相应节点时进行调用,这样就可以通过解析函数的调用序列中(相当于树的遍历)获得解析树的信息。在LL(1)解析时,是按照从根节点到叶节点的顺序执行的,所以这是一个"下降"的过程,而解析函数又可以调用自身,所以是"递归"的,因此LL(1)又叫做递归下降解析器。
LL(1)中两个L都是left-to-right的意思,第一个L表示解析器按从左到右的顺序解析输入内容,第二个L表示在下降过程中,也是按照从左到右的顺序遍历子节点,而1表示根据1个向前看单元做预测。
下面看一下列表小语言的实现,首先是语言的文法,文法用于描述一个语言,算是解析器的设计说明书。


    statlist: stat+ 
    stat: ID '=' expr 
      | 'print' expr (, expr)* 
    expr: multipart ('+' multipart)* 
      | STR 
    multipart: primary ('*' primary)* 
    primary: INT 
      | ID 
      | '[' expr (',', expr)* ']' 
    INT: (1..9)(0..9)* 
    ID: (a..z | A..Z)* 
    STR: (\".*\") | (\'.*\') 

这是用DSL描述的文法,其中大部分概念和正则表达式类似。"a|b"表示a或者b,所有以单引号括起的字符串是关键字,比如:print,=等。大写的单词是词法单元。可以看到这个小语言的文法还是很简单的。有很多解析器生成器可以自动根据文法生成对应的解析器,比如:ANTRL,flex,yacc等,这里采用手写解析器主要是为了理解解析器的原理。下面看一下这个小语言的解释器是如何实现的。
首先是词法解析器,完成字符流到token流的转换。采用LL(1)实现,所以使用1个向前看字符预测匹配的token。对于像INT、ID这样由多个字符组成的词法规则,解析器有一个与之对应的方法。由于语法解析器并不关心空白字符,所以词法解析器在遇到空白字符时直接忽略掉。每个token都有两个属性类型和值,比如整型、标识符类型等,对于整型类型它的值就是该整数。语法解析器需要根据token的类型进行预测,所以词法解析必须返回类型信息。语法解析器以iterator的方式获取token,所以词法解析器实现了next_token方法,以元组方式(type, value)返回下一个token,在没有token的情况时返回EOF。


    ''''' 
    A simple lexer of a small vector language. 

    statlist: stat+ 
    stat: ID '=' expr 
      | 'print' expr (, expr)* 
    expr: multipart ('+' multipart)* 
      | STR 
    multipart: primary ('*' primary)* 
    primary: INT 
      | ID 
      | '[' expr (',', expr)* ']' 
    INT: (1..9)(0..9)* 
    ID: (a..z | A..Z)* 
    STR: (\".*\") | (\'.*\') 

    Created on 2012-9-26 

    @author: bjzllou 
    ''' 

    EOF = -1 

    # token type 
    COMMA = 'COMMA' 
    EQUAL = 'EQUAL' 
    LBRACK = 'LBRACK' 
    RBRACK = 'RBRACK' 
    TIMES = 'TIMES' 
    ADD = 'ADD' 
    PRINT = 'print' 
    ID = 'ID' 
    INT = 'INT' 
    STR = 'STR' 

    class Veclexer: 
      ''''' 
      LL(1) lexer. 
      It uses only one lookahead char to determine which is next token. 
      For each non-terminal token, it has a rule to handle it. 
      LL(1) is a quit weak parser, it isn't appropriate for the grammar which is 
      left-recursive or ambiguity. For example, the rule 'T: T r' is left recursive. 
      However, it's rather simple and has high performance, and fits simple grammar. 
      ''' 

      def __init__(self, input): 
        self.input = input 

        # current index of the input stream. 
        self.idx = 1 

        # lookahead char. 
        self.cur_c = input[0] 

      def next_token(self): 
        while self.cur_c != EOF: 
          c = self.cur_c 

          if c.isspace(): 
            self.consume() 
          elif c == '[': 
            self.consume() 
            return (LBRACK, c) 
          elif c == ']': 
            self.consume() 
            return (RBRACK, c) 
          elif c == ',': 
            self.consume() 
            return (COMMA, c) 
          elif c == '=': 
            self.consume() 
            return (EQUAL, c) 
          elif c == '*': 
            self.consume() 
            return (TIMES, c) 
          elif c == '+': 
            self.consume() 
            return (ADD, c) 
          elif c == '\'' or c == '"': 
            return self._string() 
          elif c.isdigit(): 
            return self._int() 
          elif c.isalpha(): 
            t = self._print() 
            return t if t else self._id() 
          else: 
            raise Exception('not support token') 

        return (EOF, 'EOF') 

      def has_next(self): 
        return self.cur_c != EOF    

      def _id(self): 
        n = self.cur_c 
        self.consume() 
        while self.cur_c.isalpha(): 
          n += self.cur_c 
          self.consume() 

        return (ID, n) 

      def _int(self): 
        n = self.cur_c 
        self.consume() 
        while self.cur_c.isdigit(): 
          n += self.cur_c 
          self.consume() 

        return (INT, int(n)) 

      def _print(self): 
        n = self.input[self.idx - 1 : self.idx + 4] 
        if n == 'print': 
          self.idx += 4 
          self.cur_c = self.input[self.idx] 

          return (PRINT, n) 

        return None 

      def _string(self): 
        quotes_type = self.cur_c 
        self.consume() 
        s = '' 
        while self.cur_c != '\n' and self.cur_c != quotes_type: 
          s += self.cur_c 
          self.consume() 
        if self.cur_c != quotes_type: 
          raise Exception('string quotes is not matched. excepted %s' % quotes_type) 

        self.consume() 

        return (STR, s) 

      def consume(self): 
        if self.idx >= len(self.input): 
          self.cur_c = EOF 
          return 
        self.cur_c = self.input[self.idx] 
        self.idx += 1   


    if __name__ == '__main__': 
      exp = ''''' 
        veca = [1, 2, 3] 
        print 'veca:', veca 
        print 'veca * 2:', veca * 2 
        print 'veca + 2:', veca + 2 
      ''' 
      lex = Veclexer(exp) 
      t = lex.next_token() 

      while t[0] != EOF: 
        print t 
        t = lex.next_token() 

运行这个程序,可以得到源代码:


    veca = [1, 2, 3] 
    print 'veca:', veca 
    print 'veca * 2:', veca * 2 
    print 'veca + 2:', veca + 2 

对应的token序列:


    ('ID', 'veca') 
    ('EQUAL', '=') 
    ('LBRACK', '[') 
    ('INT', 1) 
    ('COMMA', ',') 
    ('INT', 2) 
    ('COMMA', ',') 
    ('INT', 3) 
    ('RBRACK', ']') 
    ('print', 'print') 
    ('STR', 'veca:') 
    ('COMMA', ',') 
    ('ID', 'veca') 
    ('print', 'print') 
    ('STR', 'veca * 2:') 
    ('COMMA', ',') 
    ('ID', 'veca') 
    ('TIMES', '*') 
    ('INT', 2) 
    ('print', 'print') 
    ('STR', 'veca + 2:') 
    ('COMMA', ',') 
    ('ID', 'veca') 
    ('ADD', '+') 
    ('INT', 2) 

接下来看一下语法解析器的实现。语法解析器的的输入是token流,根据一个向前看词法单元预测匹配的规则。对于每个遇到的非终结符调用对应的解析函数,而终结符(token)则match掉,如果不匹配则表示有语法错误。由于都是使用的LL(1),所以和词法解析器类似, 这里不再赘述。


    ''''' 
    A simple parser of a small vector language. 

    statlist: stat+ 
    stat: ID '=' expr 
      | 'print' expr (, expr)* 
    expr: multipart ('+' multipart)* 
      | STR 
    multipart: primary ('*' primary)* 
    primary: INT 
      | ID 
      | '[' expr (',', expr)* ']' 
    INT: (1..9)(0..9)* 
    ID: (a..z | A..Z)* 
    STR: (\".*\") | (\'.*\') 

    example: 
    veca = [1, 2, 3] 
    vecb = veca + 4  # vecb: [1, 2, 3, 4] 
    vecc = veca * 3  # vecc: 

    Created on 2012-9-26 

    @author: bjzllou 
    ''' 
    import veclexer 

    class Vecparser: 
      ''''' 
      LL(1) parser. 
      ''' 

      def __init__(self, lexer): 
        self.lexer = lexer 

        # lookahead token. Based on the lookahead token to choose the parse option. 
        self.cur_token = lexer.next_token() 

        # similar to symbol table, here it's only used to store variables' value 
        self.symtab = {} 

      def statlist(self): 
        while self.lexer.has_next(): 
          self.stat() 

      def stat(self): 
        token_type, token_val = self.cur_token 

        # Asignment 
        if token_type == veclexer.ID: 
          self.consume() 

          # For the terminal token, it only needs to match and consume. 
          # If it's not matched, it means that is a syntax error. 
          self.match(veclexer.EQUAL) 

          # Store the value to symbol table. 
          self.symtab[token_val] = self.expr() 

        # print statement 
        elif token_type == veclexer.PRINT: 
          self.consume() 
          v = str(self.expr()) 
          while self.cur_token[0] == veclexer.COMMA: 
            self.match(veclexer.COMMA) 
            v += ' ' + str(self.expr()) 
          print v 
        else: 
          raise Exception('not support token %s', token_type) 

      def expr(self): 
        token_type, token_val = self.cur_token 
        if token_type == veclexer.STR: 
          self.consume() 
          return token_val 
        else: 
          v = self.multipart() 
          while self.cur_token[0] == veclexer.ADD: 
            self.consume() 
            v1 = self.multipart() 
            if type(v1) == int: 
              v.append(v1) 
            elif type(v1) == list: 
              v = v + v1 

          return v      

      def multipart(self): 
        v = self.primary() 
        while self.cur_token[0] == veclexer.TIMES: 
          self.consume() 
          v1 = self.primary() 
          if type(v1) == int: 
            v = [x*v1 for x in v] 
          elif type(v1) == list: 
            v = [x*y for x in v for y in v1] 

        return v 

      def primary(self): 
        token_type = self.cur_token[0] 
        token_val = self.cur_token[1] 

        # int 
        if token_type == veclexer.INT: 
          self.consume() 
          return token_val 

        # variables reference 
        elif token_type == veclexer.ID: 
          self.consume() 
          if token_val in self.symtab: 
            return self.symtab[token_val] 
          else: 
            raise Exception('undefined variable %s' % token_val) 

        # parse list 
        elif token_type == veclexer.LBRACK: 
          self.match(veclexer.LBRACK) 
          v = [self.expr()] 
          while self.cur_token[0] == veclexer.COMMA: 
            self.match(veclexer.COMMA) 
            v.append(self.expr()) 
          self.match(veclexer.RBRACK) 

          return v 


      def consume(self): 
        self.cur_token = self.lexer.next_token() 

      def match(self, token_type): 
        if self.cur_token[0] == token_type: 
          self.consume() 
          return True 
        raise Exception('expecting %s; found %s' % (token_type, self.cur_token[0])) 

    if __name__ == '__main__': 
      prog = ''''' 
        veca = [1, 2, 3] 
        vecb = [4, 5, 6] 
        print 'veca:', veca 
        print 'veca * 2:', veca * 2 
        print 'veca + 2:', veca + 2 
        print 'veca + vecb:', veca + vecb 
        print 'veca + [11, 12]:', veca + [11, 12] 
        print 'veca * vecb:', veca * vecb 
        print 'veca:', veca 
        print 'vecb:', vecb 
      ''' 
      lex = veclexer.Veclexer(prog) 
      parser = Vecparser(lex) 
      parser.statlist() 

运行代码便会得到之前介绍中的输出内容。这个解释器极其简陋,只实现了基本的表达式操作,所以不需要构建语法树。如果要为列表语言添加控制结构,就必须实现语法树,在语法树的基础上去解释执行。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录