Go 编译器内核:给 Go 新增一个语句

本文介绍了如何在Go编译器中添加'until'语句,从扫描、解析、创建抽象语法树到类型检查、分析和重写AST的全过程。尽管'until'在Go中并不实用,但这个过程展示了Go编译器的内部工作原理。
摘要由CSDN通过智能技术生成

任务 —— 增加新的语句

很多语言都有 while 语句,在 Go 中对应的是 for:

for <some-condition> {
   
  <loop body>
}

增加 while 语句是比较简单的,因此 —— 我们只需简单将其转换为 for 语句。所以我选择了一个稍微有点挑战性的任务,增加 until。until 语句和 while 语句是一样的,只是有了条件判断。例如下面的代码:

i := 4
until i == 0 {
   
  i--
  fmt.Println("Hello, until!")
}

等价于:

i := 4
for i != 0 {
   
  i--
  fmt.Println("Hello, until!")
}

事实上,我们甚至可以像下面代码一样,在循环声明中使用一个初始化语句:

until i := 4; i == 0 {
   
  i--
  fmt.Println("Hello, until!")
}

我们的目标是支持这个特性。

特别声明 —— 这只是一个玩具性的探索。我觉得在 Go 中添加 until 并不好,因为 Go 的极简主义设计思想本身就是非常正确的理念。

Go 编译器的高级结构

默认情况下,Go 编译器(gc)是以相当传统的结构来设计的,如果你使用过其他编译器,你应该很快就能熟悉它:
在这里插入图片描述
Go 仓库中相对路径的根目录下,编译器实现位于 src/cmd/compile/internal;本文后续提到的所有代码路径都是相对于这个目录。编译器是用 Go 编写的,代码可读性很强。在这篇文章中,我们将一点一点的研究这些代码,同时添加支持 until 语句的实现代码.

查看 src/cmd/compile 中的 README 文件,了解编译步骤的详细说明。它将与本文息息相关。

扫描

扫描器(也称为 词法分析器)将源码文本分解为编译器所需的离散实体。例如 for 关键字会转变成常量 _For;符号 … 转变成 _DotDotDot,. 将转变成 _Dot 等等。

扫描器的实现位于 syntax 包中。我们需要做的就是理解关键字 —— until。syntax/tokens.go 文件中列出了所有 token,我们要添加一个新的:

_Fallthrough // fallthrough
_For         // for
_Until       // until
_Func        // func

token 常量右侧的注释非常重要,它们用来标识 token。这是通过 syntax/tokens.go 生成代码来实现的,文件上面的 token 列表有如下这一行:

//go:generate stringer -type token -linecomment

go generate 必须手动执行,输出文件(syntax/token_string.go)被保存在 Go 源码仓库中。为了重新生成它,我在 syntax 目录中执行如下命令:

GOROOT=<src checkout> Go generate tokens.go

环境变量 GOROOT 是从 Go 1.12 开始必须设置[1],并且必须指向检出的源码根目录,我们要修改这个编译器。

运行代码生成器并验证包含新的 token 的 syntax/token_string.go 文件,我试着重新编译编译器,却出现了 panic 提示:

panic: imperfect hash

这个 panic 是 syntax/scanner.go 中代码引起的:

// hash 是针对关键词的完美哈希函数
// 它假定参数 s 的长度至少为 2
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 // 大小必须是 2 的整数倍(2 的整数次幂)

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

编译器试图构建一个“完美”哈希表来执行关键字字符串到 token 的查询。“完美”意味着它不太可能发生冲突,是一个线性的数组,其中每个关键字都映射为一个单独的索引。哈希函数相当特殊(例如,它查看字符串 token 的第一个字符),并且不容易调试新 token 为何出现冲突等问题。为了解决这个问题,我将查找表的大小更改为 [1 << 7]token,从而将查找数组的大小从 64 改成 128。这给予哈希函数更多的空间来分配对应的键,冲突也就消失了。

解析

Go 有一个相当标准的递归下降算法的解析器,它把扫描生成的 token 流转换为 具体语法树。我们开始为 syntax/nodes.go 中的 until 添加新的节点类型:

UntilStmt struct {
   
  Init SimpleStmt
  Cond Expr
  Body *BlockStmt
  stmt
}

我借鉴了用于 for 循环的 ForStmt 的整体结构。类似于 for,until 语句有几个可选的子语句:

until <init>; <cond> {
   
  <body>
}

和 是可选的,不过省略 也不是很常见。UntilStmt.stmt 中嵌入的字段用于表示整个语法树语句,并包含对应的位置信息。

解析过程在 syntax/parser.go 中实现。parser.stmtOrNil 方法解析当前位置的语句。它查看当前 token 并决定解析哪个语句。下方是添加的代码片段:

switch p.tok {
   
case _Lbrace:
  return p.blockStmt("")

// ...

case _For:
  return p.forStmt()

case _Until:
  return p.untilStmt()

And this is untilStmt:

func (p *parser) untilStmt() Stmt {
   
  if trace {
   
    defer p.trace("untilStmt")()
  }

  s := new(UntilStmt)
  s.pos = p.pos()

  s.Init, s
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值