4.3 完善LLIR输出

基于新的AST语法树翻译到LLVM-IR需要处理嵌套的词法域问题。本节尝试通过Scope实现对嵌套的词法域支持,并完善LLIR输出。

4.3.1 词法域等例子

在不同的词法域空间,同一个名字表示不同的对象。比如:

package main

var x int

func main() {
	var x = x + 1
}

有2个变量名字都是x,最外层的x是包级变量,main函数中新定义1个x局部变量。但是main函数内x变量初始化时,右侧的表达式中的x表示的是外层的包变量x。再向外层还有一个builtin词法域空间。词法域表示基于当前上下文环境检索名字的规则,以及配套的名字符号表。

4.3.2 定义词法域

词法域属于语义解析,目前只有compiler包产生LLVM汇编代码时需要。因此在compiler包定义Scope管理词法域:

package compiler

import "github.com/wa-lang/ugo/ast"

type Scope struct {
	Outer   *Scope
	Objects map[string]*Object
}

type Object struct {
	Name        string
	MangledName string
	ast.Node
}

func NewScope(outer *Scope) *Scope {
	return &Scope{outer, make(map[string]*Object)}
}

Scope内的Outer指向外层的Scope,比如main函数的外层Scope是文件,文件的外层是builtin(目前为了简化忽略了Package级别的Scope)。

Object表示一个命名对象,其中Name是实体在uGo代码中词法域的名字,LLName是映射到LLVM汇编语言的名字。其中还有指向的AST节点,可以用于识别更多的信息。

Scope还需要几个辅助方法:

func (s *Scope) HasName(name string) bool {
	_, ok := s.Objects[name]
	return ok
}

func (s *Scope) Lookup(name string) (*Scope, *Object) {
	for ; s != nil; s = s.Outer {
		if obj := s.Objects[name]; obj != nil {
			return s, obj
		}
	}
	return nil, nil
}

func (s *Scope) Insert(obj *Object) (alt *Object) {
	if alt = s.Objects[obj.Name]; alt == nil {
		s.Objects[obj.Name] = obj
	}
	return
}

其中HasName判断当前词法空间是否包含Name,Lookup则是从内向外层查询符号,Insert用于添加一个新的符号。

4.3.3 Builtin内置函数

builtin的Scope对应最外层的宇宙空间:

var Universe *Scope = NewScope(nil)

var builtinObjects = []*Object{
	{Name: "println", MangledName: "@ugo_builtin_println"},
	{Name: "exit", MangledName: "@ugo_builtin_exit"},
}

func init() {
	for _, obj := range builtinObjects {
		Universe.Insert(obj)
	}
}

Universe是一个包级别的变量,Universe之外就没有词法域了。我们在包初始化时,向Universe注入了内置的println和exit函数信息。

然后向Compiler对象添加Scope成员:

type Compiler struct {
	file   *ast.File
	scope  *Scope
	nextId int
}

并在编译方法中初始化scope对象:

func NewCompiler() *Compiler {
	return &Compiler{
		scope: NewScope(Universe),
	}
}

NewScope(Universe) 基于 Universe 构建,因此也就具备了Builtin预先定义的println和exit内置函数。

内置函数调用翻译现在可以从scope查询了:

func (p *Compiler) compileExpr(w io.Writer, expr ast.Expr) (localName string) {
	switch expr := expr.(type) {
	...
	case *ast.CallExpr:
		var fnName string
		if _, obj := p.scope.Lookup(expr.FuncName.Name); obj != nil {
			fnName = obj.MangledName
		} else {
			panic(fmt.Sprintf("func %s undefined", expr.FuncName.Name))
		}

		localName = p.genId()
		fmt.Fprintf(w, "\t%s = call i32(i32) %s(i32 %v)\n",
			localName, fnName, p.compileExpr(w, expr.Args[0]),
		)
		return localName
	...
	}
}

现在p.scope只有一个Scope,也就是Universe,但是已经可以支持获取内置函数真实的名称了。

4.3.4 嵌套Scope

每个可以定义新变量的词法域都对应一个嵌套的Scope,目前全局变量和每个块语句都对应一个Scope。因此Compiler对象可以定义辅助函数用于进入和退出内层的Scope:

func (p *Compiler) enterScope() {
	p.scope = NewScope(p.scope)
}

func (p *Compiler) leaveScope() {
	p.scope = p.scope.Outer
}

enterScope表示进入新的词法域,leaveScope表示退出当前词法域。

我们需要在编译文件、块语句时进入新的Scope,以便于存储新词法域定义的新变量:

func (p *Compiler) compileFile(w io.Writer, file *ast.File) {
	p.enterScope()
	defer p.leaveScope()
	...
}

func (p *Compiler) compileStmt(w io.Writer, stmt ast.Stmt) {
	switch stmt := stmt.(type) {
	...
	case *ast.BlockStmt:
		p.enterScope()
		defer p.leaveScope()
	...
	}
}

当处理完成退出函数时通过defer语句离开嵌套的词法域。

4.3.5 定义全局变量

定义变量就是向当前的Scope添加新的命名对象,全局变量在编译文件时处理:

func (p *Compiler) compileFile(w io.Writer, file *ast.File) {
	p.enterScope()
	defer p.leaveScope()

	for _, g := range file.Globals {
		var mangledName = fmt.Sprintf("@ugo_%s_%s", file.Pkg.Name, g.Name.Name)
		p.scope.Insert(&Object{
			Name:        g.Name.Name,
			MangledName: mangledName,
			Node:        g,
		})
		fmt.Fprintf(w, "%s = global i32 0\n", mangledName)
	}
	...
}

通过遍历file.Globals处理每个全局变量。mangledName是全局变量修饰后在LLVM汇编中的名字,以@ugo_开头连接包名和变量名组成(包名有冲突的风险,更安全的做法是用唯一的包路径替代)。

4.3.6 全局函数对象

全局函数是和全局变量类似的命名对象,可以在compileFunc中处理:

func (p *Compiler) compileFunc(w io.Writer, file *ast.File, fn *ast.Func) {
	p.enterScope()
	defer p.leaveScope()

	var mangledName = fmt.Sprintf("@ugo_%s_%s", file.Pkg.Name, fn.Name)
	p.scope.Insert(&Object{
		Name:        fn.Name,
		MangledName: mangledName,
		Node:        fn,
	})
	...
}

虽然目前还不支持调用自定义函数,但是有了Scope之后自定义的函数已经可以通过名字查询到了。

4.3.7 定义局部变量

Compiler对象的compileStmt方法用于编译语句,在其中增加对*ast.VarSpec变量定义的处理:

func (p *Compiler) compileStmt(w io.Writer, stmt ast.Stmt) {
	switch stmt := stmt.(type) {
	case *ast.VarSpec:
		var localName = "0"
		if stmt.Value != nil {
			localName = p.compileExpr(w, stmt.Value)
		}

		var mangledName = fmt.Sprintf("%%local_%s.pos.%d", stmt.Name.Name, stmt.VarPos)
		p.scope.Insert(&Object{
			Name:        stmt.Name.Name,
			MangledName: mangledName,
			Node:        stmt,
		})

		fmt.Fprintf(w, "\t%s = alloca i32, align 4\n", mangledName)
		fmt.Fprintf(
			w, "\tstore i32 %s, i32* %s\n",
			localName, mangledName,
		)
	...
	}
}

需要注意的是,如果变量定义有初始化表达式,一定要先编译表达式。比如在处理var x = x变量定义时,先编译右边表达式则x获取的是外层的定义(因为此时左边的x尚未定义)。处理完右边的初始化表达式之后,再将左边的变量添加到Scope中。然后通过LLVM的alloca指令在栈上分配空间,并进行初始化(如果没有初始化表达式则用0初始化)。

4.3.8 变量标识符检索

在处理内置函数调用时我们已经见识过基于Scope的名字检索。对于变量我们只需要增加*ast.Ident类型的表达式处理即可:

func (p *Compiler) compileExpr(w io.Writer, expr ast.Expr) (localName string) {
	switch expr := expr.(type) {
	case *ast.Ident:
		var varName string
		if _, obj := p.scope.Lookup(expr.Name); obj != nil {
			varName = obj.MangledName
		} else {
			panic(fmt.Sprintf("var %s undefined", expr.Name))
		}

		localName = p.genId()
		fmt.Fprintf(w, "\t%s = load i32, i32* %s, align 4\n",
			localName, varName,
		)
		return localName
	...
	}
}

p.scope.Lookup(expr.Name)根据名字查询LLVM汇编中对应的局部变量的名字,然后通过LLVM的load指令加载到一个新的变量并返回。

4.3.9 赋值变量

赋值变量和变量读取到机制差不多,第一步都是要从Scope查询被赋值变量对应的LLVM的名字。compileStmt方法增加对*ast.AssignStmt赋值语句的处理:

func (p *Compiler) compileStmt(w io.Writer, stmt ast.Stmt) {
	switch stmt := stmt.(type) {
	...
	case *ast.AssignStmt:
		var varName string
		if _, obj := p.scope.Lookup(stmt.Target.Name); obj != nil {
			varName = obj.MangledName
		} else {
			panic(fmt.Sprintf("var %s undefined", stmt.Target.Name))
		}

		localName := p.compileExpr(w, stmt.Value)
		fmt.Fprintf(
			w, "\tstore i32 %s, i32* %s\n",
			localName, varName,
		)
	...
	}
}

查询到LLVM对应的变量名之后,通过LLVM的store指令赋值。

4.3.10 全局变量的初始化

到此我们已经基本完成了嵌套变量的支持,现在还需要对全局变量的初始化增加支持。全局变量的初始化语句需要放入一个init函数,builtin包做以下调整:

const MainMain = `
define i32 @main() {
	call i32() @ugo_main_init()
	call i32() @ugo_main_main()
	ret i32 0
}
`

在main函数调用@ugo_main_main()之前先调用@ugo_main_init()进行初始化。

Compiler对象增加genInit方法用于生成全局变量初始化的代码:

func (p *Compiler) genInit(w io.Writer, file *ast.File) {
	fmt.Fprintf(w, "define i32 @ugo_%s_init() {\n", file.Pkg.Name)

	for _, g := range file.Globals {
		var localName = "0"
		if g.Value != nil {
			localName = p.compileExpr(w, g.Value)
		}

		var varName string
		if _, obj := p.scope.Lookup(g.Name.Name); obj != nil {
			varName = obj.MangledName
		} else {
			panic(fmt.Sprintf("var %s undefined", g))
		}

		fmt.Fprintf(w, "\tstore i32 %s, i32* %s\n", localName, varName)
	}
	fmt.Fprintln(w, "\tret i32 0")
	fmt.Fprintln(w, "}")
}

然后在compileFile方法完成全局变量定义之后生成用于初始化的init函数:

func (p *Compiler) compileFile(w io.Writer, file *ast.File) {
	...
	for _, g := range file.Globals {}
	p.genInit(w, file)
	...
}

4.3.11 测试

构造测试用例:

package main

var x1 int
var x2 int = 134

func main() {
	{
		var x2 = x2
		x2 = x2 + 1000
		println(x2)
	}
	println(x1)
	println(x2)
}

蔬菜结果如下:

$ go run main.go -debug=true run ./_examples/hello.ugo
1134
0
134

结果正常。


© 2021-2022 | 柴树杉 保留所有权利