美文网首页
GO/ast编程--编译原理学习之词法分析

GO/ast编程--编译原理学习之词法分析

作者: 温岭夹糕 | 来源:发表于2023-12-19 11:24 被阅读0次

    1.编译器和解释器

    简单来说,一个编译器就是一个程序,它可以把以某一语言编写的程序(c代码写的源程序)翻译成一个等价的、另一种语言编写的程序(汇编代码的目标程序)


    image.png

    那么用户可以利用目标程序处理输入产生输出

    解释器它并不通过翻译方式产生目标程序,而是直接携带用户输入返回输出(典型如php经过浏览器直接显示结果) image.png
    但两者内部都是大同小异

    1.1编译器结构

    编译器的翻译过程可以看作两个部分:

    1. 分析部分,也被叫做编译器的前端,主要负责将字符输入流转为token,进一步转为ast抽象语法树,步骤包括:
    • 词法分析 生成token/记号
    • 语法分析 生成ast抽象语法树
    • 语义分析 生成中间表示


      image.png
    1. 综合部分,也被叫做编译器的后端,主要负责翻译成目标机器语言


      image.png

    1.2简单的编译器实例

    b站加法编译为汇编demo

    2.词法分析

    词法分析是编译的第一阶段,主要任务是读取字符流,将他们转为记号(也有说是token/单词等)流,记号是编译器内部定义的数据结构,编码所识别出的词法单元
    如c程序

    if (x > 5)
      y = "hello";
    else
      z=1;
    
    可以被词法分析成如下 image.png
    • IF 对应关键字if
    • ()对应LPAREN和RPAREN
    • x 对应 标识IDENT,同时还要携带上名字(x)
    • 大于号对应GT
      。。。。。。
    • 最后加上文件结束符EOF

    2.1 C语言中记号的数据结构定义

    关键字、符号等被定义在枚举中

    enum kind {IF,LPAREN,ID,...};
    

    他们分别对应唯一的数字,这也是对单词的分类

    struct token {
       enum kind k;
       char *lexeme;
    }
    

    token是这些记号在c语言中的结构体(lexeme是识别出的单词具体值,0标识没有赋予任何值,通常用于关键字),那么如下语句

    if (x>5)
    

    就会被分析成如下伪代码

    token{k=IF,lexeme=0} 
    token{k=LPAREN,lexeme=0} 
    token{k=IDENT,lexeme=x}
    token{k=GT,lexeme=0}
    token{k=RPAREN,lexeme=0}
    

    2.2词法分析器的实现方法

    主要两种:手工编码实现法和词法分析器的生成器

    2.2.1手工编码实现法

    利用转移图识别标识符、关键字和符号,来返回对应的token数据结构,这是手工编码的方法之一

    2.2.1自动生成记号

    1. 利用正则表达式
    2. 有限状态自动机

    从字符流->正则表达式->记号流的转换过程如下:
    字符流->正则表达式RA->ANF->有限状态机DNF->记号流
    这只是其中的一种例举手段,并不是所有语言全都这个流程

    3.go词法分析

    我们知道golang的编译器自身也是用golang语言开发的(自举),也就意味着我们能在源码找到go的记号

    3.1go的记号

    go中的记号被叫做token,位于src/go/token/token.go

    type token int
    

    和上面c定义记号一样使用枚举,每个类型的字面值都有对应的token

    const (
        // Special tokens
        ILLEGAL Token = iota
        EOF
        COMMENT
    
        literal_beg
        // Identifiers and basic type literals
        // (these tokens stand for classes of literals)
        IDENT  // main
        INT    // 12345
        FLOAT  // 123.45
        IMAG   // 123.45i
        CHAR   // 'a'
        STRING // "abc"
        literal_end
    
        operator_beg
        // Operators and delimiters
        ADD // +
        SUB // -
        MUL // *
        QUO // /
        REM // %
    

    不难看出go中的token主要有标识符、关键字、运算符、分隔符等类型

    • 标识符指go对各种变量、方法、函数等命名时使用的字符序列


      image.png

      前三个是特殊类型的token,分别是错误、文件结束和注释,当遇到不能识别的token时,即源文件存在词法错误,统一返回ILLEGAL
      所有token还会被放入到一个string切片中,方便快速判断token类型

    var tokens = [...]string{
        ILLEGAL: "ILLEGAL",
    
        EOF:     "EOF",
        COMMENT: "COMMENT",
    
        IDENT:  "IDENT",
    

    go/token/position.go当中定义了token相关位置,也就是属性

    type Position struct {
        Filename string // filename, if any
        Offset   int    // offset, starting at 0
        Line     int    // line number, starting at 1
        Column   int    // column number, starting at 1 (byte count)
    }
    

    顾名思义,标识token在文件中的位置,为什么要有这个属性?因为go程序可以由多个包链接成一个可执行文件,单个包又对应多个文件,因此position.go还定义了FilSet和File结构体,用于描述文件集和文件

    type FileSet struct {
       mutex sync.RWMutex // protects the file set
       base  int          // base offset for the next file
       files []*File      // list of files in the order added to the set
       last  *File        // cache of last file looked up
    }
    
    type File struct {
       set  *FileSet
       name string // file name as provided to AddFile
       base int    // Pos value range for this file is [base...base+size]
       size int    // file size as provided to AddFile
    
       // lines and infos are protected by mutex
       mutex sync.Mutex
       lines []int // lines contains the offset of the first character for each line (the first entry is always 0)
       infos []lineInfo
    }
    
    type lineInfo struct {
       Offset       int
       Filename     string
       Line, Column int
    }
    
    • FileSet利用files切片保存一个文件集下所有的File文件,利用base来标识文件File的偏移量,base计算从1开始,因为第0个位置被EOF占了
    • File中的lines可以用来计算出Token的行号和列号
    • lineinfo用于存储每个token的行列号
    FileSet和File对象关系如下 image.png

    除此之外还有一个关键变量Pos

    type Pos int
    

    FileSet把File的内容按字节顺序放在一个大数组中,而某个文件则属于数组的一个区间[base,base+size],pos是这个区间中的一个小标,表示一个文件中的位置,当需要使用时,会根据pos从fileset中转换出完整的positon对象

    3.2词法分析部分

    位于go/scanner/scanner.go中,该包提供了Scanner实现Token扫描,是在FileSet和File抽象文件集合基础上进行的,我们看同目录下example.go下提供的案例来看如何解析"cos(x) + 1i*sin(x) // Euler"这句

    func ExampleScanner_Scan() {
        src := []byte("cos(x) + 1i*sin(x) // Euler")
    
        var s scanner.Scanner
           //创建文件集FileSet,token的位置必须通过文件集定位
        fset := token.NewFileSet()  
    //向文件集中添加新文件File,文件名为空""
        file := fset.AddFile("", fset.Base(), len(src))
    //初始化扫描器,第三个参数表示自定义错误处理函数
    //最后一个参数表示不用忽略注释token
        s.Init(file, src, nil, scanner.ScanComments)
    
    //循环读取字符流解析token
        for {
            pos, tok, lit := s.Scan()
            if tok == token.EOF {
                break
            }
    //打印token的位置
            fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
        }
    
        // output:
        // 1:1  IDENT   "cos"
        // 1:4  (   ""
        // 1:5  IDENT   "x"
        // 1:6  )   ""
        // 1:8  +   ""
        // 1:10 IMAG    "1i"
        // 1:12 *   ""
        // 1:13 IDENT   "sin"
        // 1:16 (   ""
        // 1:17 IDENT   "x"
        // 1:18 )   ""
        // 1:20 COMMENT "// Euler"
        // 1:28 ;   "\n"
    }
    

    这个Scanner对象就是词法分析器,它的主要方法是Scan,主流程如下,是一个switch case的有限状态确定机:

    • 碰到字符扫调用sacnString扫描
    • 碰到数字调用scanNumber方法扫描,同上
    • 碰到计算符同上
      。。。。。
    • 每次返回一个被扫描的token,压缩表示的位置和字面值的字符串

    代码太长就不贴出了,回头再看scanner对象

    type Scanner struct {
       // immutable state
       file *token.File  // source file handle
       dir  string       // directory portion of file.Name()
       src  []byte       // source
       err  ErrorHandler // error reporting; or nil
       mode Mode         // scanning mode
    
       // 词法分析器使用的核心变量
       ch         rune // 当前字符
       offset     int  // 字符偏移量
       rdOffset   int  // 可以理解为ch的偏移量
       lineOffset int  // 当前的行偏移量
       insertSemi bool // 在换行前插入分号
    
       // public state - ok to modify
       ErrorCount int // number of errors encountered
    }
    

    遍历完当前token后,ch会指向下一个字符,实际上就是scanner.next方法把下一个字符读进这个变量

    3.3语法错误检验

    词法分析只输出token,并不检测语法错误

    func main() {
        str :=
            `
    package main
    
    func main() {
        a = 1
    }
        `
        src := []byte(str)
        var s scanner.Scanner
        fset := token.NewFileSet()
        file := fset.AddFile("my.go", fset.Base(), len(src))
        s.Init(file, src, func(pos token.Position, msg string) {
            fmt.Print(msg)
        }, scanner.ScanComments)
        for {
            pos, tok, lit := s.Scan()
            if tok == token.EOF {
                break
            }
            fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
        }
    }
    
    my.go:2:1       package "package"
    my.go:2:9       IDENT   "main"
    my.go:2:13      ;       "\n"  
    my.go:4:1       func    "func"
    my.go:4:6       IDENT   "main"
    my.go:4:10      (       ""    
    my.go:4:11      )       ""    
    my.go:4:13      {       ""    
    my.go:5:2       IDENT   "a"   
    my.go:5:4       =       ""    
    my.go:5:6       INT     "1"   
    my.go:5:7       ;       "\n"  
    my.go:6:1       }       ""    
    my.go:6:2       ;       "\n" 
    

    参考

    1.mooc的中科大《编译原理》

    1. go词法分析刨析

    相关文章

      网友评论

          本文标题:GO/ast编程--编译原理学习之词法分析

          本文链接:https://www.haomeiwen.com/subject/fbgagdtx.html