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

2023-12-19  本文已影响0人  温岭夹糕

1.编译器和解释器

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


image.png

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

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

1.1编译器结构

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

  1. 分析部分,也被叫做编译器的前端,主要负责将字符输入流转为token,进一步转为ast抽象语法树,步骤包括:
  1. 综合部分,也被叫做编译器的后端,主要负责翻译成目标机器语言


    image.png

1.2简单的编译器实例

b站加法编译为汇编demo

2.词法分析

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

if (x > 5)
  y = "hello";
else
  z=1;
可以被词法分析成如下 image.png

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主要有标识符、关键字、运算符、分隔符等类型

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和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的有限状态确定机:

代码太长就不贴出了,回头再看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词法分析刨析
上一篇下一篇

猜你喜欢

热点阅读