- KusonStack一站式可编程配置技术栈(Go): https://github.com/KusionStack/kusion
- KCL 配置编程语言(Rust): https://github.com/KusionStack/KCLVM
- 凹语言™: https://github.com/wa-lang/wa
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
结果正常。