iGo+ 实现 REPL 功能
iGo+ ( https://github.com/goplus/igop ) 是一个基于 SSA 实现的 Go/Go+ 语言解释器。igop 之前命名为 gossa ,在实现了 REPL 功能之后,重新命名为 igop 。
REPL : Read Eval Print Loop 交互式解释器
官网 REPL 运行
https://repl.goplus.org
本地安装运行
go install github.com/goplus/igop/cmd/igopigop 加入 REPL 功能之后,可以直接以交互式解释器方式运行 Go/Go+ 代码。我们可以看一下 igop REPL 的一些使用示例
1. 闭包函数示例
$ igopWelcome to Go+ (version v1.1.x) REPL!>>> a := 100>>> a100 int>>> a+100200 int>>> fn := func() { println a }>>> fn()100>>> a = 200>>> fn()200
在第 8 行时我们创建了一个 fn 函数,fn 引用了闭包环境变量 a ,可以看到在 a 改变后,fn() 打印正确的 a 值。
2. 结构体示例
$ igopWelcome to Go+ (version v1.1.x) REPL!>>> type Point struct {... x int... y int... }>>> pt := &Point{10,20}>>> pt&{10 20} *main.Point>>> import "fmt">>> func (p *Point) String() string {... return fmt.Sprintf("%v-%v",p.x,p.y)... }>>> pt2 := &Point{10,20}>>> pt210-20 *main.Point>>>
上面第 3 行声明了 Point 结构体类型 ,第 7 行创建 pt 时,因为 Point 没有实现 fmt.Stringer 接口,所以在第 8 行时以默认方式显示。而在 11 行时我们为 Point 类型加入了 String() string 函数,支持 fmt.Stringer 接口,所以我们看到在第 14 行创建了 pt2 之后,显示格式按照 (*Point).String 函数显示。
3. 常量计算支持
$ igopWelcome to Go+ (version v1.1.x) REPL!>>> 10240<<10012980742146337069071326240823050240 untyped int>>> 1/3.0+2/5.00.7333333333333333 untyped float(11/15)>>> 1e400*1.11.1e+400 untyped float(11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000)>>> import "math">>> math.Pi*26.283185307179586 untyped float(314159265358979323846264338327950288419716939937510582097494459/50000000000000000000000000000000000000000000000000000000000000)
常量计算可以直接支持计算超过 int64/float64 范围的任意整数和浮点数。如果计算结果为浮点数,也会显示内部的实际精确存储值,比如第 5 行的计算结果为无限循环小数,在第 6 行中只显示了部分值,而在第 7 行显示内部实际存储值为分数形式 11/15 。
igop 实现 REPL 的原理简介
在这里简要说明一下 igop 实现 REPL 的原理。
Go/Go+ 源码 -> AST -> types -> SSA
1. REPL 首先将终端输入的语句判断是否为 const / var / type / func / 其他,并将其和之前输入代码一起合成为可识别的 Go 源码,这个过程中会通过 AST 和 types 进行检查,以判断是否错误以及是否等待继续输入比如之前的定义结构体和函数声明的多行输入支持。
2. 对于可以正确识别的 Go 源码将进行 AST 变换以及 types 检查,最后生成对应的 SSA 代码。在 igop.Repl 内部会保存 init/main 函数执行的 frame 和 pc 指针,每次调用时 init/main 函数都能够按照 pc 值继续执行下去,以确保变量地址及函数堆栈不变。
3. 为了保证生成的 SSA 代码顺序不变,对于 for/switch 等会改变 SSA 顺序的代码我们使用了一个小技巧 —— 闭包变换。
for i := 0;i < 10; i++ {println(i)}会变换为下面的闭包函数形式。func() {for i := 0;i < 10; i++ {println(i)}}()
通过闭包变换,将循环结构变换为函数调用形式从而保证了 SSA 顺序一致性,维持 main 函数的 pc 值不变。