Golang 编译器代码浅析
  • 0. Golang 编译器代码浅析
  • 1. golang 编译器 - 前言
    • 1.1 编译器简介
    • 1.2 Golang 编译器
    • 1.3 Go 语言版本
    • 1.4 项目设置
    • 1.5 约定
    • 1.6 写作目的
  • 2. golang 编译器 - 词法分析
    • 2.1 简介
    • 2.2 代码结构
    • 2.3 处理字符
    • 2.4 扫描Token
    • 2.5 总结
  • 3.a 语法分析理论知识
    • 3A.1 语法分析简介
    • 3A.2 文法
    • 3A.3 语法解析
    • 3A.3.1 自顶向下(Top-Down)
    • 3A.3.2 自顶向下 - 递归下降
    • 3A.3.3 自顶向下 - LL(1)文法
    • 3A.3.4 自底向上(Bottom-Up)
    • 3A.3.5 自底向上 - LR(0)项集及SLR预测表
    • 3A.3.6 自底向上 - LR(1)、LALR
    • 3A.4 语法分析工具
    • 3A.5 总结
  • 3B. golang 编译器 - 语法分析
    • 3B.1 简介
    • 3B.2 代码结构
    • 3B.3 数据结构
    • 3B.4 构造语法树
    • 3B.5 Unit Test及AST可视化
  • 4. Golang 编译器 - 类型检查
    • 4.1 简介
    • 4.2 代码结构
    • 4.3 符号解析
    • 4.4.1 数据结构 - 作用域
    • 4.4.2 数据结构 - Package
    • 4.4.3 数据结构 - Object 对象
    • 4.4.4-1 类型数据结构 - 简介
    • 4.4.4-2 类型接口
    • 4.4.4-3 基础类型
    • 4.4.4-4 内置复合类型
    • 4.4.4-5 Struct 类型
    • 4.4.4-6 Interface 类型
    • 4.4.4-7 Named 类型
    • 4.4.4-8 Tuple 类型
    • 4.4.4-9 Sum 类型
    • 4.4.4-10 Function & Method 类型
    • 4.4.4-11 泛型类型
    • 4.4.4-12 类型的等价规则
    • 4.4.4-13 类型的比较规则
    • 4.4.4-14 总结
    • 4.4.5 类型检查器
    • 4.4.6 总结
    • 4.5.1 类型检查逻辑 - 包加载器
    • 4.5.2 类型检查逻辑 - 初始化
    • 4.5.2-1 全局作用域
    • 4.5.2-2 类型检查器
    • 4.5.3 类型检查逻辑 - 流程分析
    • 4.5.3-1.1 总体流程
    • 4.5.3-1.2 类型检查准备工作
    • 4.5.3-1.3 类型检查核心逻辑
    • 4.5.3-1.3a 总体介绍
    • 4.5.3-1.3b 类型表达式的类型检查
    • 4.5.3-1.3c 求值表达式的类型检查
    • 4.5.3-1.3d 类型兼容性检查
    • 4.5.3-1.3e 处理delayed队列
    • 4.5.3-1.4 构建初始化顺序
    • 4.5.3-1.5 总结
    • 4.5.3-2 特定问题分析
    • 4.5.3-2a 对象循环依赖检查
    • 4.5.3-2b 方法与属性查找
    • 4.5.3-2c Underlying Type
    • 4.6 如何测试
    • 4.7 总结
  • 5. Golang 编译器 - IR Tree
    • 5.1 简介
    • 5.2 代码结构
    • 5.3 数据结构
    • 5.4 处理逻辑
    • 5.5 编译日志
    • 5.6 Unit Test
    • 5.7 总结
  • 6. golang 编译器 - 初始化任务
    • 6.1 简介
    • 6.2 代码结构
    • 6.3 总体逻辑
    • 6.4 赋值语句
    • 6.5 编译日志
    • 6.6 Unit Test
    • 6.7 总结
  • 7. golang 编译器 - 清除无效代码
    • 7.1 简介
    • 7.2 处理逻辑
    • 7.3 Unit Test
  • 8. golang 编译器 - Inline
    • 8.1 简介
    • 8.2 Inline的问题
    • 8.3 代码结构
    • 8.4 处理逻辑
    • 8.4.1 遍历调用链
    • 8.4.2 内联判断
    • 8.4.3 内联操作
    • 8.4.4 编译日志
    • 8.4.5 Unit Test
    • 8.4.6 总结
  • 9. golang 编译器 - 逃逸分析
    • 9.1 什么是逃逸分析
    • 9.2 Go 的逃逸分析
    • 9.3 算法思路
    • 9.4 代码结构
    • 9.5 处理逻辑
    • 9.5.1总体逻辑
    • 9.5.2 数据结构
    • 9.5.3 构建数据流有向图
    • 9.5.4 逃逸分析
    • 9.6 编译日志
    • 9.7 Unit Test
    • 9.8 总结
  • 10. golang 编译器 - 函数编译及导出
    • 10.1 简介
    • 10.2 编译函数
    • 10.2.1 SSA
    • 10.2.2 ABI
    • 10.2.3 并发控制
    • 10.3 导出对象文件
    • 10.4 总结
  • 11. Golang 编译器 - 写在最后
由 GitBook 提供支持
在本页
  • tokens.go
  • scanner.go

这有帮助吗?

  1. 2. golang 编译器 - 词法分析

2.4 扫描Token

识别 Token 的逻辑位于如下源代码中:

tokens.go

该文件内定义了与 Token 相关的三种类型:

type token uint    // Token 类型
type LitKind uint8 // 如果当前 Token 是字面量,则标识该字面量类型
type Operator uint // 如果当前 Token 是操作符,则标识操作符类型

然后初始化了4种枚举常量,除了 token, LitKind, Operator 三种类型对应的常量外,还初始化了操作符优先级的常量,这些常量都在扫描 Token 时使用。详情请查看 tokens.go 文件,为了节约篇幅,这里不再贴出所有代码。

scanner.go

所有扫描 Token 的逻辑都通过结构 scanner 实现,其定义如下:

type scanner struct {
    source
    mode   uint
    nlsemi bool // if set '\n' and EOF translate to ';'

    // current token, valid after calling next()
    line, col uint
    blank     bool // line is blank up to col
    tok       token
    lit       string   // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
    bad       bool     // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
    kind      LitKind  // valid if tok is _Literal
    op        Operator // valid if tok is _Operator, _AssignOp, or _IncOp
    prec      int      // valid if tok is _Operator, _AssignOp, or _IncOp
}

重要的字段包括:

  • source: 用来扫描字符

  • tok: 当前 Token 的类型

  • lit: 保存当前 Token 的字面量

  • kind: 标识当前 lit 的种类

  • op: 记录当前操作符

  • prec: 标识当前操作符的优先级

扫描 Token 的逻辑在next 中,该方法首先忽略掉空白字符,然后通过当前字符并根据其内容解析出下一个 Token. 我们来详细看一下其如何解析出程序中的标识符的。

标识符(Identifier)就是程序中用于命名的那些词素,可以抽象的理解为用来绑定一个值的“变量”(先不要将此处的变量与程序中通过 var 申明的变量混淆,此处的变量是更加抽象的概念),先通过一个程序片段来理解一下:

package gostudy

import fmtalias "fmt"

func main() {
    content := "hello, gopher!"
    fmtalias.Println(content)
}

其中用于命名的词素包括:

  • gostudy: 为包命名

  • fmtalias: 为所引入的包 fmt 命名

  • main: 为函数命名

  • content: 为局部变量命名

  • Println: 绑定包中的函数值

这些标识符有时也被直接称为符号(Symbol),其 Token 类型被定义为_Name. Golang 将关键字也归为此类,在 init 方法中对关键字对应的 Token 类型做了初始化:

func hash(s []byte) uint {
    return (uint(s[0])<<4 ^ uint(s[1]) + uint(len(s))) & uint(len(keywordMap)-1)
}

var keywordMap [1 << 6]token // size must be power of two

func init() {
    // populate keywordMap
    for tok := _Break; tok <= _Var; tok++ {
        h := hash([]byte(tok.String()))
        if keywordMap[h] != 0 {
            panic("imperfect hash")
        }
        keywordMap[h] = tok
    }
}

init 将所有关键字的 Token 类型存放在数组keywordMap 中,后续逻辑依此来判定扫描的标识符是否是关键字。

Golang 规范的标识符命名规则是:

  • 名字由数字、英文字母、下划线、或者编码超过两个字节的 Unicode 字符(编码数字大于 utf8.RuneSelf)组成

  • 不能以数字开头

  • 大小写敏感

  • 不能使用关键字

  • 长度不限制

函数next 在过滤了空白字符后,首先就会判断当前字符是否是一个标识符的开端,其逻辑如下:

func (s *scanner) next() {
    // 忽略开头代码
    if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {
        s.nextch()
        s.ident()
        return
    }
    // 忽略后续代码
} 

如果当前字符是合法的标识符开端,则通过 scanner.ident() 方法扫描对应标识符并返回。该方法也比较紧凑,我们可以贴出来看看:

func (s *scanner) ident() {
    // accelerate common case (7bit ASCII)
    for isLetter(s.ch) || isDecimal(s.ch) {
        s.nextch()
    }

    // general case
    if s.ch >= utf8.RuneSelf {
        for s.atIdentChar(false) {
            s.nextch()
        }
    }

    // possibly a keyword
    lit := s.segment()
    if len(lit) >= 2 {
        if tok := keywordMap[hash(lit)]; tok != 0 && tokStrFast(tok) == string(lit) {
            s.nlsemi = contains(1<<_Break|1<<_Continue|1<<_Fallthrough|1<<_Return, tok)
            s.tok = tok
            return
        }
    }

    s.nlsemi = true
    s.lit = string(lit)
    s.tok = _Name
}

该方法的逻辑与 Golang 中对标识符的规范完全一致,其一直读取合法的标识符字符,再通过 s.segment() 取出标识符词素;然后根据之前初始化的 keywordMap 判断其是否是语言内置的关键字,如果是关键字则设置 tok 为对应的关键字类型,否则便将 tok 设置为 _Name 类型,并将词素存放在 lit 属性中。

我们来跑一下测试代码看看 scanner 对标识符的解析情况。在 scanner_test.go 中新增如下测试代码:

func TestScanIdentTokens(t *testing.T) {
    code := `package gostudy

    import fmtalias "fmt"

    func main() {
        content := "hello, gopher!"
        fmtalias.Println(content)
    }
`

    var scn scanner

    scn.init(strings.NewReader(code), func(line, col uint, msg string) {
        fmt.Printf("%d:%d: %s\n", line, col, msg)
    }, 0)

    scn.next()
    for scn.tok != _EOF {
        if scn.tok == _Name {
            fmt.Printf("Token Type: %s, lit: %s\n", tokStrFast(scn.tok), scn.lit)
        }

        // 关键字标识符不会设置 lit 字段
        if scn.tok >= _Break && scn.tok <= _Var {
            fmt.Printf("Token Type: %s\n", tokStrFast(scn.tok))
        }
        scn.next()
    }
}

运行后可以得到结果:

Token Type: package
Token Type: name, lit: gostudy
Token Type: import
Token Type: name, lit: fmtalias
Token Type: func
Token Type: name, lit: main
Token Type: name, lit: content
Token Type: name, lit: fmtalias
Token Type: name, lit: Println
Token Type: name, lit: content
上一页2.3 处理字符下一页2.5 总结

最后更新于3年前

这有帮助吗?

nlsemi: Golang 中使用分号来作为语句结束的标识符,但是大多数情况下都可以忽略掉,忽略的规则见:. 该字段用来判断如果当前 token 是当前行的最后一个 token 的话,是否可以自动插入一个分号。根据规则可见,如果该 token 是关键字 break, continue, fallthrough 与 return 之一时,可以插入分号;而当该 token 是一个标识符时,总是可以插入分号。

https://golang.org/ref/spec#Semicolons